Linux 内核等待队列
目录1、等待队列结构和API1.1、等待队列头1.1.1、定义等待队列头1.1.2、初始化等待队列头1.1.3、定义等待队元素1.1.4、添加/移除等待队列1.1.5、等待事件并睡眠1.1.6、唤醒队列2、等待队列(头)用法2.1、 睡眠2.1.1、方式一2.1.2、方式二2.2、 唤醒3、等待队列相关源码分析3.1、睡眠3.2、唤...
目录
等待队列在内核中有着极为广泛的应用,它主要和进程休眠以及进程调度强耦合;首先需要了解何为睡眠和唤醒;
一个进程(task)有几种状态(详见Linux进程描述符task_struct结构体详解--Linux进程的管理与调度(一)(转)),最为常见的是 RUNNING,和睡眠,处于RUNNING 状态的进程,能够被调度器调度运行,而处于休眠状态的进程将不会被调度运行;而睡眠状态下的进程又细分为两类:
TASK_INTERRUPTIBLE:能够收到信号唤醒,也能够在条件满足的时候唤醒
TASK_UNINTERRUPTIBLE:不接受信号,只能够被条件唤醒
等待队列的作用就是将进程进入睡眠和唤醒的一种手段。
对应到用户空间的行为,体现在阻塞和非阻塞的操作,比如,你期望读 1024 Bytes 的串口数据,如果当前串口收到的 buffer 中,不足 1024 Bytes 的数据,此刻的 read 存在两种逻辑:
1、立即返回 — 非阻塞
2、不返回,等数据够了在返回 — 阻塞
这种操作也和等待队列相关。
这里需要注意的一点是,阻塞并不代表低效,当获取不到资源的时候,阻塞,其实是其挂起进程,让其睡眠,从调度器的运行队列中移走,暂时不得到 CPU 的调度,待条件满足后被唤醒,挂入运行队列,被 CPU 调用继续运行。
1、等待队列结构和 API
内核中,提供对进程的睡眠和唤醒的相关操作,使用等待队列来进行表征,它常常在驱动程序中被使用到,它以队列作为基础的数据结构,与进程调度部分紧密结合;
1.1、等待队列头
在 Linux 中, 一个等待队列由一个"等待队列头"来管理, 一个 wait_queue_head_t 类型的结构, 定义在<linux/wait.h>中. 一个等待队列头可被定义和初始化!
1.1.1、定义等待队列头
wait_queue_head_t my_queue;
1.1.2、初始化等待队列头
init_waitqueue_head(&my_queue);
也可以使用 Kernel 定义的快捷方式来定义并初始化一个等待队列头:
DECLARE_WAIT_QUEUE_HEAD(name);
1.1.3、定义等待队元素
DECLARE_WAITQUEUE(name, tsk);
tsk一般为当前进行current. 这个宏定义并初始化一个名为name的等待队列.
1.1.4、添加/移除等待队列
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 添加到等待队列头部 q 指向的双向链表
后者用于将等待队列元素 wait 从 q 中移除
1.1.5、等待事件并睡眠
Linux 内核中睡眠的最简单方式是一个宏定义, 称为 wait_event(有几个变体); 它结合了处理睡眠的细节和进程在等待的条件的检查. wait_event 的形式是:
wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)
queue 是等待队列头,condition 是条件,如果调用 wait_event 前 condition == 0 ,则调用 wait_event 之后,当前进程就会休眠;
wait_event:将当前进程的状态设置为 TASK_UNINTERRUPTIBLE,然后 schedule()
wait_event_interruptible:将当前进程的状态设置为 TASK_INTERRUPTIBLE,然后 schedule()
wait_event_timeout:将当前进程的状态设置为 TASK_UNINTERRUPTIBLE,然后 schedule_timeout()
wait_event_interruptible_timeout:将当前进程的状态设置为 TASK_INTERRUPTIBLE, 然后 schedule_timeout()
TASK_INTERRUPTIBLE 与 TASK_UNINTERRUPTIBLE 区别在于,它的休眠是否会被信号打断,别的进程发来一个信号比如 kill ,TASK_INTERRUPTIBLE 就会醒来去处理。然而 TASK_UNINTERRUPTIBLE 不会。schedule(),进程调度,而schedule_timeout()进行调度之后,一定时间后自动唤醒。
1.1.6、唤醒队列
对于已经睡眠的的进程,可以调用接口去唤醒他们:
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
唤醒操作会唤醒以 queue 为等待队列头的所有进程;
wake_up 应该与 wait_event 或者 wait_event_timeout 配对使用,同样的,wake_up_interruptible 应该与 wait_event_interruptible 和 wait_event_interruptible_timeout 配对使用;
如果调用 wake_up 去唤醒一个使用 wait_event 等,进入休眠的进程,唤醒之后,它会判断 condition 是否为真,如果还是假的继续睡眠。
2、等待队列(头)用法
下面来叙述使用上述的 APIs 和结构,来达成睡眠和唤醒的调用方式和流程:
2.1、 睡眠
根据前面的 APIs 的介绍,使用等待队列实现睡眠,需要先定义个等待队列头,然后将等待队列成员挂上去:
2.1.1、方式一
自动档版本
// 先定义等待队列头
wait_queue_head_t xxx_wait_head;
init_waitqueue_head(&xxx_wait_head);
.....
// 定义等待队列,current 是当前进程
DECLARE_WAITQUEUE(xxx_wait, current);
add_wait_queue(&xxx_wait_head, &xxx_wait);
.....
// 如果条件 xxx_condition 为 1 就不睡了,否则 xxx_wait_head 挂接的等待队列进入睡眠
wait_event_interruptible(&xxx_wait_head, xxx_condition)
2.1.2、方式二
手动档版本
// 先定义等待队列头
wait_queue_head_t xxx_wait_head;
init_waitqueue_head(&xxx_wait_head);
.....
// 定义等待队列,current 是当前进程
DECLARE_WAITQUEUE(xxx_wait, current);
add_wait_queue(&xxx_wait_head, &xxx_wait);
.....
// 设置当前进程为可打断的睡眠 TASK_INTERRUPTIBLE
set_current_state(TASK_INTERRUPTIBLE)
.....
// 使 CPU 调度其他进程
schedule() [ 或者 schedule_timeout() ]
2.2、 唤醒
唤醒操作没有那么多唧唧歪歪,直接调用接口:
wake_up(&wq_head)
wake_up_interruptible(&wq_head)
即可唤醒等待队列头上的所有进程。不再多说了。
3、等待队列相关源码分析
3.1、睡眠
我们以 wait_event 作为参考,来分析具体的代码实现,其他的都差不多的套路:
#define wait_event(wq, condition) \
do { \
if (condition) \
break; \
__wait_event(wq, condition); \
} while (0)
条件满足,即 条件不为0,则不睡眠,否则进入 __wait_event:
#define __wait_event(wq, condition) \
do { \
DEFINE_WAIT(__wait); \
\
for (;;) { \
prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE); \
if (condition) \
break; \
schedule(); \
} \
finish_wait(&wq, &__wait); \
} while (0)
先通过 DEFINE_WAIT 来定义一个等待队列元素, private 元素指向了当前进程的 current 指针,并进行必要的初始化:
#define DEFINE_WAIT(name) \
wait_queue_t name = { \
.private = current, \
.func = autoremove_wake_function, \
.task_list = LIST_HEAD_INIT((name).task_list), \
}
然后调用到 prepare_to_wait:
void fastcall
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))
__add_wait_queue(q, wait);
/*
* don't alter the task state if this is just going to
* queue an async wait queue callback
*/
if (is_sync_wait(wait))
set_current_state(state);
spin_unlock_irqrestore(&q->lock, flags);
}
等待队列加入到等待队列头,设置当前进程为 TASK_UNINTERRUPTIBLE
然后后面调用到了 schedule() 进行 CPU 调度。
最后调用到了 finish_wait,当调用到这里来的时候,其实说明进程已经醒了,并满足了条件:
void fastcall finish_wait(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
__set_current_state(TASK_RUNNING);
if (!list_empty_careful(&wait->task_list)) {
spin_lock_irqsave(&q->lock, flags);
list_del_init(&wait->task_list);
spin_unlock_irqrestore(&q->lock, flags);
}
}
所以这里设置他为 RUNNING
3.2、唤醒
唤醒接口会调用到 __wake_up:
void fastcall __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);
}
走到 __wake_up_common :
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, int sync, void *key)
{
struct list_head *tmp, *next;
list_for_each_safe(tmp, next, &q->task_list) {
wait_queue_t *curr = list_entry(tmp, wait_queue_t, task_list);
unsigned flags = curr->flags;
if (curr->func(curr, mode, sync, key) &&
(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
这个时候去遍历链表,并调用睡眠时候注册挂接上去的那个 func,也就是 autoremove_wake_function :
int autoremove_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
int ret = default_wake_function(wait, mode, sync, key);
if (ret)
list_del_init(&wait->task_list);
return ret;
}
走到 default_wake_function:
int default_wake_function(wait_queue_t *curr, unsigned mode, int sync,
void *key)
{
return try_to_wake_up(curr->private, mode, sync);
}
调用 try_to_wake_up 去执行唤醒
参考文献
更多推荐
所有评论(0)