关于 Linux 进程的睡眠和唤醒 ,来看这篇就够了~

1 Linux 进程的睡眠和唤醒

 

Linux 中,仅等待 CPU 时间的进程称为就绪进程,它们被放置在一个运行队列中,一个就绪进程的状 态标志位为 TASK_RUNNING。一旦一个运行中的进程时间片用完, Linux 内核的调度器会剥夺这个进程对 CPU 的控制权,并且从运行队列中选择一个合适的进程投入运行。


当然,一个进程也可以主动释放 CPU 的控制权。函数 schedule() 是一个调度函数,它可以被一个进程主动调用,从而调度其它进程占用 CPU。一旦这个主动放弃 CPU 的进程被重新调度占用 CPU,那么它将从上次停止执行的位置开始执行,也就是说它将从调用 schedule() 的下一行代码处开始执行。


有时候,进程需要等待直到某个特定的事件发生,例如设备初始化完成、I/O 操作完成或定时器到时等。在这种情况下,进程则必须从运行队列移出,加入到一个等待队列中,这个时候进程就进入了睡眠状态。

 

Linux 中的进程睡眠状态有两种:一种是可中断的睡眠状态,其状态标志位

TASK_INTERRUPTIBLE;


另一种是不可中断 的睡眠状态,其状态标志位为 TASK_UNINTERRUPTIBLE。可中断的睡眠状态的进程会睡眠直到某个条件变为真,比如说产生一个硬件中断、释放 进程正在等待的系统资源或是传递一个信号都可以是唤醒进程的条件。不可中断睡眠状态与可中断睡眠状态类似,但是它有一个例外,那就是把信号传递到这种睡眠 状态的进程不能改变它的状态,也就是说它不响应信号的唤醒。不可中断睡眠状态一般较少用到,但在一些特定情况下这种状态还是很有用的,比如说:进程必须等 待,不能被中断,直到某个特定的事件发生。


在现代的 Linux 操作系统中,进程一般都是用调用 schedule() 的方法进入睡眠状态的,下面的代码演示了如何让正在运行的进程进入睡眠状态。

 

  1. sleeping_task = current;
  2. set_current_state(TASK_INTERRUPTIBLE);
  3. schedule();
  4. func1();
  5. /* Rest of the code ... */

 

 

在第一个语句中,程序存储了一份进程结构指针 sleeping_task,current 是一个宏,它指向正在执行的进程结构。set_current_state() 将该进程的状态从执行状态 TASK_RUNNING 变成睡眠状态TASK_INTERRUPTIBLE。 如果 schedule() 是被一个状态为TASK_RUNNING 的进程调度,那么 schedule() 将调度另外一个进程占用 CPU;如果 schedule() 是被一个状态为 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 的进程调度,那么还有一个附加的步骤将被执行:当前执行的进程在另外一个进程被调度之前会被从运行队列中移出,这将导致正在运行的那个进程进入睡眠,因为 它已经不在运行队列中了。

 

我们可以使用下面的这个函数将刚才那个进入睡眠的进程唤醒。


wake_up_process(sleeping_task);


在调用了 wake_up_process() 以后,这个睡眠进程的状态会被设置为 TASK_RUNNING,而且调度器会把它加入到运行队列中去。当然,这个进程只有在下次被调度器调度到的时候才能真正地投入运行。

 

2 无效唤醒

 

几乎在所有的情况下,进程都会在检查了某些条件之后,发现条件不满足才进入睡眠。可是有的时候进程却会在 判定条件为真后开始睡眠,如果这样的话进程就会无限期地休眠下去,这就是所谓的无效唤醒问题。在操作系统中,当多个进程都企图对共享数据进行某种处理,而 最后的结果又取决于进程运行的顺序时,就会发生竞争条件,这是操作系统中一个典型的问题,无效唤醒恰恰就是由于竞争条件导致的。


设想有两个进程 A 和 B,A 进程正在处理一个链表,它需要检查这个链表是否为空,如果不空就对链表里面的数据进行一些操作,同时 B 进程也在往这个链表添加节点。当这个链表是空的时候,由于无数据可操作,这时 A 进程就进入睡眠,当 B 进程向链表里面添加了节点之后它就唤醒 A 进程,其代码如下:


A 进程:

 

  1. 1 spin_lock(&list_lock);
  2. 2if(list_empty(&list_head)) {
  3. 3 spin_unlock(&list_lock);
  4. 4 set_current_state(TASK_INTERRUPTIBLE);
  5. 5 schedule();
  6. 6 spin_lock(&list_lock);
  7. 7 }
  8. 8
  9. 9/* Rest of the code ... */
  10. 10 spin_unlock(&list_lock);

 

