1. 程式人生 > >Unix環境高級編程-阻塞訪問原理——等待隊列

Unix環境高級編程-阻塞訪問原理——等待隊列

其他 flag 成了 隊列 內核 調用 int 要求 得到

  有些時候,一個系統調用可能無法馬上取到或者送出數據:一個溫度采集器如果沒有采用中斷或者輪詢的策略,而是在用戶發出請求時才進行采集,並在一定的時間後返回結果。如果用戶程序希望調用read或write並且在調用返回時能確保得到想要的結果,那麽用戶程序應該阻塞,直到有結果或者錯誤後返回,用戶程序的阻塞體現為進程的睡眠,也即系統調用中將進程狀態切換為睡眠態。   睡眠和等待隊列

  一個進程的睡眠意味著它的進程狀態標識符被置為睡眠,並且從調度器的運行隊列中去除,直到某些事件的發生將它們從睡眠態中喚醒,在睡眠態,該進程將不被CPU調度,並且,如果不被喚醒,它將永遠不被運行。

  在驅動中很容易通過調度等方式使當前進程睡眠,但是進程並不是在任何時候都是可以進入睡眠狀態的。

    第一條規則是:當運行在原子上下文時不能睡眠:比如持有自旋鎖,順序鎖或者RCU鎖。

    在關中斷中也不能睡眠。

    持有信號量時睡眠是合法的,但它所持有的信號量不應該影響喚醒它的進程的執行。另外任何等待該信號量的線程也將睡眠,因此發生在持有信號量時的任何睡眠都應當短暫。

    進程醒來後應該進行等待事件的檢查,以確保它確實發生了。

  等待隊列可以完成進程的睡眠並在事件發生時喚醒它,它由一個進程列表組成。在 Linux 中, 一個等待隊列由一個"等待隊列頭"來管理:

linux/wait.h
struct __wait_queue_head {
    spinlock_t 
lock; struct list_head task_list; }; typedef struct __wait_queue_head wait_queue_head_t;

  由於睡眠的進程很有可能在等待一個中斷來改變某些狀態,或通告某些事件的發生,那麽中斷上下文很有可能修改該等待隊列,所以該結構中的自旋鎖lock必須考慮禁中斷,也即使用spin_lock_irqsave。

  隊列中的成員是如下數據結構的實例,它們組成了一個雙向鏈表:

技術分享圖片

