休眠(被阻塞)的进程处于一个特殊的不可执行状态。进程休眠由多种原因,但肯定都是为了等待一些事件。事件可能是一 段时间从文件I/O读取更多数据,或者是某个硬件事件。一个进程还由可能在尝试获取一个已被占用的内核信号量时被迫进入休眠。休眠的一个常见原因就是文件 I/O —— 如进程对一个文件执行了read()操作,而这需要从磁盘里读取。还有,进程在获取键盘输入的时候也需要等待。无论哪种情况,内核的操作都相同:进程把自己标记成休眠状态,从可执行红黑树中移出,放入等待队列,然后调用schedule()选择和执行一个其他进程。唤醒的过程刚好相反:进程被设置为可执行状态,然后再从等待队列中移到可执行红黑树中。

休 眠由两种相关的进程状态:TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 。它们唯一的区别是处于 TASK_UNINTERRUPTIBLE 的进程会忽略信号,而处于 TASK_INTERRUPTIBLE 状态的进程如果接收到一个信号,会被提前唤醒并响应该信号。两种状态的进程位于同一个等待队列上,等待某些事件,不能够运行。

1.等待队列

休 眠通过等待队列进行处理。等待队列是由等待某些事件发生的进程组成的简单链表。内核用wake_queue_head_t来代表等待队列。等待队列可以通 过 DECLARE_WAITQUEUE() 静态创建,也可以由 init_waitqueue_head() 动态创建。进程把自己放入等待队列中并设置成不可执行状态。当与等待队列相关的事件发生的时候,队列上的进程会被唤醒。为了避免产生竞争条件,休眠和唤醒 的实现不能有纰漏。

针对休眠,以前曾经使用过一些简单的接口。但那些接口会带来竞争条件:有可能导致在判定条件变为真后,进程却开始了休眠,那样就会使进程无限期的休眠下去。所以,在内核中进行休眠的推荐操作就相对复杂些:

    /* 'q' 是我们希望休眠的等待队列 */
DEFINE_WAIT(wait); add_wait_queue(q, &wait);
while (!condition) { /* 'condition' 是我们在等待的事件 */
prepare_to_wait(&q, &wait, TASK_INTERRUPTIBLE);
if (signal_pending(current))
/* 处理信号 */
schedule();
}
finish_wait(&q, &wait);

进程通过执行下面几个步骤将自己加入到一个等待队列中:

  1. 调用宏 DEFINE_WAIT() 创建一个等待队列的项。
  2. 调用 add_wait_queue() 把自己加入到队列中(链表操作)。该队列会在进程等待的条件满足时唤醒它。当然我们必须在其他地方撰写相关代码,在事件发生时,对等待队列执行 wake_up() 操作。
  3. 调用 prepare_to_wait() 方法将进程的状态变更为 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 。而且该函数会在必要的情况下将进程加回到等待队列,这是在接下来的循环遍历中所需要的。
  4. 如果状态被设置为 TASK_INTERRUPTIBLE ,则信号唤醒进程。这就是所谓的伪唤醒(唤醒不是因为事件的发生),因此检查并处理信号。
  5. 当进程被唤醒的时候,它会再次检查条件是否为真。如果是,它就退出循环;如果不是,它再次调用 schedule() 并一直重复这步操作。
  6. 当条件满足后,进程将自己设置为 TASK_RUNNING 并调用 finish_wait() 方法把自己移出等待队列。

在上述代码中,函数在schedule()中找到另外一个进程运行,原来的进程进行睡眠,当原来的进程满足条件或者接受到信号时候,会将该进程的状态设置为TASK_RUNNING,此时该进程又可以被调度了,当该进程又一次被调度执行时,接着schedule()下一行代码开始执行,即继续执行while循环,如果条件满足则退出循环,执行finish_wait()函数,该函数会将进程的状态设置为TASK_RUNNING,并从等待队列中删除,如果进程是因为接收到信号而被唤醒,则再继续执行while循环的时候条件不满足时则 signal_pending(current)检查当前进程是否有信号处理,返回不为0表示有信号需要处理,返回 -ERESTARTSYS 表示信号函数处理完毕后重新执行信号函数前的某个系统调用。

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

  }

}

2. 唤醒

唤醒操作通过函数 wake_up() 进行,它会唤醒指定的等待队列上的所有进程。它调用函数 try_to_wake_up() ,该函数负责将进程设置为 TASK_RUNNING 状态,调用 enqueue_task() 将此进程放入红黑树中,如果被唤醒的进程优先级比当前执行的进程优先级高,还要设置 need_resched 标志。通常哪段代码促使等待条件达成,它就要负责随后调用 wake_up() 函数 。举例来说,当磁盘数据到来时,VFS 就要负责对等待队列调用 wake_up() ,以便唤醒队列中等待这些数据的进程。

关于休眠有一点需要注意,存在虚假的唤醒(信号)。有时候进程被唤醒并不是因为它所等待的条件达成了,所以需要用一个循环处理来保证它等待的条件真正达成

3.函数原型

简单休眠

完成唤醒任务的代码还必须能够找到我们的进程,这样才能唤醒休眠的进程。需要维护一个称为等待队列的数据结构。等待队列就是一个进程链表,其中包含了等待某个特定事件的所有进程。
linux维护一个“等待队列头”来管理,wait_queue_head_t,定义在<linux/wait.h>
struct  __wait_queue_head {
 wq_lock_t  lock;
 struct  list_head  task_list;
};
typedef  struct __wait_queue_head  wait_queue_head_t;
初始化方法:
静态方法:
DECLARE_WAIT_QUEUE_HEAD(name)
动态方法:
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);

linux中最简单的休眠方式是下面的宏,
wait_event(queue, condition)  /*进程将被置于非中断休眠(uninterruptible sleep)*/
wait_event_interruptible(queue, condition) /*进程可被信号中断休眠,返回非0值表示休眠被信号中断*/
wait_event_timeout(queue, condition, timeout)    /*等待限定时间jiffy,condition满足其一返回0*/
wait_event_interruptible_timeout(queue, condition, timeout)
queue是等待队列头,传值方式
condition是任意一个布尔表达式,在休眠前后多次对condition求值,为真则唤醒

唤醒进程的基本函数是wake_up
void wake_up(wait_queue_head_t *queue);    /*唤醒等待在给定queue上的所有进程*/
void wake_up_interruptible(wait_queue_head_t *queue);

实践中,一般是wait_event和wake_up,wait_event_interruptible和wake_up_interruptible成对使用

高级休眠
将进程置于休眠的步骤:
(1)分配和初始化一个 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;(2)设置进程状态,标记为休眠。2.6内核中,使用下面的函数:
      void set_current_state(int new_state);
    在 <linux/sched.h> 中定义有几个任务状态:TASK_RUNNING 意思是进程能够运行。有 2 个状态指示一个进程是   在睡眠: TASK_INTERRUPTIBLE 和 TASK_UNTINTERRUPTIBLE

(3)最后一步是放弃处理器。 但必须先检查进入休眠的条件。如果不做检查会引入竞态: 如果在忙于上面的这个过程时有其他的线程刚刚试图唤醒你,你可能错过唤醒且长时间休眠。因此典型的代码下
if (!condition)
     schedule( );    /*调用调度器,并让出CPU*/
如果代码只是从 schedule 返回,则进程处于TASK_RUNNING 状态。 如果不需睡眠而跳过对 schedule 的调用,必须将任务状态重置为 TASK_RUNNING,还必要从等待队列中去除这个进程,否则它可能被多次唤醒。

手工休眠
上面的进程休眠步骤可通过手工设置:
 (1)创建和初始化一个等待队列。常由宏定义完成:
DEFINE_WAIT(my_wait);
name 是等待队列入口项的名字. 也可以用2步来做:
wait_queue_t my_wait;
init_wait(&my_wait);
常用的做法是放一个 DEFINE_WAIT 在循环的顶部,来实现休眠

(2)添加等待队列入口到队列,并设置进程状态:
void prepare_to_wait(wait_queue_head_t *queue,
                               wait_queue_t *wait,
                               int state);
queue 和 wait 分别地是等待队列头和进程入口。state 是进程的新状态:TASK_INTERRUPTIBLE(可中断休眠,推荐)或TASK_UNINTERRUPTIBLE(不可中断休眠,不推荐)

(3)在检查确认仍然需要休眠之后调用 schedule
if (!condition)
     schedule( );    /*调用调度器,并让出CPU*/

(4)schedule 返回,就到了清理时间:
void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait);


真地看简单休眠中的 wait_event(queue, condition) 和 wait_event_interruptible(queue,
condition) 底层源码会发现,其实他们只是手工休眠中的函数的组合。所以怕麻烦的话还是用wait_event比较好。

独占等待

一个进程调用 wake_up 在等待队列上,所有的在这个队列上等待的进程被置为可运行的。
这在许多情况下是正确的做法。但有时,可能只有一个被唤醒的进程将成功获得需要的资源,而其余的将再次休眠。这时如果等待队列中的进程数目大,这可能严重
降低系统性能。为此,内核开发者增加了一个“独占等待”选项。它与一个正常的睡眠有 2 个重要的不同:
(1)当等待队列入口设置了 WQ_FLAG_EXCLUSEVE 标志,它被添加到等待队列的尾部;否则,添加到头部。
(2)当 wake_up 被在一个等待队列上调用, 它在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止唤醒.但内核仍然每次唤醒所有的非独占等待。
采用独占等待要满足 2 个条件:
(1)希望对资源进行有效竞争;
(2)当资源可用时,唤醒一个进程就足够来完全消耗资源。
使一个进程进入独占等待,可调用:
void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue_t *wait, int state);
注意:无法使用 wait_event 和它的变体来进行独占等待.

唤醒函数

很少会需要调用wake_up_interruptible 之外的唤醒函数,但为完整起见,这里是整个集合:
wake_up(wait_queue_head_t *queue);
wake_up_interruptible(wait_queue_head_t *queue);
wake_up 唤醒队列中的每个非独占等待进程和一个独占等待进程。wake_up_interruptible 同样, 除了它跳过处于不可中断休眠的进程。它们在返回之前, 使一个或多个进程被唤醒、被调度(如果它们被从一个原子上下文调用, 这就不会发生).
wake_up_nr(wait_queue_head_t *queue, int nr);
wake_up_interruptible_nr(wait_queue_head_t *queue, int nr);
这些函数类似 wake_up, 除了它们能够唤醒多达 nr 个独占等待者, 而不只是一个. 注意传递 0 被解释为请求所有的互斥等待者都被唤醒
wake_up_all(wait_queue_head_t *queue);
wake_up_interruptible_all(wait_queue_head_t *queue);
这种 wake_up 唤醒所有的进程, 不管它们是否进行独占等待(可中断的类型仍然跳过在做不可中断等待的进程)
wake_up_interruptible_sync(wait_queue_head_t *queue);

个被唤醒的进程可能抢占当前进程, 并且在 wake_up 返回之前被调度到处理器。 但是, 如果你需要不要被调度出处理器时,可以使用
wake_up_interruptible 的"同步"变体. 这个函数最常用在调用者首先要完成剩下的少量工作,且不希望被调度出处理器时。

参考

http://www.cnblogs.com/noaming1900/archive/2011/01/14/1935526.html
http://www.cnblogs.com/noaming1900/archive/2011/01/14/1935490.html
http://blog.csdn.net/yusiguyuan/article/details/47805091
http://www.cnblogs.com/zhuyp1015/archive/2012/06/09/2542894.html
http://guojing.me/linux-kernel-architecture/posts/wait-queue/

linux 内核睡眠与唤醒的更多相关文章

  1. linux内核睡眠状态解析

    1. 系统睡眠状态 睡眠状态是整个系统的全局低功耗状态,在这种状态下,用户空间的代码不能被执行并且整个系统的活动明显被降低 1.1 被支持的睡眠状态 取决于所运行平台的能力和配置选项,Linux内核能 ...

  2. Linux 内核睡眠的几种方式

    译至:http://geeki.wordpress.com/2010/10/30/ways-of-sleeping-in-linux-kernel/ 在Linux中睡眠有2-3种不同的方法. 睡眠的第 ...

  3. Linux进程的睡眠和唤醒简析

    COPY FROM:http://www.2cto.com/os/201204/127771.html 1 Linux进程的睡眠和唤醒 在Linux中,仅等待CPU时间的进程称为就绪进程,它们被放置在 ...

  4. Linux内核3.11的socket busy poll机制避免睡眠切换

    Linux的网络协议栈很独立,上下通过两个接口分别和用户态以及设备相连.也能够看作是北向和南向接口...北向通过socket接口,南向通过qdisc接口(你能够觉得是上层的netdev queue,对 ...

  5. ARM linux电源管理——Cortex A系列CPU(32位)睡眠和唤醒的底层汇编实现

    ARM linux电源管理——Cortex A系列CPU(32位)睡眠和唤醒的底层汇编实现 承接 http://www.wowotech.net/pm_subsystem/suspend_and_re ...

  6. Linux进程的睡眠和唤醒

    1   Linux进程的睡眠和唤醒 在Linux中,仅等待CPU时间的进程称为就绪进程,它们被放置在一个运行队列中,一个就绪进程的状态标志位为TASK_RUNNING.一旦一个运行中的进程时间片用完, ...

  7. 再思linux内核在中断路径内不能睡眠/调度的原因(2010)【转】

    转自:http://blog.csdn.net/maray/article/details/5770889 Linux内核中断路径中不能睡眠,为什么? 这里就行了很深入的讨论,值得一看:http:// ...

  8. 《Linux内核设计与实现》读书笔记 第四章 进程调度

    第四章进程调度 进程调度程序可看做在可运行太进程之间分配有限的处理器时间资源的内核子系统.调度程序是多任务操作系统的基础.通过调度程序的合理调度,系统资源才能最大限度地发挥作用,多进程才会有并发执行的 ...

  9. linux 内核与用户空间通信之netlink使用方法

    转自:http://blog.csdn.net/haomcu/article/details/7371835 Linux中的进程间通信机制源自于Unix平台上的进程通信机制.Unix的两大分支AT&a ...

随机推荐

  1. 添加服务引用和添加Web引用对比

    原文:添加服务引用和添加Web引用对比 在WindowsForm程序中添加服务引用和Web引用对比 为了验证书上有关Visual Studio 2010添加服务引用和Web引用的区别,进行实验. 一. ...

  2. R语言和数据分析十大:购物篮分析

    提到数据挖掘,我们的第一个反应是之前的啤酒和尿布的故事听说过,这个故事是一个典型的数据挖掘关联规则.篮分析的传统线性回归之间的主要差别的差别,对于离散数据的相关性分析: 常见的关联规则: 关联规则:牛 ...

  3. Nancy 框架

    Nancy 框架 Nancy 框架 1.是一个轻量级用于构建http相应的web框架: 2.与mvc类似,有自己的路由机制: 3.可以处理 DELETE ,  GET ,  HEAD ,  OPTIO ...

  4. (4)通过调用hadoop的java api实现本地文件上传到hadoop文件系统上

    (1)首先创建java project 选择eclipse菜单上File->New->Java Project. 并命名为UploadFile. (2)加入必要的hadoop jar包 右 ...

  5. net大型分布式电子商务架构

    net大型分布式电子商务架构 背景 构建具备高可用,高扩展性,高性能,能承载高并发,大流量的分布式电子商务平台,支持用户,订单,采购,物流,配送,财务等多个项目的协作,便于后续运营报表,分析,便于运维 ...

  6. 查看SQLServer 代理作业的历史信息

    原文:查看SQLServer 代理作业的历史信息 不敢说众所周知,但是大部分人都应该知道SQLServer的代理作业情况都存储在SQLServer5大系统数据库(master/msdb/model/t ...

  7. 使用jprobe建设镜面层叠的原则和见解

    忽然想起的回忆,那是2007上周五在冬季,我看我的老湿调试Linux堆IP层,只看到他改变路由查找的逻辑,然后直接make install上的立竿见影的效果有点,我只知道,,这种逻辑必须再次更改编译内 ...

  8. Visual Studio Team Services使用教程--Readers tfs组checkin权限修改

    你也可以只开启部分代码的权限 把上面开启的整个应用的权限先去掉 只开启一个文件的权限

  9. Script:SQL调优健康检查脚本

    Script:SQL调优健康检查脚本 http://www.askmaclean.com/archives/sql-tuning-health-check-script.html 以下脚本可以用于收集 ...

  10. Android采用canvas绘制各种图形

    canvas通俗的说就是一个帆布,我们可以用刷子paint,就此随机抽签显卡. 原理: 能够canvas视Surface替代或接口.图形绘制Surface向上.Canvas封装了全部的绘制调用. 通过 ...