Linux进程的睡眠和唤醒(一个定时信号唤醒睡眠中的进程)

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow

也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!

               

        突然想到Nginx中时间更新这块处理,Nginx中为了减少调用系统调用gettimeofday这个函数(因为一旦调用了系统调用,就会使得进程从用户态切换到内核态,就会发生上下文切换,这个代价很大且不值得)而设置了系统时间更新的次数,内部时间更新有两种方式,一种就是在配置文件中设置更新的评论,另一种是没有设置更新频率,在后面这种情况下,可以使用epoll_wait()这个函数的最后一个参数来控制反复的时间间隔,在Nginx内部使用了红黑树来管理定时事件,每次从堆顶选择一个最近的定时事件最为epoll_wait函数的最后一个参数。但是第一种方式是如何处理,系统内部设定了一个定时器,定时事件的时间间隔就是系统配置文件中用户自己设定的事件间隔,每次到一定的时间间隔后,都会有一个定时时间发生,如果epoll_wait处于睡眠状态,那么此进程就会被唤醒。这么一说,一个进程如果处于睡眠状态,那么用户也是可以使用别的方式来唤醒一个睡眠中的进程了!

     我们再来回想一下前面有篇介绍EAGAIN和EINTER这两个错误的文章,EINTER这个错误时慢系统调用而触发的,如果慢系统调用睡眠了,这个时候有信号到达了,睡眠的进程被唤醒了,然后习惯性的查看信号队列,然后进程了用户的信号处理函数。这是正常的处理逻辑。所以在Nginx中使用一个定时器来更新时间是可靠的(定时器中的信号处理函数就是更新时间的。)

  接下来我们看一下内核是如何处理进程的睡眠和唤醒的。



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

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

在Linux中,仅等待CPU时间的进程称为就绪进程,它们被放置在一个运行队列中,一个就绪进程的状 态标志位为TASK_RUNNING。一旦一个运行中的进程时间片用完, Linux 内核的调度器会剥夺这个进程对CPU的控制权,并且从运行队列中选择一个合适的进程投入运行。
当然,一个进程也可以主动释放CPU的控制权。函数 schedule()是一个调度函数,它可以被一个进程主动调用,从而调度其它进程占用CPU。一旦这个主动放弃CPU的进程被重新调度占用 CPU,那么它将从上次停止执行的位置开始执行,也就是说它将从调用schedule()的下一行代码处开始执行。
有时候,进程需要等待直到某个特定的事件发生,例如设备初始化完成、I/O 操作完成或定时器到时等。在这种情况下,进程则必须从运行队列移出,加入到一个等待队列中,这个时候进程就进入了睡眠状态。

等待队列

休眠通过等待队列进行处理。等待队列是由等待某些事件发生的进程组成的简单链表。内核用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() 之前需要释放掉锁,而在这以后重新获取它们,或者响应其他事件。

唤醒

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

关于休眠有一点需要注意,存在虚假的唤醒(信号)。有时候进程被唤醒并不是因为它所等待的条件达成了,所以需要用一个循环处理来保证它等待的条件真正达成。图 4-1 描述了每个调度程序状态之间的关系。

需要注意内核处理进程的睡眠和唤醒是怎么处理的。同时睡眠的进程也是有区别的!!

           

给我老师的人工智能教程打call!http://blog.csdn.net/jiangjunshow

这里写图片描述

猜你喜欢

转载自blog.csdn.net/hfhhgfv/article/details/84064200