编辑
2025-05-11
记录知识
0
请注意,本文编写于 47 天前,最后修改于 11 天前,其中某些信息可能已经过时。

目录

中断的不确定性
中断不能嵌套
softirq
spinlock影响高优先级任务的不确定性
内存的lazy模式导致进程不确定性
PREEMPT_RT的作用
PREEMPT_RT的问题
如何优化特殊场景下要求的实时任务呢
DEADLINE调度
isolate和taskset
timer
优先级继承
占住堆
禁用gc的回收
占住栈
避免数据伪共享
其他
总结

我们在讨论linux的实时的时候,有时候会拿RTOS来做对比,根据最近对RTOS的理解,总结出来关于为什么linux实时不如RTOS的几个点,分享之

中断的不确定性

中断不能嵌套

在rtems中我们看的中断在特殊情况如内核态是支持中断嵌套的,但是linux是不允许中断嵌套。
也就是说,在Linux中,如何一个中断触发来,那么必须等到同CPU的上一个中断处理完成。
这时候,中断是不确定的

softirq

linux支持中断上下文(hardirq和softirq),我们知道softirq的优先级是高于kthread这类线程任务的(tasklet,timer,netrx等)

如果某个外设驱动的softirq一直占用某个core,那么整个core上的thread很可能都得不到运行

我处理can的报文接收(netrx)的时候就遇到过这类情况,它直接导致来一个cpu core不工作。

那么这也就是另一个不确定性,也就是softirq是外设驱动代码,由客户自己编写的,如果softirq执行时间不确定,那么中断就是不确定的 。

spinlock影响高优先级任务的不确定性

例如某个cpu core上,有一个普通任务正在spin lock,那么core上的任务必须等spin unlock后,才能得以调度。

那么此时其任务就是不确定的。因为谁也没办法知道要等多久才能unlock。这样高优先级就不知道什么时候才能得以运行了。

内存的lazy模式导致进程不确定性

在linux中内存是使用的时候才分配的,高优先级任务在使用内存的时候,内存分配过程只能保证你分配到,但是不能保证你在确定时间内申请到。 甚至可能没有内存导致oom出现。

PREEMPT_RT的作用

基于上面提到的,PREEMPT_RT主要解决如下三个问题

  1. 中断可以被抢占,嵌套
  2. 中断线程化
  3. softirq可以被抢占
  4. mutex替换spin_lock

对于中断风暴,做法是在线程内disable irq,线程化完成之后,自动enable irq

而softirq线程化,做法是全部将其添加到ksoftirqd(tasklet,timer,netrx等)执行

spinlock的做法就是将raw_spin_lock 替换成 rt_mutex (可睡眠的,支持优先级继承的mutex,避免优先级翻转)

PREEMPT_RT的问题

根据上面就知道

  • 对于中断,普通中断响应更差了
  • 对于softirq,相比于原来运行路径更长了,且容易被常规任务抢占
  • 对于锁,可睡眠就会导致进程切换,多次进程切换代价太大,性能远不如spin_lock

所以可以得出结论,添加PREEMPT_RT的内核,它可以保证某个高优先级任务的一定的实时性,但其负面作用会导致其他所有常规任务性能低下。 所以只适合于定制的特殊场景。

如何优化特殊场景下要求的实时任务呢

根据上面提到的,使用PREEMPT-RT的内核,一定程度上保持了高优先级任务的实时性,那么基于此,我们还需要做哪些事情来定制操作系统,满足这个实时任务的确定性要求呢?

DEADLINE调度

内核其实提供了EDF调度,也就是SCHED_DEADLINE。我们让这个实时任务在这个特殊的调度类中。

chrt工具动态设置进程的调度政策,如chrt --deadline / --fifo
当然pthread_setschedparam也可以设置调度器种类,如SCHED_DEADLINE/SCHED_FIFO传参

isolate和taskset

通过bootargs传参隔离某个cpu,然后通过taskset将实时任务绑定到这个cpu上跑

timer

如果要高实时,建议直接汇编使用寄存器调用arm64的高精度定时器来封装特定代码。

优先级继承

对于多线程的实时任务,pthread线程创建前建议设置优先级继承,避免优先级翻转。 也就是pthread_muterattr_setprotocol中传参PTHREAD_PRIO_INHERIT。 其默认是NONE。

占住堆

默认从内核申请的内存都可能被交换,那么申请的内存调用mlock锁住即可,这样这块内存不会被换出。同时,带mlock标志位的内存会在申请的时候,自动触发缺页异常,分配物理页面。相当于lazy机制的逃逸了。

禁用gc的回收

malloc_trim(-1),这里-1就是trim的最大值,也就是gc不再帮你管理释放内存了,内存不会返回到操作系统中,如果内存通过mmap获得,那么就不会munmap了

占住栈

堆和栈都是内存,对于堆好办,我们直接mlock即可,对于栈,这是操作系统分配的,我们设置了最大栈大小之后,例如是8M,如果是线程,那么可以设置pthread_attr_setstacksize

在所有函数调用之前,调用一个函数,然后通过局部变量申请一个8M,或者你设置的栈大小的空间,将其占住,然后进行memset一次调用。此时栈会通过缺页异常申请内存,后续就不会在缺页异常申请了。 文字表现力不强,如下是代码

void stack_prefetch() { const size_t size = 8 * 1024 * 1024 - 4096; char buffer[size]; memset(buffer, 0, sizeof(buffer)); }

避免数据伪共享

跑在多核的程序,需要利用好cache一致性,例如局部性原理,但是这里要说的是避免数据伪共享,所以多线程访问同一个结构体时,最好cacheline对齐

__attribute__((aligned(cachesize)))

其他

其他就是性能优化相关的了,通过性能优化可以保证任务的低时延。

总结

本文初衷是介绍一下Linux实时内核,并说明Linux实时并不能硬实时的原因,但是为了表述清楚,也补充了使用PREEMPT-RT补丁的内核,和实时应用程序的编写建议,这些建议纯粹属于经验分享性质,不代表权威信息。

总之,PREEMPT-RT是基于linux内核上,为了保证某个实时任务的确定性做的优化,它需要配合该实时任务的很多其他措施一起使用,否则体现不了实时的确定性。 也就是说我们讨论实时任务的时候,应该讨论整个链路的实时性,而不是某一个方面的实时确定性,那没有任何意义

并且,PREEMPT-RT是牺牲整体系统性能为代价而提供的,使用时需要认清利弊。

PREEMPT-RT做不到完全的硬实时确定性,所以如果面临产品级的开发,推荐使用Linux + RTOS 双OS的策略,RTOS本身关注确定性任务,Linux本身关注复杂计算。