上一节从使用者的角度分别介绍了等待队列、完成量 、工作队列的使用场景和步骤;本节我们稍微深入一下,先看一下等待队列的基本工作原理,当然不会太深入,因为那是内核的工作。
1、等待队列头
每个等待队列都有一个等待队列头,如上节介绍的NVMe驱动中定义的等待队列头:
wait_queue_head_t state_wq;
wait_queue_head_t 的数据结构定义如下:
struct wait_queue_head
{
spinlock_t lock;
struct list_head head;
};
typedef struct wait_queue_head wait_queue_head_t;
可见其本质上就是一个链表另加一个自旋锁构成;链表头用于挂接等待项,锁是因为等待队列可以再中断时修改,所以操作队列之前需要先锁住。
2、初始化等待队列头
初始化等待队列头的方式如下:
init_waitqueue_head(&state_wq);
它的工作流程如下,可见它只是初始化了锁和链表;
//它调用__init_waitqueue_head()
void __init_waitqueue_head(struct wait_queue_head *wq_head, const char *name, struct lock_class_key *key)
{
spin_lock_init(&wq_head->lock); //初始化锁
lockdep_set_class_and_name(&wq_head->lock, key, name);
INIT_LIST_HEAD(&wq_head->head); //初始化队列
}
3、进程睡眠之等待项
最本质的工作是让进程睡眠以等待事件的发生;它的原理是给进程定义一个等待队列项,把等待项加入到等待队列头,不断检查等待项的状态,以判断是否要唤醒进程。
好消息是等待项无需用户创建,在调用wait_event()函数时,由内核自动创建,并自动加入到等待队列中。
wait_event()有两个参数,一个是等待队列头,一个是等待条件;我们稍微追踪一下等待项的结构:
struct wait_queue_entry {
unsigned int flags; //值为WQ_FLAG_EXCLUSIVE表示进程想要被独占唤醒,或者为0
void *private; //指向当前进程current
wait_queue_func_t func; //指向唤醒进程的函数
struct list_head entry; //用于连接到等待队列头
};
再稍微追踪一下wait_event()的工作流程,它把具体的工作交由__wait_event(),这是内核的通用做法,以下函数看似复杂,其实很简单;
首先创建并初始化一个等待项( __wq_entry),等待项的初始化如下,没有什么特别之处,private执行本进程,所以本进程就是一个等待项;func指向一个内核实现的函数,用于唤醒进程:
//等待项的初始化
void init_wait_entry(struct wait_queue_entry *wq_entry, int flags)
{
wq_entry->flags = flags;
wq_entry->private = current; //指向当前进程
wq_entry->func = autoremove_wake_function; //唤醒进程的函数
INIT_LIST_HEAD(&wq_entry->entry);
}
再看一下__wait_event()的流程,初始化等待项之和,调用 prepare_to_wait_event()将等待项(即本进程,毕竟等待项的private指针指向本进程)加入到等待队列:
#define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) \
({ \
__label__ __out; \
//定义等待项
struct wait_queue_entry __wq_entry; \
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);\
\
if (condition) \
break; \
\
if (___wait_is_interruptible(state) && __int) { \
__ret = __int; \
goto __out; \
} \
\
cmd; \
} \
//移除等待项
finish_wait(&wq_head, &__wq_entry); \
__out: __ret; \
})
我们看一下简化版的prepare_to_wait_event(),还是非常好理解的,最终目的就是把等待项加入等待队列中:
long prepare_to_wait_event(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state)
{
unsigned long flags;
long ret = 0;
spin_lock_irqsave(&wq_head->lock, flags);
if (list_empty(&wq_entry->entry)) {
//首先队列头要是空的,说明一个队列头只能有一个等待项
if (wq_entry->flags & WQ_FLAG_EXCLUSIVE)
__add_wait_queue_entry_tail(wq_head, wq_entry); //独占唤醒
else
__add_wait_queue(wq_head, wq_entry);
}
set_current_state(state);
spin_unlock_irqrestore(&wq_head->lock, flags);
return ret;
}
//把等待项加入等待队列头
static inline void __add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
list_add(&wq_entry->entry, &wq_head->head);
}
总之以上函数会让进程睡眠,直到事件发生被内核唤醒;具体的唤醒工作就是初始化工作项时fun所指定函数的工作,它完全由内核实现,我们不用关心。
至此,我的理解是,虽然是等待队列,但实际队列上只能有一个等待项,就是当前进程;且等待项是由内核定义和实现的,用户完全不用关心。