B 进程:

  1. 100 spin_lock(&list_lock);
  2. 101 list_add_tail(&list_head, new_node);
  3. 102 spin_unlock(&list_lock);
  4. 103 wake_up_process(processa_task);

 

 

这里会出现一个问题,假如当 A 进程执行到第 3 行后第 4 行前的时候,B 进程被另外一个处理器调度投入运行。在这个时间片内,B 进程执行完了它所有的指令,因此它试图唤醒 A 进程,而此时的 A 进程还没有进入睡眠,所以唤醒操作无效。在这之后,A 进程继续执行,它会错误地认为这个时候链表仍然是空的,于是将自己的状态设置为 TASK_INTERRUPTIBLE 然后调用 schedule() 进入睡 眠。由于错过了 B 进程唤醒,它将会无限期的睡眠下去,这就是无效唤醒问题,因为即使链表中有数据需要处理,A 进程也还是睡眠了。

 

3 避免无效唤醒

 

如何避免无效唤醒问题呢?我们发现无效唤醒主要发生在检查条件之后和进程状态被设置为睡眠状态之前, 本来 B 进程的 wake_up_process() 提供了一次将 A 进程状态置为 TASK_RUNNING 的机会,可惜这个时候 A 进程的状态仍然是 TASK_RUNNING,所以 wake_up_process() 将 A 进程状态从睡眠状态转变为运行状态的努力 没有起到预期的作用。要解决这个问题,必须使用一种保障机制使得判断链表为空和设置进程状态为睡眠状态成为一个不可分割的步骤才行,也就是必须消除竞争条 件产生的根源,这样在这之后出现的 wake_up_process () 就可以起到唤醒状态是睡眠状态的进程的作用了。
找到了原因后,重新设计一下 A 进程的代码结构,就可以避免上面例子中的无效唤醒问题了。


A 进程:

 

  1. 1 set_current_state(TASK_INTERRUPTIBLE);
  2. 2 spin_lock(&list_lock);
  3. 3if(list_empty(&list_head)) {
  4. 4 spin_unlock(&list_lock);
  5. 5 schedule();
  6. 6 spin_lock(&list_lock);
  7. 7 }
  8. 8 set_current_state(TASK_RUNNING);
  9. 9
  10. 10/* Rest of the code ... */
  11. 11 spin_unlock(&list_lock);

 

 

可以看到,这段代码在测试条件之前就将当前执行进程状态转设置成 TASK_INTERRUPTIBLE 了,并且在链表不为空的情况下又将自己置为 TASK_RUNNING 状态。这样一来如果 B 进程在 A 进程进程检查了链表为空以后调用 wake_up_process(),那么 A 进程的状态就会自动由原来 TASK_INTERRUPTIBLE变成 TASK_RUNNING,此后即使进程又调用了 schedule(),由于它现在的状态是 TASK_RUNNING,所以仍然不会被从运行队列中移出,因而不会错误的进入睡眠,当然也就避免了无效唤醒问题。

 

4 Linux 内核的例子

 

在 Linux 操作系统中,内核的稳定性至关重要,为了避免在 Linux 操作系统内核中出现无效唤醒问题,
Linux 内核在需要进程睡眠的时候应该使用类似如下的操作:

 

  1. /* ‘q’是我们希望睡眠的等待队列 */
  2. DECLARE_WAITQUEUE(wait,current);
  3. add_wait_queue(q, &wait);
  4. set_current_state(TASK_INTERRUPTIBLE);
  5. /* 或 TASK_INTERRUPTIBLE */
  6. while(!condition) /* ‘condition’ 是等待的条件 */
  7. schedule();
  8. set_current_state(TASK_RUNNING);
  9. remove_wait_queue(q, &wait);

 

 

上面的操作,使得进程通过下面的一系列步骤安全地将自己加入到一个等待队列中进行睡眠:首先调用 DECLARE_WAITQUEUE () 创建一个等待队列的项,然后调用 add_wait_queue() 把自己加入到等待队列中,并且将进程的状态设置为TASK_INTERRUPTIBLE 或者 TASK_INTERRUPTIBLE。然后循环检查条件是否为真:如果是的话就没有必要睡眠,如果条件不为真,就调用 schedule()。当进程 检查的条件满足后,进程又将自己设置为 TASK_RUNNING 并调用 remove_wait_queue() 将自己移出等待队列。


