一文讀懂Linux延時隊列工作原理
本文轉載自微信公眾號「Linux內核那些事」,作者songsong001 。轉載本文請聯系Linux內核那些事公眾號。
當進程要獲取某些資源(例如從網卡讀取數據)的時候,但資源并沒有準備好(例如網卡還沒接收到數據),這時候內核必須切換到其他進程運行,直到資源準備好再喚醒進程。
waitqueue (等待隊列) 就是內核用于管理等待資源的進程,當某個進程獲取的資源沒有準備好的時候,可以通過調用 add_wait_queue() 函數把進程添加到 waitqueue 中,然后切換到其他進程繼續執行。當資源準備好,由資源提供方通過調用 wake_up() 函數來喚醒等待的進程。
等待隊列初始化
要使用 waitqueue 首先需要聲明一個 wait_queue_head_t 結構的變量,wait_queue_head_t 結構定義如下:
- struct __wait_queue_head {
- spinlock_t lock;
- struct list_head task_list;
- };
waitqueue 本質上是一個鏈表,而 wait_queue_head_t 結構是 waitqueue 的頭部,lock 字段用于保護等待隊列在多核環境下數據被破壞,而 task_list 字段用于保存等待資源的進程列表。
可以通過調用 init_waitqueue_head() 函數來初始化 wait_queue_head_t 結構,其實現如下:
- void init_waitqueue_head(wait_queue_head_t *q)
- {
- spin_lock_init(&q->lock);
- INIT_LIST_HEAD(&q->task_list);
- }
初始化過程很簡單,首先調用 spin_lock_init() 來初始化自旋鎖 lock,然后調用 INIT_LIST_HEAD() 來初始化進程鏈表。
向等待隊列添加等待進程
要向 waitqueue 添加等待進程,首先要聲明一個 wait_queue_t 結構的變量,wait_queue_t 結構定義如下:
- typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int sync, void *key);
- struct __wait_queue {
- unsigned int flags;
- void *private;
- wait_queue_func_t func;
- struct list_head task_list;
- };
下面說明一下各個成員的作用:
flags: 可以設置為 WQ_FLAG_EXCLUSIVE,表示等待的進程應該獨占資源(解決驚群現象)。
private: 一般用于保存等待進程的進程描述符 task_struct。
func: 喚醒函數,一般設置為 default_wake_function() 函數,當然也可以設置為自定義的喚醒函數。
task_list: 用于連接其他等待資源的進程。
可以通過調用 init_waitqueue_entry() 函數來初始化 wait_queue_t 結構變量,其實現如下:
- static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p)
- {
- q->flags = 0;
- q->private = p;
- q->func = default_wake_function;
- }
也可以通過調用 init_waitqueue_func_entry() 函數來初始化為自定義的喚醒函數:
- static inline void init_waitqueue_func_entry(wait_queue_t *q, wait_queue_func_t func)
- {
- q->flags = 0;
- q->private = NULL;
- q->func = func;
- }
初始化完 wait_queue_t 結構變量后,可以通過調用 add_wait_queue() 函數把等待進程添加到等待隊列,其實現如下:
- void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
- {
- unsigned long flags;
- wait->flags &= ~WQ_FLAG_EXCLUSIVE;
- spin_lock_irqsave(&q->lock, flags);
- __add_wait_queue(q, wait);
- spin_unlock_irqrestore(&q->lock, flags);
- }
- static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new)
- {
- list_add(&new->task_list, &head->task_list);
- }
add_wait_queue() 函數的實現很簡單,首先通過調用 spin_lock_irqsave() 上鎖,然后調用 list_add() 函數把節點添加到等待隊列即可。
wait_queue_head_t 結構與 wait_queue_t 結構之間的關系如下圖:
waitqueue
休眠等待進程
當把進程添加到等待隊列后,就可以休眠當前進程,讓出CPU給其他進程運行,要休眠進程可以通過一下方式:
- set_current_state(TASK_INTERRUPTIBLE);
- schedule();
代碼 set_current_state(TASK_INTERRUPTIBLE) 可以把當前進程運行狀態設置為 可中斷休眠 狀態,調用 schedule() 函數可以使當前進程讓出CPU,切換到其他進程執行。
喚醒等待隊列
當資源準備好后,就可以喚醒等待隊列中的進程,可以通過 wake_up() 函數來喚醒等待隊列中的進程。wake_up() 最終會調用 __wake_up_common(),其實現如下:
- static void __wake_up_common(wait_queue_head_t *q,
- unsigned int mode, int nr_exclusive, int sync, void *key)
- {
- wait_queue_t *curr, *next;
- list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
- unsigned flags = curr->flags;
- if (curr->func(curr, mode, sync, key) &&
- (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
- break;
- }
- }
可以看出,喚醒等待隊列就是變量等待隊列的等待進程,然后調用喚醒函數來喚醒它們。