Linux 系统的电源管理与进程线程机制解析
1. CPUIdle 驱动
CPUIdle 与 CPUFreq 子系统类似,由属于板级支持包(BSP)的驱动和决定策略的调节器组成。不过,与 CPUFreq 不同的是,CPUIdle 的调节器在运行时不能更改,且没有用户空间调节器的接口。
CPUIdle 在
/sys/devices/system/cpu/cpu0/cpuidle
目录中公开每个空闲状态的信息,每个睡眠状态都有一个子目录,命名为
state0
到
stateN
。
state0
是最浅的睡眠状态,
stateN
是最深的。需要注意的是,编号与 C 状态的编号不匹配,并且 CPUIdle 没有与 C0(运行)等效的状态。每个状态下有以下文件:
-
desc
:状态的简短描述
-
disable
:通过写入 1 来禁用此状态的选项
-
latency
:CPU 核心退出此状态恢复正常操作所需的时间,以微秒为单位
-
name
:此状态的名称
-
power
:处于此空闲状态时消耗的功率,以毫瓦为单位
-
time
:在此空闲状态下花费的总时间,以微秒为单位
-
usage
:进入此状态的次数
以 BeagleBone Black 上的 AM335x SoC 为例,有两个空闲状态:
# cd /sys/devices/system/cpu/cpu0/cpuidle
# grep "" state0/*
state0/desc:ARM WFI
state0/disable:0
state0/latency:1
state0/name:WFI
state0/power:4294967295
state0/residency:1
state0/time:1780015
state0/usage:2159
这个状态名为 WFI,指的是 ARM 暂停指令“Wait For Interrupt”。延迟为 1 微秒,因为它只是一个暂停指令,消耗的功率显示为 -1,意味着(至少 CPUIdle)不知道功率预算。
# cd /sys/devices/system/cpu/cpu0/cpuidle
# grep "" state1/*
state1/desc:Bypass MPU PLL
state1/disable:0
state1/latency:100
state1/name:C1
state1/power:497
state1/residency:200
state1/time:8763012731
state1/usage:345285
这个状态名为 C1,延迟为 100 微秒,功率为 497 毫瓦。空闲状态可以硬编码到 CPUIdle 驱动中,也可以在设备树中呈现。AM335x 采用前者,以下是另一个 SoC 的示例:
cpus {
cpu: cpu0 {
compatible = "arm,cortex-a9";
enable-method = "ti,am4372";
device-type = "cpu";
reg = <0>;
cpu-idle-states = <&mpu_gate>;
};
idle-states {
compatible = "arm,idle-state";
entry-latency-us = <40>;
exit-latency-us = <100>;
min-residency-us = <300>;
local-timer-stop;
};
};
CPUIdle 有两个调节器:
-
ladder
:根据上一个空闲周期所花费的时间,一次向上或向下调整一个空闲状态。它在使用规则定时器滴答时效果良好,但在动态滴答时效果不佳。
-
menu
:根据预期的空闲时间选择空闲状态。它在动态滴答系统中效果良好。
用户可以根据 NOHZ 的配置选择其中一个调节器。在
/sys/devices/system/cpu/cpuidle
目录中有两个文件:
-
current_driver
:cpuidle 驱动的名称
-
current_governor_ro
:调节器的名称
这些文件显示正在使用的驱动和调节器。空闲状态可以在 PowerTOP 的“Idle stats”选项卡中查看。
2. 无滴答操作
无滴答(NOHZ)选项是一个相关主题。如果系统真正空闲,最可能的中断源将是系统定时器,它被编程为以每秒 HZ 次的速率生成规则的时间滴答,通常 HZ 为 100。历史上,Linux 使用定时器滴答作为测量超时的主要时间基准。
然而,如果在特定时刻没有注册定时器事件,唤醒 CPU 来处理定时器中断显然是浪费的。动态滴答内核配置选项
CONFIG_NO_HZ
会在定时器处理例程结束时查看定时器队列,并在下次事件发生时安排下一次中断,避免不必要的唤醒,使 CPU 能够更长时间地空闲。在任何对功率敏感的应用中,都应该启用此内核配置选项。
3. 外设断电
前面的讨论主要围绕 CPU 以及如何在其运行或空闲时降低功耗。现在关注系统的其他部分——外设,看看是否能在这里实现节能。
在 Linux 内核中,这由运行时电源管理系统(runtime pm)管理。它与支持运行时 pm 的驱动程序配合工作,关闭未使用的设备,并在下次需要时唤醒它们。这是动态的,并且对用户空间应该是透明的。设备驱动程序负责实现硬件管理,通常包括关闭子系统的时钟(时钟门控),并在可能的情况下关闭核心电路。
运行时电源管理通过 sysfs 接口公开。每个设备都有一个名为
power
的子目录,其中包含以下文件:
-
control
:允许用户空间确定是否在此设备上使用运行时 pm。如果设置为
auto
,则启用运行时 pm;如果设置为
on
,则设备始终开启,不使用运行时 pm。
-
runtime_enabled
:报告运行时 pm 是否启用、禁用,如果
control
设置为
on
,则报告
forbidden
。
-
runtime_status
:报告设备的当前状态,可能是
active
、
suspended
或
unsupported
。
-
autosuspend_delay_ms
:设备暂停前的时间,-1 表示永远等待。如果暂停设备硬件的成本很高,一些驱动程序会实现此功能,以防止快速的暂停/恢复循环。
以 BeagleBone Black 上的 MMC 驱动为例:
# cd /sys/devices/platform/ocp/481d8000.mmc/
mmc_host/mmc1/mmc1:0001/power
# grep "" *
async:disabled
autosuspend_delay_ms:3000
control:auto
runtime_active_kids:0
runtime_active_time:5170
runtime_enabled:enabled
runtime_status:suspended
runtime_suspended_time:137560
runtime_usage:0
运行时 pm 已启用,设备当前处于暂停状态,最后一次使用后 3000 毫秒会再次暂停。读取设备的一个块后:
# dd if=/dev/mmcblk1p3 of=/dev/null count=1
1+0 records in
1+0 records out
# grep "" *
async:disabled
autosuspend_delay_ms:3000
control:auto
runtime_active_kids:0
runtime_active_time:7630
runtime_enabled:enabled
runtime_status:active
runtime_suspended_time:200680
runtime_usage:0
MMC 驱动变为活动状态,板卡的功率从 320 mW 增加到 500 mW。3 秒后再次暂停,功率恢复到 320 mW。
4. 系统睡眠
另一种电源管理技术是将整个系统置于睡眠模式,预计一段时间内不会再使用。在 Linux 内核中,这称为系统睡眠,通常由用户发起,例如用户决定让设备关闭一段时间。
在笔记本电脑领域,通常有两个选项:挂起(suspend)或休眠(hibernate)。
- 挂起(也称为挂起到 RAM):关闭除系统内存之外的所有设备,因此机器仍会消耗少量功率。系统唤醒时,内存保留所有先前的状态,笔记本电脑可以在几秒钟内恢复运行。
- 休眠:将内存内容保存到硬盘。系统完全不消耗功率,可以无限期保持此状态,但唤醒时需要一些时间从磁盘恢复内存。在嵌入式系统中很少使用休眠,主要是因为闪存存储的读写速度较慢,而且会干扰工作流程。
Linux 支持四种睡眠状态,与 ACPI S 状态的对应关系如下表所示:
| Linux 系统睡眠状态 | ACPI S 状态 | 描述 |
| — | — | — |
| freeze | [S0] | 停止(冻结)用户空间的所有活动,但 CPU 和内存正常运行。由于不运行用户空间代码而实现节能。ACPI 没有等效状态,S0 最接近,S0 是运行系统的状态。 |
| standby | S1 | 与 freeze 类似,但除了引导 CPU 外,将所有 CPU 离线。 |
| mem | S3 | 关闭系统电源,将内存置于自刷新模式,也称为挂起到 RAM。 |
| disk | S4 | 将内存保存到硬盘并关闭电源,也称为挂起到磁盘。 |
并非所有系统都支持所有状态。可以通过读取
/sys/power/state
文件来查看可用状态,例如:
# cat /sys/power/state
freeze standby mem disk
要进入系统睡眠状态,只需将所需状态写入
/sys/power/state
。对于嵌入式设备,最常见的需求是使用
mem
选项挂起到 RAM,例如:
# echo mem > /sys/power/state
[ 1646.158274] PM: Syncing filesystems ...done.
[ 1646.178387] Freezing user space processes ...(elapsed 0.001 seconds) done.
[ 1646.188098] Freezing remaining freezable tasks ...
(elapsed 0.001 seconds) done.
[ 1646.197017] Suspending console(s) (use
no_console_suspend to debug)
[ 1646.338657] PM: suspend of devices complete
after 134.322 msecs
[ 1646.343428] PM: late suspend of devices
complete after 4.716 msecs
[ 1646.348234] PM: noirq suspend of devices
complete after 4.755 msecs
[ 1646.348251] Disabling non-boot CPUs ...
[ 1646.348264] PM: Successfully put all
powerdomains to target state
设备在不到一秒内断电,功率消耗降至 10 毫瓦以下。
5. 唤醒事件
在暂停设备之前,必须有唤醒它的方法。如果没有至少一个唤醒源,系统将拒绝暂停,并显示消息:“No sources enabled to wake-up! Sleep abort.”。这意味着即使在最深的睡眠状态下,系统的某些部分也必须保持通电,通常包括电源管理集成电路(PMIC)、实时时钟(RTC),可能还包括 GPIO、UART 和以太网等接口。
唤醒事件通过 sysfs 控制。
/sys/device
中的每个设备都有一个
power
子目录,其中包含一个
wakeup
文件,该文件包含以下字符串之一:
-
enabled
:此设备将生成唤醒事件
-
disabled
:此设备不会生成唤醒事件
- (空):此设备无法生成唤醒事件
要获取可以生成唤醒事件的设备列表,可以搜索
wakeup
文件中包含
enabled
或
disabled
的所有设备:
$ find /sys/devices -name wakeup | xargs grep “abled”
在 BeagleBone Black 中,UART 是唤醒源,因此在控制台按下按键可以唤醒设备。
6. 实时时钟定时唤醒
大多数系统都有一个实时时钟(RTC),可以在未来 24 小时内生成闹钟中断。如果存在,
/sys/class/rtc/rtc0
目录将存在,其中应包含
wakealarm
文件。向
wakealarm
写入一个数字将使其在该数字表示的秒数后生成闹钟。如果还启用了 RTC 的唤醒事件,它将唤醒暂停的设备。例如,在 30 秒后唤醒系统:
# cd /sys/devices/platform/pmic_rtc.1/rtc/rtc0
# echo “+30” > wakealarm
# echo “enabled” > power/wakeup
7. 进程与线程的选择
许多熟悉实时操作系统(RTOS)的嵌入式开发人员认为 Unix 进程模型很繁琐。另一方面,他们看到 RTOS 任务和 Linux 线程之间的相似性,并倾向于将现有的 RTOS 设计一对一映射到线程。
一个进程是一个内存地址空间和一个执行线程。地址空间是进程私有的,不同进程中的线程无法访问。进程运行时会分配资源,如栈空间、堆内存、文件引用等,进程终止时,这些资源会被系统回收。进程之间可以使用进程间通信(IPC),如本地套接字进行通信。
一个线程是进程内的执行线程。所有进程都从一个运行
main()
函数的主线程开始,可以使用 POSIX 函数
pthread_create(3)
创建额外的线程,多个线程在同一地址空间中执行。同一进程中的线程共享资源,可以读写相同的内存并使用相同的文件描述符,线程间通信相对容易,但需要注意同步和锁定问题。
假设有一个包含 40 个 RTOS 任务的系统要移植到 Linux,有两种极端设计:
- 将任务映射到进程,有 40 个独立的程序通过 IPC 通信,如通过套接字发送消息。这样可以大大减少内存损坏问题,因为每个进程中的主线程相互保护,并且每个进程退出后会清理资源。但进程间的消息接口很复杂,在一组进程紧密协作的情况下,消息数量可能很大,成为系统性能的限制因素。而且任何一个进程可能因错误而终止,其他 39 个进程需要处理邻居进程不再运行的情况并优雅恢复。
- 将任务映射到线程,将系统实现为一个包含 40 个线程的单进程。这样协作变得容易,因为它们共享相同的地址空间和文件描述符,发送消息的开销减少或消除,线程间的上下文切换比进程间更快。但引入了一个任务损坏另一个任务的堆或栈的可能性,如果任何一个线程遇到致命错误,整个进程将终止,调试复杂的多线程进程可能是一场噩梦。
结论是这两种设计都不理想,需要更深入地研究 API 以及进程和线程的行为来找到更好的方法。
8. 进程与线程的详细分析
8.1 进程的特点
- 资源独立性 :进程拥有独立的内存地址空间,这意味着一个进程的内存操作不会影响到其他进程。例如,一个进程崩溃时,不会导致其他进程的数据丢失或运行异常。
- 资源管理 :进程在运行过程中会分配各种资源,如内存、文件描述符等。当进程终止时,系统会自动回收这些资源,保证系统资源的有效利用。
- 进程间通信 :进程之间可以通过多种方式进行通信,如管道、消息队列、共享内存、套接字等。这些通信方式各有优缺点,适用于不同的场景。例如,管道适用于父子进程之间的简单数据传输;消息队列适用于不同进程之间的异步通信;共享内存适用于需要大量数据共享的场景;套接字则适用于网络通信。
8.2 线程的特点
- 资源共享性 :线程是进程内的执行单元,同一进程内的多个线程共享进程的资源,如内存、文件描述符等。这使得线程之间的通信更加高效,因为它们可以直接访问相同的内存区域。
- 轻量级 :线程的创建和销毁开销相对较小,上下文切换也比进程快。这使得多线程程序在处理并发任务时具有更高的性能。
- 同步问题 :由于线程共享资源,多个线程同时访问共享资源时可能会产生竞争条件,导致数据不一致。因此,在多线程编程中,需要使用同步机制,如互斥锁、信号量、条件变量等,来保证线程安全。
9. 调度策略
调度策略决定了操作系统如何分配 CPU 时间给不同的进程或线程。在 Linux 中,主要有两种调度策略:分时调度和实时调度。
9.1 分时调度
- 原理 :分时调度是一种基于时间片的调度策略,每个进程或线程被分配一个固定的时间片,在该时间片内可以执行任务。当时间片用完后,操作系统会将 CPU 分配给下一个进程或线程。
- 适用场景 :分时调度适用于大多数普通应用程序,如桌面应用、Web 服务器等。它可以保证每个进程或线程都有机会执行,避免某个进程或线程长时间占用 CPU。
9.2 实时调度
- 原理 :实时调度是为了满足对时间要求严格的应用程序而设计的。实时任务具有较高的优先级,操作系统会优先分配 CPU 时间给实时任务,以保证其能够及时响应。
- 适用场景 :实时调度适用于对时间敏感的应用程序,如工业控制、航空航天、医疗设备等。在这些应用中,任务的执行时间必须严格控制,否则可能会导致严重的后果。
以下是一个简单的 mermaid 流程图,展示了 Linux 系统中进程和线程的调度过程:
graph TD;
A[系统启动] --> B[创建进程];
B --> C{进程是否创建线程};
C -- 是 --> D[创建线程];
C -- 否 --> E[进程运行];
D --> F[线程运行];
E --> G{是否时间片用完};
F --> H{是否时间片用完};
G -- 是 --> I[调度到其他进程];
H -- 是 --> J[调度到其他线程];
G -- 否 --> E;
H -- 否 --> F;
I --> E;
J --> F;
10. 总结
Linux 系统提供了丰富的电源管理和进程线程机制,以满足不同应用场景的需求。在电源管理方面,通过 CPUIdle 驱动、无滴答操作、外设断电、系统睡眠等技术,可以有效降低系统功耗。在进程线程方面,进程和线程各有优缺点,需要根据具体的应用场景选择合适的设计方案。同时,合理的调度策略可以提高系统的性能和响应速度。
在实际开发中,需要根据系统的需求和特点,综合考虑电源管理、进程线程设计和调度策略等因素,以实现高效、稳定的嵌入式系统。例如,在对功耗要求较高的应用中,可以启用无滴答操作和运行时电源管理;在对实时性要求较高的应用中,可以选择实时调度策略。
总之,深入理解 Linux 系统的电源管理和进程线程机制,对于开发高质量的嵌入式系统具有重要意义。
超级会员免费看
1378

被折叠的 条评论
为什么被折叠?



