linuxsleep(linux学习14)

管理员 2024-11-25 16:37:39 0
linux 中进程的状态

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

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

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

若干毫秒对于人类来说可能稍纵即逝,但是对于 cpu 这种常常以 ns 衡量运算时间的器件来说,就太漫长了。进程的睡眠状态非常重要linux学习14,进程为什么要“睡眠”?怎么“唤醒”它呢?

所以,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;linux学习14,进程为什么要“睡眠”?怎么“唤醒”它呢?

内核正是使用 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 }linux学习14,进程为什么要“睡眠”?怎么“唤醒”它呢?

变量 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 }linux学习14,进程为什么要“睡眠”?怎么“唤醒”它呢?

可以看出,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 }linux学习14,进程为什么要“睡眠”?怎么“唤醒”它呢?

这个函数也很简单,它处理了自旋锁,并且在恰当的时候把进程设置为睡眠状态(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); ...linux学习14,进程为什么要“睡眠”?怎么“唤醒”它呢?

这里我们只关心进程“睡眠”相关的代码。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 }linux学习14,进程为什么要“睡眠”?怎么“唤醒”它呢?

容易看出,这个宏其实就是使用 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 }linux学习14,进程为什么要“睡眠”?怎么“唤醒”它呢?

关键就是 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 }linux学习14,进程为什么要“睡眠”?怎么“唤醒”它呢?

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

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

欢迎在评论区一起讨论,质疑。文章都是手打原创(本文部分参考linux内核原理和设计),每天最浅显的介绍C语言、linux等嵌入式开发,喜欢我的文章就关注一波吧,可以看到最新更新和之前的文章哦。

相关文章