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 内核中进程睡眠和唤醒的设计和实现,应该已经明白了。