看 Linux 的 wait_event
源码时,联想到我们平时经常用得比较多的 wait/notify、double-check 和 volatile
,突然意识 wait_event
简简单单几行代码的背后,涉及的知识点其实非常丰富。本篇文章我们就一起了来探索它背后的知识,然后尝试着和我们的日常开发关联起来。
wait_event
这里使用 Linux-2.6.24 版本的源码
背景
在某些情况下,我们会需要等待某个事件,在这个事件发生前,把进程投入睡眠。比方说,同步写 IO;在发出写磁盘命令后,进程要进入休眠,等等磁盘完成。为了支持这一类场景,Linux 引入了 wait queue;wait queue 从概念上跟我们应用层使用的 condition queue 是一样的。
实现
这里我们着重讲 wait_event
的实现,一些相关的知识读者可以参考《深入理解LINUX内核》。
下面我们开始看代码:
// ${linux_source}/include/linux/wait.h
/**
* wait_event - sleep until a condition gets true
* @wq: the waitqueue to wait on
* @condition: a C expression for the event to wait for
*
* The process is put to sleep (TASK_UNINTERRUPTIBLE) until the
* @condition evaluates to true. The @condition is checked each time
* the waitqueue @wq is woken up.
*
* wake_up() has to be called after changing any variable that could
* change the result of the wait condition.
*/
#define wait_event(wq, condition) \
do { \
if (condition) \
break; \
__wait_event(wq, condition); \
} while (0)
这里只是先检测一遍条件,然后直接又调用 __wait_event
:
// ${linux_source}/include/linux/wait.h
#define __wait_event(wq, condition) \
do { \
DEFINE_WAIT(__wait); \
\
for (;;) { \
prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE); \
if (condition) \
break; \
// schedule 使用调度器调度另一个线程去执行。当前线程被重新 \
// 调度时,schedule 函数才会返回 \
schedule(); \
} \
finish_wait(&wq, &__wait); \
} while (0)
DEFINE_WAIT
宏用于定义局部变量 __wait
:
// ${linux_source}/include/linux/wait.h
#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
和 finish_wait
源码如下:
// ${linux_source}/kernel/wait.c
/*
* Note: we use "set_current_state()" _after_ the wait-queue add,
* because we need a memory barrier there on SMP, so that any
* wake-function that tests for the wait-queue being active
* will be guaranteed to see waitqueue addition _or_ subsequent
* tests in this thread will see the wakeup having taken place.
*
* The spin_unlock() itself is semi-permeable and only protects
* one way (it only protects stuff inside the critical region and
* stops them from bleeding out - it would still allow subsequent
* loads to move into the critical region).
*/
void fastcall
prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
unsigned long flags;
// 非独占等待(可以同时唤醒多个进程)
wai