嵌入式系统实时性能优化与测量
1. 可抢占内核锁
在实时系统中,内核锁的可抢占性是一个关键问题。自旋锁是内核中常用的锁机制,它是一种忙等待互斥锁,在竞争情况下无需上下文切换,只要锁的持有时间较短,效率就非常高。理想情况下,自旋锁的锁定时间应小于两次重新调度所需的时间。
然而,持有自旋锁的线程不能被抢占,因为这可能导致新线程进入相同代码并在尝试锁定同一自旋锁时发生死锁。因此,在主线 Linux 中,锁定自旋锁会禁用内核抢占,创建一个原子上下文。这意味着持有自旋锁的低优先级线程可能会阻止高优先级线程被调度。
为了解决这个问题,PREEMPT_RT 采用的解决方案是将几乎所有自旋锁替换为 RT 互斥锁。互斥锁比自旋锁慢,但它是完全可抢占的。此外,RT 互斥锁实现了优先级继承,因此不易受到优先级反转的影响。
2. 获取 PREEMPT_RT 补丁
RT 开发人员不会为每个内核版本创建补丁集,平均每两个内核版本创建一次补丁。目前支持的最新内核版本如下:
- 4.9 - rt
- 4.8 - rt
- 4.6 - rt
- 4.4 - rt
- 4.1 - rt
- 4.0 - rt
- 3.18 - rt
- 3.14 - rt
- 3.12 - rt
- 3.10 - rt
- 3.4 - rt
- 3.2 - rt
这些补丁可在 https://www.kernel.org/pub/linux/kernel/projects/rt 获取。
如果你使用的是 Yocto 项目,已经有内核的 rt 版本。否则,你获取内核的地方可能已经应用了 PREEMPT_RT 补丁。如果没有,你需要自己应用补丁。首先,确保 PREEMPT_RT 补丁版本与你的内核版本完全匹配,否则无法干净地应用补丁。然后,按以下命令行正常应用补丁,之后你就可以使用 CONFIG_PREEMPT_RT_FULL 配置内核:
$ cd linux-4.1.10
$ zcat patch-4.1.10-rt11.patch.gz | patch -p1
需要注意的是,RT 补丁仅适用于兼容的主线内核,而嵌入式 Linux 内核通常并非如此。因此,你可能需要花费一些时间查看失败的补丁并进行修复,然后分析目标板的支持情况并添加缺失的实时支持。
3. Yocto 项目与 PREEMPT_RT
Yocto 项目提供了两个标准内核配方:linux - yocto 和 linux - yocto - rt,后者已经应用了实时补丁。假设你的目标受 Yocto 内核支持,你只需选择 linux - yocto - rt 作为首选内核,并声明你的机器兼容,例如在 conf/local.conf 中添加以下类似行:
PREFERRED_PROVIDER_virtual/kernel = "linux-yocto-rt"
COMPATIBLE_MACHINE_beaglebone = "beaglebone"
4. 高分辨率定时器
对于有精确计时要求的实时应用程序,定时器分辨率非常重要。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
这两种方法得到的数字有所不同,但都低于 1 微秒。
5. 避免页面错误
当应用程序读写未提交到物理内存的内存时,会发生页面错误。由于很难预测页面错误何时发生,因此它们是计算机中非确定性的另一个来源。
幸运的是,有一个函数 mlockall(2) 可以让你提交进程使用的所有内存并将其锁定,从而避免页面错误。它有两个标志:
- MCL_CURRENT:锁定当前映射的所有页面
- MCL_FUTURE:锁定以后映射的页面
通常在应用程序启动时同时设置这两个标志调用 mlockall,以锁定所有当前和未来的内存映射。
需要注意的是,MCL_FUTURE 并非万能,使用 malloc()/free() 或 mmap() 分配或释放堆内存时仍会有非确定性延迟。因此,这些操作最好在启动时完成,而不是在主控制循环中进行。
栈上分配的内存更棘手,因为它是自动完成的。如果你调用一个使栈比以前更深的函数,会遇到更多内存管理延迟。一个简单的解决方法是在启动时将栈增长到比你认为需要的更大的大小,代码如下:
#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);
// ...
}
stack_grow() 函数在栈上分配一个大变量,然后将其清零,以强制将这些内存页面提交给该进程。
6. 中断屏蔽
使用线程化中断处理程序可以通过让一些线程以比不影响实时任务的中断处理程序更高的优先级运行来减轻中断开销。如果你使用的是多核处理器,可以采用不同的方法,将一个或多个核心完全屏蔽,使其不处理中断,从而专门用于实时任务。这在普通 Linux 内核或 PREEMPT_RT 内核中都可以实现。
要实现这一点,需要将实时线程固定到一个 CPU,将中断处理程序固定到另一个 CPU。你可以使用命令行工具 taskset 设置线程或进程的 CPU 亲和力,也可以使用 sched_setaffinity(2) 和 pthread_setaffinity_np(3) 函数。
要设置中断的亲和力,首先注意 /proc/irq/ 中有每个中断号的子目录,其中包含中断的控制文件,包括 smp_affinity 中的 CPU 掩码。向该文件写入一个位掩码,为每个允许处理该 IRQ 的 CPU 设置一个位。
7. 测量调度延迟
所有的配置和调优如果不能证明你的设备满足最后期限,都是毫无意义的。你需要自己的基准测试进行最终测试,但这里介绍两个重要的测量工具:cyclictest 和 Ftrace。
7.1 cyclictest
cyclictest 最初由 Thomas Gleixner 编写,现在大多数平台上的 rt - tests 包中都可以找到。如果你使用的是 Yocto 项目,可以通过构建实时映像配方创建包含 rt - tests 的目标映像:
$ bitbake core-image-rt
如果你使用的是 Buildroot,需要在菜单“Target packages | Debugging, profiling and benchmark | rt - tests”中添加包 BR2_PACKAGE_RT_TESTS。
cyclictest 通过比较睡眠的实际时间和请求时间来测量调度延迟。如果没有延迟,两者应该相同,报告的延迟为零。cyclictest 假设定时器分辨率小于 1 微秒。
它有大量的命令行选项,你可以作为根用户在目标上尝试运行以下命令:
# 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):这是 PID 320。
- P:99:优先级为 99。
- I:1000:循环间隔为 1000 微秒。可以使用参数 -i N 设置间隔。
- C:100000:该线程的最终循环计数为 100000。
- Min: 9:最小延迟为 9 微秒。
- Act:13:实际延迟为 13 微秒。实际延迟是最近一次的延迟测量,只有在你观察 cyclictest 运行时才有意义。
- Avg:15:平均延迟为 15 微秒。
- Max:134:最大延迟为 134 微秒。
为了获得更有意义的结果,你应该在 24 小时或更长时间内运行测试,并模拟预期的最大负载。
通过添加 -h 选项,可以获得最多延迟 N 微秒的样本直方图,以了解延迟值的分布情况。例如,使用以下命令:
# cyclictest -p 99 -m -n -l 100000 -q -h 500 > cyclictest.data
然后使用 gnuplot 创建图形。测试结果表明,无抢占内核下,大多数样本的延迟在 100 微秒内,但有一些高达 500 微秒的异常值;标准抢占内核下,样本在低端分散,但没有超过 120 微秒的;RT 抢占内核表现最佳,所有样本都集中在 20 微秒左右,没有超过 35 微秒的。
cyclictest 是调度延迟的标准度量工具,但它无法帮助你识别和解决内核延迟的具体问题,这就需要使用 Ftrace。
7.2 使用 Ftrace
内核函数跟踪器有一些跟踪器可以帮助追踪内核延迟,它们最初就是为此目的而编写的。这些跟踪器会捕获运行期间检测到的最坏情况延迟的跟踪信息,显示导致延迟的函数。
感兴趣的跟踪器及其对应的内核配置参数如下:
| 跟踪器名称 | 内核配置参数 | 功能描述 |
| — | — | — |
| irqsoff | CONFIG_IRQSOFF_TRACER | 跟踪禁用中断的代码,记录最坏情况 |
| preemptoff | CONFIG_PREEMPT_TRACER | 类似于 irqsoff,但跟踪内核抢占被禁用的最长时间(仅适用于可抢占内核) |
| preemptirqsoff | 无 | 结合前两个跟踪,记录中断和/或抢占被禁用的最长时间 |
| wakeup | 无 | 跟踪并记录最高优先级任务被唤醒后到被调度的最大延迟 |
| wakeup_rt | 无 | 与 wakeup 相同,但仅适用于具有 SCHED_FIFO、SCHED_RR 或 SCHED_DEADLINE 策略的实时线程 |
| wakeup_dl | 无 | 与 wakeup 相同,但仅适用于具有 SCHED_DEADLINE 策略的截止日期调度线程 |
需要注意的是,运行 Ftrace 会增加大量延迟,每次捕获新的最大值时,延迟可达数十毫秒,Ftrace 本身可以忽略这些延迟,但会影响用户空间跟踪器(如 cyclictest)的结果。因此,在捕获跟踪时,应忽略 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>
从这个跟踪信息中可以看到,运行跟踪期间内核抢占被禁用的最长时间为 1160 微秒。通过读取 /sys/kernel/debug/tracing/tracing_max_latency 可以获取这个简单的事实,但前面的跟踪信息更详细地给出了导致该测量结果的内核函数调用序列。标记为“delay”的列显示了每个函数被调用的时间点,最后在 1162 微秒时调用了 trace_preempt_on(),此时内核抢占再次启用。根据这些信息,你可以回溯调用链,判断这是否是一个问题。其他跟踪器的工作方式类似。
7.3 结合 cyclictest 和 Ftrace
如果 cyclictest 报告了意外的长延迟,你可以使用 breaktrace 选项中止程序并触发 Ftrace 获取更多信息。
你可以使用 -b
或 –breaktrace =
调用 breaktrace,其中 N 是触发跟踪的延迟微秒数。使用 -T[跟踪器名称] 或以下选项之一选择 Ftrace 跟踪器:
- -C:上下文切换
- -E:事件
- -f:函数
- -w:唤醒
- -W:实时唤醒
例如,当测量到的延迟大于 100 微秒时触发 Ftrace 函数跟踪器:
# cyclictest -a -t -n -p99 -f -b100
总结
实时系统需要明确的最后期限和可接受的错过率才有意义。在确定 Linux 是否适合作为操作系统后,需要对系统进行调优以满足实时要求。调优 Linux 和应用程序以处理实时事件意味着提高其确定性,使实时线程能够可靠地满足其最后期限,但确定性通常会牺牲总吞吐量。
由于无法为像 Linux 这样的复杂操作系统提供数学证明以确保其总是满足给定的最后期限,因此唯一的方法是使用 cyclictest 和 Ftrace 等工具进行广泛测试,更重要的是使用针对自己应用程序的基准测试。
为了提高确定性,需要同时考虑应用程序和内核。编写实时应用程序时,应遵循关于调度、锁定和内存的准则。内核对系统的确定性有很大影响,启用内核抢占是一个好的开始。如果仍然发现错过最后期限的情况过于频繁,可以考虑使用 PREEMPT_RT 内核补丁,但可能会在与特定板卡的供应商内核集成时遇到问题。此外,还可以使用 Ftrace 等工具找出延迟的原因。
作为嵌入式系统工程师,需要具备广泛的技能,包括对硬件的底层知识以及内核与硬件的交互方式。要成为优秀的系统工程师,能够配置用户应用程序并对其进行调优,使其高效运行。希望通过本文提供的信息,你能够在嵌入式系统开发中取得更好的成果。
8. 实时性能优化关键路径总结
为了更清晰地展示实时性能优化的关键步骤和操作,我们可以通过一个流程图来概括整个过程,以下是使用 mermaid 绘制的流程图:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B{是否需要实时性能?}:::decision
B -->|是| C(内核配置):::process
B -->|否| K([结束]):::startend
C --> D{是否使用 PREEMPT_RT?}:::decision
D -->|是| E(获取并应用 PREEMPT_RT 补丁):::process
D -->|否| F(启用标准内核抢占):::process
E --> G(配置高分辨率定时器):::process
F --> G
G --> H(应用程序优化):::process
H --> I(避免页面错误):::process
H --> J(中断屏蔽):::process
I --> L(测量调度延迟):::process
J --> L
L --> M{是否满足实时要求?}:::decision
M -->|是| K
M -->|否| N(使用 Ftrace 分析问题):::process
N --> C
这个流程图展示了实时性能优化的关键路径:
1. 需求判断 :首先确定系统是否需要实时性能。
2. 内核配置 :根据需求选择是否使用 PREEMPT_RT 内核补丁,或者启用标准内核抢占。
3. 定时器配置 :配置高分辨率定时器以满足精确计时要求。
4. 应用程序优化 :包括避免页面错误和进行中断屏蔽。
5. 延迟测量 :使用 cyclictest 等工具测量调度延迟。
6. 结果评估 :判断是否满足实时要求,如果不满足则使用 Ftrace 分析问题并重新进行配置。
9. 操作步骤总结
9.1 内核配置操作步骤
| 操作 | 步骤 | 命令示例 |
|---|---|---|
| 获取 PREEMPT_RT 补丁 | 1. 确认内核版本与补丁版本匹配 2. 从 https://www.kernel.org/pub/linux/kernel/projects/rt 下载补丁 | $ cd linux-4.1.10 $ zcat patch-4.1.10-rt11.patch.gz | patch -p1 |
| 配置 Yocto 项目使用实时内核 | 1. 选择 linux-yocto-rt 作为首选内核 2. 在 conf/local.conf 中声明机器兼容 | PREFERRED_PROVIDER_virtual/kernel = "linux-yocto-rt" COMPATIBLE_MACHINE_beaglebone = "beaglebone" |
| 启用高分辨率定时器 | 配置内核使用 CONFIG_HIGH_RES_TIMERS = y | 在内核配置文件中设置 CONFIG_HIGH_RES_TIMERS=y |
9.2 应用程序优化操作步骤
| 操作 | 步骤 | 代码示例 |
|---|---|---|
| 避免页面错误 | 1. 在应用程序启动时调用 mlockall 锁定内存 2. 增长栈大小 | c<br>#define MAX_STACK (512*1024)<br>static void stack_grow (void) <br>{<br> char dummy[MAX_STACK]; <br> memset(dummy, 0, MAX_STACK); <br> return; <br>}<br>int main(int argc, char* argv[])<br>{<br> stack_grow ();<br> mlockall(MCL_CURRENT \| MCL_FUTURE);<br>}<br> |
| 中断屏蔽 | 1. 使用 taskset 或相关函数设置线程 CPU 亲和力 2. 设置中断的 CPU 亲和力 | $ taskset -c 0 ./your_realtime_app # echo 0x1 > /proc/irq/<IRQ number>/smp_affinity |
9.3 延迟测量与分析操作步骤
| 操作 | 步骤 | 命令示例 |
|---|---|---|
| 使用 cyclictest 测量延迟 | 1. 安装 rt - tests 包 2. 运行 cyclictest 命令 | $ bitbake core-image-rt (Yocto 项目) # cyclictest -l 100000 -m -n -p 99 |
| 使用 Ftrace 分析延迟 | 1. 选择跟踪器 2. 开始跟踪 3. 结束跟踪并查看结果 | # echo preemptoff > /sys/kernel/debug/tracing/current_tracer # echo 1 > /sys/kernel/debug/tracing/tracing_on # sleep 60 # echo 0 > /sys/kernel/debug/tracing/tracing_on # cat /sys/kernel/debug/tracing/trace |
| 结合 cyclictest 和 Ftrace | 1. 设置 breaktrace 触发条件 2. 选择 Ftrace 跟踪器 | # cyclictest -a -t -n -p99 -f -b100 |
10. 常见问题与解决方案
在实时性能优化过程中,可能会遇到一些常见问题,以下是一些问题及对应的解决方案:
10.1 补丁应用失败
- 问题描述 :在应用 PREEMPT_RT 补丁时,可能会因为内核版本不兼容或其他原因导致补丁应用失败。
- 解决方案 :
- 确保补丁版本与内核版本完全匹配。
- 手动查看失败的补丁并进行修复,分析目标板的支持情况并添加缺失的实时支持。如果不确定如何操作,可以咨询内核开发者或在相关论坛寻求帮助。
10.2 调度延迟过高
- 问题描述 :使用 cyclictest 测量时,发现调度延迟过高,无法满足实时要求。
- 解决方案 :
- 使用 Ftrace 工具分析导致延迟的具体内核函数和代码段。
- 检查是否有低优先级任务长时间占用资源,考虑调整任务优先级或使用中断屏蔽等技术。
- 检查是否启用了高分辨率定时器,确保定时器配置正确。
10.3 内存管理问题
- 问题描述 :应用程序在运行过程中出现页面错误或内存管理延迟,影响实时性能。
- 解决方案 :
- 在应用程序启动时使用 mlockall 函数锁定内存,避免页面错误。
- 尽量在启动时完成堆内存的分配和释放操作,避免在主控制循环中进行。
- 增长栈大小,避免因栈增长导致的内存管理延迟。
11. 实时性能优化的未来趋势
随着嵌入式系统的不断发展,实时性能优化也面临着新的挑战和机遇。以下是一些可能的未来趋势:
11.1 内核集成与标准化
PREEMPT_RT 内核补丁虽然能够显著提高实时性能,但目前尚未完全集成到主线内核中。未来可能会看到更多的努力将实时功能更好地集成到主线内核,实现标准化,减少开发者在集成过程中遇到的问题。
11.2 硬件与软件协同优化
随着硬件技术的不断进步,如多核处理器、专用实时硬件加速器等,未来的实时性能优化将更加注重硬件与软件的协同工作。通过硬件加速和软件优化的结合,可以进一步提高系统的实时性能和效率。
11.3 智能化优化工具
随着人工智能和机器学习技术的发展,未来可能会出现智能化的实时性能优化工具。这些工具可以自动分析系统的性能瓶颈,并提供针对性的优化建议,甚至自动进行优化配置,减少开发者的工作量。
11.4 安全与实时性能的平衡
在实时系统中,安全性也是一个重要的考虑因素。未来需要在保证实时性能的同时,更好地平衡系统的安全性。例如,开发安全的实时通信协议和加密算法,确保数据在实时传输过程中的安全性。
12. 总结与展望
实时性能优化是嵌入式系统开发中的一个重要课题,对于许多对时间敏感的应用场景至关重要。通过本文介绍的方法和工具,如可抢占内核锁的使用、高分辨率定时器的配置、页面错误的避免、中断屏蔽技术以及调度延迟的测量和分析等,可以有效地提高系统的实时性能。
然而,实时性能优化是一个复杂的过程,需要综合考虑内核、应用程序和硬件等多个方面。在实际应用中,开发者需要根据具体的需求和场景,选择合适的优化策略和工具,并进行不断的测试和调整。
未来,随着技术的不断发展,实时性能优化将迎来更多的机遇和挑战。作为嵌入式系统工程师,我们需要不断学习和掌握新的技术和方法,以适应不断变化的需求,开发出更加高效、稳定和安全的实时系统。希望本文能够为你在实时性能优化方面提供有价值的参考,帮助你在嵌入式系统开发中取得更好的成果。
超级会员免费看
729

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



