Linux等待隊列(Wait Queue)

1. Linux等待隊列概述

Linux內核的等待隊列(Wait Queue)是重要的數據結構,與進程調度機制緊密相關聯,能夠用來同步對系統資源的訪問、異步事件通知、跨進程通訊等。
在Linux中,等待隊列以循環鏈表爲基礎結構,包括兩種數據結構:等待隊列頭(wait queue head)等待隊列元素(wait queue),整個等待隊列由等待隊列頭進行管理。下文將用內核源碼(基於Linux kernel 5.2)對等待隊列進行介紹,詳細說明採用等待隊列實現進程阻塞和喚醒的方法。html

 

2. 等待隊列頭和等待隊列元素

等待隊列以循環鏈表爲基礎結構,鏈表頭和鏈表項分別爲等待隊列頭和等待隊列元素,分別用結構體 wait_queue_head_t 和 wait_queue_entry_t 描述(定義在 linux/wait.h )。linux

2.1 基本概念

struct wait_queue_head {
    spinlock_t          lock;
    struct list_head    head;
};

typedef struct wait_queue_head wait_queue_head_t;
typedef int (*wait_queue_func_t)(struct wait_queue_entry *wq_entry, unsigned mode, int flags, void *key);
int default_wake_function(struct wait_queue_entry *wq_entry, unsigned mode, int flags, void *key);

/* wait_queue_entry::flags */
#define WQ_FLAG_EXCLUSIVE   0x01
#define WQ_FLAG_WOKEN       0x02
#define WQ_FLAG_BOOKMARK    0x04

/*
 * A single wait-queue entry structure:
 */
struct wait_queue_entry {
    unsigned int        flags;
    void                *private;
    wait_queue_func_t   func;
    struct list_head    entry;
};

typedef struct wait_queue_entry wait_queue_entry_t;

等待隊列頭結構包括一個自旋鎖和一個鏈表頭。等待隊列元素除了包括鏈表項,還包括:git

  •  flags : 標識隊列元素狀態和屬性
  •  *private : 用於指向關聯進程 task_struct 結構體的指針
  •  func : 函數指針,用於指向等待隊列被喚醒時的回調的喚醒函數

以進程阻塞和喚醒的過程爲例,等待隊列的使用場景能夠簡述爲:
進程 A 因等待某些資源(依賴進程 B 的某些操做)而不得不進入阻塞狀態,便將當前進程加入到等待隊列 Q 中。進程 B 在一系列操做後,可通知進程 A 所需資源已到位,便調用喚醒函數 wake up 來喚醒等待隊列上 Q 的進程,注意此時全部等待在隊列 Q 上的進程均被置爲可運行狀態。
藉助上述描述場景,說明等待隊列元素屬性 flags 標誌的做用,下文也將結合源碼進行詳細解讀。數據結構

  (1) WQ_FLAG_EXCLUSIVE 異步

上述場景中看到,當某進程調用 wake up 函數喚醒等待隊列時,隊列上全部的進程均被喚醒,在某些場合會出現喚醒的全部進程中,只有某個進程得到了指望的資源,而其餘進程因爲資源被佔用不得再也不次進入休眠。若是等待隊列中進程數量龐大時,該行爲將影響系統性能。
內核增長了"獨佔等待」(WQ_FLAG_EXCLUSIVE)來解決此類問題。一個獨佔等待的行爲和一般的休眠相似,但有以下兩個重要的不一樣:函數

  • 等待隊列元素設置 WQ_FLAG_EXCLUSIVE 標誌時,會被添加到等待隊列的尾部,而非頭部。
  • 在某等待隊列上調用 wake up 時,執行獨佔等待的進程每次只會喚醒其中第一個(全部非獨佔等待進程仍會被同時喚醒)。

  (2) WQ_FLAG_WOKEN 
暫時還未理解,TODOoop

  (3) WQ_FLAG_BOOKMARK 
用於 wake_up() 喚醒等待隊列時實現分段遍歷,減小單次對自旋鎖的佔用時間。源碼分析

 

2.2 等待隊列的建立和初始化

等待隊列頭的定義和初始化有兩種方式: init_waitqueue_head(&wq_head) 和宏定義 DECLARE_WAITQUEUE(name, task) 。post

#define init_waitqueue_head(wq_head)                            \
    do {                                                        \
        static struct lock_class_key __key;                     \
        __init_waitqueue_head((wq_head), #wq_head, &__key);     \
    } while (0)

void __init_waitqueue_head(struct wait_queue_head *wq_head, const char *name, struct lock_class_key *key)
{
    spin_lock_init(&wq_head->lock);
    lockdep_set_class_and_name(&wq_head->lock, key, name);
    INIT_LIST_HEAD(&wq_head->head);
}
#define DECLARE_WAIT_QUEUE_HEAD(name)                       \
    struct wait_queue_head name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

#define __WAIT_QUEUE_HEAD_INITIALIZER(name) {               \
    .lock       = __SPIN_LOCK_UNLOCKED(name.lock),          \
    .head       = { &(name).head, &(name).head } }

 

2.3 等待隊列元素的建立和初始化

建立等待隊列元素較爲廣泛的一種方式是調用宏定義 DECLARE_WAITQUEUE(name, task) ,將定義一個名爲 name 的等待隊列元素, private 數據指向給定的關聯進程結構體 task ,喚醒函數爲 default_wake_function() 。後文介紹喚醒細節時詳細介紹喚醒函數的工做。性能

#define DECLARE_WAITQUEUE(name, tsk)                        \
    struct wait_queue_entry name = __WAITQUEUE_INITIALIZER(name, tsk)

#define __WAITQUEUE_INITIALIZER(name, tsk) {                \
    .private    = tsk,                                      \
    .func       = default_wake_function,                    \
    .entry      = { NULL, NULL } }

內核源碼中還存在其餘定義等待隊列元素的方式,調用宏定義 DEFINE_WAIT(name) 和 init_wait(&wait_queue) 。
這兩種方式都將當前進程(current)關聯到所定義的等待隊列上,喚醒函數爲 autoremove_wake_function() ,注意此函數與上述宏定義方式時不一樣(上述定義中使用 default_wake_function() )。
下文也將介紹 DEFINE_WAIT() 和 DECLARE_WAITQUEUE() 在使用場合上的不一樣。

#define DEFINE_WAIT(name)   DEFINE_WAIT_FUNC(name, autoremove_wake_function)

#define DEFINE_WAIT_FUNC(name, function)                    \
    struct wait_queue_entry name = {                        \
        .private    = current,                              \
        .func       = function,                             \
        .entry      = LIST_HEAD_INIT((name).entry),         \
    }
#define init_wait(wait)                                     \
    do {                                                    \
        (wait)->private = current;                          \
        (wait)->func = autoremove_wake_function;            \
        INIT_LIST_HEAD(&(wait)->entry);                     \
        (wait)->flags = 0;                                  \
    } while (0)

2.4 添加和移除等待隊列

內核提供了兩個函數(定義在 kernel/sched/wait.c )用於將等待隊列元素 wq_entry 添加到等待隊列 wq_head 中: add_wait_queue() 和 add_wait_queue_exclusive() 。

  •  add_wait_queue() :在等待隊列頭部添加普通的等待隊列元素(非獨佔等待,清除 WQ_FLAG_EXCLUSIVE 標誌)。
  •  add_wait_queue_exclusive() :在等待隊列尾部添加獨佔等待隊列元素(設置了 WQ_FLAG_EXCLUSIVE 標誌)。
void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    unsigned long flags;

    // 清除WQ_FLAG_EXCLUSIVE標誌
    wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
    spin_lock_irqsave(&wq_head->lock, flags);
    __add_wait_queue(wq_head, wq_entry);
    spin_unlock_irqrestore(&wq_head->lock, flags);
}   

static inline void __add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    list_add(&wq_entry->entry, &wq_head->head);
}
void add_wait_queue_exclusive(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    unsigned long flags;

    // 設置WQ_FLAG_EXCLUSIVE標誌
    wq_entry->flags |= WQ_FLAG_EXCLUSIVE;
    spin_lock_irqsave(&wq_head->lock, flags);
    __add_wait_queue_entry_tail(wq_head, wq_entry);
    spin_unlock_irqrestore(&wq_head->lock, flags);
}

static inline void __add_wait_queue_entry_tail(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    list_add_tail(&wq_entry->entry, &wq_head->head);
}

 remove_wait_queue() 函數用於將等待隊列元素 wq_entry 從等待隊列 wq_head 中移除。

void remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    unsigned long flags;

    spin_lock_irqsave(&wq_head->lock, flags);
    __remove_wait_queue(wq_head, wq_entry);
    spin_unlock_irqrestore(&wq_head->lock, flags);
}

static inline void
__remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    list_del(&wq_entry->entry);
}

添加和移除等待隊列的示意圖以下所示:

Wait_Queue

3. 等待事件

內核中提供了等待事件 wait_event() 宏(以及它的幾個變種),可用於實現簡單的進程休眠,等待直至某個條件成立,主要包括以下幾個定義:

wait_event(wq_head, condition)
wait_event_timeout(wq_head, condition, timeout) 
wait_event_interruptible(wq_head, condition)
wait_event_interruptible_timeout(wq_head, condition, timeout)
io_wait_event(wq_head, condition)

上述全部形式函數中, wq_head 是等待隊列頭(採用」值傳遞「的方式傳輸函數), condition 是任意一個布爾表達式。使用 wait_event ,進程將被置於非中斷休眠,而使用 wait_event_interruptible 時,進程能夠被信號中斷。
另外兩個版本 wait_event_timeout 和 wait_event_interruptible_timeout 會使進程只等待限定的時間(以jiffy表示,給定時間到期時,宏均會返回0,而不管 condition 爲什麼值)。

詳細介紹 wait_event() 函數的實現原理。

#define wait_event(wq_head, condition)                      \
    do {                                                    \
        might_sleep();                                      \
        // 若是condition知足,提早返回                       \
        if (condition)                                      \
           break;                                           \
        __wait_event(wq_head, condition);                   \
    } while (0)
 
#define __wait_event(wq_head, condition)                    \
     (void)___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0, schedule())


/* 定義等待隊列元素,並將元素加入到等待隊列中
 * 循環判斷等待條件condition是否知足,若條件知足,或者接收到中斷信號,等待結束,函數返回
 * 若condition知足,返回0;不然返回-ERESTARTSYS
 */
#define ___wait_event(wq_head, condition, state, exclusive, ret, cmd)       \
({                                                          \
     __label__ __out;                                       \
     struct wait_queue_entry __wq_entry;                    \
     long __ret = ret;          /* explicit shadow */       \
                                                            \
     // 初始化等待隊列元素__wq_entry,關聯當前進程,根據exclusive參數初始化屬性標誌 \
     // 喚醒函數爲autoremove_wake_function()                                        \
     init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0);    \
     // 等待事件循環                                        \
     for (;;) {                                             \
        // 若是進程可被信號中斷而且恰好有信號掛起,返回-ERESTARTSYS     \
        // 不然,將等待隊列元素加入等待隊列,而且設置進程狀態,返回0    \
        long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state);\
                                                            \
        // 當前進程讓出調度器前,判斷condition是否成立。若成立,提早結束,後續將返回0   \
        if (condition)                                      \
            break;                                          \
                                                            \
        // 當前進程讓出調度器前,判斷當前進程是否接收到中斷信號(或KILL信號)       \
        // 若是成立,將提早返回-ERESTARTSYS                 \
        if (___wait_is_interruptible(state) && __int) {     \
            __ret = __int;                                  \
            goto __out;                                     \
        }                                                   \
                                                            \
        // 此處實際執行schedule(),當前進程讓出調度器       \
        cmd;                                                \
     }                                                      \
     // 設置進程爲可運行狀態,而且將等待隊列元素從等待隊列中刪除    \
     finish_wait(&wq_head, &__wq_entry);                    \
     __out:  __ret;                                         \
})  

void init_wait_entry(struct wait_queue_entry *wq_entry, int flags) 
{
    wq_entry->flags = flags;
    wq_entry->private = current;
    wq_entry->func = autoremove_wake_function;
    INIT_LIST_HEAD(&wq_entry->entry);
}

long prepare_to_wait_event(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state)
{
    unsigned long flags;
    long ret = 0;

    spin_lock_irqsave(&wq_head->lock, flags);
    // 返回非0值條件:可被信號中斷而且確實有信號掛起
    if (signal_pending_state(state, current)) {
        // 將等待隊列元素從等待隊列中刪除,返回-ERESTARTSYS
        list_del_init(&wq_entry->entry);
        ret = -ERESTARTSYS;
    } else {
        // 判斷wq_entry->entry是否爲空,即等待隊列元素是否已經被添加到等待隊列中
        if (list_empty(&wq_entry->entry)) {
            // WQ_FLAG_EXCLUSIVE標誌被設置時,將等待隊列元素添加到等待隊列尾部(獨佔等待)
            // 不然,將等待隊列元素添加到等待隊列頭部。同2.1中對WQ_FLAG_EXCLUSIVE標誌介紹。
            if (wq_entry->flags & WQ_FLAG_EXCLUSIVE)
                __add_wait_queue_entry_tail(wq_head, wq_entry);
            else
                __add_wait_queue(wq_head, wq_entry);
        }
        // 改變當前進程的狀態
        set_current_state(state);
    }
    spin_unlock_irqrestore(&wq_head->lock, flags);

    return ret;
}

// 用state_value改變當前的進程狀態,而且執行了一次內存屏障
// 注意,只是改變了調度器處理該進程的方式,但還沒有使該進程讓出處理器
#define set_current_state(state_value)              \
    do {                            \
        WARN_ON_ONCE(is_special_task_state(state_value));\
        current->task_state_change = _THIS_IP_;     \
        smp_store_mb(current->state, (state_value));    \
    } while (0)

/*  設置進程爲可運行狀態,而且將等待隊列元素從等待隊列中刪除  */
void finish_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    unsigned long flags;
    // 將當前進程狀態改成可運行狀態(TASK_RUNNING)
    // 相似set_current_state(),差異在於未進行內存屏障
    __set_current_state(TASK_RUNNING);

    // 等待隊列元素若仍在等待隊列中,則將其刪除
    if (!list_empty_careful(&wq_entry->entry)) {
        spin_lock_irqsave(&wq_head->lock, flags);
        list_del_init(&wq_entry->entry);
        spin_unlock_irqrestore(&wq_head->lock, flags);
    }
}

 通過源碼分析能夠看到, wait_event 使進程進入非中斷休眠狀態,循環等待直至特定條件知足,不然進程繼續保持休眠狀態。

能夠簡單總結出使用等待隊列使進程休眠的通常步驟:

  • 將當前進程關聯的等待隊列元素加入到等待隊列中。 __add_wait_queue()/__add_wait_queue_entry_tail() 
  • 設置當前進程狀態(可中斷 TASK_INTERRUPTIBLE 或不可中斷 TASK_UNINTERRUPTIBLE)。 set_current_state() 
  • 判斷資源是否獲得,或是否捕獲中斷信號。
  • 進程讓出調度器,進入休眠狀態。 schedule() 
  • 資源獲得知足時,將等待隊列元素從等待隊列中移除。

 

4. 等待隊列喚醒

前文已經簡單提到, wake_up 函數可用於將等待隊列上的全部進程喚醒,和 wait_event 相對應, wake_up 函數也包括多個變體。主要包括:

wake_up(&wq_head)
wake_up_interruptible(&wq_head)
wake_up_nr(&wq_head, nr)
wake_up_interruptible_nr(&wq_head, nr)
wake_up_interruptible_all(&wq_head)

4.1 wake_up()

 wake_up() 能夠用來喚醒等待隊列上的全部進程,而 wake_up_interruptible() 只會喚醒那些執行可中斷休眠的進程。所以約定, wait_event() 和 wake_up() 搭配使用,而 wait_event_interruptible() 和 wake_up_interruptible() 搭配使用。
前文提到,對於獨佔等待的進程, wake_up() 只會喚醒第一個獨佔等待進程。 wake_up_nr() 函數提供功能,它能喚醒給定數目nr個獨佔等待進程,而不是隻有一個。

 wake_up() 函數的實現以下:

#define TASK_NORMAL         (TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE)
// 能夠看出wake_up函數將喚醒TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE的全部進程
#define wake_up(x)          __wake_up(x, TASK_NORMAL, 1, NULL)

void __wake_up(struct wait_queue_head *wq_head, unsigned int mode, int nr_exclusive, void *key)
{
    __wake_up_common_lock(wq_head, mode, nr_exclusive, 0, key);
}

static void __wake_up_common_lock(struct wait_queue_head *wq_head, unsigned int mode,
        int nr_exclusive, int wake_flags, void *key)
{
    unsigned long flags;
    wait_queue_entry_t bookmark;

    bookmark.flags = 0;
    bookmark.private = NULL;
    bookmark.func = NULL;
    INIT_LIST_HEAD(&bookmark.entry);

    // 第一次嘗試調用__wake_up_common(),若是須要進行BOOKMARK過程,bookmark.flags會被置爲WQ_FLAG_BOOKMARK
    spin_lock_irqsave(&wq_head->lock, flags);
    nr_exclusive = __wake_up_common(wq_head, mode, nr_exclusive, wake_flags, key, &bookmark);
    spin_unlock_irqrestore(&wq_head->lock, flags);

    // 若是還有須要處理的元素,那麼bookmark.flags確定置上WQ_FLAG_BOOKMARK;不然,在一個loop內便處理完成
    while (bookmark.flags & WQ_FLAG_BOOKMARK) {
        spin_lock_irqsave(&wq_head->lock, flags);
        nr_exclusive = __wake_up_common(wq_head, mode, nr_exclusive, wake_flags, key, &bookmark);
        spin_unlock_irqrestore(&wq_head->lock, flags);
    }
}

#define WAITQUEUE_WALK_BREAK_CNT 64

static int __wake_up_common(struct wait_queue_head *wq_head, unsigned int mode,
        int nr_exclusive, int wake_flags, void *key, wait_queue_entry_t *bookmark)
{
    wait_queue_entry_t *curr, *next;
    int cnt = 0;
    
    // 判斷自旋鎖已經被持有
    lockdep_assert_held(&wq_head->lock);

    // 若是bookmark元素中標誌`WQ_FLAG_BOOKMARK`已被設置,則curr被設置爲bookmark下一個元素
    // 同時將bookmark從等待隊列中刪除,bookmark->flags清零
    // 不然,curr設置爲等待隊列wq_head的第一個元素(實際上爲第一次調用__wake_up_common)
    if (bookmark && (bookmark->flags & WQ_FLAG_BOOKMARK)) {
        curr = list_next_entry(bookmark, entry);

        list_del(&bookmark->entry);
        bookmark->flags = 0;
    } else
        curr = list_first_entry(&wq_head->head, wait_queue_entry_t, entry);

    if (&curr->entry == &wq_head->head)
        return nr_exclusive;

    // 在等待隊列頭指向的鏈表上,從curr指向的元素開始依次遍歷元素
    list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {
        unsigned flags = curr->flags;
        int ret;

        // 跳過標記爲WQ_FLAG_BOOKMARK的元素,等待隊列元素被置上WQ_FLAG_BOOKMARK?
        if (flags & WQ_FLAG_BOOKMARK)
            continue;

        // 調用等待隊列元素綁定的喚醒回調函數
        // 注意,具體喚醒何種進程(TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE),做爲參數傳遞給喚醒函數處理
        // 當進程不符合喚醒條件時,ret爲0,詳見try_to_wake_up()
        ret = curr->func(curr, mode, wake_flags, key);
        if (ret < 0)
            break;

        // 若是當前等待隊列元素爲獨佔等待,而且獨佔等待個數已經等於nr_exclusive,提早退出循環
        // 如2.1所述,獨佔等待進程被加入到等待隊列的尾部,所以此時代表全部喚醒工做已經完成
        if (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
            break;
 
        // 連續喚醒的進程數目達到指定數目WAITQUEUE_WALK_BREAK_CNT(仍有進程元素須要處理),
        // 標記bookmark->flags爲WQ_FLAG_BOOKMARK,同時將下一個要處理的元素添加到bookmark做爲頭節點的鏈表尾部,並退出遍歷循環
        // 經過這種機制,實現了進程分批次喚醒,避免了等待隊列中自旋鎖被持有時間過長
        if (bookmark && (++cnt > WAITQUEUE_WALK_BREAK_CNT) &&
                (&next->entry != &wq_head->head)) {
            bookmark->flags = WQ_FLAG_BOOKMARK;
            list_add_tail(&bookmark->entry, &next->entry);
            break;
        }
    }

    return nr_exclusive;
}

 wake_up() 函數會遍歷等待隊列上的全部元素(包括TASK_INTERRUPTIBLETASK_UNINTERRUPTIBLE)),根據 nr_exclusive 參數的要求喚醒進程,同時實現了分批次喚醒工做。最終會回調等待隊列元素所綁定的喚醒函數。

前文已經提到,定義等待隊列元素時主要涉及到兩種喚醒回調函數:

  •  default_wake_function() :宏定義 DECLARE_WAITQUEUE(name, tsk) 使用的喚醒函數。
  •  autoremove_wake_function() : DEFINE_WAIT(name) , init_wait(wait) 和 wait_event() 中調用的 init_wait_entry() 使用此喚醒函數。

4.2 default_wake_function()

int default_wake_function(wait_queue_entry_t *curr, unsigned mode, int wake_flags, void *key)
{
    return try_to_wake_up(curr->private, mode, wake_flags);
}

static int
try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
{
    unsigned long flags;
    int cpu, success = 0;

    raw_spin_lock_irqsave(&p->pi_lock, flags);
    smp_mb__after_spinlock();
    // 此處對進程的狀態進行篩選,跳過不符合狀態的進程(TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE)
    if (!(p->state & state))
        goto out;

    trace_sched_waking(p);

    /* We're going to change ->state: */
    success = 1;
    cpu = task_cpu(p);

    smp_rmb();
    if (p->on_rq && ttwu_remote(p, wake_flags))
        goto stat;

    ... ...

    // Try-To-Wake-Up
    ttwu_queue(p, cpu, wake_flags);
stat:
    ttwu_stat(p, cpu, wake_flags);
out:
    raw_spin_unlock_irqrestore(&p->pi_lock, flags);

    return success;
}

static void ttwu_queue(struct task_struct *p, int cpu, int wake_flags)
{
    struct rq *rq = cpu_rq(cpu);
    struct rq_flags rf;

    ... ...
    rq_lock(rq, &rf);
    update_rq_clock(rq);
    ttwu_do_activate(rq, p, wake_flags, &rf);
    rq_unlock(rq, &rf);
}

static void
ttwu_do_activate(struct rq *rq, struct task_struct *p, int wake_flags,
        struct rq_flags *rf)
{
    int en_flags = ENQUEUE_WAKEUP | ENQUEUE_NOCLOCK;

    lockdep_assert_held(&rq->lock);

    ... ...
    activate_task(rq, p, en_flags);
    ttwu_do_wakeup(rq, p, wake_flags, rf);
}

