wait_queue机制浅析
在内核中,如果一个任务需要等待一个事件,如何实现当事件未发生时该任务睡眠节省CPU资源,当事件发生时任务及时被唤醒继续工作呢?wait_event/wake_up机制是一个不错的选择。下面这个场景展示了wait_queue的基本用法
void taskA(void) {/* do_something */..../* 如果事件未发生(condition表达式不为真) taskA睡眠*/wait_event(wq, condition);/* do_something */....
}void taskB(void) {/* do_something */..../* taskA等待的事件发生,唤醒taskA*/wake_up(wq, condition);/* do_something */....
}
个人理解如果要浅层次的理解wait queue机制,可以简单概括为,在需要等待某个事件时,将任务挂载到等待链表上,然后使该任务随眠,切换至其他任务执行;当事件发生时,遍历该链表上的任务,将其一一从等待链表上摘除唤醒。这是wait queue机制的骨架,至于超时唤醒,条件判断等机制都是在该骨架上衍生而来。
1 数据结构
struct wait_queue_head {spinlock_t lock;struct list_head head;
};struct wait_queue_entry {/* 配置标志 */unsigned int flags;/* task_struct */void *private;/* 唤醒函数 */wait_queue_func_t func;struct list_head entry;
};
wait_queue_head这个数据结构很简单,就是一个带锁的链表,wait_queue机制用这个链表来代表一个事件,所有等待这个事件的任务都会用wait_queue_entry结构体表示,并挂载到这个链表上
2 wait_event()
#define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) \
({ \__label__ __out; \struct wait_queue_entry __wq_entry; /* (1) */ \long __ret = ret; /* explicit shadow */ \\init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0); \for (;;) { \long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state); /* (2) */\\if (condition) /* (3) */ \break; \\if (___wait_is_interruptible(state) && __int) { /* (4) */ \__ret = __int; \goto __out; \} \\cmd; /* (5) */ \} \finish_wait(&wq_head, &__wq_entry); (6) \
__out: __ret; \
})
以wait_event为例,当使用wait_event(wq_head, condition),最终会调用___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0, schedule())
(1) 定义一个wait_queue_entry, 并使用init_wait_entry进行初始化;
(2) 调用prepare_to_wait_event()将wait_queue_entry挂入链表,同时将当前任务状态设置为state(一般为TASK_UNINTERRUPTIBLE或TASK_INTERRUPTIBLE),准备修眠;
(3) 如果等待的条件已被满足,结束等待,跳出循环至(6);
(4) 如果在加入队列过程中进程(调用prepare_to_wait_event)睡眠状态被打断此处会结束等待过程;
(5) 对于wait_event接口,这个cmd是schedule(),此时任务被切走随眠
(6) 结束等待
从上述过程不难看出,等待事件condition的任务在等待条件被满足的过程中可能会被唤醒不只一次,condition不满足时,其会继续睡眠。如果这里的state被配置为TASK_INTERRUPTIBLE时,那么该进程被打断时,其会结束这个等待过程。
3 wake_up()
wake_up函数最终会调用__wake_up_common_lock(),这个函数的功能就是将等待链表上的任务一一唤醒,并将这些任务的wait_queue_entry从链表上摘下。这个函数里有一个比较有趣的机制,bookmark,正如其名书签,当wake的数目超过一个阈值WAITQUEUE_WALK_BREAK_CNT(默认是64)时,其会主动中断唤醒操作,尝试让出cpu,之后再从上次中断的地方继续唤醒。个人理解这是为了避免一次性唤醒的任务过多,一直占用CPU。
4 结语
上文分析了,最基本的wait_event()和wake_up()流程,其余诸如wait_event_timeout()等,大多类似,只在睡眠或唤醒时有一些细节上的差异,本次不再赘述,如果再发现有趣的点再补充吧。