【轉載】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部落格