在linux 0.11中,當程序嘗試訪問乙個邊界資料時,有可能由於資源已經被占用而進入睡眠狀態。當資源被釋放後,就需要把睡眠的程序喚醒。
我們先來看乙個包括睡眠和喚醒步驟的實際情況:
1. 程序a在訪問硬碟某區塊bh時發現這一塊並不在告訴快取中,進而發起請求讀取硬碟(此時不一定會立即排程)。
2. 隨後程序排程執行程序b(這裡存在兩種前提,一種是a自己必須要等待該資源bh從而主動掛起,這時a會把自己掛在該資源bh的等待佇列中,此時a程序的喚醒可以參考後續討論;另一種是a程序的時間片為0,則a暫時被系統掛起,那麼再執行a的情況可能非常複雜,這裡不再討論),程序b同樣要使用該資源bh,發現該資源被鎖定後呼叫sleep_on()進行睡眠,並繼續排程並執行c。此時隱式等待隊列為*p=b(p=&bh->b_wait,即p指向資源bh的等待佇列b_wait)
3. c也要訪問該資源,睡眠後再排程執行d。此時隱式等待隊列為*p=c, c->tmp=b
4. d同樣要訪問該資源,睡眠後再排程其他程序。這時,組成了乙個等待佇列*p=d, d->tmp=c, c->tmp=b
5. 當硬碟資源請求結束後,cpu呼叫end_request()函式,其中的wake_up()就開始喚醒p指向的頭程序,即程序d。但此時是在當前執行程序的中斷過程中,並不會直接排程執行d,而是會返回正在執行的程序。
6. 過了一段時間後程序排程到d時,d會從掛起的地方,也就是從sleep_on()呼叫schedule()返回後的地方開始執行,然後,在d程序執行到sleep_on()的164行時,會喚醒c程序。同樣,此時不會立即排程執行c。
7. 又過了一段時間,程序排程到c時,c會從掛起的地方,同樣是sleep_on()呼叫schedule()返回後的地方開始執行。同樣,c會在執行到sleep_on()的164行時,喚醒b程序。同樣此時不會立即排程執行b。至此,所有之前睡眠的程序都已經被喚醒(如果考慮步驟2中兩種前提中的第一種,此時還有a程序在睡眠狀態,而a程序的喚醒要在b程序被排程執行時完成)。
下面考慮乙個更複雜的情況:
如果在步驟5和6之間,有其他程序修改該資源,導致該資源又被鎖定。然後執行步驟6,那麼此時的等待佇列是如何排列的呢?
在《linux核心完全注釋》中,作者說linux 0.11在sleep_on()和wake_up()中各有一些錯誤,在原**的基礎上,wake_up()需要刪除一行***p=null
,sleep_on()需要加入一行***p=tmp
。
按照這種改法,在sleep_on()退出後,等待佇列就變為*p=c, c->tmp=b。而且在呼叫sleep_on()時,都有用while迴圈檢查鎖定標誌,發現被鎖定後,繼續呼叫sleep_on(),會重新睡眠程序d,把其加入等待佇列的頭部,此時等待佇列看似和步驟4中的一致,同樣都是*p=d, d->tmp=c, c->tmp=b,但在schedule()之前,c是被喚醒的狀態,這點和步驟4大不一樣,後續有可能先排程c。假如c被排程,那麼就會喚醒b,但如果此時資源依舊被鎖定,那麼c會重新加入等待佇列,佇列就變成了*p=c, c->tmp=d, d->tmp=c, 此時c就在佇列中出現了2次。
而我認為作者所說的改動沒有必要,而且wake_up()中的***p=null
是整套機制中乙個很關鍵的地方。
在步驟5中通過wake_up()喚醒程序d時,*p=null
清空等待佇列。這樣後續步驟中,等待隊列為空,也就不需要在程序喚醒時把*p指向當前程序的tmp,即下乙個需要喚醒的程序。而且程序d,c,b之間依舊保持一種隱性的級聯關係,即d->tmp=c, c->tmp=b,喚醒的順序依舊不變。
就算在步驟6之前,有人鎖定了資源bh,那麼在d喚醒後重新檢查資源的鎖定標誌,發現被鎖定後重新呼叫sleep_on()睡眠程序d,此時就變成了*p=d, d->tmp=null。而該邏輯不影響c,即c已經在就緒態,很可能被先排程。在排程執行c時,發現該資源被鎖定,也會呼叫sleep_on()睡眠,此時*p=c, c->tmp=d, d->tmp=null。這樣c程序就不會像之前那樣在等待佇列中出現2次。
而且,按照這樣的邏輯,如果b被排程時同樣發現資源被鎖定,那麼等待佇列就變為*p=b, b->tmp=c, c->tmp=d, d->tmp=null,整個等待佇列和步驟4中的相比完全翻轉了。
根據上述討論,可以看到整套睡眠和喚醒機制中的關鍵如下:
1. 睡眠隱性佇列
2. 隱性佇列順序反轉
3. 喚醒時清空等待佇列,卻不影響喚醒順序
4. 程序重新執行時仍需檢視鎖定標誌
本文目前只討論了sleep_on()和wake_up()函式,暫未考慮linux 0.11中的函式interruptible_sleep_on()。剛在閱讀oldlinux論壇中的帖子時,看到這一篇
中關於sleep_on()和wake_up()的討論,討論的實際情況和我說的稍有不同,不過同樣也是支援linus原**的設計。
其中給出了乙個鏈結在這個帖子裡,樓主和管理員討論了interruptible_sleep_on()中的喚醒邏輯bug。我看完後贊同管理員的邏輯。
但是除去interruptible_sleep_on()的bug不說,如果只討論sleep_on()和wake_up(),我認為還是linus的原**是正確的。
linux0 11程序睡眠喚醒原理分析
程序的睡眠是通過呼叫sleep on函式,該函式修改了程序的狀態並且通過schedule函式切換到其他程序執行,從而實現程序的掛起,task uninterruptible狀態的程序只能被wake up函式喚醒。task interruptible狀態的程序可以被wake up和訊號喚醒。喚醒的時候...
linux0 11程序排程分析
10ms時鐘中斷 時鐘中斷函式timer interrupt,將jiffier加1 呼叫do timer函式,將當前程序counter計數減1,如果 counter大於0,則返回繼續執行該任務,否則 呼叫schedule函式,如下 void schedule void if p signal blo...
Linux0 11核心 fork 函式建立程序
用fork建立程序 除了程序0,其它所有的程序都是fork產生的。子程序是通過複製父程序的資料和 產生的。建立結束後,子程序和父程序的 段 資料段共享。但是子程序有自己的程序控制塊 核心堆疊和頁表。我們知道乙個程序需要有如下3個結構 1 task陣列中的一項,即程序控制塊 task struct 2...