等待队列
正如list_head结构那样,等待队列(wait queue)作为linux内核中的基础数据结构,与进程调度紧密结合在一起;在驱动程序中,常常使用等待队列来实现进程的阻塞和进程的唤醒。因此,我们很有必要对它的内部实现进行分析。
0.数据结构
一般我们的链式线性表都会有一个头结点,以使我们迅速找到这个线性链表的“领导”。在等待队列中,同样有队列头,只不过等待队列头和普通的等待队列结点定义有所不同。
struct __wait_queue_head {
/*因为等待队列可以在中断时随时修改,因此设置一个自旋锁保证一致性*/
spinlock_t lock;
struct list_headtask_list;
};
typedef struct __wait_queue wait_queue_t
可以看到,等待队列头结构中封装了list_head结构。这么做是可以想象到的,因为队列和栈本质上还是双联表。当我们适当限制双联表的某些操作时,就可以实现这样的功能。另外,等待队列头结构中还有一个自旋锁结构的变量lock,它起到了对等待队列进行互斥操作的作用。
在等待队列结构中,除了使用list_head结构外,还有以下几个字段:
struct __wait_queue {
unsigned int flags; /*指明等待的进程是互斥进程还是非互斥进程*/
#define WQ_FLAG_EXCLUSIVE 0x01
void *private; /*指向任务的task_struct*/
wait_queue_func_t func;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t
flag:指明该等待的进程是互斥还是非互斥,为0时非互斥,为1时互斥;
WQ_FLAG_EXCLUSIVE :从变量可以看出,此宏代表进程是互斥的;
private:void型指针变量功能强大,你可以赋给它你需要的结构体指针。一般赋值为task_struct类型的指针,也就是说指向一个进程;
func:函数指针,指向该等待队列项的唤醒函数;
1.定义及初始化
内核中使用init_waitqueue_head宏来初始化等待队列头。
#define init_waitqueue_head(q) \
do { \
static struct lock_class_key __key; \
__init_waitqueue_head((q), &__key); \
} while (0)
我们可以看到这个宏使用了do-while型循环语句,里面包含两条语句。首先定义了一个变量__key,然后再调用init_waitqueue_head函数。
在init_waitqueue_head宏中调用的__init_waitqueue_head函数定义如下:
void __init_waitqueue_head(wait_queue_head_t *q, struct lock_class_key *key)
{
spin_lock_init(&q->lock);
lockdep_set_class(&q->lock, key);
INIT_LIST_HEAD(&q->task_list);
}
在这个函数中,首先利用自旋锁初始化函数初始化这个自旋锁;在上述等待队列头的定义中,我们可以看到task_list字段是一个list_head结构类型,因此我们使用INIT_LIST_HEAD对这个链表进行初始化。这些过程都是我们所熟悉的。
同时,我们也可以使用初始化宏DECLARE_WAIT_QUEUE_HEAD一步的进行定义和初始化。
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) { \
.lock = __SPIN_LOCK_UNLOCKED(name.lock), \
.task_list = { &(name).task_list, &(name).task_list } }
#define DECLARE_WAIT_QUEUE_HEAD(name) \
wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
上述代码对于有C基础的人理解起来并不困难。需要注意的是对task_list进行赋值后,这个结点的前后指针都会指向自己。
同样,对于等待队列,我们可以使用DECLARE_WAITQUEUE宏来定义并初始化一个等待队列项。
int default_wake_function(wait_queue_t *wait, unsigned mode, int flags, void *key);
#define __WAITQUEUE_INITIALIZER(name, tsk) { \
.private = tsk, \
.func = default_wake_function, \
.task_list = { NULL, NULL } }
#define DECLARE_WAITQUEUE(name, tsk) \
wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)
name要定义的等待队列项的名称;tsk是task_struct类型的指针变量,它指向这个等待队列项所对应的进程。default_wake_function是默认的唤醒函数,本文后面有介绍。
2.添加/移除等待队列
add_wait_queue添加函数将等待队列wait添加到以q为等待队列头的那个等待队列链表中。
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
wait->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
__add_wait_queue(q, wait);
spin_unlock_irqrestore(&q->lock, flags);
}
我们可以看到flags的结果必然是0,也就是说这个函数是将非互斥进程添加到等待队列当中。而且在调用具体的添加函数时候,使用关中断并保护现场的自旋锁方式使得添加操作每次只被一个进程访问。
具体的添加过程是将当前进程所对应的等待队列结点wait添加到等待队列头结点q之后。具体来说,就是将new->task_list结点添加到以head->task_list为头指针的双链表当中。另外,通过add_wait_queue_exclusive函数可以将一个互斥进程添加到等待队列当中。从添加过程可以发现,add_wait_queue函数会将非互斥进程添加到等待队列的前部。
static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new)
{
list_add(&new->task_list, &head->task_list);
}
另外, add_wait_queue_exclusive添加函数则会将互斥进程添加到等待队列的末尾。
void add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
wait->flags |= WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
__add_wait_queue_tail(q, wait);
spin_unlock_irqrestore(&q->lock, flags);
}
remove_wait_queue函数用于将等待队列项wait从以q为等待队列头的等待队列中移除,源码如下。
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
spin_lock_irqsave(&q->lock, flags);
__remove_wait_queue(q, wait);
spin_unlock_irqrestore(&q->lock, flags);
}
static inline void __remove_wait_queue(wait_queue_head_t *head, wait_queue_t *old)
{
list_del(&old->task_list);
}
有了上述的基础,那么移除函数就简单了许多。
3.在等待队列上睡眠
如何实现进程的阻塞?大致过程就是将当前进程的状态设置成睡眠状态,然后将这个进程加入到等待队列即可。在linux内核中有一组函数接口来实现这个功能。
void __sched interruptible_sleep_on(wait_queue_head_t *q)
{
sleep_on_common(q, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
EXPORT_SYMBOL(interruptible_sleep_on);
long __sched
interruptible_sleep_on_timeout(wait_queue_head_t *q, long timeout)
{
return sleep_on_common(q, TASK_INTERRUPTIBLE, timeout);
}
EXPORT_SYMBOL(interruptible_sleep_on_timeout);
void __sched sleep_on(wait_queue_head_t *q)
{
sleep_on_common(q, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
EXPORT_SYMBOL(sleep_on);
long __sched sleep_on_timeout(wait_queue_head_t *q, long timeout)
{
return sleep_on_common(q, TASK_UNINTERRUPTIBLE, timeout);
}
EXPORT_SYMBOL(sleep_on_timeout);
通过上述源码,你可以发现这些函数在内部都调用了sleep_on_common函数,通过传递不同的值来实现不同的功能。而这个通用函数的三个参数分别关注的是:进程要加入到那个等待队列?进程是那种睡眠状态(TASK_UNINTERRUPTIBLE还是TASK_INTERRUPTIBLE)?进程睡眠的时间?
static long __sched
sleep_on_common(wait_queue_head_t *q, int state, long timeout)
{
unsigned long flags;
wait_queue_t wait;
init_waitqueue_entry(&wait, current);
__set_current_state(state);
spin_lock_irqsave(&q->lock, flags);
__add_wait_queue(q, &wait);
spin_unlock(&q->lock);
timeout = schedule_timeout(timeout);
spin_lock_irq(&q->lock);
__remove_wait_queue(q, &wait);
spin_unlock_irqrestore(&q->lock, flags);
return timeout;
}
在此函数中,首先定义了一个等待队列项结点,通过 init_waitqueue_entry函数对其进行初始化。可以从下述初始化源码中看到,此时该等待队列项指向当前当前进程。而唤醒函数指针func则指向内核自定义的一个默认唤醒函数default_wake_function。
static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p)
{
q->flags = 0;
q->private = p;
q->func = default_wake_function;
}
如果想设置自己的唤醒函数q->func,则可以通过 init_waitqueue_func_entry(wait_queue_t *q, wait_queue_func_t func) 函数设置。
static inline void init_waitqueue_func_entry(wait_queue_t *q, wait_queue_func_t func) {
q->flags = 0;
q->private = NULL;
q->func = func;
}
初始化完毕后,通过__set_current_state函数将当前进程的状态设置成state。接着,在自旋锁的保护下,将当前进程对应的等待队列结点插入到等待队列链表当中。更重要的是,在schedule_timeout函数中不仅要设置进程的睡眠时间(以jiffies为单位的),还要使用schedule函数进行重新调度。一旦使用了schedule函数后,也就意味这当前这个进程真正的睡眠了,那么接下来的代码会在它唤醒后执行。当该进程被唤醒后(资源可用时),会从等待队列中将自己对应的那个等待队列结点wait移除。
上述过程都是在自旋锁保护下进行的,并且在整个执行过程中不可被其他中断所打断。现在再回过头去看一开始的那四个睡眠函数接口,你就明白了它们各自的不同之处了。
4.唤醒函数
唤醒函数会唤醒以x为头结点的等待队列中的等待队列项所对应的进程。与睡眠函数类似,内核中也有一组函数可以对阻塞的进程进行唤醒。通常哪段代码促使等待条件达成,它就负责随后调用wake_up()函数。
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
#define wake_up_nr(x, nr) __wake_up(x, TASK_NORMAL, nr, NULL)
#define wake_up_all(x) __wake_up(x, TASK_NORMAL, 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)
通过上述代码,我们可以发现这些唤醒函数均调用了__wake_up函数。__wake_up函数的四个参数分别指:头结点指针、唤醒进程的类型、唤醒进程的数量和一个附加的void型指针变量。
void __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函数又通过传递不同的参数调用__wake_up_common函数来实现不同的唤醒功能。
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, int wake_flags, void *key)
{
wait_queue_t *curr, *next;
list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
unsigned flags = curr->flags;
if (curr->func(curr, mode, wake_flags, key) &&(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
list_for_each_entry_safe函数将遍历整个等待队列中的链表,通过每次的逻辑判断来唤醒相应的进程。这个if语句初看起来有点麻烦,不过我们一点一点的将它拆解。
curr->func(curr, mode, sync, key):即执行默认的唤醒函数,将指定的进程curr以mode方式唤醒。成功唤醒返回1;否则,返回0;
(flags & WQ_FLAG_EXCLUSIVE):判断当前进程是否以互斥形式唤醒。是互斥形式则返回1;否则返回0;
!–nr_exclusive:nr_exclusive为需要唤醒的互斥进程的数量。
这三个部分是通过逻辑与连接起来的。根据逻辑与的运算规则,只有当第一部分为真时才会判断第二部分的值,依次再判断第三部分的值。
通过上述的等待队列的添加过程我们知道,等待队列中前面均是非互斥进程,后面才是互斥进程。因此,唤醒函数总先唤醒全部的非互斥进程。因为当__wake_up_commom函数每一次去判断if语时,总会“不自觉”去执行默认的唤醒函数(除非唤醒失败,那么会退出遍历宏);当全部的非互斥进程被唤醒后,第二个判断条件也就成立了。因此__wake_up_commom函数会依次唤醒nr_exclusive个互斥进程;当–nr_exclusive为0时(!–nr_exclusive也就为真),整个遍历过程也恰好结束,而此时if语句的三个判断条件才刚好满足(这段代码技巧性很强)。
代码中curr->func最终调用的是default_wake_function。default_wake_function会调用try_to_wake_up,该函数负责将进程设置为 TASK_RUNNING状态,调用activate_task将此进程放入可执行队列,如果被唤醒的进程优先级比当前正在运行的进程的优先级高,还有设置need_resched标志。
5.有条件的睡眠
与睡眠函数不同,条件睡眠是指当某些条件发生时,这个进程才会加入到等待队列当中。关于条件睡眠有下列的宏:
wait_event(wq, condition)
wait_event_timeout(wq, condition, timeout)
wait_event_interruptible(wq, condition)
wait_event_interruptible_timeout(wq, condition, timeout)
关于条件睡眠,虽然函数实现与睡眠函数不同,但是基本思想是相似的。
wait_event源码如下:
/*
条件不满足时等待。condition为true直接返回,不等待。
*/
#define wait_event(wq, condition) \
do { \
if (condition) \
break; \
__wait_event(wq, condition); \
} while (0)
wait_event调用__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)
__wait_event的执行过程:
1. 先声明一个等待队列成员__wait,
2. 调用prepare_to_wait(加入队列,并设置进程状态)。
3. 调用schedule,放弃CPU。
4. 被唤醒后执行schedule之后的代码,即for(;;)下一次循环,先将进程加入队列并设置状态,再次检查condition。如果条件不满足,则继续等待,如果条件满足,则执行finish_wait,finish_wait函数将进程从等待队列中移除,并设置进程状态为TASK_RUNNING(当前进程正占用CPU)。
参考链接:
http://edsionte.com/techblog/archives/1854