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);


}
[root]# gcc -g -O2 -o test mb_user.c -lpthread
[root]# ./test 
x=255 y=1

結果輸出一行打印後,陷入了死循環,問題現象一致。
嘗試對應用層源代碼反彙編一下:

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=255 y=1
Dec  1 01:31:40 localhost kernel: x=0 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;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章