typedef struct __wait_queue wait_queue_t;
typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int
flags, void *key); int default_wake_function(wait_queue_t *wait, unsigned mode, int flags, void *key); struct __wait_queue { unsigned int flags; #define WQ_FLAG_EXCLUSIVE 0x01 void *private; wait_queue_func_t func; struct list_head task_list; };

flags的值或者為0,或者為WQ_FLAG_EXCLUSIVE。後者表示等待進程想要被獨占地喚醒。 
private指針指向等待進程的task_struct實例。該變量本質上可以指向任何私有數據,單內核只有很少情況下才這麽用。 
調用func,喚醒等待進程。 
task_list用作一個鏈表元素,將wait_queue_t實例放置到等待隊列中。

  為了使用等待隊列,通常需要如下步驟:首先應該建立一個等待隊列頭:

DECLARE_WAIT_QUEUE_HEAD(name);

  另外一種方法是靜態聲明,並顯式初始化它:

wait_queue_head_t wait_queue;
init_waitqueue_head(&wait_queue);

  接著為使得當前進程進入睡眠,並等待某一事件的發生,需要將它加入到等待隊列中,內核提供了以下函數完成此功能:

wait_event(queue, condition);
wait_event_interruptible(queue, condition);
wait_event_timeout(queue, condition, timeout);
wait_event_interruptible_timeout(queue, condition, timeout);

  在所有的形式中,參數queue是要等待的隊列頭,由於這幾個函數都是通過宏實現的,這裏的隊列頭不是指針類型,而是對它的直接使用。條件condition是一個被這些宏在睡眠前後所要求值的任意的布爾表達式。直到條件求值為真,進程持續睡眠。

  通過wait_event進入睡眠的進程是不可中斷的,此時進程的state成員置TASK_UNINTERRUPTIBLE位。但是它應該被wait_event_interruptible所替代,它可以被信號中斷,這意味著用戶程序在等待的過程中可以通過信號中斷程序的執行。一個不能被信號中斷的程序很容易激怒使用它的用戶。wait_event函數沒有返回值,而wait_event_interruptible有一個可以識別睡眠被某些信號打斷的返回值-ERESTARTSYS。

  wait_event_timeout和wait_event_interruptible_timeout意味著等待一段時間,它以滴答數表示,在這個時間期間超時後,該宏返回一個0值,而不管事件是否發生。

  最後,我們需要在其他進程或者線程(也可能是中斷)中通過相對應的函數,喚醒這些隊列上沈睡的進程。內核提供了如下函數:

void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
wake_up喚醒所有的在給定隊列上等待的進程。 
wake_up_interruptible喚醒所有的在給定隊列上等待的可中斷的睡眠的進程。

  盡管wake_up可以替代wake_up_interruptible的功能,但是它們應該使用與wait_event對應的函數。通過等待隊列實現一個管道的讀寫是可行的,內核中fs/pipe.c對管道的實現就是基於等待隊列實現的,盡管它有些復雜。另外對於設備驅動來說,一個溫度采集器在收到讀數據請求後,該進程被放入等待隊列,然後喚醒它的布爾變量在該設備對應的中斷處理程序中被置為真。

  註意 wake_up_interruptible的調用可能使多個個睡眠進程醒來,而它們又是獨占訪問某一資源,如何使僅一個進程看到這個真值,這就是WQ_FLAG_EXCLUSIVE的作用,其他進程將繼續睡眠。

  等待隊列實現原理

  wait_event函數的核心實現如下:

#define __wait_event(wq, condition)                     do {                                        DEFINE_WAIT(__wait);                                                                for (;;) {                                    prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE);            if (condition)                                    break;                                schedule();                            }                                    finish_wait(&wq, &__wait);                    } while (0) 
DEFINE_WAIT註冊了一個名為__wait的隊列元素,其中包含一個名為autoremove_wake_function的鉤子函數,它用來喚醒的進程並將該元素從等待隊列中刪除。 
prepare_to_wait用來將隊列元素計入等待隊列,並指定進程的state狀態標識為TASK_UNINTERRUPTIBLE,當然對應wait_event_interruptible,則是TASK_INTERRUPTIBLE。 
for無限循環決定了當前進程在不滿足condition時總是被調度,其他進程將替換該進程執行。並且這個循環實際上永遠只執行一次,並且只在喚醒時直接 
在滿足條件時,finish_wait將進程狀態設置為TASK_RUNNING,並從等待隊列中將其移除。

  需要仔細考慮的是for循環的執行,顯然它可能執行一次,也可能是多次,當condition不滿足時,將會產生調度,而在此被調度時,將執行for的下一次循環,那麽prepare_to_wait不是每次都添加一次__wait元素嗎?查看prepare_to_wait代碼可以發現,只有wait->task_list指向的鏈表為空時,也即__wait元素沒有加入任何其他等待隊列時才會把它加入到當前等待隊列中,這也表明一個等待隊列元素只能加入一個等待隊列。

void 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);
    set_current_state(state);
    spin_unlock_irqrestore(&q->lock, flags);
}

  喚醒一個等待隊列是通過wake_up系列函數完成的,一些列的喚醒函數都有對應的可中斷形式:

#define wake_up(x)            __wake_up(x, TASK_NORMAL, 1, NULL)
#define wake_up_nr(x, nr)        __wake_up(x, TASK_NORMAL, nr, NULL)
#define wake_up_all(x)            __wake_up(x, TASK_NORMAL, 0, NULL)
#define wake_up_locked(x)        __wake_up_locked((x), TASK_NORMAL)

  這裏分析它們的核心實現:

kernel/sched.c
void __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, 0, key);
    spin_unlock_irqrestore(&q->lock, flags);
}

  __wake_up首先獲取了自旋鎖,然後調用__wake_up_common。該函數通過list_for_each_entry_safe遍歷等待隊列,如果沒有設置獨占標誌,則根據mode喚醒每個睡眠的進程。nr_exclusiv表示需要喚醒的設置了獨占標誌進程的數目,它在wake_up中設置為1,表明當處理了一個含有WQ_FLAG_EXCLUSIVE標誌進程後,將不再處理,獨占標誌的意義也在於此。另外看到這裏通過func指針執行了真正的喚醒函數。

kernel/sched.c
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
            int nr_exclusive, int wake_flags, 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, wake_flags, key) &&
                (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
            break;
    }
}

  如果含有獨占標誌的進程並不位於隊列尾部,將導致其後的不含有該標誌的進程無法執行,prepare_to_wait_exclusive解決了該問題,它總是將含有獨占標誌的進程插入到隊列尾部,該函數被wait_event_interruptible_exclusive宏調用。

  轉自:http://blog.chinaunix.net/uid-20608849-id-3126863.html

Unix環境高級編程-阻塞訪問原理——等待隊列