LWN:realtime开发者的清单!

关注了就能看到更多这么棒的文章哦~

A realtime developer's checklist

November 16, 2020

This article was contributed by Marta Rybczyńska 

OSS EU

Linux 下的 realtime application(实时应用)开发时,必须注意要确保关键的 realtime task(实时任务)不会受到其他应用的干扰,也不会受到系统其他部分的干扰。在 2020 年 Embedded Linux Conference(ELC)期间,John Ogness 为 realtime 开发者提供了一份 checklist(检查清单),参见https://ogness.net/ese2020/ese2020_johnogness_rtchecklist.pdf ,其中提供了实用的建议。有很多工具和功能可供 realtime 开发者使用,甚至对于没有打上 RT_PREEMPT patch 的系统也有帮助。

What is realtime

Ogness 开场时说道:我们希望所有的应用程序都是正确的、无 bug 的。而在 realtime 领域,正确性 "意味着在正确的时间运行"。当有时间紧迫(time-critical)的工作要做时,应用程序必须在约定的时间范围内被唤醒并执行。Ogness 强调,在 realtime 系统中任务要在正确时间来执行,这是一个硬性要求。如果不满足的话就会出问题。开发人员需要定义哪些任务和应用程序是 time-critical 的。他指出,很多人错误地认为 realtime 系统中的所有任务都是 realtime 的,实际上大多数任务都不是 realtime。

对开发者来说有个好消息,在 Linux 下,他们只需要使用带有 realtime extension 的 POSIX API 就可以编写 realtime 应用程序了。代码看起来会很熟悉,只需要三个额外的头文件:sched.h(有观众指出 musl C library 没有实现 sched.h)、time.h 和 pthread.h。

有三个特性是 realtime 操作系统所必须具备的:有确定性的调度行为(deterministic scheduling behavior)、可中断性(interruptibility, 因为 CPU 总是在执行一些工作,所以应该有方法来打断一个任务),以及避免优先级倒置(priority inversion,即当一个高优先级的任务必须等待一个低优先级的任务时)。第三个特性,没有进行过 realtime 开发的开发者可能不太熟悉,Ogness 用了一个例子来描述:

假设有三个任务(task1、task2 和 task3,分别具有高、中、低优先级)。任务 3 正拿着一个锁,当 task1 来的时候,占用 CPU 开始运行,并请求获取同一把锁。由于 task1 是高优先级的,scheduler(调度器)会让 CPU 重新执行 task3 ,让它完成工作并释放锁。不过在完成之前,task2 来了。它与那把锁无关,但它的优先级比 task3 高。scheduler 就会做出一个似乎很合理的决定,就是把 CPU 让给 task2,但这间接地阻止了 task1 的执行。

在这种情况下,我们在 task1 和 task2 之间产生了优先级倒置问题。Ogness 指出,在 Linux 这样的复杂系统中,这种情况很容易出现。Linux 处理优先级倒置的方式是将 task3 的优先级暂时提升到 task1 的优先级。当 task3 交回锁时再恢复到原有的优先级,task1 可以正常获取到锁并开始运行。

Scheduling and affinity

Ogness 开始介绍 realtime checklist 了,首先是 scheduling policy(调度策略),这些策略在 realtime 环境和非 realtime 环境是不一样的。non-realtime policy 采用了固定的时间片,如果某个应用程序正在运行一个无限循环,当它的时间片耗尽时,它还是得让出 CPU。在 realtime 系统上,大多数任务,包括 logging daemon(日志记录)、Web 服务器和数据库等,仍然会使用 non-realtime policy。他说:"这些进程不应该按照 realtime 标准来运行"。此外,开发者还可以配置那些 non-realtime 任务,使用 nice 和 cgroup 来限制它们可以拥有多少比例的 CPU 时间。例如,可以限制 Web 浏览器永远不占用超过系统所有可用 CPU 时间的 20%。他 "强烈鼓励" 开发者要对所有任务的资源总需求进行评估,而不仅仅是只关注 realtime 任务。

而 realtime 的任务通常会持续运行,直到它主动放弃 CPU,或者被更高优先级的任务打断。如果有一个优先级为 30 的任务,它会一直运行到有更高优先级的任务进来(比如优先级为 31)。对于 realtime 任务,在写代码的时候需要特别小心,要避免无限循环,否则会导致系统变得无法正常使用了。

Ogness 说,SCHED_FIFO realtime scheduling policy 就是人们通常使用的调度策略。在这个策略下运行的任务会一直执行,直到它被阻塞(比如等待资源)或自己决定放弃 CPU。优先级可以是 1 到 99(最高)。Ogness 说,"请永远不要使用 99",因为使用这个优先级的 kernel thread 会 "比你的应用程序要重要得多"。还有另外一个类似的策略 SCHED_RR,但它对相同优先级的任务会使用时间片方式来安排调度。第三种调度策略是 SCHED_DEADLINE,scheduler 会选择运行那个最接近截止时间的任务。Ogness 没有详细讲这个策略,但他指出,如果使用这个策略的话,SCHED_DEADLINE 的决策会比其他调度策略中的最高优先级任务还要优先。混合使用 scheduling class 是 "奇怪 "的行为。

Ogness 指出了一个关于 realtime scheduling policy 的很重要的内核特性:默认情况下,scheduler 将把所有分配给 realtime 任务的 CPU 时间的总和,限制在每秒可用时间的 95%。在每秒钟剩下的 50ms 中,不允许运行任何 realtime 任务。假如 realtime 任务失控了的话,这里就给了管理员一个干预的机会。不过这种情况是应该避免的,因为这形成了优先级倒置。他指出,如果内核真的走到了这一步,它会在内核日志中打印一条 throttling (流控)消息,但此时这个 realtime 系统其实已经被破坏了。可以通过在/proc/sys/kernel/sched_rt_runtime_us 中写入-1 来禁用这个功能。这个设置在重启之后会需要重新设置,所以可以将其添加到启动脚本中。

开发者可以使用 chrt 工具来设置优先级和调度策略。-p 选项可以设置此任务的优先级。该工具也可以用来以一个指定优先级来启动某个应用程序。在代码中可以通过使用 sched_setscheduler()系统调用达到同样的效果。

CPU affinity 是 realtime 系统中的另一个重要因素。很可能需要把代码隔离到一些指定的 CPU 上来运行。Ogness 举了一个例子,一个 8 CPU 的系统中,划分了 6 个 CPU 用于非 realtime 应用,2个 CPU 用于 realtime 应用。在 Linux 中,CPU affinity 针对每个任务定义的,也就是一个 bitmask,其中每个 bit 都代表了允许运行这个 task 的 CPU 的编号。除了用户空间的任务外,中断和内核线程也可以设置它们的 affinity。这一点很重要,因为它们可能会干扰 realtime 的关键代码。Ogness 指出,处理器的内部架构会影响到 realtime 的配置。如果两个 CPU 共享 L2 cache,那么这两个 CPU 应该都是 realtime 的,因为 realtime 和 non-realtime 应用之间的 cache 共享可能影响 realtime latency。

taskset 设置和查询 affinity 关系的工具,它可以启动一个任务,也可以修改一个已有的任务,taskset 也适用于线程。如果执行时没有设置新的 bitmask,就会直接显示当前的 bitmask 的值。同优先级一样,这也存在一个相关的系统调用:sched_setaffinity()。Ogness 提醒说需要已经定义了_GNU_SOURCE 才行,因为 sched_setaffinity() wrapper 是 GNU C 库(glibc)的一个 extension,不是 POSIX API 的一部分。

maxcpus 和 isolcpus 这两个启动参数也可以改变 CPU affinity 的效果。maxcpus 限制了内核可以看到的 CPU 数量。如果在一个 8 CPU 系统中将 maxcpus 设置为 4,意味着内核只能看到 4 个处理器,而其他 4 个 CPU 就可以用在其他各种不同的工作上,比如 bare-metal realtime 应用程序(不运行在操作系统里的实时代码)。同时,isolcpus 则告诉内核,内核线程不受这些指定处理器限制。当使用 isolcpus 时,Linux 会看得到所有的 CPU,当线程被明确设置为在这些 CPU 上运行时,可以使用这些被隔离的(isolated) CPU。

关于 interrupt affinity,可以在/proc/irq/default_smp_affinity 中查看和更改新中断的默认设置。对于已有的中断,比如编号中断号如果是<intr>,可以在 /proc/irq/<intr>/smp_affinity 文件中进行设置更改。Ogness 说,开发人员应该意识到,在设置 affinity 时可能还会受硬件限制。在设置了 interrupt affinity 之后,可以从/proc/irq/<intr>/effective_affinity 查看到,这样可以检查在经过硬件限制的调整之后,真正设置生效的值。

Avoiding page fault

内存管理可能是 latency 方面 "最重要的" 影响因素了。他解释说,当应用程序分配内存时,并不是在内核中真正进行分配,而只是在 page table 中标记出来。真正的分配动作发生在该内存第一次被访问时,是通过 page fault 来触发的。page fault 是 "相当耗时" 的,因为内核需要找到一个物理页面,分配出来,才能让应用程序继续使用此页面。开发者可以观察到 malloc()调用的速度很快,但是当应用程序第一次使用所分配的内存时就会变慢。这不仅适用于堆(heap),而是适用于所有内存,包括代码段(code)和堆栈(stack)。当堆栈指针移动指向了相邻的 page 时,也会像分配新内存一样引起 page fault。

Ogness 介绍了避免 page fault 的三个步骤。首先是调整 glibc,使其只有在分配新的页面时才使用 heap。malloc()针对不同的 memory chunk 可以使用两种分配内存的方式:heap 或 mmap()。realtime 开发者不希望使用独立的 chunk,因为这些 chunk 在 free()之后可能会还给内核,这样在重新分配这个页面时就会再次出现 page fault。开发者可以通过使用 mallopt() 来告诉 glibc 只使用 heap memory。

mallopt(M_MMAP_MAX, 0);

另外还有一个有用的选项是禁止 glibc 将未使用的内存还给内核:

mallopt(M_TRIM_THRESHOLD,-1);

第二步,就是 lock down 已分配的页面,使它们 "永远不会回到内核",包括内核回收内存或开始 swapping(换出到磁盘)时也不受影响。嵌入式的开发人员可能认为他们的系统不会发生 swap,但这并不正确,Ogness 解释说。当内存不足时,内核开始回收任何可以回收的内存,包括应用程序的 text 段。对于 realtime 来说"这是很可怕的"行为,因为当应用程序再次运行时,它需要从磁盘上读取出原来这个 page。为了将所有 page 都锁定在内存中,realtime 应用程序应该使用 mlockall()。

mlockall(MCL_CURRENT | MCL_FUTURE)。

第三步也是最后一步是进行 pre-faulting 操作,也就是提前触发所有将会出现的 page fault,并使所有内存 page 都 "准备就绪"。Heap 的 pre-faulting 是通过分配所有可能需要的内存,然后给每个页面写一个非零值(确保写入的操作不会被优化掉)。Stack 的 pre-faulting 也应该使用类似的方式,创建并写入一个很大的堆栈帧(stack frame),这是开发者们普遍 "认为不应该做的动作"。

Synchronization without priority inversion

Ogness 说,在 realtime 系统中,开发人员应该使用 POSIX mutexes 进行 locking 操作。这很重要,因为 mutexes 有 owner,只有 owner 才能释放锁。这就给内核提供了所需的信息,当一个更高优先级的任务需要此锁时,可以对持有这个锁的 Owner 进程来提升权限。然而,默认情况下,优先级继承并没有打开(他指出,哪怕打开了 PREEMPT_RT,也并没有缺省打开优先级继承),所以开发者需要使用下面的函数来激活优先级继承功能,以避免产生优先级反转:

pthread_mutexattr_setprotocol(&mattr, PTHREAD_PRIO_INHERIT);

对于线程之间的信号传递,Ogness 推荐使用条件变量(conditional variables),因为它们可以与 mutexes 关联起来。如果一个应用程序需要等待拿一个锁,那么就可以使用 pthread_cond_wait()。realtime 开发人员应该避免使用 signal,因为 signal handler 的运行环境很难预测到是什么样的。

Clocks and measurements

Ogness 建议 "使用单调时钟(monotonic clock)"。也就是一个始终向前移动的时钟,不会考虑时区和闰秒等等。它给出的绝对时间用来计算任务应该睡眠多长时间的最理想的选择。他推荐的技巧包括在程序一开始时获得一次当前时间,此后只需递增 clock 就够了。他说,这样一来,任务就会在合适时刻醒过来,"哪怕是 10 年后" 也会一样有效。

演讲的最后一部分涉及到可以用来评估 realtime 系统的工具。前两个工具来自 rt-tests 包。cyclictest 会测量指定优先级的 latency。它会反复睡眠,然后检查时钟,来测量原定唤醒时间和实际唤醒时间之间的差异。这个工具可以根据所产生的数据来生成直方图。开发者会希望在系统高负载的情况下运行这个测试,以测量系统可能产生的最大延迟。另一个工具 hackbench 可以用网络包、CPU 工作负载、甚至包括 out-of-memory 这种内存不足的事件来生成一个负载很重的系统。一个 realtime 系统即使在这些条件下也应该能够良好地运行。Ogness 指出,开发者应该对所有 component 来进行压力测试。例如,如果系统要使用蓝牙,那么就需要运行蓝牙测试。他还提醒开发者要加入 idle-mode 测试,因为当进入 low-power 模式时,系统的 latency 可能会产生变化。

perf 对开发者也会有帮助,因为它可以显示 page fault 和 cache miss 的数量。Ogness 提到了内核的 tracing 机制,它不仅可以帮助显示应用程序执行过程中发生了什么,还可以指出发生的具体时刻。有听众问是否有办法理论计算出来某个任务的最差响应时间(worst response time),Ogness 回答说,唯一的解决办法就是进行测试。

Ogness 最后列出了一个 kernel configuration option 清单,供大家检查。例如,CONFIG_PREEMPT_*可以打开更多的 kernel realtime 属性(如果打上了 PREEMPT_RT 补丁的话会更多)。CONFIG_LOCKUP_DETECTOR 和 CONFIG_DETECT_HUNG_TASK 会以 realtime 优先级 99 来运行,所以如果不是必须的话,应该把它们禁用掉。CONFIG_NO_HZ 则可以消除 kernel clock ticks,可以降低功耗,但也会增加 latency。他说,设置任何一个特定的选项都不一定是错误的,但需要对它们进行分析和检查。比如可能需要在省电和实时性之间做出权衡。

在讲座的最后,他把 checklist 又过了一遍,强调了需要验证实时性效果。对于想了解更多的听众,可以查看 realtime wiki 页面(https://wiki.linuxfoundation.org/realtime/start) 。他的结语是:"Have fun with realtime Linux"。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值