1. 程式人生 > 其它 >【轉載】linux 工作佇列上睡眠的認識--不要在預設共享佇列上睡眠

【轉載】linux 工作佇列上睡眠的認識--不要在預設共享佇列上睡眠

最近專案組做xen底層,我已經被完爆無數遍了,關鍵在於對核心、驅動這塊不熟悉,導致分析xen程式碼非常吃力。於是準備細細的將 幾本 linux 書籍慢慢啃啃。

正好看到LINUX核心設計與實現,對於核心中中斷下半段該如何選擇?大牛的原話是這樣的:“從根本上來說,你有休眠的需要嗎?要是有,工作佇列就是你的唯一選擇,否則最好用tasklet。……”

書中一直強調 工作佇列是可以休眠的,而且翻譯的人總是強調”工作佇列是執行在程序上下文的”, 對於這個翻譯,我不是很理解,程序上下文難道就是指使用者態而言嗎,完全糊塗了,準備自己做個實驗。於是我在網上收了下,並自己寫了一個工作佇列的例子,基本程式碼如下:


struct work_struct test_task;
 
void task_handler(void *data)
{
    char c = 'a';
    int i = 0;
   
    while (task_doing == 1)
    {
        c = 'a'+ i%26;
        printk(KERN_ALERT "---%c\n", c);
        
        if (i++ > 50)
        {
            printk(KERN_ALERT "i beyone so quit");
            break;
        }
       
        //msleep(1000);
        wait_event_interruptible(my_dev->test_queue, my_dev->test_task_step !=0);
    }
 
    printk(KERN_ALERT "quit task task_doing %d\n",task_doing);
       
}
static int
test_ioctl(struct inode *inode, struct file *filp,
		  unsigned int cmd, unsigned long arg)
{
    switch(cmd)
    {
        case IOCTL_INIT_TASK:
            task_doing = 1;
            INIT_WORK(&test_task, task_handler);
            printk(KERN_ALERT "ioctl init task \n");
            break;
         case IOCTL_DO_TASK:
            printk(KERN_ALERT "ioctl do task \n");
            schedule_work(&test_task);
            break;
        default:
            printk(KERN_ALERT "unknown ioctl cmd\n");
            break;
    }
    return 0;
}

使用者態測試程式通過 ioctl 命令傳送IOCTL_INIT_TASK 和 IOCTL_DO_TASK 命令。通過書中介紹,INIT_WORK 是初始化一個工作佇列,其後呼叫schedule_work(&test_task) 後,才會執行工作佇列上註冊的回撥函式。

在回撥函式中,我進行了睡眠,開始用的是 msleep ,這個函式會放棄CPU到指定的時間,沒想到我的核心居然掛住了,再也無法響應。看看驅動設計的程式碼,很少看到有人用msleep的,可能是自己用了不恰當的函式,於是換成如下程式碼:

wait_event_interruptible(my_dev->test_queue, my_dev->test_task_step !=0
);

重新將虛擬機器恢復後,執行同樣的測試,還是不行,一執行註冊的回撥函式,核心就立刻掛起,再也無法操作。


更加無法理解了,說好的工作佇列是可以睡眠的,但是我呼叫睡眠,核心居然就永遠無法醒來啦。已經沒有機會執行一個動作讓 my_dev->test_task_step == 1 了,那麼書中所說的 工作佇列可以睡眠是什麼意思呢 ?

同時看了裝置驅動詳解中阻塞IO的例子,書中說在 linux 中一個等待佇列頭可以如下動態建立:

wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);

但是常常更容易的做法是放一個 DEFINE_WAIT 行在迴圈的頂部, 來實現你的睡眠.

下一步是新增你的等待佇列入口到佇列, 並且設定程序狀態. 2 個任務都由這個函式處理:

void prepare_to_wait(wait_queue_head_t *queue, wait_queue_t *wait, int state); 

這裡, queue 和 wait 分別地是等待佇列頭和程序入口. state 是程序的新狀態; 它應當或者是 TASK_INTERRUPTIBLE(給可中斷的睡眠, 這常常是你所要的)或者 TASK_UNINTERRUPTIBLE(給不可中斷睡眠).

在呼叫 prepare_to_wait 之後, 程序可呼叫 schedule -- 在它已檢查確認它仍然需要等待之後. 一旦 schedule 返回, 就到了清理時間. 這個任務, 也, 被一個特殊的函式處理:

void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait); 

同時,書中還有一個例子:

/* Wait for space for writing; caller must hold device semaphore. On
 * error the semaphore will be released before returning. */
static int scull_getwritespace(struct scull_pipe *dev, struct file *filp)
{
 
        while (spacefree(dev) == 0)
        { /* full */
                DEFINE_WAIT(wait);
 
                up(&dev->sem);
                if (filp->f_flags & O_NONBLOCK)
                        return -EAGAIN;
 
                PDEBUG("\"%s\" writing: going to sleep\n",current->comm);
                prepare_to_wait(&dev->outq, &wait, TASK_INTERRUPTIBLE);
                if (spacefree(dev) == 0)
                        schedule();
                finish_wait(&dev->outq, &wait);
                if (signal_pending(current))
 
                        return -ERESTARTSYS; /* signal: tell the fs layer to handle it */
                if (down_interruptible(&dev->sem))
                        return -ERESTARTSYS;
        }
        return 0;
 
}

問題在於,手動睡眠的方式和上面呼叫 wait_event_interruptible 有什麼區別呢 ?從程式碼上看,手動睡眠有一個等待佇列頭,而且有一個等待佇列單個元素 wait, prepare_to_wait 函式會將 該單個等待元素掛到等待佇列頭裡面去。 一直想搞明白呼叫 prepare_to_wait 後,會不會進入睡眠 ? 做了一個實驗,答案是肯定的,呼叫prepare_to_wait後,核心立刻進入睡眠狀態,只有在其他地方呼叫 wake_up_interruptible 後才會通知它醒來。。。而且 不必再每次 prepare_to_wait醒來後都呼叫 finish_wait ,只需要最後呼叫一次就可以了,因為prepare_to_wait 的內部會做檢查,發現該元素不在頭連結串列上時,才會新增該元素到頭連結串列。

在看看 wait_event_interruptible 的程式碼:

#define __wait_event_interruptible(wq, condition, ret)            \
do {                                    \
    DEFINE_WAIT(__wait);                        \
                                    \
    for (;;) {                            \
        prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE);    \
        if (condition)                        \
            break;                        \
        if (!signal_pending(current)) {                \
            schedule();                    \
            continue;                    \
        }                            \
        ret = -ERESTARTSYS;                    \
        break;                            \
    }                                \
    finish_wait(&wq, &__wait);                    \
} while (0)

原來這個函式對手動睡眠的過程進行了封裝,所以呼叫的時候只用到工作佇列(實際就是等待佇列)頭,它內部自己封裝了一個等待元素。。

現在看來,linux 核心設計與實現中,對工作佇列可以睡眠的說法是比較模糊的,工作佇列上的回撥函式是不能睡眠的。工作佇列本身就是一種等待佇列,佇列是可以睡眠的,但是工作佇列的上任務回撥函式,看來是不能睡眠的。 今天先睡了,後面還要進一步分析看看。

今天在網上查了下相關的東西,有個傢伙寫得不錯:“使用核心提供的共享列隊,列隊是保持順序執行的,做完一個工作才做下一個,如果一個工作內有耗時大的處理如阻塞等待訊號或鎖,那麼後面的工作都不會執行。如果你不喜歡排隊或不好意思讓別人等太久,那麼可以建立自己的工作者執行緒,所有工作可以加入自己建立的工作列隊,列隊中的工作執行在建立的工作者執行緒中。”

問題可能就是出在上面了,如果我使用了核心提供的共享佇列,可想而知,如果我進入了睡眠或者阻塞,核心中肯定有其他的工作也在這個共享佇列上執行,此時便會阻塞核心的某些工作,當然系統就看起來卡死一樣了。這樣說,如果我建立自己的工作佇列,然後在自己的工作佇列上掛起,那樣就不會出現卡死現象了。做了下試驗,果然是這樣。

看來,紙上得來總覺淺,深知此事要恭行。linux 核心設計與實現這本書是比較簡潔的,作者只告訴我們,利用工作佇列甚至可以睡眠,但是他沒有強調:“最好不要在系統提供的共享佇列上進行睡眠,如果自己的工作是非阻塞的,可以就近利用預設的共享佇列。但是如果自己的工作需要睡眠或者阻塞,此時萬萬不可使用系統提供的預設共享佇列,否則會導致核心中一部分關鍵工作得不到執行,而陷入系統卡死的狀態。

這是一個坑,如果不小心處理,會導致系統掛起。
轉自:linux 工作佇列上睡眠的認識--不要在預設共享佇列上睡眠_槍與玫瑰的專欄-CSDN部落格