Linux內核:通過wait_event和wake_up內在機制分析等待隊列

版權聲明:本文爲博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/JansonZhe/article/details/47341383

等待隊列在linux內核中,等待隊列是一個非常重要的概念,也是一個非常重要的機制。我們會在很多函數當中用到等待隊列的知識,例如completion機制、wait_event機制等等。在解釋這些機制之前,我們首先要弄清楚什麼是等待隊列。

在linux內核裏面,我們將進程分爲以下幾種狀態:

可運行狀態(TASK_RUNNING)
處於這種狀態的進程,要麼正在運行,要麼正準備被CPU調度運行。正在運行的進程就是當前進程(由current所指向的進程),而準備運行的進程只要得到CPU就可以立即投入運行,CPU是這些進程唯一等待的系統資源。系統中有一個運行隊列(run_queue),用來容納所有處於可運行狀態的進程,調度程序執行時,從中選擇一個進程投入運行。在後面我們討論進程調度的時候,可以看到運行隊列的作用。當前運行進程一直處於該隊列中,也就是說,current總是指向運行隊列中的某個元素,只是具體指向誰由調度程序(schedule)決定。

等待狀態(TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE)
處於該狀態的進程正在等待某個事件(event)或某個資源,它肯定位於系統中的某個等待隊列(wait_queue)中。Linux中處於等待狀態的進程分爲兩種:可中斷的等待狀態(TASK_INTERRUPTIBLE)和不可中斷的等待狀態(TASK_UNINTERRUPTIBLE))。處於可中斷等待態的進程可以被信號喚醒,如果收到信號,該進程就從等待狀態進入可運行狀態,並且加入到運行隊列中,等待被調度;而處於不可中斷等待態的進程是因爲硬件環境不能滿足而等待,例如等待特定的系統資源,它任何情況下都不能被打斷,只能用特定的方式來喚醒它,例如喚醒函數wake_up()等。

暫停狀態
此時的進程暫時停止運行來接受某種特殊處理。通常當進程接收到SIGSTOP、SIGTSTP、SIGTTIN或 SIGTTOU信號後就處於這種狀態。例如,正接受調試的進程就處於這種狀態。

僵死狀態
進程雖然已經終止,但由於某種原因,父進程還沒有執行wait()系統調用,終止進程的信息也還沒有回收。顧名思義,處於該狀態的進程就是死進程,這種進程實際上是系統中的垃圾,必須進行相應處理以釋放其佔用的資源。

實際使用
瞭解了Linux內核的幾個狀態之後,現在我們開始着重講解其中的一個狀態,等待狀態,由上面的等待狀態的描述,我們可以知道處於等待狀態的進程正在等待某一個事件或者某一個資源,它肯定位於系統中的某一個等待隊列中。這裏我們就以wait_event和wake_up機制來講解。
在這一機制當中,wait_event用於將當前進程加入某一等待隊列中,同時將該進程的狀態修改爲等待狀態。而wake_up則用於將某一個等待隊列上面所有的等待進程喚醒,也就是將其從等待隊列上面刪掉,同時將其的進程狀態置爲可運行狀態。
等待隊列由等待隊列頭等待隊列項構成,所以當我們定義了一個等待隊列頭,也就是定義了一個等待隊列了,等待隊列的結構如下圖所示:
等待隊列

等待隊列頭(wait_queue_head)在內核文件中的定義(include\Linux\Wait.h)

typedef struct __wait_queue_head wait_queue_head_t;
struct __wait_queue_head {
    spinlock_t lock;
    struct list_head task_list;
};

從上面的等待隊列頭的結構體可以看出,其由兩部分組成,第一部分是一個自旋鎖,用於保護自身資源不被多個進程同時訪問。第二部分是有一個list_head結構體構成的雙向鏈表,當然等待隊列頭裏面只有next存放下一個等待隊列項(wait_queue_t)的地址。
等待隊列項(wait_queue_t)在內核文件中的定義(include\Linux\Wait.h)

typedef struct __wait_queue wait_queue_t;
struct __wait_queue {
    unsigned int flags;//
#define WQ_FLAG_EXCLUSIVE   0x01 //表示等待進程想要被獨佔的喚醒
    void *private; //私有指針變量,使用過程中會用來存放task_struct結構體
    wait_queue_func_t func; //用於喚醒等待進程
    struct list_head task_list; //鏈表,用於將等待隊列頭、等待隊列連接起來
};

等待隊列項一共由五個部分構成,其中我們要引起注意的是func,這個func在wake_up函數中會用到,用於喚醒這個等待隊列裏面的這個等待線程。

使用方法:

第一步、定義一個等待隊列頭

wait_queue_head_t rd_waitq;

作用:其實只要定義一個等待隊列頭,並且初始化,就相當於在Linux內核中重新開闢了一條等待隊列,後面的等待隊列項只要往後加等待隊列項就可以了。該等待隊列是獨一無二的。
第二步、初始化等待隊列頭

init_waitqueue_head(rd_waitq);

這裏我們使用的是init_waitqueue_head函數,這個函數主要做兩件事情,
第一件初始化等待隊列頭的自旋鎖,即使自旋鎖設置爲未鎖狀態;
第二件事情初始化等待隊列頭裏面的task_list結構體,使之不指向任何一個等待隊列頭。所以在這裏我們也可以通過此來判斷等待隊列是否有等待隊列項,如果沒有等待隊列項,task_list鏈表的nest指針應該是指向自己的,而不會指向其他等待隊列項。

第三步、添加/移除等待隊列項
顧名思義,我們在上兩步中已經初始化了等待隊列頭了,那麼現在就應該是在這個等待隊列頭後面增加等待隊列項了,增加等待隊列項的函數

//增加等待隊列項
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait); 
//刪除等待隊列項
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)

這裏我們以等待事件函數爲例,來說明如何使用這兩個函數。wait_event函數用於使當前線程進入休眠等待狀態。

#define wait_event(wq, condition)                   
do {                                    
    if (condition) //判斷條件是否滿足,如果滿足則退出等待         
        break;                          
    __wait_event(wq, condition);//如果不滿足,則進入__wait_event宏
} while (0)

#define __wait_event(wq, condition)                     
do {    
DEFINE_WAIT(__wait);
/*定義並且初始化等待隊列項,後面我們會將這個等待隊列項加入我們的等待隊列當中,同時在初始化的過程中,會定義func函數的調用函數autoremove_wake_function函數,該函數會調用default_wake_function函數。*/                    

    for (;;) {                          
        prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE);    
/*調用prepare_to_wait函數,將等待項加入等待隊列當中,並將進程狀態置爲不可中斷TASK_UNINTERRUPTIBLE;*/
        if (condition)  //繼續判斷條件是否滿足                    
            break;                      
        schedule(); //如果不滿足,則交出CPU的控制權,使當前進程進入休眠狀態                      
    }
    /**如果condition滿足,即沒有進入休眠狀態,跳出了上面的for循環,便會將該等待隊列進程設置爲可運行狀態,並從其所在的等待隊列頭中刪除    */                          
    finish_wait(&wq, &__wait);              
} while (0)                             
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))//判斷等待隊列是否爲空,即只要檢查等待隊列頭的task_list是否指向本身就可以了。
        __add_wait_queue(q, wait); //如果爲空,則將該等待隊列項加入等待隊列
    set_current_state(state);//設置當前進程的狀態
    spin_unlock_irqrestore(&q->lock, flags);//釋放自旋鎖
}

下面我來分析一下等待隊列的喚醒機制。
與wait_event函數對應的就是wake_up函數了,wake_up函數用於喚醒處於該等待隊列的進程。首先我們來看一下位於include\linux\wait.h文件中wake_up函數的定義

/**定義wake_up函數宏,同時其需要一個wait_queue_head_t的結構體指針,在該宏中調用__wake_up方法。*/
#define wake_up(x)          __wake_up(x, TASK_NORMAL, 1, NULL)

//該方法中主要是在自旋鎖的狀態下調用__wake_up_common方法
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);
}
/*其中:q是等待隊列,mode指定進程的狀態,用於控制喚醒進程的條件,nr_exclusive表示將要喚醒的設置了WQ_FLAG_EXCLUSIVE標誌的進程的數目,這裏其值是1,表示只有一個這樣白標識的等待進程。 然後掃描鏈表,調用func(註冊的進程喚醒函數,默認爲default_wake_function)喚醒每一個進程,直至隊列爲空,或者沒有更多的進程被喚醒,或者被喚醒的的獨佔進程數目已經達到規定數目。*/
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;
    //這個宏的作用是遍歷整個等待隊列,其實就相當於一個for函數。
    list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
        //將當前進程的標誌位賦給flag,再調用func函數,以及其他判斷機制喚醒等待隊列上的進程
        unsigned flags = curr->flags;
        if (curr->func(curr, mode, wake_flags, key) &&(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
            break;
    }
}
//list_for_each_entry_safe的定義
#define list_for_each_entry_safe(pos, n, head, member)          \
    for (pos = list_entry((head)->next, typeof(*pos), member),  \
        n = list_entry(pos->member.next, typeof(*pos), member); \
        //只要pose的task_list不是該等待隊列頭的task_list就繼續下去。
         &pos->member != (head);                    \
         pos = n, n = list_entry(n->member.next, typeof(*n), member))

通過上面的源代碼的分析,應該可以基本瞭解等待隊列以及整個wait_event宏和wake_up宏的工作流程了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章