linux学习14,进程的睡眠与唤醒

linux 中进程的状态

第10节曾提到,进程一共只有 5 种状态,也必定是这 5 种状态之一:

  • TASK_RUNNING,表示进程是可执行的,或者正在运行,或者正在运行队列里排队等待运行。
  • TASK_INTERRUPTIBLE,表示进程正在睡眠,并且可能随时被唤醒信号唤醒,唤醒后,进程会被设置为 TASK_RUNNING。
  • TASK_UNINTERRUPTIBLE,表示进程正在睡眠,不会被信号唤醒。
  • ‘__TASK_TRACED,表示进程正在被其他进程跟踪,例如正在被 gdb 调试的进程就会是这个状态。
  • ‘__TASK_STOPPED,表示进程停止执行,不能被投入运行。

现在来设想下面这种情况:某个进程使用了文件系统,正在阻塞等待磁盘返回数据,这个过程可能需要若干 ms。该进程一直处于运行状态,但是只是等待数据,没有做任何其他事,此时 cpu 的性能就被白白浪费了,整个系统的效率也就非常低下。

若干毫秒对于人类来说可能稍纵即逝,但是对于 cpu 这种常常以 ns 衡量运算时间的器件来说,就太漫长了。

进程的睡眠状态非常重要

所以,linux 中进程的睡眠状态也是非常重要的。结合上一节的说法,睡眠的进程被从可执行红黑树中移出,所以 linux 内核不会调度它投入运行,也就不会消耗过多 cpu 的性能。

TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进程,会被放入同一个等待队列,等待特定的事件到来,才会被 linux 内核继续唤醒调度运行。

特定的事件例如:磁盘数据到达、等待的信号到达等。

linux 内核中,进程睡眠的源码分析

那么,linux 内核是如何实现进程睡眠的呢?现在从C语言源码分析。请看:

-    50 struct __wait_queue_head {
|    51     spinlock_t lock;
|    52     struct list_head task_list;    
|    53 };
     54 typedef struct __wait_queue_head wait_queue_head_t;


内核正是使用 wait_queue_head_t 结构体表示等待队列的,它的结构非常简单,就是一个带有自旋锁的链表而已。内核设置进程睡眠,大体框架都是相似的,请看:

wait_queue_head_t wait;
init_waitqueue_head(&wait);

add_wait_queue(q, &wait);
while(!condition){
    prepare_to_wait(&q, &wait, TASK_INTERRUPTIBLE);
    if(signal_pending(current))
        /* 处理信号 */
    schedule();
}
finish_wait(&q, &wait);
以上代码假设 q 是进程睡眠的队列。

内核先使用 init_waitqueue_head() 函数初始化 wait, 然后调用 add_wait_queue() 函数将进程放入等待队列,它的C语言源码如下:

     21 void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
-    22 {
|    23     unsigned long flags;
|    24 
|    25     wait->flags &= ~WQ_FLAG_EXCLUSIVE;
|    26     spin_lock_irqsave(&q->lock, flags);
|    27     __add_wait_queue(q, wait);
|    28     spin_unlock_irqrestore(&q->lock, flags);
|    29 }


变量 condition 表示要等待的条件,如果它发生了,则进程就不会再被设置成睡眠状态,这是 linux 内核会调用 finish_wait() 函数结束等待,finish_wait() 函数的C语言定义如下:

    104 void finish_wait(wait_queue_head_t *q, wait_queue_t *wait)
-   105 {
|   106     unsigned long flags;
|   107 
|   108     __set_current_state(TASK_RUNNING);
|-  122     if (!list_empty_careful(&wait->task_list)) {
||  123         spin_lock_irqsave(&q->lock, flags);
||  124         list_del_init(&wait->task_list);
||  125         spin_unlock_irqrestore(&q->lock, flags);
||  126     }
|   127 }


可以看出,finish_wait() 函数要做的工作很简单,它首先将进程设置为 TASK_RUNNING 状态,接着清理了相关的锁。

如果进程要等待的条件没有发生,那么 linux 内核将调用 prepare_to_wait() 函数将进程加入等待队列,它的C语言代码如下,请看:

     66 void prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
-    68 {
|    69     unsigned long flags;
|    70 
|    71     wait->flags &= ~WQ_FLAG_EXCLUSIVE;
|    72     spin_lock_irqsave(&q->lock, flags);
|    73     if (list_empty(&wait->task_list))
|    74         __add_wait_queue(q, wait);
|    75     /*
|    76      * don't alter the task state if this is just going to
|    77      * queue an async wait queue callback
|    78      */
|    79     if (is_sync_wait(wait))
|    80         set_current_state(state);
|    81     spin_unlock_irqrestore(&q->lock, flags);
|    82 }


这个函数也很简单,它处理了自旋锁,并且在恰当的时候把进程设置为睡眠状态(TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 状态)。

如果该进程等待的条件一直没有发生,则 linux 内核会一直调用 schedule() 函数,从可执行红黑树中挑选一个进程投入运行。

实例,linux 中进程被设置睡眠状态

现在,对 linux 内核将进程加入睡眠的大框架已经了解了,来看一个实例——文件系统中的 inotify_read() 函数。它的功能就是从通知文件描述符中读取信息,C语言定义如下:

    423 static ssize_t inotify_read(struct file *file, char __user *buf,
    424                 size_t count, loff_t *pos)
