有些时候,一个系统调用可能无法马上取到或者送出数据:一个温度采集器如果没有采用中断或者轮询的策略,而是在用户发出请求时才进行采集,并在一定的时间后返回结果。如果用户程序希望调用read或write并且在调用返回时能确保得到想要的结果,那么用户程序应该阻塞,直到有结果或者错误后返回,用户程序的阻塞体现为进程的睡眠,也即系统调用中将进程状态切换为睡眠态。
 
  睡眠和等待队列

  一个进程的睡眠意味着它的进程状态标识符被置为睡眠,并且从调度器的运行队列中去除,直到某些事件的发生将它们从睡眠态中唤醒,在睡眠态,该进程将不被CPU调度,并且,如果不被唤醒,它将永远不被运行。

  在驱动中很容易通过调度等方式使当前进程睡眠,但是进程并不是在任何时候都是可以进入睡眠状态的。

    第一条规则是:当运行在原子上下文时不能睡眠:比如持有自旋锁,顺序锁或者RCU锁。

    在关中断中也不能睡眠。

    持有信号量时睡眠是合法的,但它所持有的信号量不应该影响唤醒它的进程的执行。另外任何等待该信号量的线程也将睡眠,因此发生在持有信号量时的任何睡眠都应当短暂。

    进程醒来后应该进行等待事件的检查,以确保它确实发生了。

  等待队列可以完成进程的睡眠并在事件发生时唤醒它,它由一个进程列表组成。在 Linux 中, 一个等待队列由一个"等待队列头"来管理:

linux/wait.h
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

  由于睡眠的进程很有可能在等待一个中断来改变某些状态,或通告某些事件的发生,那么中断上下文很有可能修改该等待队列,所以该结构中的自旋锁lock必须考虑禁中断,也即使用spin_lock_irqsave。

  队列中的成员是如下数据结构的实例,它们组成了一个双向链表:

typedef struct __wait_queue wait_queue_t;
typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int flags, void *key);
int default_wake_function(wait_queue_t *wait, unsigned mode, int flags, void *key);
struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01
void *private;
wait_queue_func_t func;
struct list_head task_list;
};
flags的值或者为0,或者为WQ_FLAG_EXCLUSIVE。后者表示等待进程想要被独占地唤醒。
private指针指向等待进程的task_struct实例。该变量本质上可以指向任何私有数据,单内核只有很少情况下才这么用。
调用func,唤醒等待进程。
task_list用作一个链表元素,将wait_queue_t实例放置到等待队列中。

  为了使用等待队列,通常需要如下步骤:首先应该建立一个等待队列头:

DECLARE_WAIT_QUEUE_HEAD(name);

  另外一种方法是静态声明,并显式初始化它:

wait_queue_head_t wait_queue;
init_waitqueue_head(&wait_queue);

  接着为使得当前进程进入睡眠,并等待某一事件的发生,需要将它加入到等待队列中,内核提供了以下函数完成此功能:

wait_event(queue, condition);
wait_event_interruptible(queue, condition);
wait_event_timeout(queue, condition, timeout);
wait_event_interruptible_timeout(queue, condition, timeout);

  在所有的形式中,参数queue是要等待的队列头,由于这几个函数都是通过宏实现的,这里的队列头不是指针类型,而是对它的直接使用。条件condition是一个被这些宏在睡眠前后所要求值的任意的布尔表达式。直到条件求值为真,进程持续睡眠。

  通过wait_event进入睡眠的进程是不可中断的,此时进程的state成员置TASK_UNINTERRUPTIBLE位。但是它应该被wait_event_interruptible所替代,它可以被信号中断,这意味着用户程序在等待的过程中可以通过信号中断程序的执行。一个不能被信号中断的程序很容易激怒使用它的用户。wait_event函数没有返回值,而wait_event_interruptible有一个可以识别睡眠被某些信号打断的返回值-ERESTARTSYS。

  wait_event_timeout和wait_event_interruptible_timeout意味着等待一段时间,它以滴答数表示,在这个时间期间超时后,该宏返回一个0值,而不管事件是否发生。

  最后,我们需要在其他进程或者线程(也可能是中断)中通过相对应的函数,唤醒这些队列上沉睡的进程。内核提供了如下函数:

void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
wake_up唤醒所有的在给定队列上等待的进程。
wake_up_interruptible唤醒所有的在给定队列上等待的可中断的睡眠的进程。
 

  尽管wake_up可以替代wake_up_interruptible的功能,但是它们应该使用与wait_event对应的函数。通过等待队列实现一个管道的读写是可行的,内核中fs/pipe.c对管道的实现就是基于等待队列实现的,尽管它有些复杂。另外对于设备驱动来说,一个温度采集器在收到读数据请求后,该进程被放入等待队列,然后唤醒它的布尔变量在该设备对应的中断处理程序中被置为真。

  注意 wake_up_interruptible的调用可能使多个个睡眠进程醒来,而它们又是独占访问某一资源,如何使仅一个进程看到这个真值,这就是WQ_FLAG_EXCLUSIVE的作用,其他进程将继续睡眠。

  等待队列实现原理

  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 () 
