linux学习14,进程的睡眠与唤醒
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.
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 内核中进程睡眠和唤醒的设计和实现,应该已经明白了。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK