前言
CPU調度如下圖所示:
等待隊列其原理是:
- cpu會調度就緒隊列,或者打斷執行線程,運行就緒隊列
- 創建等待隊列頭和隊列,使用wait event,當condition不滿足時,當前線程進入等待隊列
- 通過將當前線程加入等待隊列中,同時schedule調度走cpu執行別的線程,下次cpu便不會再調度當前線程了
- 當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中刪除!