eagain(linux学习14)

精英怪
广告

linux 中进程的状态

eagain(linux学习14)

第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内核原理和设计),每天最浅显的介绍C语言、linux等嵌入式开发,喜欢我的文章就关注一波吧,可以看到最新更新和之前的文章哦。

发表评论

快捷回复: 表情:
AddoilApplauseBadlaughBombCoffeeFabulousFacepalmFecesFrownHeyhaInsidiousKeepFightingNoProbPigHeadShockedSinistersmileSlapSocialSweatTolaughWatermelonWittyWowYeahYellowdog
评论列表 (暂无评论,250人围观)

还没有评论,来说两句吧...