Linux內核之——等待隊列wait queue

前言

等待隊列1
等待隊列2
調度器

CPU調度如下圖所示:
在這裏插入圖片描述
等待隊列其原理是:

  1. cpu會調度就緒隊列,或者打斷執行線程,運行就緒隊列
  2. 創建等待隊列頭和隊列,使用wait event,當condition不滿足時,當前線程進入等待隊列
  3. 通過將當前線程加入等待隊列中,同時schedule調度走cpu執行別的線程,下次cpu便不會再調度當前線程了
  4. 當wakeup後,將wait線程加入run queue或者就緒隊列中,同時condition滿足,下次被阻塞的線程會被調度,如果condition不滿足,則繼續cpu被調度走,不會執行當前進程,繼續阻塞

等待隊列是一種基於資源狀態的線程管理的機制,它可以使線程在資源不滿足的情況下處於休眠狀態,讓出CPU資源,而資源狀態滿足時喚醒線程,使其繼續進行業務的處理。等待隊列(wait queue)用於使線程等待某一特定的事件發生而無需頻繁的輪詢,進程在等待期間睡眠,在某件事發生時由內核自動喚醒。它是以雙循環鏈表爲基礎數據結構,與進程的休眠喚醒機制緊密相聯,是實現異步事件通知、跨進程通信、同步資源訪問等技術的底層技術支撐。

1. 創建一個等待隊列

在Linux內核中,wait_queue_head_t代表一個等待隊列,只需要定義一個wait_queue_head_t類型的變量,就表示創建一個等待隊列,還需要調用如下接口來初始化此隊列:

staitc wait_queue_head_t prod_wq;
init_waitqueue_head(&prod_wq);

具體看一下wait_queue_head_t數據類型:

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

就是一個鏈表和一把自旋鎖,鏈表是用於保存等待該隊列的wait_queue_t類型waiter對象(此類型對象內部的private成員保存了當前的任務對象task_struct *),自旋鎖是爲了保證對鏈表操作的原子性。這裏簡單的看一下wait_queue_t數據類型:

typedef struct __wait_queue wait_queue_t;
struct __wait_queue {
    unsigned int flags;
#define WQ_FLAG_EXCLUSIVE   0x01
    void *private; // 保存當前任務的task_struct對象地址,爲pcb
    wait_queue_func_t func; // 用於喚醒被掛起任務的回調函數,該回調函數是將該進程加入run queue,等待cpu調度
    struct list_head task_list; // 連接到wait_queue_head_t中的task_list鏈表                                                              
};

在這裏插入圖片描述
圖中,task是PCB

2. 讓當前進程開始等待

內核提供瞭如下的接口來讓當前進程在條件不滿足的情況下,阻塞等待:

wait_event(wq, condition)
wait_event_timeout(wq, condition, timeout)
wait_event_interruptible(wq, condition)
wait_event_interruptible_timeout(wq, condition, timeout)

返回值如下:

1)    -ERESTARTSYS: 表示被信號激活喚醒

2)    > 0: 表示condition滿足,返回值表示距離設定超時還有多久

3)    = 0: 表示超時發生

其內部實現源碼都很類似,只是有些細節不太一樣,這裏以wait_event_interruptible()爲例子,看看其源碼:

#define __wait_event_interruptible(wq, condition, ret)          \
do {                                    \
    // 定義一個waiter對象
    DEFINE_WAIT(__wait);                        \
                                    \
for (;;) {                          \
    // 將waiter對象加入到等待鏈表中,並設置當前task的狀態爲TASK_INTERRUPTIBLE
        prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE);  \                                                                            
        if (condition)                      \
            break;                      \
        if (!signal_pending(current)) {             \
            // 進行任務調度,
            schedule();                 \
            continue;                   \
        }                           \
        ret = -ERESTARTSYS;                 \
        break;                          \
}                               \
    // 將waiter對象從等待鏈表中刪除
    finish_wait(&wq, &__wait);                  \
} while (0)

當我們調用wait_event_interruptible()接口時,會先判斷condition是否滿足,如果不滿足,則會suspend當前task。

這裏再看一下DEFINE_WAIT宏的源碼,可以發現其private成員總是保存這當前task對象的地址current,還有一個成員func也是非常重要的,保存着task被喚醒前的操作方法,這裏暫不說明,待下面的wait_up喚醒等待隊列時再進行分析:

#define DEFINE_WAIT(name)                       \
    wait_queue_t name = {                       \
        .private    = current,              \
        .func       = autoremove_wake_function,     \
        .task_list  = LIST_HEAD_INIT((name).task_list), \
    }

3. 喚醒此等待隊列上的進程:

內核提供瞭如下的接口:

void wake_up(wait_queue_head_t *q);
void wake_up_interruptible(wait_queue_head_t *q);
void wake_up_interruptible_all(wait_queue_head_t *q);

這裏以分析wake_up_interruptible()函數的源碼進行說明喚醒task的原理,因爲其他的喚醒過程都是類似的。最後都會調用到__wake_up_common()這個函數:

void __wake_up_common(wait_queue_head_t *q, unsigned int mode,                                                                           
            int nr_exclusive, int sync, 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, sync, key) &&
                (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
            break;
    }
}

從上面的源碼可以看出最終就是調用了等待隊列q上的task_list鏈表上的waiter對象的func方法,在前面又提到過這個方法就是autoremove_wake_function():

int autoremove_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
   // 將wait對象private成員保存的task添加到run queue中,便於系統的調度
    int ret = default_wake_function(wait, mode, sync, key);
 
    // 將此wait對象從鏈表中刪除
    if (ret)                                                                                                                             
        list_del_init(&wait->task_list);
    return ret;
}

defailt_wake_function()的源碼如下,又看到我們熟悉的private成員

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

4. 實例探索

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/kthread.h>
#include <linux/delay.h>
 
MODULE_AUTHOR("Jimmy");
MODULE_DESCRIPTION("wait queue example");
MODULE_LICENSE("GPL");
 
static int condition;
static struct task_struct *task_1;
static struct task_struct *task_2;
static struct task_struct *task_3;
 
DECLARE_WAIT_QUEUE_HEAD(wq);
 
 
static int thread_func_1(void *data)
{
    msleep(100);//延時100ms,使得這個進程的等待隊列插入在整個鏈表的頭部,最先被喚醒。所以先打印這個進程!
    
    wait_event_interruptible(wq, condition);
    condition = 0;
    printk(">>>>>this task 1\n");
    
    do {
        msleep(1000);
    }while(!kthread_should_stop());
    
    return 1;
}
 
static int thread_func_2(void *data)
{
        
    wait_event_interruptible(wq, condition);
    condition = 0;
    printk(">>>>>this task 2\n");
    
    do {
        msleep(1000);
    }while(!kthread_should_stop());
    
    return 2;
}
 
static int thread_func_3(void *data)
{    
    msleep(2000);
    printk(">>>>>this task 3\n");
    condition = 1;
    wake_up_interruptible(&wq);
 
    msleep(2000);
    condition = 1;
    wake_up_interruptible(&wq);
    
    do {
        msleep(1000);
    }while(!kthread_should_stop());
        
    return 3;
}
 
static int __init mod_init(void)
{
    condition = 0;
 
    task_1 = kthread_run(thread_func_1, NULL, "thread%d", 1);
    if (IS_ERR(task_1)) {
        printk("******create thread 1 failed\n");
    } else {
        printk("======success create thread 1\n");
    }
    
    task_2 = kthread_run(thread_func_2, NULL, "thread%d", 2);
    if (IS_ERR(task_2)) {
        printk("******create thread 2 failed\n");
    } else {
        printk("======success create thread 2\n");
    }
 
    task_3 = kthread_run(thread_func_3, NULL, "thread%d", 3);
    if (IS_ERR(task_2)) {
        printk("******create thread 3 failed\n");
    } else {
        printk("======success create thread 3\n");
    }
        
    return 0;
}
 
static void __exit mod_exit(void)
{
    int ret;
    
    if (!IS_ERR(task_1)) {
        ret = kthread_stop(task_1);
        printk("<<<<<<<<task 1 exit, ret = %d\n", ret);
    }
        
    if (!IS_ERR(task_2)) {
        ret = kthread_stop(task_2);
        printk("<<<<<<<<task 2 exit, ret = %d\n", ret);
    }
 
    if (!IS_ERR(task_3)) {
        ret = kthread_stop(task_3);
        printk("<<<<<<<<task 3 exit, ret = %d\n", ret);
    }
    
    return;
}
 
module_init(mod_init);
module_exit(mod_exit);
   

注意:
1、要從wait_event()函數跳出,需要兩個條件。1、condition = 1; 2、在內核的另一個地方調用了wake_up()函數。
2、wake_up()每次只能喚醒一個進程,而且是從隊列頭開始喚醒的,而wait_event()函數每次會將新建的等待隊列插到隊列頭,因此最後調用wait_event()函數的進程先被喚醒!如果要喚醒某個特定的進程,沒有現成的函數,只能使用wake_up_all()函數喚醒所有進程,然後在通過條件condition來控制(每個進程使用不同的變量來控制,在wake_up_all()函數後只將要喚醒的進程的變量置成真)。
3、當wake_up_all後,運行continue,結束本次循環,再次上鎖,如果condition不滿足,則繼續掛起,只有當condition滿足時,才break,跳出for循環,finish_wait將_wait從wq中刪除!

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