前言
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中删除!