实时Linux编程:内核锁、定时器与延迟测量
1. 可抢占内核锁
在实时Linux内核中,可抢占内核锁是一项重要的改进。PREEMPT_RT 对内核锁进行了重大修改,使大多数内核锁变为可抢占的,不过这部分代码尚未纳入主线内核。
1.1 自旋锁问题
自旋锁是内核中常用的锁机制,它是一种忙等待互斥锁,在竞争情况下无需上下文切换,只要锁的持有时间较短,效率就非常高。理想情况下,自旋锁的锁定时间应小于两次重新调度所需的时间。
然而,持有自旋锁的线程不能被抢占,因为抢占可能导致新线程进入相同代码,在尝试锁定同一自旋锁时发生死锁。因此,在主线 Linux 中,锁定自旋锁会禁用内核抢占,创建一个原子上下文,这可能导致低优先级线程持有自旋锁时阻止高优先级线程调度,即优先级反转问题。
1.2 PREEMPT_RT 的解决方案
PREEMPT_RT 采用的解决方案是将几乎所有自旋锁替换为 RT - mutexes。互斥锁虽然比自旋锁慢,但它是完全可抢占的,并且实现了优先级继承,因此不会出现优先级反转问题。
2. 获取 PREEMPT_RT 补丁
RT 开发者不会为每个内核版本创建补丁集,平均每两个内核版本创建一次补丁。目前支持的最新内核版本如下:
- 5.10 - rt
- 5.9 - rt
- 5.6 - rt
- 5.4 - rt
- 5.2 - rt
- 5.0 - rt
- 4.19 - rt
- 4.18 - rt
- 4.16 - rt
- 4.14 - rt
- 4.13 - rt
- 4.11 - rt
补丁可从以下地址获取:https://www.kernel.org/pub/linux/kernel/projects/rt 。
如果你使用的是 Yocto Project,已经有内核的 rt 版本。否则,你获取内核的地方可能已经应用了 PREEMPT_RT 补丁。若没有,你需要自己应用补丁。首先,确保 PREEMPT_RT 补丁版本与你的内核版本完全匹配,然后按以下命令行正常应用补丁,之后就可以使用 CONFIG_PREEMPT_RT_FULL 配置内核:
$ cd linux - 5.4.93
$ zcat patch - 5.4.93 - rt51.patch.gz | patch - p1
3. 高分辨率定时器
对于有精确计时要求的实时应用,定时器分辨率非常重要。Linux 的默认定时器是一个可配置速率的时钟,嵌入式系统通常为 100 Hz,服务器和桌面系统为 250 Hz。两个定时器滴答之间的间隔称为 jiffy,在嵌入式 SoC 上为 10 毫秒,在服务器上为 4 毫秒。
从 2.6.18 版本开始,Linux 从实时内核项目中获得了更精确的定时器,现在只要有高分辨率定时器源和相应的设备驱动(通常情况都是如此),所有平台都可以使用。你需要将内核配置为 CONFIG_HIGH_RES_TIMERS = y。
启用后,所有内核和用户空间时钟将精确到底层硬件的粒度。确定实际时钟粒度比较困难,虽然 clock_getres(2) 总是声称分辨率为 1 纳秒,但可以使用 cyclictest 工具来分析时钟报告的时间以猜测分辨率:
# cyclictest -R
# /dev/cpu_dma_latency set to 0us
WARN: reported clock resolution: 1 nsec
WARN: measured clock resolution approximately: 708 nsec
你也可以查看内核日志消息:
# dmesg | grep clock
OMAP clockevent source: timer2 at 24000000 Hz
sched_clock: 32 bits at 24MHz, resolution 41ns, wraps every 178956969942ns
OMAP clocksource: timer1 at 24000000 Hz
Switched to clocksource timer1
4. 避免页面错误
页面错误是指应用程序读写未提交到物理内存的内存时发生的情况。由于很难预测页面错误何时发生,它是计算机中不确定性的另一个来源。
幸运的是,有一个函数 mlockall(2) 可以将进程使用的所有内存提交并锁定,防止页面错误。它有两个标志:
- MCL_CURRENT:锁定当前映射的所有页面。
- MCL_FUTURE:锁定后续映射的页面。
通常在应用程序启动时同时设置这两个标志来锁定所有当前和未来的内存映射:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#define MAX_STACK (512*1024)
static void stack_grow (void)
{
char dummy[MAX_STACK];
memset(dummy, 0, MAX_STACK);
return;
}
int main(int argc, char* argv[])
{
stack_grow ();
mlockall(MCL_CURRENT | MCL_FUTURE);
// 其他代码
return 0;
}
5. 中断屏蔽
使用线程化中断处理程序可以通过让一些线程以比不影响实时任务的中断处理程序更高的优先级运行来减轻中断开销。如果你使用的是多核处理器,可以采用另一种方法,完全屏蔽一个或多个核心处理中断,让它们专门处理实时任务,这在普通 Linux 内核或 PREEMPT_RT 内核中都适用。
要实现这一点,需要将实时线程固定到一个 CPU,将中断处理程序固定到另一个 CPU。可以使用 taskset 命令行工具设置线程或进程的 CPU 亲和性,也可以使用 sched_setaffinity(2) 和 pthread_setaffinity_np(3) 函数。
设置中断亲和性时,首先注意 /proc/irq/ 下每个中断编号都有一个子目录,其中包含中断的控制文件,包括 smp_affinity 中的 CPU 掩码。向该文件写入一个位掩码,为每个允许处理该 IRQ 的 CPU 设置相应位。
6. 测量调度延迟
所有的配置和调优如果不能证明设备满足最后期限,都是没有意义的。下面介绍两个重要的测量工具:cyclictest 和 Ftrace。
6.1 cyclictest
cyclictest 最初由 Thomas Gleixner 编写,现在大多数平台上的 rt - tests 包中都有。如果你使用的是 Yocto Project,可以通过以下命令构建包含 rt - tests 的目标镜像:
$ bitbake core - image - rt
如果你使用的是 Buildroot,需要在 Target packages | Debugging, profiling and benchmark | rt - tests 菜单中添加 BR2_PACKAGE_RT_TESTS 包。
cyclictest 通过比较实际睡眠时间和请求睡眠时间来测量调度延迟。如果没有延迟,两者应该相同,报告的延迟为 0。它假设定时器分辨率小于 1 微秒,有大量命令行选项。例如,以 root 身份在目标上运行以下命令:
# cyclictest -l 100000 -m -n -p 99
# /dev/cpu_dma_latency set to 0us
policy: fifo: loadavg: 1.14 1.06 1.00 1/49 320
T: 0 ( 320) P:99 I:1000 C: 100000 Min: 9 Act: 13 Avg: 15 Max: 134
选项说明:
- -l N:循环 N 次(默认无限制)。
- -m:使用 mlockall 锁定内存。
- -n:使用 clock_nanosleep(2) 而不是 nanosleep(2)。
- -p N:使用实时优先级 N。
结果行从左到右表示:
- T: 0:这是线程 0,本次运行中唯一的线程。可以使用 -t 参数设置线程数。
- (320):进程 ID 为 320。
- P:99:优先级为 99。
- I:1000:循环间隔为 1000 微秒。可以使用 -i N 参数设置间隔。
- C:100000:该线程的最终循环次数为 100000。
- Min: 9:最小延迟为 9 微秒。
- Act:13:实际延迟为 13 微秒。
- Avg:15:平均延迟为 15 微秒。
- Max:134:最大延迟为 134 微秒。
为了更全面地了解延迟值的分布,可以添加 -h 选项获取最多延迟 N 微秒的样本直方图。例如:
# cyclictest -p 99 -m -n -l 100000 -q -h 500 > cyclictest.data
6.2 Ftrace
内核函数跟踪器有一些跟踪器可以帮助追踪内核延迟。以下是一些感兴趣的跟踪器及其内核配置参数:
| 跟踪器 | 内核配置参数 | 说明 |
| ---- | ---- | ---- |
| irqsoff | CONFIG_IRQSOFF_TRACER | 跟踪禁用中断的代码,记录最坏情况 |
| preemptoff | CONFIG_PREEMPT_TRACER | 跟踪内核抢占被禁用的最长时间(仅在可抢占内核中可用) |
| preemptirqsoff | 无 | 结合前两个跟踪器,记录中断和/或抢占被禁用的最长时间 |
| wakeup | 无 | 跟踪并记录最高优先级任务唤醒后被调度的最大延迟 |
| wakeup_rt | 无 | 与 wakeup 相同,但仅适用于具有 SCHED_FIFO、SCHED_RR 或 SCHED_DEADLINE 策略的实时线程 |
| wakeup_dl | 无 | 与 wakeup 相同,但仅适用于具有 SCHED_DEADLINE 策略的截止日期调度线程 |
需要注意的是,运行 Ftrace 会增加大量延迟,每次捕获新的最大值时延迟约为几十毫秒,Ftrace 本身可以忽略这些延迟,但会影响 cyclictest 等用户空间跟踪器的结果。
选择跟踪器的方法与之前介绍的函数跟踪器相同。例如,捕获 60 秒内内核抢占被禁用的最大周期的跟踪:
# echo preemptoff > /sys/kernel/debug/tracing/current_tracer
# echo 0 > /sys/kernel/debug/tracing/tracing_max_latency
# echo 1 > /sys/kernel/debug/tracing/tracing_on
# sleep 60
# echo 0 > /sys/kernel/debug/tracing/tracing_on
查看跟踪结果:
# cat /sys/kernel/debug/tracing/trace
# tracer: preemptoff
#
# preemptoff latency trace v1.1.5 on 3.14.19-yocto-standard
# ------------------------------------------------------------
# latency: 1160 us, #384/384, CPU#0 | (M:preempt VP:0, KP:0, SP:0 HP:0)
# -----------------
# | task: init-1 (uid:0 nice:0 policy:0 rt_prio:0)
# -----------------
# => started at: ip_finish_output
# => ended at: __local_bh_enable_ip
#
#
# _------=> CPU#
# / _-----=> irqs-off
# | / _----=> need-resched
# || / _---=> hardirq/softirq
# ||| / _--=> preempt-depth
# |||| / delay
# cmd pid ||||| time | caller
# \ / ||||| \ | /
init-1 0..s. 1us+: ip_finish_output
init-1 0d.s2 27us+: preempt_count_add <-cpdma_chan_submit
init-1 0d.s3 30us+: preempt_count_add <-cpdma_chan_submit
init-1 0d.s4 37us+: preempt_count_sub <-cpdma_chan_submit
[…]
init-1 0d.s2 1152us+: preempt_count_sub <-__local_bh_enable
init-1 0d..2 1155us+: preempt_count_sub <-__local_bh_enable_ip
init-1 0d..1 1158us+: __local_bh_enable_ip
init-1 0d..1 1162us!: trace_preempt_on <-__local_bh_enable_ip
init-1 0d..1 1340us : <stack trace>
6.3 结合 cyclictest 和 Ftrace
如果 cyclictest 报告的延迟意外长,可以使用 breaktrace 选项中止程序并触发 Ftrace 获取更多信息。使用 -b
或 –breaktrace =
调用 breaktrace,其中 N 是触发跟踪的延迟微秒数。使用 -T[跟踪器名称] 或以下选项选择 Ftrace 跟踪器:
- -C:上下文切换
- -E:事件
- -f:函数
- -w:唤醒
- -W:唤醒 - RT
例如,当测量到的延迟大于 100 微秒时触发 Ftrace 函数跟踪器:
# cyclictest -a -t -n -p99 -f -b100
7. 总结
实时性只有在明确了最后期限和可接受的错过率才有意义。有了这两个信息,就可以确定 Linux 是否适合作为操作系统,并开始调整系统以满足要求。调整 Linux 和应用程序以处理实时事件意味着使其更具确定性,以便实时线程能够可靠地满足最后期限。确定性通常会牺牲总吞吐量,因此实时系统处理的数据量不如非实时系统多。
由于无法从数学上证明像 Linux 这样的复杂操作系统总是能满足给定的最后期限,唯一的方法是通过使用 cyclictest 和 Ftrace 等工具进行广泛测试,更重要的是使用针对自己应用程序的基准测试。为了提高确定性,需要同时考虑应用程序和内核。编写实时应用程序时,应遵循有关调度、锁定和内存的准则。
通过以上方法,可以更好地开发和优化实时 Linux 应用程序,确保系统的实时性能。
实时Linux编程:内核锁、定时器与延迟测量
8. 实时编程综合优化策略
在实时编程中,为了确保系统的实时性能,需要综合运用前面提到的各种技术和工具进行优化。以下是一个综合优化策略的流程图:
graph TD
A[开始] --> B[选择合适内核]
B --> C{是否使用PREEMPT_RT补丁}
C -- 是 --> D[获取并应用补丁]
C -- 否 --> E[使用现有内核]
D --> F[配置内核参数]
E --> F
F --> G[启用高分辨率定时器]
G --> H[避免页面错误]
H --> I[设置中断屏蔽]
I --> J[测量调度延迟]
J --> K{延迟是否满足要求}
K -- 是 --> L[完成优化]
K -- 否 --> M[调整参数或代码]
M --> J
9. 优化案例分析
为了更直观地展示优化效果,下面通过一个具体案例进行分析。假设我们有一个实时嵌入式系统,要求任务的最大延迟不超过 50 微秒。
9.1 初始状态
在未进行任何优化之前,使用 cyclictest 进行测试:
# cyclictest -l 100000 -m -n -p 99
# /dev/cpu_dma_latency set to 0us
policy: fifo: loadavg: 1.20 1.15 1.10 1/50 350
T: 0 ( 350) P:99 I:1000 C: 100000 Min: 12 Act: 18 Avg: 22 Max: 150
从结果可以看出,最大延迟达到了 150 微秒,远远超过了要求。
9.2 优化步骤
- 应用 PREEMPT_RT 补丁 :
$ cd linux - 5.4.93
$ zcat patch - 5.4.93 - rt51.patch.gz | patch - p1
-
配置内核参数
:
启用 CONFIG_PREEMPT_RT_FULL 和 CONFIG_HIGH_RES_TIMERS = y。 -
避免页面错误
:
在应用程序中添加 mlockall 函数:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#define MAX_STACK (512*1024)
static void stack_grow (void)
{
char dummy[MAX_STACK];
memset(dummy, 0, MAX_STACK);
return;
}
int main(int argc, char* argv[])
{
stack_grow ();
mlockall(MCL_CURRENT | MCL_FUTURE);
// 其他代码
return 0;
}
-
设置中断屏蔽
:
使用 taskset 命令将实时任务固定到一个 CPU 核心,将中断处理程序固定到另一个核心。 - 再次测量调度延迟 :
# cyclictest -l 100000 -m -n -p 99
# /dev/cpu_dma_latency set to 0us
policy: fifo: loadavg: 0.80 0.75 0.70 1/50 380
T: 0 ( 380) P:99 I:1000 C: 100000 Min: 8 Act: 12 Avg: 15 Max: 30
9.3 优化效果分析
通过以上优化步骤,最大延迟从 150 微秒降低到了 30 微秒,满足了系统的实时要求。这表明综合运用各种优化技术可以显著提高系统的实时性能。
10. 实时编程的注意事项
在进行实时编程时,还需要注意以下几点:
-
资源管理
:合理管理系统资源,避免资源竞争和浪费。例如,在使用锁时,尽量减少锁的持有时间,避免死锁的发生。
-
代码优化
:优化代码逻辑,减少不必要的计算和内存访问。例如,使用高效的算法和数据结构,避免频繁的内存分配和释放。
-
系统监控
:定期监控系统的性能指标,如 CPU 使用率、内存使用率、调度延迟等。及时发现并解决潜在的问题。
-
兼容性问题
:在使用新的内核版本或工具时,要注意兼容性问题。确保所有的软件和硬件都能正常工作。
11. 未来发展趋势
随着科技的不断发展,实时编程也在不断演进。未来,实时 Linux 系统可能会朝着以下几个方向发展:
-
更高的实时性
:通过进一步优化内核和硬件,实现更低的调度延迟和更高的确定性。
-
智能化
:引入人工智能和机器学习技术,实现自适应的实时调度和优化。
-
多核和异构计算
:充分利用多核处理器和异构计算资源,提高系统的并行处理能力。
-
安全性
:加强实时系统的安全性,防止恶意攻击和数据泄露。
12. 总结
实时编程是一个复杂而重要的领域,对于许多对时间敏感的应用至关重要。通过本文介绍的可抢占内核锁、高分辨率定时器、避免页面错误、中断屏蔽等技术,以及 cyclictest 和 Ftrace 等测量工具,可以有效地提高实时 Linux 系统的性能。同时,结合综合优化策略和注意事项,能够更好地开发出满足实时要求的应用程序。
未来,实时编程将不断发展,我们需要持续关注新技术和趋势,不断提升自己的编程能力和水平,以应对日益复杂的实时应用需求。希望本文能够为从事实时编程的开发者提供一些有用的参考和指导。
实时Linux编程:内核锁、定时器与延迟测量
超级会员免费看
73

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



