linux 等待队列
一、等待队列
在linux内核中提供了阻塞机制,等待队列(wait queque)。在驱动中使用的也比较多。例如,应用程序去读取设备上的数据时,如果设备还没有准备好数据,可以将这个进程挂起,进入阻塞状态。等到设备准备好数据时才将这个进程唤醒,并且将数据返回给应用程序,继续执行。阻塞进程的实现方法就使用到了等待队列。
二、等待队列的数据结构
1、等待队列头
struct __wait_queue_head {
/* 自旋锁,主要防止并发访问这个这个等待队列 */
spinlock_t lock;
/* 等待队列链表头,这是一个双向链表 */
struct list_head task_list;
};
/* wait_queue_head_t 等待队列头类型,创建一个等待队列的时,可以使用这个等待队列 */
typedef struct __wait_queue_head wait_queue_head_t;
2、等待队列项
struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01
/* 当前进程的task_sturct对象地址<一个进程由一个task_sturct管理> */
void *private;
/* 用于唤醒这个进程的回调函数, */
wait_queue_func_t func;
/* list_head 链表节点 */
struct list_head task_list;
};
3、等待队列的示意图
队列是由内核链表实现的,在后面查找节点的时候需要做一下转换,如果不了解内核链表的可以百度一下。其实内核链表和企业链表比较像,只不过内核链表的挂钩在下面,企业链表的挂钩在上面。有兴趣的可以去百度一下内核链表、企业链表和普通链表的区别。
二、等待队列的进程睡眠过程
1、睡眠的使用方法
在使用等待队列的时候首先需要定义一个等待队列头;定义等待队列头有两种方式:
- 使用
DECLARE_WAIT_QUEUE_HEAD(name)
宏定义,这种方式定义并且初始化好了- 使用
wait_queue_head_t
定义,使用init_waitqueue_head
函数初始化
定义完头队列头之后,使用wait_event*
系列的宏来将进程挂起
2、将进程挂起的函数
宏定义名/方法名 | 作用 | 返回值 |
---|---|---|
wait_event(wq, condition) | 将进程挂起,直到condition为真才将进程唤醒;使用这种方法将进程挂起,这种方式是不能被打断的(例如接收到ctrl + c的信号也不会被唤醒结束进程) | 无 |
wait_event_timeout(wq, condition, timeout) | 将进程挂起,直到condition为真或者等待超时将进程唤醒,这种方式也是不能被打断的 | 0:超时返回; >0 被唤醒时,返回剩余的时间 |
wait_event_interruptible(wq, condition) | 将进程挂起,直到condition为真才将进程唤醒;这种方式可以被打断的 | -ERESTARTSYS:被信号打断唤醒; 0:condition为真时唤醒 |
wait_event_interruptible_timeout(wq, condition, timeout) | 将进程挂起, 直到condition为真或者等待超时将进程唤醒,这种方式可以被打断的 | -ERESTARTSYS:被信号打断唤醒,0:超时结束返回, >0:condition为真被唤醒时,返回剩余的时间 |
3、实现原理
以比较常用的
wait_event_interruptible
来分析,源码如下:
/* wait_event_interruptible 是一个带参宏,wq等待队列头,condition唤醒条件 */
#define wait_event_interruptible(wq, condition) \
({ \
int __ret = 0;
/* 如果 condition 为0,将进程挂起*/ \
if (!(condition))
/* __wait_event_interruptible 也是一个带参宏,源码在下面 */ \
__wait_event_interruptible(wq, condition, __ret); \
__ret; \
})
__wait_event_interruptible
宏
/* wq:等待队列头,condition唤醒条件,ret返回值变量 */
#define __wait_event_interruptible(wq, condition, ret) \
do { \
/* 定义一个等待队列节点__wait,并且初始化 */
/*
#define DEFINE_WAIT(name)
wait_queue_t name = {
/* 指向当前的task_struct */
.private = current,
/* 默认的唤醒方法 */
.func = autoremove_wake_function,
/* 初始化节点的链表指针 */
.task_list = LIST_HEAD_INIT((name).task_list),
}
*/
DEFINE_WAIT(__wait); \
\
for (;;) { \
/* 将等待队列的节点挂到等待队列头里面,并且将任务的状态设置为TASK_INTERRUPTIBLE,源代在下面 */
prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE); \
/* condition为真,则被唤醒返回 */
if (condition) \
break;
/* 判断是否接收到信号唤醒 */ \
if (!signal_pending(current)) { \
/* 如果没有接收到信号则执行调度,进程在这里进入休眠,等待唤醒后又从这里开始执行 */
schedule(); \
continue; \
} \
/* 如果接收到信号返回ERESTARTSYS */
ret = -ERESTARTSYS; \
break; \
} \
/* 将进程设置为 TASK_RUNNING,并且将等待队列的节点从链表中删除 */
finish_wait(&wq, &__wait); \
} while (0)
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
*/
/* 判断wait 和 (wait)->private) 不为空*/
if (is_sync_wait(wait))
/* 设置当前任务的任务状态 */
set_current_state(state);
spin_unlock_irqrestore(&q->lock, flags);
}
finish_wait函数
/* q : 等待队列头
wait : 等待对列节点
*/
void fastcall finish_wait(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
/* 将当前的任务状态设置为运行态,等待CPU调度 */
__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);
}
}
三、等待队列的进程唤醒过程
等待队列的唤醒的方法也有下面的几种,都是对
__wake_up
带参宏定义,__wake_up_locked
和__wake_up_locked
的实现和__wake_up
差不多,都是调用__wake_up_common
函数,接下来就分析__wake_up函数吧
/* x是调用的时候传进来的等待队列头 */
/* 唤醒挂起模式为TASK_UNINTERRUPTIBLE和TASK_INTERRUPTIBLE的任务(可中断和不可中断类型) */
#define wake_up(x) __wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 1, NULL)
#define wake_up_nr(x, nr) __wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, nr, NULL)
#define wake_up_all(x) __wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 0, NULL)
#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
#define wake_up_interruptible_nr(x, nr) __wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)
#define wake_up_interruptible_all(x) __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)
#define wake_up_locked(x) __wake_up_locked((x), TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE)
#define wake_up_interruptible_sync(x) __wake_up_sync((x),TASK_INTERRUPTIBLE, 1)
__wake_up函数
/*
* q : 等待队列头
* mode : 唤醒那种挂起方式的任务
* nr_exclusive: 唤醒任务的个数,为0唤醒所有的任务
* key: 传递给唤醒函数的参数,在默认的唤醒函数里面没有使用到
*/
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;
/* 对于每个等待队列的节点调用唤醒方法,nr_exclusive是唤醒的个数,为0时每一个节点都调用 */
if (curr->func(curr, mode, sync, key) &&
(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
curr->func在wait_event的时候被定义等待队列节点时候初始化为 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函数
/* 函数也比较简单,之调用了try_to_wake_up函数 */
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函数
static int try_to_wake_up(struct task_struct *p, unsigned int state, int sync){
{
int cpu, this_cpu, success = 0;
unsigned long flags;
long old_state;
struct rq *rq;
#ifdef CONFIG_SMP
struct sched_domain *sd, *this_sd = NULL;
unsigned long load, this_load;
int new_cpu;
#endif
/* 中间做了比较多的处理,比较复杂,就不分析了 */
...
...
/* 在这里将任务的状态设置为TASK_RUNNING(可运行转台),等待cpu的调度后,任务就被唤醒了 */
p->state = TASK_RUNNING;
out:
task_rq_unlock(rq, &flags);
return success;
}
四、总结
一、等待队列的使用时需要定义一个等待队列头和一个唤醒条件,然后调用
wait_event*
的宏来将进程休眠,调用wake_up*
宏将进程唤醒。在linux中断管理四有使用的例子,这里就不过多啰嗦了
二、
wait_event*
里面主要做了下面几件事情
- 构建一个等待队列的节点,并且初始化
- 将等待队列节点链入到等待队列,并且设置任务状态
- 调用
schedule
函数进行任务调度,任务开始休眠
三、
wake_up*
做了下面几件事情
- 遍历等待队列的的节点,节点数可以设定
- 调用默认的唤醒函数
default_wake_function
default_wake_function
里面讲任务的任务状态设置为TASK_RUNNING