-   425 {
|   426     size_t event_size = sizeof (struct inotify_event);
|   427     struct inotify_device *dev;
|   428     char __user *start;
|   429     int ret;
|   430     DEFINE_WAIT(wait);
|   431         
|   432     start = buf;
|   433     dev = file->private_data;
|   434 
|-  435     while (1) {
||  436         int events;
||  437         
||  438         prepare_to_wait(&dev->wq, &wait, TASK_INTERRUPTIBLE);
||  439 
||  440         mutex_lock(&dev->ev_mutex);
||  441         events = !list_empty(&dev->events);
||  442         mutex_unlock(&dev->ev_mutex);
||- 443         if (events) {
||| 444             ret = 0;
||| 445             break;
||| 446         }
||  447     
||- 448         if (file->f_flags & O_NONBLOCK) {
||| 449             ret = -EAGAIN;
||| 450             break;
||| 451         }
||  452 
||- 453         if (signal_pending(current)) {
||| 454             ret = -EINTR;
||| 455             break;
||| 456         }
||  457 
||  458         schedule();
||  459     }
|   460 
|   461     finish_wait(&dev->wq, &wait);
        ...


这里我们只关心进程“睡眠”相关的代码。DEFINE_WAIT() 是一个宏,它的 C语言定义如下:

    446 #define DEFINE_WAIT(name)                       \
-   447     wait_queue_t name = {                       \
|   448         .private    = current,              \
|   449         .func       = autoremove_wake_function,     \
|   450         .task_list  = LIST_HEAD_INIT((name).task_list), \
|   451     }


容易看出,这个宏其实就是使用 wait_queue_t 结构体定义并且初始化了 wait。接着,函数进入了 while(1) 循环,因为有一些锁资源,所以这里不是按照前面介绍的 while(!condtion) 框架,而是使用 break 跳出循环,能够看出 inotify_read() 函数等待的事件在 dev->events 链表里,其他的都与前面讨论的框架一致,就不再赘述了。

唤醒进程

当进程等待的事件发生时,linux 内核要唤醒进程,将其加入可执行红黑树。这一过程是由 wake_up 宏实现的,它的C语言定义如下:

#define wake_up(x)          __wake_up(x, TASK_NORMAL, 1, NULL)

继续跟踪:

    4316 void __wake_up(wait_queue_head_t *q, unsigned int mode,
    4317             int nr_exclusive, void *key)
-   4318 {
|   4319     unsigned long flags;
|   4320 
|   4321     spin_lock_irqsave(&q->lock, flags);
|   4322     __wake_up_common(q, mode, nr_exclusive, 0, key);
|   4323     spin_unlock_irqrestore(&q->lock, flags);
|   4324 }

    4295 static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
    4296                  int nr_exclusive, int sync, void *key)
-   4297 {
|   4298     wait_queue_t *curr, *next;
|   4299 
|-  4300     list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
||  4301         unsigned flags = curr->flags;
||  4302    
||  4303         if (curr->func(curr, mode, sync, key) &&
||  4304                 (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
||  4305             break;
||  4306     } 
|   4307 }


关键就是 curr->func,这里C语言使用了面向对象的思想(详细可参照我的这篇文章:为C语言找一个对象)。func 的原型是什么呢?其实正是在 DEFINE_WAIT 宏里初始化时的 autoremove_wake_function() 函数,它的 C语言定义如下:

    130 int autoremove_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key)
-   131 {
|   132     int ret = default_wake_function(wait, mode, sync, key);
|   133 
|   134     if (ret)
|   135         list_del_init(&wait->task_list);
|   136     return ret;
|   137 }

   4279 int default_wake_function(wait_queue_t *curr, unsigned mode, int sync,
    4280               void *key)
-   4281 {  
|   4282     return try_to_wake_up(curr->private, mode, sync);
|   4283 } 

继续跟踪,发现 linux 内核唤醒进程的核心函数是 try_to_wake_up() 函数,它的C语言定义如下,请看:

2078 static int try_to_wake_up(struct task_struct *p, unsigned int state, int sync)
-   2079 {             
|   2080     int cpu, orig_cpu, this_cpu, success = 0;
|   2081     unsigned long flags;
|   2082     long old_state;
|   2083     struct rq *rq;
|   2084    
|   2085     if (!sched_feat(SYNC_WAKEUPS))
|   2086         sync = 0;
...
|   2151 out_running:
|   2152     check_preempt_curr(rq, p);
|   2153 
|   2154     p->state = TASK_RUNNING;
|   2155 #ifdef CONFIG_SMP
|   2156     if (p->sched_class->task_wake_up)
|   2157         p->sched_class->task_wake_up(rq, p);
|   2158 #endif
|   2159 out:
|   2160     task_rq_unlock(rq, &flags);
|   2161 
|   2162     return success;
|   2163 }


这个函数虽然很长,但是最核心的其实只有一行,就是将进程的状态设置为 TASK_RUNNING 状态。

至此,linux 内核中进程睡眠和唤醒的设计和实现,应该已经明白了。

阅读更多:   Linux笔记
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif