- 1. 等待隊列數據結構
等待隊列由雙向鏈表實現,其元素包括指向進程描述符的指針。每個等待隊列都有一個等待隊列頭(wait queue head),等待隊列頭是一個類型為wait_queque_head_t的數據結構:
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
其中,lock是用來防止並發訪問,task_list字段是等待進程鏈表的頭。
等待隊列鏈表中的元素類型為wait_queue_t,我們可以稱之為等待隊列項:
struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01
void *private;
wait_queue_func_t func;
struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;
每一個等待隊列項代表一個睡眠進程,該進程等待某一事件的發生。它的描述符地址通常放在private字段中。Task_list字段中包含的是指針,由這個指針把一個元素鏈接到等待相同事件的進程鏈表中。
等待隊列元素的func字段用來表示等待隊列中睡眠進程應該用什么方式喚醒(互斥方式和非互斥方式)。
整個等待隊列的結構如下圖所示:
下面看看等待隊列的工作原理。
- 2. 等待隊列的睡眠過程
使用等待隊列前通常先定義一個等待隊列頭:static wait_queue_head_t wq ,然后調用wait_event_*函數將等待某條件condition的當前進程插入到等待隊列wq中並睡眠,一直等到condition條件滿足后,內核再將睡眠在等待隊列wq上的某一進程或所有進程喚醒。
定義等待隊列頭沒什么好講的,下面從調用wait_event_*開始分析:
這里我們舉比較常用的wait_event_interruptible:
/**
* wait_event_interruptible - sleep until a condition gets true
* @wq: the waitqueue to wait on
* @condition: a C expression for the event to wait for
*
* The process is put to sleep (TASK_INTERRUPTIBLE) until the
* @condition evaluates to true or a signal is received.
* The @condition is checked each time the waitqueue @wq is woken up.
*
* wake_up() has to be called after changing any variable that could
* change the result of the wait condition.
*
* The function will return -ERESTARTSYS if it was interrupted by a
* signal and 0 if @condition evaluated to true.
*/
#define wait_event_interruptible(wq, condition) \
({ \
int __ret = 0; \
if (!(condition)) \
__wait_event_interruptible(wq, condition, __ret); \
__ret; \
})
這里很簡單,判斷一下condition條件是否滿足,如果不滿足則調用__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)
__wait_event_interruptible首先定義了一個wait_queue_t類型的等待隊列項__wait :
#define DEFINE_WAIT(name) \
wait_queue_t name = { \
.private = current, \
.func = autoremove_wake_function, \
.task_list = LIST_HEAD_INIT((name).task_list), \
}
可以發現,這里__wait的private成員(通常用來存放進程的描述符)已經被初始化為current, 表示該等待隊列項對應為當前進程。func成員為該等待隊列項對應的喚醒函數,該進程被喚醒后會執行它,已經被初始化為默認的autoremove_wake_function函數。
然后在一個for (;;) 循環內調用prepare_to_wait函數:
void fastcall prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
unsigned long flags;
wait->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
if (list_empty(&wait->task_list))
__add_wait_queue(q, wait);
/*
* don't alter the task state if this is just going to
* queue an async wait queue callback
*/
if (is_sync_wait(wait))
set_current_state(state);
spin_unlock_irqrestore(&q->lock, flags);
}
prepare_to_wait做如下兩件事,將先前定義的等待隊列項__wait插入到等待隊列頭wq,然后將當前進程設為TASK_INTERRUPTIBLE狀態。prepare_to_wait執行完后立馬再檢查一下condition有沒有滿足,如果此時碰巧滿足了則不必要在睡眠了。如果還沒有滿足,則准備睡眠。
睡眠是通過調用schedule()函數實現的,由於之前已經將當前進程設置為TASK_INTERRUPTIBLE狀態,因而這里再執行schedule()進行進程切換的話,之后就永遠不會再調度到該進程運行的,直到該進程被喚醒(即更改為TASK_RUNNING狀態)。
這里在執行schedule()切換進程前會先判斷一下有沒signal過來,如果有則立即返回ERESTARTSYS。沒有的話則執行schedule()睡眠去了。
for (;;) 循環的作用是讓進程被喚醒后再一次去檢查一下condition是否滿足。主要是為了防止等待隊列上的多個進程被同時喚醒后有可能其他進程已經搶先把資源占有過去造成資源又變為不可用,因此最好再判斷一下。(當然,內核也提供了僅喚醒一個或多個進程(獨占等待進程)的方式,有興趣的可以參考相關資料)
進程被喚醒后最后一步是調用finish_wait(&wq, &__wait)函數進行清理工作。finish_wait將進程的狀態再次設為TASK_RUNNING並從等待隊列中刪除該進程。
void fastcall finish_wait(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
__set_current_state(TASK_RUNNING);
if (!list_empty_careful(&wait->task_list)) {
spin_lock_irqsave(&q->lock, flags);
list_del_init(&wait->task_list);
spin_unlock_irqrestore(&q->lock, flags);
}
}
再往后就是返回你先前調用wait_event_interruptible(wq, condition)被阻塞的地方繼續往下執行。
- 3. 等待隊列的喚醒過程
直到這里我們明白等待隊列是如何睡眠的,下面我們分析等待隊列的喚醒過程。
使用等待隊列有個前提,必須得有人喚醒它,如果沒人喚醒它,那么同眠在該等待隊列上的所有進程豈不是變成“僵屍進程”了。
對於設備驅動來講,通常是在中斷處理函數內喚醒該設備的等待隊列。驅動程序通常會提供一組自己的讀寫等待隊列以實現上層(user level)所需的BLOCK和O_NONBLOCK操作。當設備資源可用時,如果驅動發現有進程睡眠在自己的讀寫等待隊列上便會喚醒該等待隊列。
喚醒一個等待隊列是通過wake_up_*函數實現的。這里我們舉對應的wake_up_interruptible作為例子分析。定義如下:
#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
這里的參數x即要喚醒的等待隊列對應的等待隊列頭。喚醒TASK_INTERRUPTIBLE類型的進程並且默認喚醒該隊列上所有非獨占等待進程和一個獨占等待進程。
__wake_up定義如下:
/**
* __wake_up - wake up threads blocked on a waitqueue.
* @q: the waitqueue
* @mode: which threads
* @nr_exclusive: how many wake-one or wake-many threads to wake up
* @key: is directly passed to the wakeup function
*/
void fastcall __wake_up(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, void *key)
{
unsigned long flags;
spin_lock_irqsave(&q->lock, flags);
__wake_up_common(q, mode, nr_exclusive, 1, key);
spin_unlock_irqrestore(&q->lock, flags);
preempt_check_resched_delayed();
}
__wake_up 簡單的調用__wake_up_common進行實際喚醒工作。
__wake_up_common定義如下:
/*
* The core wakeup function. Non-exclusive wakeups (nr_exclusive == 0) just
* wake everything up. If it's an exclusive wakeup (nr_exclusive == small +ve
* number) then we wake all the non-exclusive tasks and one exclusive task.
*
* There are circumstances in which we can try to wake a task which has already
* started to run but is not in state TASK_RUNNING. try_to_wake_up() returns
* zero in this (rare) case, and we handle it by continuing to scan the queue.
*/
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, int sync, void *key)
{
struct list_head *tmp, *next;
list_for_each_safe(tmp, next, &q->task_list) {
wait_queue_t *curr = list_entry(tmp, wait_queue_t, task_list);
unsigned flags = curr->flags;
if (curr->func(curr, mode, sync, key) &&
(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
__wake_up_common循環遍歷等待隊列內的所有元素,分別執行其對應的喚醒函數。
這里的喚醒函數即先前定義等待隊列項DEFINE_WAIT(__wait)時默認初始化的autoremove_wake_function函數。autoremove_wake_function最終會調用try_to_wake_up函數將進程置為TASK_RUNNING狀態。這樣后面的進程調度便會調度到該進程,從而喚醒該進程繼續執行。
==================================================================================================
在軟件開發中任務經常由於某種條件沒有得到滿足而不得不進入睡眠狀態,然后等待條件得到滿足的時候再繼續運行,進入運行狀態。這種需求需要等待隊列機制的支持。Linux中提供了等待隊列的機制,該機制在內核中應用很廣泛。
在Linux內核中使用等待隊列的過程很簡單,首先定義一個wait_queue_head,然后如果一個task想等待某種事件,那么調用wait_event(等待隊列,事件)就可以了。
等待隊列應用廣泛,但是內核實現卻十分簡單。其涉及到兩個比較重要的數據結構:__wait_queue_head,該結構描述了等待隊列的鏈頭,其包含一個鏈表和一個原子鎖,結構定義如下:
struct __wait_queue_head {
spinlock_t lock; /* 保護等待隊列的原子鎖 */
struct list_head task_list; /* 等待隊列 */
};
__wait_queue,該結構是對一個等待任務的抽象。每個等待任務都會抽象成一個wait_queue,並且掛載到wait_queue_head上。該結構定義如下:
struct __wait_queue {
unsigned int flags;
void *private; /* 通常指向當前任務控制塊 */
/* 任務喚醒操作方法,該方法在內核中提供,通常為autoremove_wake_function */
wait_queue_func_t func;
struct list_head task_list; /* 掛入wait_queue_head的掛載點 */
};
Linux中等待隊列的實現思想如下圖所示,當一個任務需要在某個wait_queue_head上睡眠時,將自己的進程控制塊信息封裝到wait_queue中,然后掛載到wait_queue的鏈表中,執行調度睡眠。當某些事件發生后,另一個任務(進程)會喚醒wait_queue_head上的某個或者所有任務,喚醒工作也就是將等待隊列中的任務設置為可調度的狀態,並且從隊列中刪除。
使用等待隊列時首先需要定義一個wait_queue_head,這可以通過DECLARE_WAIT_QUEUE_HEAD宏來完成,這是靜態定義的方法。該宏會定義一個wait_queue_head,並且初始化結構中的鎖以及等待隊列。當然,動態初始化的方法也很簡單,初始化一下鎖及隊列就可以了。
一個任務需要等待某一事件的發生時,通常調用wait_event,該函數會定義一個wait_queue,描述等待任務,並且用當前的進程描述塊初始化wait_queue,然后將wait_queue加入到wait_queue_head中。函數實現流程說明如下:
1、用當前的進程描述塊(PCB)初始化一個wait_queue描述的等待任務。
2、在等待隊列鎖資源的保護下,將等待任務加入等待隊列。
3、判斷等待條件是否滿足,如果滿足,那么將等待任務從隊列中移出,退出函數。
4、 如果條件不滿足,那么任務調度,將CPU資源交與其它任務。
5、 當睡眠任務被喚醒之后,需要重復(2)、(3)步驟,如果確認條件滿足,退出等待事件函數。
等待隊列編程接口
序號 |
編程接口 |
使用說明 |
1 |
wait_event |
這是一個宏,讓當前任務處於等待事件狀態。輸入參數如下:
@wq:等待隊列
@conditions:等待條件 |
2 |
wait_event_timeout |
功能與wait_event類似,多了一個超時機制。參數中多了一項超時時間。 |
3 |
wait_event_interruptible |
這是一個宏,與前兩個宏相比,該宏定義的等待能夠被消息喚醒。如果被消息喚醒,那么返回- ERESTARTSYS。輸入參數如下:
@wq:等待隊列
@condition:等待條件
@rt:返回值 |
4 |
wait_event_interruptible_timeout |
與(3)相比,多了超時機制 |
5 |
wake_up |
喚醒等待隊列中的一個任務 |
6 |
wake_up_all |
喚醒等待隊列中的所有任務
|