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

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

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

  在驅動中很容易經過調度等方式使當前進程睡眠,可是進程並非在任什麼時候候都是能夠進入睡眠狀態的。 linux

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

    在關中斷中也不能睡眠。函數

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

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

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

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。線程

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

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

相關文章
相關標籤/搜索