1. 1.       等待队列数据结构

等待队列由双向链表实现,其元素包括指向进程描述符的指针。每个等待队列都有一个等待队列头(wait queue head),等待队列头是一个类型为wait_queque_head_t的数据结构:

struct __wait_queue_head {

spinlock_t lock;

struct list_head task_list;

};

typedef struct __wait_queue_head wait_queue_head_t;

其中,lock是用来防止并发访问,task_list字段是等待进程链表的头。

等待队列链表中的元素类型为wait_queue_t,我们可以称之为等待队列项:

struct __wait_queue {

unsigned int flags;

#define WQ_FLAG_EXCLUSIVE        0x01

void *private;

wait_queue_func_t func;

struct list_head task_list;

};

typedef struct __wait_queue wait_queue_t;

每一个等待队列项代表一个睡眠进程,该进程等待某一事件的发生。它的描述符地址通常放在private字段中。Task_list字段中包含的是指针,由这个指针把一个元素链接到等待相同事件的进程链表中。

等待队列元素的func字段用来表示等待队列中睡眠进程应该用什么方式唤醒(互斥方式和非互斥方式)。

整个等待队列的结构如下图所示:

下面看看等待队列的工作原理。

  1. 2.       等待队列的睡眠过程

使用等待队列前通常先定义一个等待队列头:static wait_queue_head_t wq ,然后调用wait_event_*函数将等待某条件condition的当前进程插入到等待队列wq中并睡眠,一直等到condition条件满足后,内核再将睡眠在等待队列wq上的某一进程或所有进程唤醒。

定义等待队列头没什么好讲的,下面从调用wait_event_*开始分析:

这里我们举比较常用的wait_event_interruptible:

/**

* wait_event_interruptible - sleep until a condition gets true

* @wq: the waitqueue to wait on

* @condition: a C expression for the event to wait for

*

* The process is put to sleep (TASK_INTERRUPTIBLE) until the

* @condition evaluates to true or a signal is received.

* The @condition is checked each time the waitqueue @wq is woken up.

*

* wake_up() has to be called after changing any variable that could

* change the result of the wait condition.

*

* The function will return -ERESTARTSYS if it was interrupted by a

* signal and 0 if @condition evaluated to true.

*/

#define wait_event_interruptible(wq, condition)                               \

({                                                                                 \

int __ret = 0;                                                            \

if (!(condition))                                                        \

__wait_event_interruptible(wq, condition, __ret);         \

__ret;                                                                        \

})

这里很简单,判断一下condition条件是否满足,如果不满足则调用__wait_event_interruptible函数。

#define __wait_event_interruptible(wq, condition, ret)                            \

do {                                                                             \

DEFINE_WAIT(__wait);                                                  \

\

for (;;) {                                                             \

prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE);    \

if (condition)                                                   \

break;                                                     \

if (!signal_pending(current)) {                             \

schedule();                                             \

continue;                                      \

}                                                                \

ret = -ERESTARTSYS;                                              \

break;                                                               \

}                                                                         \

finish_wait(&wq, &__wait);                                         \

} while (0)

__wait_event_interruptible首先定义了一个wait_queue_t类型的等待队列项__wait :

#define DEFINE_WAIT(name)                                                \

wait_queue_t name = {                                                  \

.private    = current,                                     \

.func                   = autoremove_wake_function,                 \

.task_list = LIST_HEAD_INIT((name).task_list),     \

}

可以发现,这里__wait的private成员(通常用来存放进程的描述符)已经被初始化为current, 表示该等待队列项对应为当前进程。func成员为该等待队列项对应的唤醒函数,该进程被唤醒后会执行它,已经被初始化为默认的autoremove_wake_function函数。

然后在一个for (;;) 循环内调用prepare_to_wait函数:

void fastcall 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);

/*

* don't alter the task state if this is just going to

* queue an async wait queue callback

*/

if (is_sync_wait(wait))

set_current_state(state);

spin_unlock_irqrestore(&q->lock, flags);

}

prepare_to_wait做如下两件事,将先前定义的等待队列项__wait插入到等待队列头wq,然后将当前进程设为TASK_INTERRUPTIBLE状态。prepare_to_wait执行完后立马再检查一下condition有没有满足,如果此时碰巧满足了则不必要在睡眠了。如果还没有满足,则准备睡眠。

睡眠是通过调用schedule()函数实现的,由于之前已经将当前进程设置为TASK_INTERRUPTIBLE状态,因而这里再执行schedule()进行进程切换的话,之后就永远不会再调度到该进程运行的,直到该进程被唤醒(即更改为TASK_RUNNING状态)。

这里在执行schedule()切换进程前会先判断一下有没signal过来,如果有则立即返回ERESTARTSYS。没有的话则执行schedule()睡眠去了。

for (;;) 循环的作用是让进程被唤醒后再一次去检查一下condition是否满足。主要是为了防止等待队列上的多个进程被同时唤醒后有可能其他进程已经抢先把资源占有过去造成资源又变为不可用,因此最好再判断一下。(当然,内核也提供了仅唤醒一个或多个进程(独占等待进程)的方式,有兴趣的可以参考相关资料)

进程被唤醒后最后一步是调用finish_wait(&wq, &__wait)函数进行清理工作。finish_wait将进程的状态再次设为TASK_RUNNING并从等待队列中删除该进程。

void fastcall finish_wait(wait_queue_head_t *q, wait_queue_t *wait)

{

unsigned long flags;

__set_current_state(TASK_RUNNING);

if (!list_empty_careful(&wait->task_list)) {

spin_lock_irqsave(&q->lock, flags);

list_del_init(&wait->task_list);

spin_unlock_irqrestore(&q->lock, flags);

}

}

再往后就是返回你先前调用wait_event_interruptible(wq, condition)被阻塞的地方继续往下执行。

  1. 3.       等待队列的唤醒过程

直到这里我们明白等待队列是如何睡眠的,下面我们分析等待队列的唤醒过程。

使用等待队列有个前提,必须得有人唤醒它,如果没人唤醒它,那么同眠在该等待队列上的所有进程岂不是变成“僵尸进程”了。

对于设备驱动来讲,通常是在中断处理函数内唤醒该设备的等待队列。驱动程序通常会提供一组自己的读写等待队列以实现上层(user level)所需的BLOCK和O_NONBLOCK操作。当设备资源可用时,如果驱动发现有进程睡眠在自己的读写等待队列上便会唤醒该等待队列。

唤醒一个等待队列是通过wake_up_*函数实现的。这里我们举对应的wake_up_interruptible作为例子分析。定义如下:

#define wake_up_interruptible(x)   __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)

这里的参数x即要唤醒的等待队列对应的等待队列头。唤醒TASK_INTERRUPTIBLE类型的进程并且默认唤醒该队列上所有非独占等待进程和一个独占等待进程。

__wake_up定义如下:

/**

* __wake_up - wake up threads blocked on a waitqueue.

* @q: the waitqueue

* @mode: which threads

* @nr_exclusive: how many wake-one or wake-many threads to wake up

* @key: is directly passed to the wakeup function

*/

void fastcall __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, 1, key);

spin_unlock_irqrestore(&q->lock, flags);

preempt_check_resched_delayed();

}

__wake_up 简单的调用__wake_up_common进行实际唤醒工作。

__wake_up_common定义如下:

/*

* The core wakeup function.  Non-exclusive wakeups (nr_exclusive == 0) just

* wake everything up.  If it's an exclusive wakeup (nr_exclusive == small +ve

* number) then we wake all the non-exclusive tasks and one exclusive task.

*

* There are circumstances in which we can try to wake a task which has already

* started to run but is not in state TASK_RUNNING.  try_to_wake_up() returns

* zero in this (rare) case, and we handle it by continuing to scan the queue.

*/

static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,

int nr_exclusive, int sync, void *key)

{

struct list_head *tmp, *next;

list_for_each_safe(tmp, next, &q->task_list) {

wait_queue_t *curr = list_entry(tmp, wait_queue_t, task_list);

unsigned flags = curr->flags;

if (curr->func(curr, mode, sync, key) &&

(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)

break;

}

}

__wake_up_common循环遍历等待队列内的所有元素,分别执行其对应的唤醒函数。

这里的唤醒函数即先前定义等待队列项DEFINE_WAIT(__wait)时默认初始化的autoremove_wake_function函数。autoremove_wake_function最终会调用try_to_wake_up函数将进程置为TASK_RUNNING状态。这样后面的进程调度便会调度到该进程,从而唤醒该进程继续执行。

==================================================================================================

在软件开发中任务经常由于某种条件没有得到满足而不得不进入睡眠状态,然后等待条件得到满足的时候再继续运行,进入运行状态。这种需求需要等待队列机制的支持。Linux中提供了等待队列的机制,该机制在内核中应用很广泛。

在Linux内核中使用等待队列的过程很简单,首先定义一个wait_queue_head,然后如果一个task想等待某种事件,那么调用wait_event(等待队列,事件)就可以了。
     等待队列应用广泛,但是内核实现却十分简单。其涉及到两个比较重要的数据结构:__wait_queue_head,该结构描述了等待队列的链头,其包含一个链表和一个原子锁,结构定义如下:

struct __wait_queue_head {

spinlock_t lock;                    /* 保护等待队列的原子锁 */

struct list_head task_list;          /* 等待队列 */

};

__wait_queue,该结构是对一个等待任务的抽象。每个等待任务都会抽象成一个wait_queue,并且挂载到wait_queue_head上。该结构定义如下:

struct __wait_queue {

unsigned int flags;

void *private;                       /* 通常指向当前任务控制块 */

/* 任务唤醒操作方法,该方法在内核中提供,通常为autoremove_wake_function */

wait_queue_func_t func;

struct list_head task_list;              /* 挂入wait_queue_head的挂载点 */

};

Linux中等待队列的实现思想如下图所示,当一个任务需要在某个wait_queue_head上睡眠时,将自己的进程控制块信息封装到wait_queue中,然后挂载到wait_queue的链表中,执行调度睡眠。当某些事件发生后,另一个任务(进程)会唤醒wait_queue_head上的某个或者所有任务,唤醒工作也就是将等待队列中的任务设置为可调度的状态,并且从队列中删除。

使用等待队列时首先需要定义一个wait_queue_head,这可以通过DECLARE_WAIT_QUEUE_HEAD宏来完成,这是静态定义的方法。该宏会定义一个wait_queue_head,并且初始化结构中的锁以及等待队列。当然,动态初始化的方法也很简单,初始化一下锁及队列就可以了。

一个任务需要等待某一事件的发生时,通常调用wait_event,该函数会定义一个wait_queue,描述等待任务,并且用当前的进程描述块初始化wait_queue,然后将wait_queue加入到wait_queue_head中。函数实现流程说明如下:

1、用当前的进程描述块(PCB)初始化一个wait_queue描述的等待任务。

2、在等待队列锁资源的保护下,将等待任务加入等待队列。

3、判断等待条件是否满足,如果满足,那么将等待任务从队列中移出,退出函数。

4、 如果条件不满足,那么任务调度,将CPU资源交与其它任务。

5、 当睡眠任务被唤醒之后,需要重复(2)、(3)步骤,如果确认条件满足,退出等待事件函数。

等待队列编程接口

序号

编程接口

使用说明

1

wait_event

这是一个宏,让当前任务处于等待事件状态。输入参数如下:

@wq:等待队列

@conditions:等待条件

2

wait_event_timeout

功能与wait_event类似,多了一个超时机制。参数中多了一项超时时间。

3

wait_event_interruptible

这是一个宏,与前两个宏相比,该宏定义的等待能够被消息唤醒。如果被消息唤醒,那么返回- ERESTARTSYS。输入参数如下:

@wq:等待队列

@condition:等待条件

@rt:返回值

4

wait_event_interruptible_timeout

与(3)相比,多了超时机制

5

wake_up

唤醒等待队列中的一个任务

6

wake_up_all

唤醒等待队列中的所有任务

Linux中等待队列的实现的更多相关文章

  1. linux中的等待队列

    最近看epoll 和 select 都涉及到一个东西叫做设备等待队列,等待队列是如何工作的,内核是怎么管理的?看这篇文章 问题:进程是如何组织起来的?我们知道,进程是有很多种状态的:include/l ...

  2. linux中的阻塞机制及等待队列

    阻塞与非阻塞是设备访问的两种方式.驱动程序需要提供阻塞(等待队列,中断)和非阻塞方式(轮询,异步通知)访问设备.在写阻塞与非阻塞的驱动程序时,经常用到等待队列. 一.阻塞与非阻塞 阻塞调用是没有获得资 ...

  3. linux中的阻塞机制及等待队列【转】

    转自:http://www.cnblogs.com/gdk-0078/p/5172941.html 阻塞与非阻塞是设备访问的两种方式.驱动程序需要提供阻塞(等待队列,中断)和非阻塞方式(轮询,异步通知 ...

  4. 查看linux中的TCP连接数【转】

     转自:http://blog.csdn.net/he_jian1/article/details/40787269 查看linux中的TCP连接数 本文章已收录于:   计算机网络知识库  分类: ...

  5. 深入理解Java AIO(三)—— Linux中的AIO实现

    我们调用的Java AIO底层也是要调用OS的AIO实现,而OS主要也就Windows和Linux这两大类,当然还有Solaris和mac这些小众的. 在 Windows 操作系统中,提供了一个叫做 ...

  6. linux中c多线程同步方法

    https://blog.csdn.net/jkx01whg/article/details/78119189 Linux下提供了多种方式来处理线程同步,最常用的是互斥锁.条件变量和信号量. 一.互斥 ...

  7. Linux中信号量的实现

    如果一个任务获取信号量失败,该任务就必须等待,直到其他任务释放信号量.本文的重点是,在Linux中,当有任务释放信号量之后,如何唤醒正在等待该信号量的任务. 信号量定义如下: struct semap ...

  8. 在 Linux 中安装 Oracle JDK 8 以及 JVM 的类加载机制

    参考资料 该文中的内容来源于 Oracle 的官方文档 Java SE Tools Reference .Oracle 在 Java 方面的文档是非常完善的.对 Java 8 感兴趣的朋友,可以直接找 ...

  9. Linux中find常见用法示例

    ·find   path   -option   [   -print ]   [ -exec   -ok   command ]   {} \; find命令的参数: pathname: find命 ...

随机推荐

  1. 【bzoj2351】[BeiJing2011]Matrix 二维Hash

    题目描述 给定一个M行N列的01矩阵,以及Q个A行B列的01矩阵,你需要求出这Q个矩阵哪些在原矩阵中出现过.所谓01矩阵,就是矩阵中所有元素不是0就是1. 输入 输入文件的第一行为M.N.A.B,参见 ...

  2. list+map

    通常读取数据库表中的一条记录后,可以存储于Hashmap变量中:若要读取多条记录,则依次读取每个记录时,先用Hashmap变量存取,然后将Hashmap加到ArrayList变量中. 注意: 每次读取 ...

  3. CentOS 7下安装pptp服务端手记 ok

    主要配置步骤 1. 安装前检查系统支持 2. 安装必要包 3. 修改相关配置文件 4. 设置开机自动启动 pptpd, iptables 5. iptables配置网络 6. 阿里云ECS可能还需要几 ...

  4. 【JQuery】遍历

    一.前言        接着上一章的内容,继续本章的学习. 二.内容 .add 将元素添加到匹配元素的集合中 .add(object) .add(selector,context) .addSelf ...

  5. Linux内核分析2

    周子轩原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 本次实验是通过分析一个简单 ...

  6. openresty--centos7下开发环境安装

    1. 安装依赖的软件包 yum install readline-devel pcre-devel openssl-devel gcc 2. 安装openresty -- 1. 下载openresty ...

  7. git 撤销上一次 commit

    1.本地 commit,没有推到远程仓库 可以 git reset --soft <commit_id>,commit_id 是要回退到的某一版本 然后再进行修改,再commit, 如果需 ...

  8. 题解【bzoj3529 [SDOI2014]数表】

    Description \(T\) 组询问,定义 \(F(n)=\sum\limits_{d|n}d\).每次给出 \(n,m,a\) 求 \[\sum\limits_{i=1,j=1,F(\gcd( ...

  9. 9.Android UiAutomator正则表达式的使用

    一.正则表达式元字符: 1.一些常用元字符: 元字符 描述 . 表示任意一个字符 \s 空格字符(空格键.tab.换行.换页.回车) \S 非空字符串([^\s]) \d 一个数字(相当于[0-9]中 ...

  10. Exchange-fanout 广播模式

    一.前言 我们看到生产者将消息投递到Queue中,实际上这在RabbitMQ中这种事情永远都不会发生.实际的情况是,生产者将消息发送到Exchange(交换器,下图中的X),由Exchange将消息路 ...