DEFINE_WAIT注册了一个名为__wait的队列元素,其中包含一个名为autoremove_wake_function的钩子函数,它用来唤醒的进程并将该元素从等待队列中删除。
prepare_to_wait用来将队列元素计入等待队列,并指定进程的state状态标识为TASK_UNINTERRUPTIBLE,当然对应wait_event_interruptible,则是TASK_INTERRUPTIBLE。
for无限循环决定了当前进程在不满足condition时总是被调度,其他进程将替换该进程执行。并且这个循环实际上永远只执行一次,并且只在唤醒时直接
在满足条件时,finish_wait将进程状态设置为TASK_RUNNING,并从等待队列中将其移除。

  需要仔细考虑的是for循环的执行,显然它可能执行一次,也可能是多次,当condition不满足时,将会产生调度,而在此被调度时,将执行for的下一次循环,那么prepare_to_wait不是每次都添加一次__wait元素吗?查看prepare_to_wait代码可以发现,只有wait->task_list指向的链表为空时,也即__wait元素没有加入任何其他等待队列时才会把它加入到当前等待队列中,这也表明一个等待队列元素只能加入一个等待队列。

void prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
unsigned long flags;
wait->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
if (list_empty(&wait->task_list))
__add_wait_queue(q, wait);
set_current_state(state);
spin_unlock_irqrestore(&q->lock, flags);
}

  唤醒一个等待队列是通过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_locked(x) __wake_up_locked((x), TASK_NORMAL)

  这里分析它们的核心实现:

kernel/sched.c
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, , key);
spin_unlock_irqrestore(&q->lock, flags);
}

  __wake_up首先获取了自旋锁,然后调用__wake_up_common。该函数通过list_for_each_entry_safe遍历等待队列,如果没有设置独占标志,则根据mode唤醒每个睡眠的进程。nr_exclusiv表示需要唤醒的设置了独占标志进程的数目,它在wake_up中设置为1,表明当处理了一个含有WQ_FLAG_EXCLUSIVE标志进程后,将不再处理,独占标志的意义也在于此。另外看到这里通过func指针执行了真正的唤醒函数。

kernel/sched.c
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;
}
}

  如果含有独占标志的进程并不位于队列尾部,将导致其后的不含有该标志的进程无法执行,prepare_to_wait_exclusive解决了该问题,它总是将含有独占标志的进程插入到队列尾部,该函数被wait_event_interruptible_exclusive宏调用。

  转自:http://blog.chinaunix.net/uid-20608849-id-3126863.html

Unix环境高级编程-阻塞访问原理——等待队列的更多相关文章

  1. (十三) [终篇] 一起学 Unix 环境高级编程 (APUE) 之 网络 IPC:套接字

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  2. Unix 环境高级编程 (APUE) 之 网络 IPC:套接字

    一起学 Unix 环境高级编程 (APUE) 之 网络 IPC:套接字 . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级 ...

  3. (八) 一起学 Unix 环境高级编程 (APUE) 之 信号

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  4. (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  5. (五) 一起学 Unix 环境高级编程 (APUE) 之 进程环境

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  6. (六) 一起学 Unix 环境高级编程 (APUE) 之 进程控制

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  7. (九) 一起学 Unix 环境高级编程 (APUE) 之 线程

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  8. (十一) 一起学 Unix 环境高级编程 (APUE) 之 高级 IO

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  9. (十二) 一起学 Unix 环境高级编程 (APUE) 之 进程间通信(IPC)

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

随机推荐

  1. NiftyNet开源平台使用

    NiftyNet是一款开源的卷积神经网络平台,专门针对医学图像处理分析,上一篇博客已经详细介绍了这个平台,接下来让我简单介绍一下目前我了解到的使用方法.更详细的使用方法.以及配置过程请查看NiftyN ...

  2. hr相关的

    1.自我介绍? 2.为什么要离职?之前几家公司离职的原因分别是什么? 3.从上一家公司离职的原因? 4.目前就职的公司最大的收获是什么? 从上家公司到目前公司的最大收获是什么,要突出目标明确,在当前的 ...

  3. mysql数据库操作语句整合

    查看版本:select version();显示当前时间:select now(); 注意:在语句结尾要使用分号; 远程连接 一般在公司开发中,可能会将数据库统一搭建在一台服务器上,所有开发人员共用一 ...

  4. SpringMVC(十二) RequestMapping使用POJO作为参数

    将一个普通的JAVA类对象作为一个参数传入. POJO类Address: package com.tiekui.springmvc.pojo; public class Address { priva ...

  5. VeeamOne(Free Edition 9.5 )-安装与配置

    ---恢复内容开始--- Veeam ONE则主要用于监控平台之用,可以监控Veeam Backup & Replication的备份及同步情况,也可以监控VMware vSphere虚拟化平 ...

  6. python基础一 ------顺序结构队列的python实现

    队列:先进先出的线性表(FIFO),只允许在一段插入并在另一端取出 以下是python实现 #-*-coding:utf-8-*- #顺序存储队列的python实现 class Queue(objec ...

  7. python基础一 ------如何统计一个列表元素的频度

    如何统计一个列表元素的频度 两个需求: 1,统计一个随机序列[1,2,3,4,5,6...]中的出现次数前三的元素及其次数 2,统计一片英文文章中出现次数前10 的单词 两种方法: 1,普通的for循 ...

  8. 如何避免Android程序的ANR?

    In Android, the system guards against applications that are insufficiently responsive for a period o ...

  9. MySQL(六)

    自关联 设计省信息的表结构provinces id ptitle 设计市信息的表结构citys id ctitle proid citys表的proid表示城市所属的省,对应着provinces表的i ...

  10. 2017.07.11【NOIP提高组】模拟赛B组

    Summary 今天的比赛打得还不错,第一题被同桌灌输的贪心,纯模拟洗脑了,然后steal的看了一下,发现怎么也对不了,一直在检查.最后10分钟才找出反例,推出动态规划方程,没有想到怎么转移,比赛就结 ...