/*
 * Mark the task runnable and perform wakeup-preemption.
 */
static void ttwu_do_wakeup(struct rq *rq, struct task_struct *p, int wake_flags,
        struct rq_flags *rf)
{
    check_preempt_curr(rq, p, wake_flags);
    p->state = TASK_RUNNING;
    trace_sched_wakeup(p);
    ... ...
}

從函數調用過程當中能夠看到, default_wake_function() 實現喚醒進程的過程爲:

default_wake_function() --> try_to_wake_up() --> ttwu_queue() --> ttwu_do_activate() --> ttwu_do_wakeup()

值得一提的是, default_wake_function() 的實現中並未將等待隊列元素從等待隊列中刪除。所以,編寫程序時不能忘記添加步驟將等待隊列元素從等待隊列元素中刪除。

4.3 autoremove_wake_function()

int autoremove_wake_function(struct wait_queue_entry *wq_entry, unsigned mode, int sync, void *key)
{
    int ret = default_wake_function(wq_entry, mode, sync, key);

    if (ret)
        list_del_init(&wq_entry->entry);

    return ret;
}

 autoremove_wake_function() 相比於 default_wake_function() ,在成功執行進程喚醒工做後,會自動將等待隊列元素從等待隊列中移除。

 

5. 源碼實例

等待隊列在內核中有着普遍的運用,此處以 MMC 驅動子系統中 mmc_claim_host() 和 mmc_release_host() 來講明等待隊列的運用實例。
 mmc_claim_host() 的功能爲:藉助等待隊列申請得到 MMC 主控制器 (host) 的使用權,相對應, mmc_release_host() 則是放棄 host 使用權,並喚醒全部等待隊列上的進程。

static inline void mmc_claim_host(struct mmc_host *host)
{
    __mmc_claim_host(host, NULL, NULL);
}

int __mmc_claim_host(struct mmc_host *host, struct mmc_ctx *ctx, atomic_t *abort)
{
    struct task_struct *task = ctx ? NULL : current;

    // 定義等待隊列元素,關聯當前進程,喚醒回調函數爲default_wake_function()
    DECLARE_WAITQUEUE(wait, current);
    unsigned long flags;
    int stop;
    bool pm = false;

    might_sleep();

    // 將當前等待隊列元素加入到等待隊列host->wq中
    add_wait_queue(&host->wq, &wait);
    spin_lock_irqsave(&host->lock, flags);
    while (1) {
        // 當前進程狀態設置爲 TASK_UPINTERRUPTIBLE,此時仍未讓出CPU
        set_current_state(TASK_UNINTERRUPTIBLE);
        stop = abort ? atomic_read(abort) : 0;
        // 真正讓出CPU前判斷等待的資源是否已經獲得
        if (stop || !host->claimed || mmc_ctx_matches(host, ctx, task))
            break;
        spin_unlock_irqrestore(&host->lock, flags);
        // 調用調度器,讓出CPU,當前進程可進入休眠
        schedule();
        spin_lock_irqsave(&host->lock, flags);
    }
    // 從休眠中恢復,設置當前進程狀態爲可運行(TASK_RUNNING)
    set_current_state(TASK_RUNNING);
    if (!stop) {
        host->claimed = 1;
        mmc_ctx_set_claimer(host, ctx, task);
        host->claim_cnt += 1;
        if (host->claim_cnt == 1)
            pm = true;
    } else
        // 可利用abort參數執行一次等待隊列喚醒工做
        wake_up(&host->wq);
    spin_unlock_irqrestore(&host->lock, flags);

    // 等待隊列結束,將等待隊列元素從等待隊列中移除
    remove_wait_queue(&host->wq, &wait);

    if (pm)
        pm_runtime_get_sync(mmc_dev(host));

    return stop;
}

