等待队列在Linux内核中用来阻塞或唤醒一个进程,也可以用来同步对系统资源的访问,还可以实现延迟功能
在软件开发中任务经常由于某种条件没有得到满足而不得不进入睡眠状态,然后等待条件得到满足的时候再继续运行,进入运行状态。这种需求需要等待队列机制的支持。Linux中提供了等待队列的机制,该机制在内核中应用很广泛。
在Linux内核中使用等待队列的过程很简单,首先定义一个wait_queue_head,然后如果一个task想等待某种事件,那么调用wait_event(等待队列,事件)就可以了。
等待队列应用广泛,但是内核实现却十分简单。其涉及到两个比较重要的数据结构:__wait_queue_head,该结构描述了等待队列的链头,其包含一个链表和一个原子锁,结构定义如下:
struct wait_queue_head {
spinlock_t lock; /* 保护等待队列的原子锁 */
struct list_head task_list; /* 等待队列 */
};
typedef struct wait_queue_head wait_queue_head_t;
wait_queue_entry,该结构是对一个等待任务的抽象。每个等待任务都会抽象成一个wait_queue_entry,并且挂载到wait_queue_head上。
该结构定义如下:struct wait_queue_entry {
unsigned int flags; //1:互斥进程,0:非互斥进程
void *private; /* 通常指向当前任务控制块 */
/* 任务唤醒操作方法,该方法在内核中提供,通常为autoremove_wake_function */
wait_queue_func_t func;
struct list_head task_list; /* 挂入wait_queue_head的挂载点 */
};
typedef struct wait_queue_entry wait_queue_entry_t;
定义"等待队列":
定义并初始化一个名为name的等待队列 ,注意此处是定义一个wait_queue_t类型的变量name,并将其private与设置为tsk
DECLARE_WAITQUEUE(name,tsk); //任务唤醒操作方法为:default_wake_function
Linux中等待队列的实现思想如下图所示,当一个任务需要在某个wait_queue_head上睡眠时,将自己的进程控制块信息封装到wait_queue_entry中,然后挂载到wait_queue_head的链表中,执行调度睡眠。当某些事件发生后,另一个任务(进程)会唤醒wait_queue_head上的某个或者所有任务,唤醒工作也就是将等待队列中的任务设置为可调度的状态,并且从队列中删除。
使用等待队列时首先需要定义一个wait_queue_head,这可以通过DECLARE_WAIT_QUEUE_HEAD宏来完成,这是静态定义的方法。该宏会定义一个wait_queue_head,并且初始化结构中的锁以及等待队列。当然,动态初始化的方法也很简单,初始化一下锁及队列就可以了。
一个任务需要等待某一事件的发生时,通常调用wait_event,该函数会定义一个wait_queue_entry,描述等待任务,并且用当前的进程描述块初始化wait_queue_entry,然后将wait_queue_entry加入到wait_queue_head中。函数实现流程说明如下:
1、 用当前的进程描述块(PCB)初始化一个wait_queue_entry描述的等待任务。
2、 在等待队列锁资源的保护下,将等待任务加入等待队列。
3、 判断等待条件是否满足,如果满足,那么将等待任务从队列中移出,退出函数。
4、 如果条件不满足,那么任务调度,将CPU资源交与其它任务。
5、 当睡眠任务被唤醒之后,需要重复(2)、(3)步骤,如果确认条件满足,退出等待事件数。
等待队列编程接口
等待队列接口函数介绍
#include <linux/wait.h> //头文件包含
1.定义一个等待队列头
wait_queue_head_t my_queue;
2.初始一个等待队列头
init_waitqueue_head(&my_queue);
定义并初始化一个等待队列头
DECLARE_WAIT_QUEUE_HEAD(my_queue);
3.进程的睡眠操作——条件睡眠
//判断condition条件,决定是否将当前进程推入等待队列(条件为假时)此函数为宏函数
wait_event(wait_queue_head_t wq, int condition);
/*可以被系统消息打断*/
wait_event_interruptible(wait_queue_head_t wq,int condition);
wait_event_timeout(wait_queue_head_t wq, int condition, long timeout);
wait_event_interruptiblble_timeout(wait_queue_head_t wq,int condition, long timeout);
参数wq:表示等待队列头
参数condition:阻塞条件,为假(0)则进入休眠直到wake_up且condition为真条件成立才退出
参数timeout:表示睡眠指定时长(时钟滴答度量,eg.延时2秒=2*HZ)后,自动转入唤醒状态
3.进程的睡眠操作——无条件睡眠(不建议使用,新内核将去掉这些接口,请使用上面的接口)
/*
* These are the old interfaces to sleep waiting for an event.
* They are racy. DO NOT use them, use the wait_event* interfaces above.
* We plan to remove these interfaces.
*/
//将当前进程推入等待队列将其睡眠,wake_up唤醒
sleep_on(wait_queue_head_t *q);
/*可以被系统消息打断*/
interruptible_sleep_on(wait_queue_head_t *q);
long sleep_on_timeout(wait_queue_head_t *q, long timeout);
long interruptible_sleep_on_timeout(wait_queue_head_t *q, long timeout);
参数wq:表示等待队列头
参数timeout:表示睡眠指定时长后,自动转入唤醒状态
4.将非互斥进程插入等待队列链表的第一个位置,需要自己定义wait_queue_t
add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
函数将互斥进程插入等待队列链表的最后一个位置
add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait)
5.进程唤醒函数
wake_up(wait_queue_head_t *wq);
wake_up_all(wait_queue_head_t *wq);
wake_up_interruptible(wait_queue_head_t *wq);
注意事项:
1.唤醒函数和导致睡眠函数要配对使用,如果导致睡眠函数使用带interruptible的,则唤醒函数也要使用interruptible的。
2.在使用wake_up唤醒进程之前要将wait_event中的condition变量的值赋为真,否则该进程被唤醒后会立即再次进入睡眠
3. wake_up()每次只能唤醒一个进程,而且是从队列头开始唤醒的,而wait_event()函数每次会将新建的等待队列插到队列头,因此最后调用wait_event()函数的进程先被唤醒!如果要唤醒某个特定的进程,没有现成的函数,按照本人理解,只能使用wake_up_all()函数唤醒所有进程,然后在通过条件condition来控制(每个进程使用不同的变量来控制,在wake_up_all()函数后只将要唤醒的进程的变量置成真)。