Linux性能分析与实时编程指南
在Linux系统中,性能分析和实时编程是非常重要的主题。下面将详细介绍Linux系统中性能分析工具以及实时编程的相关知识。
性能分析工具概述
在Linux系统中,有多种性能分析和跟踪工具可供选择:
-
top
:当系统性能不佳时,可首先使用
top
命令尝试识别问题。它能提供系统中各个进程的资源使用情况,帮助我们快速定位可能存在问题的进程。
-
perf record/report
:如果问题出在单个应用程序上,可以使用
perf record
和
perf report
来对其进行性能分析。不过,使用
perf
需要对内核进行配置以启用该功能,并且需要为二进制文件和内核提供调试符号。
-
OProfile
:它是
perf record
的替代方案,能提供类似的分析结果。
-
gprof
:虽然已经有些过时,但它的优点是不需要内核支持。
-
Ftrace
:当你对内核行为有特定疑问时,Ftrace就派上用场了。其函数和函数图跟踪器能详细展示函数调用的关系和顺序,事件跟踪器则可提取函数的更多信息,包括参数和返回值。
-
LTTng
:利用事件跟踪机制,添加了高速环形缓冲区,可从内核中提取大量数据,与Ftrace有相似的作用。
-
Valgrind
:它在沙箱中运行代码,能报告一些难以通过其他方式追踪的错误。使用Callgrind工具,它可以生成调用图并报告处理器缓存的使用情况;使用Helgrind,它可以报告与线程相关的问题。
-
strace
:是查找程序进行哪些系统调用的好工具,可用于跟踪文件打开调用、查找文件路径名以及检查系统唤醒和传入信号等。
在使用这些工具时,要注意避免观察者效应,确保所做的测量对生产系统是有效的。
实时编程基础
实时编程在计算机系统与现实世界的交互中至关重要,特别是对于嵌入式系统开发者来说。那么,什么是实时呢?
一个任务如果必须在某个特定时间点(即截止时间)之前完成,那么它就是一个实时任务。例如,在计算机上播放音频流时,由于音频驱动会不断接收数据流,音频样本块必须以播放速率写入音频接口,所以这是一个实时任务;而编译Linux内核则没有明确的截止时间,只是希望能尽快完成,因此不是实时任务。
错过截止时间的后果各不相同,我们通常将实时任务分为以下几类:
-
软实时
:截止时间是理想的,但有时错过也不会导致系统被视为失败。例如播放音频流和移动点击鼠标,错过截止时间可能只会带来一些小困扰。
-
硬实时
:错过截止时间会产生严重影响。硬实时又可进一步细分为任务关键系统和安全关键系统。任务关键系统中,错过截止时间会带来成本损失,如生产线上打印瓶子保质期;安全关键系统中,错过截止时间会危及生命安全,如电源浪涌检测系统。
对于安全关键系统的软件,必须符合各种标准以确保其可靠运行,像Linux这样的复杂操作系统很难满足这些要求。而对于任务关键系统,Linux常用于各种控制系统,软件的要求取决于截止时间和置信水平,通常可通过大量测试来确定。
要使系统成为实时系统,需要在最大预期负载下测量其响应时间,并证明在一定比例的时间内能够满足截止时间。一般来说,配置良好的使用主线内核的Linux系统适用于截止时间低至几十毫秒的软实时任务,而应用了PREEMPT_RT补丁的内核则适用于截止时间低至几百微秒的软实时和硬实时任务关键系统。
创建实时系统的关键在于减少响应时间的可变性,使系统更加确定,但这往往会牺牲一定的性能。例如,缓存虽然能使系统运行更快,但会增加缓存未命中时的最大访问时间,使系统变得不那么确定。
非确定性来源分析
实时编程的核心是确保控制实时输出的线程在需要时能够被调度,从而在截止时间前完成任务。以下是一些可能导致问题的非确定性来源:
1.
调度
:实时线程必须具有实时调度策略(SCHED_FIFO或SCHED_RR),并且应根据速率单调分析理论,按照截止时间从短到长的顺序为线程分配优先级。
2.
调度延迟
:内核必须能够在中断或定时器等事件发生时尽快重新调度,避免无界延迟。减少调度延迟是实时编程中的关键问题。
3.
优先级反转
:这是基于优先级调度的结果,当高优先级线程被低优先级线程持有的互斥锁阻塞时,会导致无界延迟。在用户空间有优先级继承和优先级上限互斥锁,在内核空间有rt - mutexes实现优先级继承。
4.
精确的定时器
:如果要管理低毫秒或微秒级的截止时间,就需要匹配的高精度定时器。几乎所有内核都将高精度定时器作为一个配置选项。
5.
页面错误
:在执行关键代码段时发生页面错误会打乱所有的时间估计。可以通过锁定内存来避免页面错误。
6.
中断
:中断发生的时间是不可预测的,如果突然大量出现,可能会导致意外的处理开销。有两种方法可以避免这种情况:一是将中断作为内核线程运行,二是在多核设备上屏蔽一个或多个CPU的中断处理。
7.
处理器缓存
:它在CPU和主内存之间提供缓冲,但和所有缓存一样,是不确定性的来源,尤其是在多核设备上。不过,这超出了本文的讨论范围。
8.
内存总线争用
:当外设通过DMA通道直接访问内存时,会占用一部分内存总线带宽,从而减慢CPU核心的访问速度,导致程序执行的不确定性。这是一个硬件问题,也超出了本文的讨论范围。
调度延迟的理解
实时线程在有任务时需要尽快被调度,但即使没有相同或更高优先级的其他线程,从唤醒事件(如中断或系统定时器)发生到线程开始运行之间总会有延迟,这就是调度延迟。它可以分解为以下几个部分:
-
硬件中断延迟
:从中断被触发到中断服务例程(ISR)开始运行的时间。其中一小部分是中断硬件本身的延迟,但主要问题是软件中禁用了中断。因此,尽量减少IRQ关闭时间很重要。
-
中断延迟
:ISR处理中断并唤醒等待该事件的任何线程所需的时间。这主要取决于ISR的编写方式,通常应该只需要几微秒。
-
抢占延迟
:从内核得知线程准备好运行到调度器实际运行该线程的时间。这取决于内核是否可以被抢占,如果内核正在执行关键代码段,重新调度就必须等待。延迟的长度取决于内核抢占的配置。
内核抢占设置
由于并非总是安全或理想地抢占当前执行线程并调用调度器,主线Linux有三种内核抢占设置,可通过“Kernel Features | Preemption Model”菜单进行选择:
-
CONFIG_PREEMPT_NONE
:无抢占。内核代码将继续执行,直到通过系统调用返回用户空间(用户空间总是允许抢占)或遇到睡眠等待停止当前线程。这种设置减少了内核和用户空间之间的转换次数,可能会减少总的上下文切换次数,因此吞吐量最高,但抢占延迟较大。这是服务器和一些桌面内核的默认设置,因为在这些场景中吞吐量比响应性更重要。
-
CONFIG_PREEMPT_VOLUNTARY
:启用额外的抢占请求检查。当
need_resched
标志被设置时,调度器会被调用,这降低了最坏情况下的抢占延迟,但会稍微降低吞吐量。一些发行版在桌面系统上使用此设置。
-
CONFIG_PREEMPT
:允许内核被抢占。只要内核不在原子上下文中执行,中断就可以导致立即重新调度。这降低了最坏情况下的抢占延迟,因此在典型的嵌入式硬件上,整体调度延迟可降低到几毫秒左右。这通常被描述为软实时选项,大多数嵌入式内核都采用这种配置。当然,整体吞吐量会有小幅下降,但对于嵌入式设备来说,更确定的调度通常比吞吐量更重要。
实时Linux内核(PREEMPT_RT)
为了进一步减少延迟,有一个长期的项目,即PREEMPT_RT。该项目由Ingo Molnar、Thomas Gleixner和Steven Rostedt发起,多年来有许多开发者参与贡献。内核补丁可在https://www.kernel.org/pub/linux/kernel/projects/rt获取,相关的维基页面(包括一个稍显过时的FAQ)在https://rt.wiki.kernel.org。
多年来,该项目的许多部分已被整合到主线Linux中,包括高精度定时器、内核互斥锁和线程化中断处理程序。但核心补丁仍未纳入主线,因为它们的改动较大,且有人认为只对一小部分Linux用户有益。也许有一天,整个补丁集会被合并到上游。
PREEMPT_RT的核心计划是减少内核在原子上下文中运行的时间,原子上下文是指调用调度器并切换到不同线程不安全的情况,例如:
- 运行中断或陷阱处理程序时。
- 持有自旋锁或处于RCU关键部分时。
- 在调用
preempt_disable()
和
preempt_enable()
之间时。
- 硬件中断被禁用(IRQs关闭)时。
PREEMPT_RT的更改主要集中在两个方面:一是将中断处理程序转换为内核线程,以减少其影响;二是使锁可被抢占,以便线程在持有锁时可以睡眠。虽然这些更改会带来较大的开销,使平均情况下的中断处理变慢,但能让系统更加确定,这正是我们所追求的。
线程化中断处理程序
并非所有中断都是实时任务的触发因素,但所有中断都会占用实时任务的CPU周期。线程化中断处理程序允许为中断关联一个优先级,并在适当的时间进行调度。
如果将中断处理程序代码作为内核线程运行,那么它就可以被更高优先级的用户空间线程抢占,从而不会增加用户空间线程的调度延迟。自Linux 2.6.30起,主线Linux就支持线程化中断处理程序。你可以使用
request_threaded_irq()
代替
request_irq()
来请求将单个中断处理程序线程化。通过将内核配置为
CONFIG_IRQ_FORCED_THREADING=y
,可以使线程化IRQ成为默认设置,除非中断处理程序通过设置
IRQF_NO_THREAD
标志明确禁止。当应用PREEMPT_RT补丁时,中断默认会被配置为线程化。
例如,在运行linux - yocto - rt的BeagleBone上,使用以下命令查看线程化中断处理程序:
# ps -Leo pid,tid,class,rtprio,stat,comm,wchan | grep FF
PID TID CLS RTPRIO STAT COMMAND WCHAN
3 3 FF 1 S ksoftirqd/0 smpboot_th
7 7 FF 99 S posixcputmr/0 posix_cpu_
19 19 FF 50 S irq/28-edma irq_thread
20 20 FF 50 S irq/30-edma_err irq_thread
42 42 FF 50 S irq/91-rtc0 irq_thread
43 43 FF 50 S irq/92-rtc0 irq_thread
44 44 FF 50 S irq/80-mmc0 irq_thread
45 45 FF 50 S irq/150-mmc0 irq_thread
47 47 FF 50 S irq/44-mmc1 irq_thread
52 52 FF 50 S irq/86-44e0b000 irq_thread
59 59 FF 50 S irq/52-tilcdc irq_thread
65 65 FF 50 S irq/56-4a100000 irq_thread
66 66 FF 50 S irq/57-4a100000 irq_thread
67 67 FF 50 S irq/58-4a100000 irq_thread
68 68 FF 50 S irq/59-4a100000 irq_thread
76 76 FF 50 S irq/88-OMAP UAR irq_thread
在这个例子中,只有
gp_timer
中断没有被线程化,定时器中断处理程序通常以内联方式运行是正常的。
中断线程默认被赋予SCHED_FIFO策略和优先级50,但不建议使用默认设置,应根据中断相对于实时用户空间线程的重要性来分配优先级。以下是一个建议的线程优先级降序排列:
1. POSIX定时器线程
posixcputmr
应始终具有最高优先级。
2. 与最高优先级实时线程关联的硬件中断。
3. 最高优先级实时线程。
4. 优先级逐渐降低的实时线程的硬件中断,然后是该线程本身。
5. 次高优先级实时线程。
6. 非实时接口的硬件中断。
7. 软IRQ守护进程
ksoftirqd
,在RT内核中负责运行延迟中断例程,在Linux 3.6之前还负责运行网络栈、块I/O层等。你可能需要尝试不同的优先级水平来达到平衡。
可以在启动脚本中使用
chrt
命令更改优先级,例如:
# chrt -f -p 90 `pgrep irq/28-edma`
其中
pgrep
命令是
procps
包的一部分。
通过合理使用这些性能分析工具和实时编程技术,我们可以更好地优化Linux系统的性能,满足不同场景下的实时需求。
Linux性能分析与实时编程指南
高分辨率定时器
在实时编程中,高分辨率定时器起着至关重要的作用。当我们需要管理低毫秒甚至微秒级别的截止时间时,普通的定时器往往无法满足需求,这时就需要高精度的定时器来确保任务能够在准确的时间执行。
几乎所有的Linux内核都将高分辨率定时器作为一个可配置选项。启用高分辨率定时器后,系统能够更精确地控制任务的执行时间,从而提高系统的实时性。例如,在一些对时间精度要求极高的工业自动化场景中,高分辨率定时器可以确保各个设备之间的协同工作按照预定的时间顺序进行,避免因时间误差导致的生产故障。
避免页面错误
页面错误是实时编程中需要重点关注的问题之一。在执行关键代码段时,如果发生页面错误,会打乱所有的时间估计,导致任务无法按时完成。为了避免页面错误,我们可以通过锁定内存的方式来确保关键代码和数据始终驻留在物理内存中,不会被交换出去。
具体操作步骤如下:
1. 在代码中使用
mlock()
或
mlockall()
函数来锁定内存。
mlock()
函数用于锁定指定的内存区域,而
mlockall()
函数则可以锁定整个进程的地址空间。示例代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#define SIZE 1024 * 1024 // 锁定1MB的内存
int main() {
char *buffer = (char *)malloc(SIZE);
if (buffer == NULL) {
perror("malloc");
return 1;
}
if (mlock(buffer, SIZE) == -1) {
perror("mlock");
free(buffer);
return 1;
}
// 在这里执行关键代码
if (munlock(buffer, SIZE) == -1) {
perror("munlock");
}
free(buffer);
return 0;
}
-
在编译和运行程序时,确保程序具有足够的权限来锁定内存。可以通过设置
CAP_IPC_LOCK能力来实现,或者以root用户身份运行程序。
中断屏蔽
中断的不确定性是实时编程中的一个挑战。中断发生的时间不可预测,如果突然大量出现中断,会导致系统的处理开销大幅增加,影响实时任务的执行。为了避免这种情况,我们可以采用中断屏蔽的方法。
在多核设备上,一种有效的方法是屏蔽一个或多个CPU的中断处理。这样可以确保这些CPU专门用于执行实时任务,不受中断的干扰。具体操作步骤如下:
1. 确定需要屏蔽中断的CPU核心。可以通过查看系统的CPU信息来选择合适的核心。
2. 使用
irqbalance
工具来调整中断的分配,将中断尽量分配到非实时任务使用的CPU核心上。例如,可以通过修改
/etc/sysconfig/irqbalance
文件来配置
irqbalance
的行为:
# 禁用irqbalance
ENABLED="0"
-
在代码中使用
cpu_set_t结构体和CPU_SET()、CPU_ZERO()等函数来设置线程的CPU亲和性,将实时任务绑定到指定的CPU核心上。示例代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <sched.h>
#define CPU_CORE 1 // 选择第2个CPU核心(编号从0开始)
int main() {
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(CPU_CORE, &cpuset);
if (sched_setaffinity(0, sizeof(cpu_set_t), &cpuset) == -1) {
perror("sched_setaffinity");
return 1;
}
// 在这里执行实时任务
return 0;
}
测量调度延迟
为了评估系统的实时性能,我们需要测量调度延迟。常用的工具包括
cyclictest
和
Ftrace
。
cyclictest
cyclictest
是一个专门用于测量系统调度延迟的工具。它通过周期性地执行任务,并记录任务的实际执行时间与预期执行时间之间的差异,来计算调度延迟。具体操作步骤如下:
1. 安装
cyclictest
工具。在大多数Linux发行版中,可以通过包管理器来安装,例如在Ubuntu上可以使用以下命令:
sudo apt-get install rt-tests
-
运行
cyclictest命令。可以根据需要调整测试参数,例如测试的时间、任务的周期等。示例命令如下:
sudo cyclictest -p 99 -n -i 1000 -l 100000
-
-p 99:设置任务的优先级为99。 -
-n:使用FIFO调度策略。 -
-i 1000:设置任务的周期为1000微秒。 -
-l 100000:测试100000个周期。
-
分析测试结果。
cyclictest会输出调度延迟的统计信息,包括最小延迟、最大延迟和平均延迟等。通过分析这些数据,我们可以评估系统的实时性能,并找出可能存在的问题。
Ftrace
Ftrace
是Linux内核提供的一个强大的跟踪工具,它可以用于分析内核的行为和性能。在测量调度延迟方面,
Ftrace
可以记录中断和调度事件的时间戳,从而帮助我们分析调度延迟的各个组成部分。具体操作步骤如下:
1. 启用
Ftrace
功能。可以通过修改
/sys/kernel/debug/tracing
目录下的相关文件来启用和配置
Ftrace
。例如,启用
irq_handler_entry
和
irq_handler_exit
事件来记录中断处理的时间:
echo irq_handler_entry > /sys/kernel/debug/tracing/set_event
echo irq_handler_exit > /sys/kernel/debug/tracing/set_event
-
开始跟踪。使用
echo 1 > /sys/kernel/debug/tracing/tracing_on命令来开始记录事件。 -
执行测试任务。在测试过程中,
Ftrace会记录相关的事件信息。 -
停止跟踪。使用
echo 0 > /sys/kernel/debug/tracing/tracing_on命令来停止记录事件。 -
查看跟踪结果。可以通过读取
/sys/kernel/debug/tracing/trace文件来查看记录的事件信息,并进行分析。
总结
实时编程在嵌入式系统等领域具有重要的应用价值。通过合理使用性能分析工具,如
top
、
perf
、
Valgrind
等,我们可以深入了解系统的性能瓶颈,为优化系统性能提供依据。同时,针对实时编程中的各种问题,如调度延迟、优先级反转、页面错误等,我们可以采取相应的措施来解决,如使用线程化中断处理程序、锁定内存、屏蔽中断等。通过测量调度延迟,我们可以评估系统的实时性能,并不断优化系统配置,以满足不同场景下的实时需求。
总之,要实现一个高效、稳定的实时Linux系统,需要综合考虑多个方面的因素,并不断进行测试和优化。只有这样,才能确保系统在复杂的环境下依然能够准确地响应实时任务,为用户提供可靠的服务。
超级会员免费看

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