从上面可以看到,Linux 的内核代码维护者也是在进程检查条件之前就设置进程的状态为睡眠状态,然后才循环检查条件。如果在进程开始睡眠之前条件就已经达成了,那么循环会退出并用 set_current_state() 将自己的状态设置为就绪,这样同样保证了进程不会存在错误的进入睡眠的倾向,当然也就不会导致出现无效唤醒问题。


下面让我们用 linux 内核中的实例来看看 Linux 内核是如何避免无效睡眠的,这段代码出自 Linux2.6 的内核 (linux-2.6.11/kernel/sched.c: 4254):

 

  1. 4253/* Wait for kthread_stop */
  2. 4254 set_current_state(TASK_INTERRUPTIBLE);
  3. 4255while (!kthread_should_stop()) {
  4. 4256 schedule();
  5. 4257 set_current_state(TASK_INTERRUPTIBLE);
  6. 4258 }
  7. 4259 __set_current_state(TASK_RUNNING);
  8. 4260return0;

 

上面的这些代码属于迁移服务线程 migration_thread,这个线程不断地检查 kthread_should_stop(),

直 到 kthread_should_stop() 返回 1 它才可以退出循环,也就是说只要 kthread_should_stop() 返回 0 该进程就会一直睡 眠。从代码中我们可以看出,检查 kthread_should_stop() 确实是在进程的状态被置为 TASK_INTERRUPTIBLE 后才开始执行 的。因此,如果在条件检查之后但是在 schedule() 之前有其他进程试图唤醒它,那么该进程的唤醒操作不会失效。

 

小结

 

通过上面的讨论,可以发现在 Linux 中避免进程的无效唤醒的关键是在进程检查条件之前就将进程的状态置为 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE,并且如果检查的条件满足的话就应该将其状态重新设置为 TASK_RUNNING。这样无论进程等待的条件是否满足, 进程都不会因为被移出就绪队列而错误地进入睡眠状态,从而避免了无效唤醒问题。

 

作者:chumojing

原文地址:http://blog.chinaunix.net/uid-12461657-id-3178775.html专业Linux

