大家好!我是大聪明-PLUS!
Linux 内核虽然具有很强的灵活性,但它能否在运行应用程序时保证响应时间?
Linux 内核功能强大,从微型嵌入式设备到巨型服务器,几乎无所不能!但是,如此强大的内核能否保证应用程序在所有这些平台上都能获得稳定的响应时间呢?如果您的应用程序可接受的响应时间在 200 微秒以内,答案是肯定的!(顺便一提,这对于 Linux 来说并非难事,但要达到这个目标,需要仔细选择硬件,或许还需要咨询 Linux 实时系统专家。)
那么,为什么Linux应用程序有时会出现超过200微秒的延迟呢?Linux内核的多功能性要求其在吞吐量、响应时间和CPU资源分配的公平性之间取得平衡,以满足这种多功能性的需求。如果其中任何一项要求非常严格,则需要对内核本身和应用程序的行为进行微调。在本文中,我们将介绍开发具有严格实时性要求的Linux系统时需要考虑的10个关键点。对于每个要点,我还会重点指出一些新手开发者在刚开始接触Linux实时编程时容易迷失方向的地方。
❯ 1. 规划中的政策和优先事项
如果待解决的任务对响应时间有严格的要求,则必须对其进行适当的优先级排序并选择合适的调度策略。可以使用相应的工具chrt(1)或函数来完成这项sched_setscheduler(2)工作。对于实时系统,通常会选择相应的策略SCHED_FIFO。实时服务的优先级(从 1 = 低到 98 = 高)根据特定任务和其他实时任务的需求来选择。优先级较高的任务会优先于优先级较低的任务执行。
警告:请务必禁用实时时钟跳跃(为此,请在配置文件-1中添加注释/proc/sys/kernel/sched_rt_runtime_us)。时钟跳跃会触发优先级反转等不可接受的情况,从而扰乱实时系统。
❯ 2. 绝缘
许多系统同时执行多个实时任务。如果将这些任务分配给同一个处理器核心,则每个任务的响应时间都可能过长。通过将实时任务分配到明显不同的处理器核心组,可以避免这个问题。可以使用工具taskset(1)或函数来实现这一点sched_setaffinity(2)。
实时任务通常需要等待特定的中断。中断可能在一个核心上触发,但实时任务却会被调度到另一个核心上------这同样会导致延迟。为了解决这个问题,可以限制硬件中断,使其仅调度到与依赖于该中断的实时任务相同的核心上。CPU 核心上硬件中断的亲和性掩码可以在smp_affinity位于. 的虚拟文件中指定/proc/irq/IRQ-NUMBER/。
最后,有时可能需要专门分配一部分 CPU 核心来处理实时任务。启动参数isolcpus会告知内核在启动时应将哪些 CPU 核心从系统默认的 CPU 亲和性掩码中排除。利用上述工具和接口,可以将特定的 CPU 核心专门分配给实时任务和中断处理。
警告:并非所有硬件都支持将 CPU 核心分配给中断的功能。请务必检查配置文件effective_affinity,查看其中的设置。
❯ 3. 页面故障
在使用实时应用程序时,最容易造成系统崩溃的情况之一是内存分配或交换。这可能是由于 Linux 系统配置为在首次访问已分配或已保留的内存时"慷慨地请求"内存所致。另一种情况是,在首次调用某个函数时,需要从磁盘交换某些数据(例如文本段)。无论原因如何,为了满足响应速度的要求,都必须避免这种情况的发生。
在使用实时应用程序时,第一步是配置库glibc,使其在访问应用程序时仅使用单个不可收缩的堆。这可以确保为实时应用程序提供随时可用的物理 RAM 池。这是通过 ` mallopt(3) (M_MMAP_MAX=0, M_ARENA_MAX=1, ` 函数实现的M_TRIM_THRESHOLD=-1)。
此外,所有已分配和映射的虚拟内存都必须分配给物理 RAM 并进行绑定,以防止其被重新分配给其他用途。这是通过以下方式完成的mlockall(2) (MCL_CURRENT | MCL_FUTURE):
最后,我们来讨论一下实时应用程序在其生命周期内需要多少堆内存和栈内存------以确保从栈和堆到物理内存的内存传输操作能够及时完成。这种做法称为"预故障处理";它通常涉及在栈帧中填充一个大缓冲区,并在堆中分配、填充和释放一个大缓冲区。
警告:请记住,每个线程都有自己的堆栈。
❯ 4. 同步
实时应用程序通常需要访问共享资源(例如,共享内存中的数据),而这种访问需要同步。为此,请使用 `Target`pthread_mutex对象。它是唯一一个既提供锁定功能,又具备所有权语义和动态优先级变更功能的对象------这对于防止优先级反转至关重要。
遗憾的是,动态优先级重调度默认情况下未启用。要启用此功能,请使用相应的函数pthread_mutexattr_setprotocol(3) (PTHREAD_PRIO_INHERIT)。启用后,优先级较低的所有者将被提升至与等待者相同的优先级。这可以确保争用的互斥锁能够尽快被释放。
警告:使用可能实现自身互斥锁或其他同步机制的库时务必谨慎。
❯ 5. 启用同步时的通知
在开发实时应用程序时,经常需要等待某个事件发生,然后在事件触发后访问共享资源。条件变量支持这种模式pthread_cond。它是一个与互斥锁关联的等待对象。当高优先级任务被唤醒时,内核可以根据需要提升其优先级。
警告:通知任务必须先执行通知操作,然后再释放关联的互斥锁。这可以确保必要的锁争用,防止优先级反转的情况发生。
❯ 6. 循环任务
在处理循环任务时,使用专用线程至关重要,这些线程会休眠直至完成任务的绝对时间。最佳选择是使用 `func` 函数clock_nanosleep(2),它是唯一能够保证实时任务会被硬件中断触发的高精度定时器唤醒的 API。
警告:使用CLOCK_MONOTONIC`and` 时,TIMER_ABSTIME请确保任务处于睡眠状态。否则,循环可能变得不稳定或出现时间偏移。
❯ 7. 内核配置
Linux 内核支持多种抢占模型。为了实现最小延迟(亚毫秒级),应使用"完全抢占内核"(用于实时操作)模型(CONFIG_PREEMPT_RT)。这可以确保满足关键要求,包括细粒度中断、确定性和中断优先级。如果此模型不可用,则必须应用一系列内核补丁PREEMPT_RT。
注意:请务必正确配置其他功能,特别是 CPU 频率调节,因为配置不当可能会导致硬件性能出现波动。
❯ 8. 测试
当您的实时系统准备就绪后,确定不同性能设置下的最大响应延迟至关重要。这将有助于您更好地了解不同系统组件中可能出现的各种延迟类型。该工具可以轻松提供此信息cyclictest(8),且对系统干扰极小,不会影响实时应用程序的运行。
通常,为了确定最坏情况下的延迟,您应该在后台运行各种压力测试。在实时系统中,这种非实时操作的压力测试应该对应用程序本身的影响最小。诸如以下工具:
hackbench(8)此外stress-ng(1),确保压力测试覆盖所有硬件组件也至关重要。此类测试的目标是找出内核中最耗费资源的执行路径,即那些无法被抢占的路径。
注意:务必在系统完全空闲的情况下测试延迟(idle)。根据内核的配置方式,这可能是最糟糕的情况。
9. 验证
即使实时系统看起来运行良好,所有时间要求都得到满足,也必须验证实时应用程序是否真的按预期运行。锁争用期间优先级是否提高?它是clock_nanosleep()循环任务的唯一 API 吗?是否发生页面错误?此实时应用程序实际发生的最坏情况响应时间是多少?
所有这些问题的答案都可以使用各种内核工具获得,特别是 ftrace(对应的调用分别为:` ftrace` trace-cmd(1)、kernelshark(1))` ftrace`perf(1)和eBPF`ftrace` bpftrace(8)))。这些工具可以实时跟踪、分析和测量系统中发生的几乎所有事件。请务必花时间学习如何有效地使用这些工具,因为它们将帮助您确保系统按预期运行。
危险:ftrace它们perf非常eBPF有效。然而,与任何测量仪器一样,了解测量行为如何影响整个系统至关重要。
❯ 10. 外部干扰
了解硬件特有的特性和限制也很重要,即使它们通常与 Linux 无关。众所周知的例子包括系统管理中断 (SMI)、内存/带宽争用、CPU 拓扑结构以及 CPU 内核之间的缓存共享。了解硬件组件对实时系统可能产生的负面影响,有助于您设计和实现程序,从而避免这些问题或通过变通方法克服它们。
危险:硬件选择往往忽略了实时系统的需求,导致在软件编程过程中需要花费大量精力来解决不断涌现的问题。通过让程序员和硬件工程师共同参与硬件选择,可以创建一个长期来看极其有用的实时系统。
最后......
这份清单并不详尽,但这里列出的前十点信息量很大,可以帮助您设计出一个最坏情况下延迟不超过 200 微秒的系统------正如我在本文开头提到的那样。实际上,延迟还可以进一步降低,但这需要对 Linux 系统进行精细调优,仔细选择硬件,以及精心设计、实现和组装实时应用程序。