Linux内核驱动编程的一道陷阱题(转载)
原文如下:
------
看过一道linux内核驱动编程的题目,我觉得有点价值。
题目很简单,凭记忆整理了下,代码如下:
#include <linux/init.h> #include <linux/module.h> #include <linux/delay.h> #include <linux/kthread.h> struct task_struct *task = NULL; int x = 0; int y = 0; static int test_loop(void *data) { unsigned long time_end = jiffies; while (y == 0) { if (time_after(jiffies, time_end + 10 *HZ)) { printk("break out!\n"); break; } } if (y) { printk("test_loop : x=%d y=%d\n", x, y); } return 0; } static int __init test_init(void) { task = kthread_create(test_loop, 0,"test_loop"); wake_up_process(task); msleep(100); x = 255; y = 1; printk("x=%d y=%d\n", x, y); kthread_stop(task); task = NULL; return 0; } static void __exit test_exit(void) { } module_init(test_init); module_exit(test_exit); MODULE_LICENSE("GPL");
如果有兴趣可以在自己的linux环境上操作下,看下输出结果。
Makefile如下:
obj-m := test.o KERNEL_DIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: make -C $(KERNEL_DIR) SUBDIRS=$(PWD) modules clean: rm *.o *.ko *.mod.c .PHONY:clean
驱动代码很简洁,只有一个内核线程和2个全局变量,全局变量在内核线程唤醒后才会改变。
其实这个题目有2处问题。
我们先尝试编译运行一下,结果输出:
[ 5886.273365] x=255 y=1 [ 5896.174020] break out! [ 5896.174023] test_loop : x=255 y=1
这里打印了“break out”, 且2行输出之间间隔了10s.
这是一个并发编程很容易碰到的坑,不管是内核驱动代码还是应用层代码,都有可能碰到。
为了方便定位,使用应用层代码复现一下:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> int y = 0; int x = 0; static void *test_loop(void *data) { while (y == 0 ){ } if (y) { printf("test_loop: x=%d y=%d\n", x, y); } } int main(int argc, char *argv) { pthread_t t1; int ret = 0; ret = pthread_create(&t1, NULL, &test_loop, 0); if (ret) { printf("create pthread error!\n"); return -1; } usleep(1000); x = 255; y = 1; printf("x=%d y=%d\n", x, y); pthread_join(t1, NULL); }
结果输出一行打印后,陷入了死循环,问题现象一致。
尝试对应用层源代码反汇编一下:
objdump -S test > test.s 00000000004006f0 <test_loop>: { 4006f0: 8b 15 5a 09 20 00 mov 0x20095a(%rip),%edx #****赋值y**** while (y == 0 ){ 4006f6: 85 d2 test %edx,%edx #****比较y和0的大小**** 4006f8: 74 1b je 400715 <test_loop+0x25> #****跳转到400175偏移 test_loop**** { 4006fa: 48 83 ec 08 sub $0x8,%rsp printf("test_loop: x=%d y=%d\n", x, y); 4006fe: 8b 35 50 09 20 00 mov 0x200950(%rip),%esi 400704: bf b0 07 40 00 mov $0x4007b0,%edi 400709: 31 c0 xor %eax,%eax 40070b: e8 30 fe ff ff callq 400540 <printf@plt> } 400710: 48 83 c4 08 add $0x8,%rsp 400714: c3 retq 400715: eb fe jmp 400715 <test_loop+0x25> # *****无限死循环************ 400717: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1) 40071e: 00 00
反汇编的结果,可以看到的确在便宜“400715”的位置陷入了死循环。为什么会这样呢?
是编译器优化导致的吗?
对代码重新编译,不适用编译器优化:
[root]# gcc -g -O0 -o test mb_user.c -lpthread [root]# ./test x=255 y=1 test_loop: x=255 y=1
现在输出显示正常。
这个问题是由于编译器优化导致的,y的值被更改之后,并没有被线程识别到,这个时候需要程序员在编码时就应该识别到会有这种现象,通过volatile 声明y向量
volatile int y = 0;
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,就会保证不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
对代码重新进行编写后,反复卸载加载驱动:
while true do insmod test.ko rmmod test.ko done
执行上百次后,出现:
Dec 1 01:31:37 localhost kernel: test_loop : x=255 y=1 Dec 1 01:31:37 localhost kernel: x=255 y=1 Dec 1 01:31:38 localhost kernel: test_loop : x=255 y=1 Dec 1 01:31:38 localhost kernel: x=255 y=1 Dec 1 01:31:39 localhost kernel: test_loop : x=255 y=1 Dec 1 01:31:39 localhost kernel: x=255 y=1 Dec 1 01:31:40 localhost kernel: test_loop : x=0 y=1 Dec 1 01:31:40 localhost kernel: x=255 y=1 Dec 1 01:31:41 localhost kernel: test_loop : x=255 y=1 Dec 1 01:31:41 localhost kernel: x=255 y=1 Dec 1 01:31:42 localhost kernel: test_loop : x=255 y=1 Dec 1 01:31:42 localhost kernel: x=255 y=1
其中出现了x=0 y=1的打印,为什么x在y之前赋值,反而出现了x=0的现象呢?
这个问题的主要原因是因为x和y变量弱依赖性,CPU在执行指令时乱序执行。
防止CPU在关键位置进行乱序执行,可以使用内存屏障保证。
在x和y赋值之间,加入内存屏障
x = 255; smp_wmb(); y = 1;