在前面的文件 I/O 文章中,我們有提到 Linux 文件 I/O 支持阻塞和非阻塞的數據讀取方式,當采用阻塞方式進行 I/O 時,進程將會阻塞在read()
或者write()
系統調用上,直到文件可讀或者是內核緩沖區可寫。這些阻塞與喚醒的實現與內核調度緊密相關,Linux 內核使用等待隊列和完成量來實現該功能。
注: 本篇文章所用Linux內核源碼版本為v5.8
1. 進程狀態有限狀態機
進程並不總是可以立即運行的,一方面是 CPU 資源有限,另一方面則是進程時常需要等待外部事件的發生,例如 I/O 事件、定時器事件等。
因此,對進程的狀態進行分類就是一件非常有必要的事情,對於等待某事件發生的進程給予 CPU 資源是沒有任何意義的,因為此時事件可能仍未發生。而對於正等待 CPU 資源的進程而言,在得到 CPU 之后即可立即執行。調度器為了盡可能最大地使用硬件資源,通常會將進程分為3個主要的狀態: 運行、等待和睡眠。
處於運行狀態的進程正在使用 CPU 等資源,從上圖中可以看到,運行態的進程在執行完任務后結束,進入到結束狀態。當 CPU 時間片到期之后,調度器選擇其它進程執行,此時將進入等待狀態。同時,當運行時的進程發起 I/O 操作,或者等待其它事件的發生時,將進入睡眠狀態。
處於等待狀態的進程由於缺少 CPU 資源而被迫停止運行,只要調度器下次選中該進程即可立即執行,由等待狀態轉變為運行狀態。
處於睡眠狀態的進程在等待外部事件的發生,例如 I/O 操作的數據抵達,創建的定時器到期等等,處於睡眠狀態的進程永遠不會被調度器進行選擇並執行。當期望的事件到達后,進程由睡眠狀態更改為等待狀態,等待調度器的下一次選擇。
處於等待的進程將會被放置於就緒隊列中(紅黑樹實現),而處於睡眠狀態的進程則放置於等待隊列(雙鏈表實現)中。調度器的目光主要放在就緒隊列上,從該隊列中取出下一個將要執行的進程,而等待隊列和就緒隊列中的進程會因為事件的發生而進行相互轉移。
在實際的內核實現中,進程的運行狀態表示要比上文所述更加詳細一些,進程狀態定義於include/linux/sched.h
:
TASK_RUNNING
,可運行狀態。此時進程並不一定正在運行,一旦得到調度器的調度即可立即運行。TASK_INTERRUPTIBLE
,可中斷睡眠狀態。此時進程因為等待外部事件的發生而睡眠,此時可由信號或者是內核喚醒。TASK_UNINTERRUPTIBLE
,不可中斷睡眠狀態。和TASK_INTERRUPTIBLE
狀態類似,等待外部事件發生的睡眠狀態。不同的是改狀態只能由內核親自喚醒,不能由信號喚醒,通常用於進程必須等待某件工作完成,不能被 Kill。
除了這三個核心進程狀態以外,還有__TASK_STOPPED
、__TASK_TRACED
等狀態,由於這些狀態在本文中並不重要,所以略去。
2. 等待隊列
等待隊列相關的源碼位於include/linux/wait.h
以及kernel/sched/wait.c
文件中,頭文件中定義了等待隊列以及隊列元素的基本數據結構,wait.c
源文件則主要包含具體的方法實現。
首先來看等待隊列的基本結構,分為隊列頭和隊列項:
/* 等待隊列頭 */ struct wait_queue_head { spinlock_t lock; /* 自旋鎖 */ struct list_head head; /* previous、next指針 */ }; typedef struct wait_queue_head wait_queue_head_t; /* 等待隊列元素 */ struct wait_queue_entry { unsigned int flags; /* 標識位 */ void *private; /* 通常指向等待進程 */ wait_queue_func_t func; /* 喚醒函數 */ struct list_head entry; /* previous、next指針 */ };
在內核的鏈表實現中,絕大多數的鏈表均為循環雙鏈表,等待隊列也不例外。因為等待隊列可能會在系統中斷時進行修改,所以必須要添加互斥鎖機制保護隊列元素。
等待隊列元素的設計也非常簡潔,除了雙鏈表必要的前后指針以外,僅包含一個指向等待進程task_struct
實例的指針,一個喚醒函數和一個標識位。
喚醒函數通常由調度器實現,如kernel/sched/core.c
中定義的try_to_wake_up
方法,可以簡單的認為喚醒函數就是將進程的狀態由TASK_INTERRUPTIBLE
或TASK_UNINTERRUPTIBLE
修改為TASK_RUNNING
,並將其加入至就緒隊列中。
wait.h
中提供了一系列與等待隊列相關的宏定義供外部使用,例如wait_event
,本質上是對等待隊列的進一步封裝:
#define wait_event(wq_head, condition) \ do { \ might_sleep(); \ if (condition) \ break; \ /* 這里將原有的__wait_event宏展開,使用___wait_event代替 */ \ ___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0, schedule()) \ } while (0)
其中wq_head
即wait_queue_head
,condition
則是一個C語言表達式,表示一個等待條件。宏定義的wait_event
使得使用標准C表達式指定條件成為可能,如果使用函數實現的話,無法做到如宏實現的靈活性。注意到在調用___wait_event
之前會首先檢查一遍條件是否滿足,避免進行無效的睡眠。
在___wait_event
宏定義中傳入的進程狀態為TASK_UNINTERRUPTIBLE
,也就是說,wait_event
實現的事件等待是不可中斷的。當然,wait.h
中同樣提供了其它時間等待實現:
wait_event_timeout
: 帶有超時時間的不可中斷事件等待wait_event_interruptible
: 可中斷的事件等待wait_event_interruptible_timeout
: 帶有超時時間的可中斷事件等待
最后再來看___wait_event
實現,該方法將會把當前進程包裝成wait_queue_entry
對象,並發安全地放置於等待隊列中,並且在實際的讓出CPU資源、引發調度器重新調度之前會再一次的檢查等待事件是否發生,避免無效睡眠。由於源代碼中該方法宏定義實現符號較多,所以將原實現抽象成偽代碼:
___wait_event(wq_head, condition, state, exclusive, ret, cmd) { /* 初始化隊列元素 */ init_wait_entry(...); for (;;) { /* 將隊列元素插入至等待隊列中(線程安全) */ long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state); /* 檢查事件條件是否滿足 */ if (condition) break; /* 觸發調度器重新調度 */ schedule(); }
對於喚醒一個進程在前文中已經描述過了,通用方法為wake_up()
,本質上會調用內核調度模塊中的try_to_wake_up()
來喚醒某個進程,喚醒的實質是將進程狀態修改為TASK_RUNNING
,從等待隊列中移出並加入至就緒隊列中。
轉載:
https://smartkeyerror.com/Linux-Blocking