void mmc_release_host(struct mmc_host *host)
{
    unsigned long flags;

    WARN_ON(!host->claimed);

    spin_lock_irqsave(&host->lock, flags);
    if (--host->claim_cnt) {
        /* Release for nested claim */
        spin_unlock_irqrestore(&host->lock, flags);
    } else {
        host->claimed = 0;
        host->claimer->task = NULL;
        host->claimer = NULL;
        spin_unlock_irqrestore(&host->lock, flags);

        // 喚醒等待隊列host->wq上的全部進程
        wake_up(&host->wq);
        pm_runtime_mark_last_busy(mmc_dev(host));
        if (host->caps & MMC_CAP_SYNC_RUNTIME_PM)
            pm_runtime_put_sync_suspend(mmc_dev(host));
        else
            pm_runtime_put_autosuspend(mmc_dev(host));
    }
}

從源碼實現過程能夠看到,此實例中等待隊列的使用和第3節中總結得基本過程一致,使用到的函數依次爲:

  •  DECLARE_WAITQUEUE(wait, current) 
  •  add_wait_queue(&host->wq, &wait) 
  •  set_current_state(TASK_UNINTERRUPTIBLE) 
  •  schedule() 
  •  set_current_state(TASK_RUNNING) 
  •  remove_wait_queue(&host->wq, &wait) 

 

6. 另外一種休眠方式

回顧上文的介紹,2.3節中介紹了另一種初始化等待隊列元素的方式 DEFINE_WAIT() ,而至目前仍未見使用。實際上此宏定義和另外一個函數搭配使用: prepare_to_wait() 。

void
prepare_to_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state)
{
    unsigned long flags;

    wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
    spin_lock_irqsave(&wq_head->lock, flags);
    if (list_empty(&wq_entry->entry))
        __add_wait_queue(wq_head, wq_entry);
    set_current_state(state);
    spin_unlock_irqrestore(&wq_head->lock, flags);
}

能夠看到 prepare_to_wait() 實際作的事情也就是將等待隊列元素加入到等待隊列中,而後更新當前進程狀態。能夠看出此過程依舊符合以前介紹的等待隊列通常使用流程,只是內核源碼將部分流程封裝成爲此函數。

 prepare_to_wait() 配合 finish_wait() 函數可實現等待隊列。

 

7. 總結

綜上文分析,等待隊列的使用主要有三種方式:
(1) 等待事件方式
 wait_event() 和 wake_up() 函數配合,實現進程阻塞睡眠和喚醒。


(2) 手動休眠方式1

DECLARE_WAIT_QUEUE_HEAD(queue);
DECLARE_WAITQUEUE(wait, current);

for (;;) {
    add_wait_queue(&queue, &wait);
    set_current_state(TASK_INTERRUPTIBLE);
    if (condition)
        break;
    schedule();
    remove_wait_queue(&queue, &wait);
    if (signal_pending(current))
        return -ERESTARTSYS;
}
set_current_state(TASK_RUNNING);
remove_wait_queue(&queue, &wait);

 

(3) 手動休眠方式2(藉助內核封裝函數)

DELARE_WAIT_QUEUE_HEAD(queue);
DEFINE_WAIT(wait);

while (! condition) {
    prepare_to_wait(&queue, &wait, TASK_INTERRUPTIBLE);
    if (! condition)
        schedule();
    finish_wait(&queue, &wait)
}

 

 

 

參考資料

[1] LINUX 設備驅動程序(LDD3),2012年
[2] Linux設備驅動開發詳解(基於最新的Linux4.0內核),宋寶華編著,2016年
[3] linux設備驅動模型:http://www.javashuo.com/article/p-drftedcw-m.html
[4] Linux 等待隊列 (wait queue):https://xyfu.me/posts/236f51d8/
[5] Linux Wait Queue 等待隊列:http://www.javashuo.com/article/p-bdcgqbzw-mp.html
[6] 源碼解讀Linux等待隊列:http://gityuan.com/2018/12/02/linux-wait-queue/
[7] Driver porting: sleeping and waking up:https://lwn.net/Articles/22913/

相關文章
相關標籤/搜索