03-等待队列
8.1.1 等待队列
在Linux驱动程序中,可以使用等待队列(wait queue)来实现阻塞进程的唤醒。wait queue很早就作为一个基本的功能单位出现在Linux内核里了,它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现内核中的异步事件通知机制。等待队列可以用来同步对系统资源的访问,第7章中所讲述的信号量在内核中也依赖等待队列来实现。
Linux 2.6提供如下关于等待队列的操作。
1.定义“等待队列头”
wait_queue_head_t my_queue;
2.初始化“等待队列头”
init_waitqueue_head(&my_queue);
而下面的DECLARE_WAIT_QUEUE_HEAD()宏可以作为定义并初始化等待队列头的“快捷方式”。
DECLARE_WAIT_QUEUE_HEAD (name)
3.定义等待队列
DECLARE_WAITQUEUE(name, tsk)
该宏用于定义并初始化一个名为name的等待队列。
4.添加/移除等待队列
void fastcall add_wait_queue(wait_queue_head_t q, wait_queue_t wait);
void fastcall remove_wait_queue(wait_queue_head_t q, wait_queue_t wait);
add_wait_queue()用于将等待队列wait添加到等待队列头q指向的等待队列链表中,而remove_wait_queue()用于将等待队列wait从附属的等待队列头q指向的等待队列链表中移除。
5.等待事件
wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)
等待第1个参数queue作为等待队列头的等待队列被唤醒,而且第2个参数condition必须满足,否则继续阻塞。wait_event()和wait_event_interruptible()的区别在于后者可以被信号打断,而前者不能。加上_timeout后的宏意味着阻塞等待的超时时间,以jiffy为单位,在第3个参数的timeout到达时,不论condition是否满足,均返回。
wait()的定义如代码清单8.3所示,从其源代码可以看出,当condition满足时,wait_event()会立即返回,否则,阻塞等待condition满足。
代码清单8.3 wait_event()函数
1 #define wait_event(wq, condition) \
2 do { \
3 if (condition) /条件满足立即返回/ \
4 break; \
5 __wait_event(wq, condition);/添加等待队列并阻塞/
6 } while (0)
7
8 #define __wait_event(wq, condition) \
9 do { \
10 DEFINE_WAIT(__wait); \
11 \
12 for (;;) { \
13 prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE); \
14 if (condition) \
15 break; \
16 schedule();/放弃CPU/ \
17 } \
18 finish_wait(&wq, &__wait); \
19 } while (0)
20
21 void
22 prepare_to_wait(wait_queue_head_t q, wait_queue_t wait, int state)
23 {
24 unsigned long flags;
25
26 wait->flags &= ~WQ_FLAG_EXCLUSIVE;
27 spin_lock_irqsave(&q->lock, flags);
28 if (list_empty(&wait->task_list))
29 __add_wait_queue(q, wait); / 加入等待队列 /
30 set_current_state(state); / 设置进程状态 /
31 spin_unlock_irqrestore(&q->lock, flags);
32 }
33
34 void finish_wait(wait_queue_head_t q, wait_queue_t wait)
35 {
36 unsigned long flags;
37
38 __set_current_state(TASK_RUNNING); / 恢复进程状态为TASK_RUNNING /
39
40 if (!list_empty_careful(&wait->task_list)) {
41 spin_lock_irqsave(&q->lock, flags);
42 list_del_init(&wait->task_list);
43 spin_unlock_irqrestore(&q->lock, flags);
44 }
45 }
6.唤醒队列
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
上述操作会唤醒以queue作为等待队列头的所有等待队列中所有属于该等待队列头的等待队列对应的进程。
wake_up()应该与wait_event()或wait_event_timeout()成对使用,而wake_up_interruptible()则应与wait_event_interruptible()或wait_event_interruptible_timeout()成对使用。wake_up()可唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE的进程,而wake_up_interruptible()只能唤醒处于TASK_INTERRUPTIBLE的进程。
7.在等待队列上睡眠
sleep_on(wait_queue_head_t *q );
interruptible_sleep_on(wait_queue_head_t *q );
sleep_on()函数的作用就是将目前进程的状态置成TASK_UNINTERRUPTIBLE,并定义一个等待队列,之后把它附属到等待队列头q,直到资源可获得,q引导的等待队列被唤醒。
interruptible_sleep_on()与sleepon()函数类似,其作用是将目前进程的状态置成TASK INTERRUPTIBLE,并定义一个等待队列,之后把它附属到等待队列头q,直到资源可获得,q引导的等待队列被唤醒或者进程收到信号。
sleep_on()函数应该与wake_up()成对使用,interruptible_sleep_on()应该与wake_up_interruptible()成对使用。
代码清单8.4和8.5分别列出了sleep_on()和interruptible_sleep_on()函数的源代码。
代码清单8.4 sleep_on()函数
1 void _ _sched sleep_on(wait_queue_head_t *q)
2 {
3 sleep_on_common(q, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
4 }
5
6 static long _ _sched
7 sleep_on_common(wait_queue_head_t *q, int state, long timeout)
8 {
9 unsigned long flags;
10 wait_queue_t wait;
11
12 init_waitqueue_entry(&wait, current);
13
14 _ _set_current_state(state);
15
16 spin_lock_irqsave(&q->lock, flags);
17 _ _add_wait_queue(q, &wait); / 加入等待队列 /
18 spin_unlock(&q->lock);
19 timeout = schedule_timeout(timeout); / 进程切换 /
20 spin_lock_irq(&q->lock);
21 _ _remove_wait_queue(q, &wait); / 移除等待队列 /
22 spin_unlock_irqrestore(&q->lock, flags);
23
24 return timeout;
25 }
代码清单8.5 interruptible_sleep_on()函数
1 void _ _sched interruptible_sleep_on(wait_queue_head_t *q)
2 {
3 sleep_on_common(q, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
4 }
从代码清单8.4和8.5可以看出,不论是sleep_on()还是interruptible_sleep_on(),都会调用sleep_on_common(),其流程如下。
(1)定义并初始化一个等待队列,将进程状态改变为TASK_UNINTERRUPTIBLE(不能被信号打断)或TASK_INTERRUPTIBLE(可以被信号打断),并将等待队列添加到等待队列头。
(2)通过schedule_timeout()放弃CPU(这两个函数传递的超时参数都是MAXSCHEDULE TIMEOUT,即不会发生超时),调度其他进程执行。
(3)进程被其他地方唤醒,将等待队列移出等待队列头。
在内核中使用set_currentstate()函数或 _add_current_state()函数来实现目前进程状态的改变,直接采用current->state = TASK_UNINTERRUPTIBLE类似的赋值语句也是可行的。通常而言,set_currentstate()函数在任何环境下都可以使用,不会存在并发问题,但是效率要低于 _add_current_state()。
因此,在许多设备驱动中,并不调用sleep_on()或interruptible_sleep_on(),而是亲自进行进程的状态改变和切换,如代码清单8.6所示。
代码清单8.6 在驱动程序中改变进程状态并调用schedule()
1 static ssize_t xxx_write(struct file file, const char buffer, size_t count,
2 loff_t *ppos)
3 {
4 ...
5 DECLARE_WAITQUEUE(wait, current); / 定义等待队列 /
6 add_wait_queue(&xxx_wait, &wait); / 添加等待队列 /
7
8 ret = count;
9 / 等待设备缓冲区可写 /
10 do {
11 avail = device_writable(...);
12 if (avail < 0)
13 _ _set_current_state(TASK_INTERRUPTIBLE);/ 改变进程状态 /
14
15 if (avail < 0) {
16 if (file->f_flags &O_NONBLOCK) {/ 非阻塞 /
17 if (!ret)
18 ret = - EAGAIN;
19 goto out;
20 }
21 schedule(); /* 调度其他进程执行
22 if (signal_pending(current)) {/ 如果是因为信号唤醒 /
23 if (!ret)
24 ret = - ERESTARTSYS;
25 goto out;
26 }
27 }
28 }while (avail < 0);
29
30 / 写设备缓冲区 /
31 device_write(...)
32 out:
33 remove_wait_queue(&xxx_wait, &wait);/ 将等待队列移出等待队列头 /
34 set_current_state(TASK_RUNNING);/ 设置进程状态为 TASK_RUNNING/
35 return ret;
36 }
读懂代码清单8.6对理解Linux进程状态切换非常重要,所以提请读者反复阅读此段代码(尤其注意其中黑体的部分),直至完全领悟。几个要点如下。
① 如果是非阻塞访问(O_NONBLOCK被设置),设备忙时,直接返回“-EAGAIN”。
② 对于阻塞访问,会进行状态切换并显式通过“schedule()”调度其他进程执行;
③ 醒来的时候要注意,由于调度出去的时候,进程状态是TASK_INTERRUPTIBLE,即浅度睡眠,因此唤醒它的有可能是信号,因此,我们首先通过“signal_pending(current)”了解是不是信号唤醒的,如果是,立即返回“- ERESTARTSYS”。