<think>嗯,用户想了解Linux系统中休眠唤醒的具体流程及机制。用户明确询问了休眠流程、唤醒流程原理三部分,需要系统级的专业解释。 用户可能正在开发嵌入式Linux设备或优化系统电源管理,需要深入理解内核机制。从问题表述看,用户有一定Linux基础,但需要更底层的实现细节。需要特别注意用户要求使用中文回答,所有数学表达式要用$...$格式,独立公式用$$...$$格式。 参考提供的三引用材料:第一进程睡眠唤醒机制,第二重点在嵌入式系统休眠唤醒流程,第三提到语音唤醒但相关性较低。其中第二提到的SUSPEND/RESUME两阶段模型特别重要,需要重点展开。 准备分三部分构建回答: 第一块详细解释休眠流程,从用户空间触发到内核冻结设备、保存状态的全过程。特别要说明PM核心的层次化处理,以及ACPI状态转换。可以用数学表达描述状态机,比如用$S_0 \to S_3$表示状态转换。 第二块解析唤醒流程的逆过程,强调从休眠状态恢复到运行状态的触发机制。这里要区分硬件触发(如RTC定时)软件触发(如中断),结合进程调度机制说明唤醒链。准备用$$ \text{RTC中断} \Rightarrow \text{BIOS固件} \Rightarrow \text{内核恢复} $$这样的独立公式展示流程。 第三块剖析原理机制,重点讲三个核心:冻结进程如何通过保存寄存器状态实现,设备驱动如何参与电源管理回调,内存镜像如何压缩存储。考虑用文件系统同步的公式$$ \text{fsync}() \rightarrow \text{存储队列} \xrightarrow{\text{flush}} \text{持久化} $$来说明数据一致性保障。 最后需要生成相关问题。根据用户的技术深度,问题要偏向实现细节优化方向,比如问"如何减少休眠唤醒延迟"或"休眠过程掉电如何处理"这类工程实际问题。 需要特别注意术语准确性:比如"休眠"(suspend)"挂起"(hibernate)在Linux PM框架中是不同概念,不能混用。所有技术名词首次出现时加英文标注,如进程冻结(freeze)。</think>### Linux 系统休眠与唤醒机制详解 #### **一、休眠流程** 1. **用户/内核触发** 用户通过`echo mem > /sys/power/state`或内核事件(如合盖、超时)触发休眠。 2. **冻结进程** 内核调用`freeze_processes()`暂停用户空间进程内核线程,确保系统状态静止: $$ \text{冻结阶段} : \text{用户进程} \xrightarrow{\text{suspend_freeze_processes}} \text{冻结态} $$ 3. **挂起设备** 按设备树层级依次调用驱动的`.suspend()`回调: ```plaintext PM核心 → 总线驱动 → 设备驱动 ``` 设备保存寄存器状态,切断非必要电源(如 USB 控制器、外设时钟)[^1]。 4. **保存系统状态** - **挂起(Suspend-to-RAM)**:仅保留内存供电,状态存入 RAM。 - **休眠(Hibernate)**:将内存镜像压缩后写入磁盘交换区(如 `/dev/sdaX`),公式为: $$ \text{内存镜像} \xrightarrow{\text{swsusp}} \text{磁盘存储} $$ - 调用`syscore_suspend()`保存 CPU/中断控制器等核心状态[^2]。 5. **进入低功耗状态** 内核执行`sysdev_suspend()`,CPU 切入 ACPI 定义的休眠状态(如 `S3`): $$ \text{运行态 } (S_0) \xrightarrow{\text{ACPI 指令}} \text{休眠态 } (S_3) $$ --- #### **二、唤醒流程** 1. **硬件事件触发** 由 RTC 定时器、电源键、外设中断(如网卡唤醒包)触发 CPU 复位。 2. **BIOS/固件初始化** CPU 从复位向量重启,BIOS 恢复基本硬件(内存控制器、PCIe 总线)。 3. **内核恢复** - 执行`sysdev_resume()`恢复核心硬件状态。 - 调用设备驱动的`.resume()`回调,重新初始化设备(如磁盘控制器、GPU)。 - 公式化表示: $$ \text{休眠态 } (S_3) \xrightarrow{\text{中断触发}} \text{恢复核心状态} \xrightarrow{\text{设备初始化}} \text{运行态} $$ 4. **解冻进程** 调用`thaw_processes()`恢复用户空间进程,系统继续运行。 5. **状态同步** 文件系统执行日志回写(如 ext4 的 `journal_replay`),确保数据一致性[^2]。 --- #### **三、核心原理** 1. **进程冻结机制** 通过发送`SIGSTOP`信号暂停用户进程,内核线程则通过`freeze_kernel_threads()`进入可中断睡眠(`TASK_INTERRUPTIBLE`),唤醒后从断点继续执行[^1]。 2. **设备驱动协作** 驱动需实现电源管理回调: ```c struct dev_pm_ops { int (*suspend)(struct device *dev); int (*resume)(struct device *dev); }; ``` 例如网卡驱动在`suspend()`中关闭 DMA 引擎,`resume()`中重载固件。 3. **内存镜像处理** - **挂起(S3)**:依赖 RAM 保持刷新,恢复速度最快(毫秒级)。 - **休眠(S4)**:使用 `swsusp` 算法压缩镜像,存储至磁盘交换区,公式为: $$ \text{压缩率} = \frac{\text{镜像大小}}{\text{物理内存}} \approx 0.6 \sim 0.8 $$ 4. **中断与时钟管理** 休眠前关闭所有非唤醒源中断;唤醒后 RTC 重新初始化系统时钟(`ktime_get_boottime()`)[^1]。 --- ### 相关问题 1. **如何配置 Linux 使其支持特定外设唤醒(如 USB 键盘)?** > *需在驱动中设置唤醒使能位,例如 `device_set_wakeup_enable(dev, true)`,并在 ACPI 表中声明唤醒能力。* 2. **休眠过程中系统掉电会导致什么后果?如何防护?** > *挂起(S3)时掉电将丢失内存数据;休眠(S4)因数据存盘可避免损失。防护措施包括:使用 UPS、启用磁盘写缓存刷新(`/sys/power/sync_on_suspend`)。* 3. **如何优化休眠唤醒延迟?** > *可裁剪不必要的设备回调、关闭深度休眠状态(如禁用 `S4`)、使用 `CONFIG_HIBERNATION_FAST` 跳过 BIOS 初始化。* 4. **嵌入式设备如何实现低功耗休眠?** > *需定制内核:关闭非必要外设时钟(`clk_disable_unused()`)、切换 CPU 到空闲状态(`cpuidle`)、启用 Tickless 模式(`CONFIG_NO_HZ_IDLE`)[^2]。* [^1]: 进程冻结通过信号任务状态控制实现,确保唤醒后恢复执行点。 [^2]: 嵌入式系统常省略磁盘存储步骤,直接利用 RAM 保持状态以加速恢复。 [^3]: 外设唤醒依赖中断控制器配置,例如设置 GPIO 边沿触发为唤醒源。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值