8

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

 3 years ago
source link: https://blog.popkx.com/linux%E5%AD%A6%E4%B9%A014-%E8%BF%9B%E7%A8%8B%E7%9A%84%E7%9D%A1%E7%9C%A0%E4%B8%8E%E5%94%A4%E9%86%92/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

linux 中进程的状态

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

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

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

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

进程的睡眠状态非常重要

3dcdb3f8f5f0b900ac7be4991d65fe19.png

所以,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;
23b04ab01029d3dd503dfbb4fdcfbdec.png

内核正是使用 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 }
feca3494ff6e89afc95fd5a0df8749be.png

变量 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 }
d26dc4a086030f8d629a8ac53832da33.png

可以看出,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 }
ff6f850f0a472d83ca3b84738276bbee.png

这个函数也很简单,它处理了自旋锁,并且在恰当的时候把进程设置为睡眠状态(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);
        ...
1481d75724220a0c5edb51cf836882e4.png

这里我们只关心进程“睡眠”相关的代码。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     }
a390a9cd57bf8c8f344da9287042645b.png

容易看出,这个宏其实就是使用 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 }
0d2ade4c2d5efbefb562efb5f76358c6.png

关键就是 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 }
4f6c3bb0728efa7200b40474e8c04a70.png

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK