加入星计划,您可以享受以下权益:

  • 创作内容快速变现
  • 行业影响力扩散
  • 作品版权保护
  • 300W+ 专业用户
  • 1.5W+ 优质创作者
  • 5000+ 长期合作伙伴
立即加入
  • 正文
    • 硬实时是什么?
    • Linux为什么不硬实时?
    • Linux preempt-rt如何解决这些问题?
  • 推荐器件
  • 相关推荐
  • 电子产业图谱
申请入驻 产业图谱

在实时操作系统里面随便怎么写代码都能硬实时吗?

04/21 10:25
637
阅读需 18 分钟
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

硬实时是什么?

众所周知,硬实时的概念,其核心并非追求速度的极致,而是确保系统能在预定的、可重复的时间范围内给予确定的响应。这意味着,实时系统的正确性不仅在于计算逻辑的正确,更在于结果的产生时间是否符合预期。以汽车为例,当发生碰撞时,安全气囊必须在极短的时间内弹开,否则可能无法起到应有的保护作用。

在评估实时操作系统(RTOS)的性能时,我们通常会考虑其在最恶劣情况下的延迟。比如,当对Linux进行改造,以实现中断或高优先级任务在100微秒内的确定性延迟时,我们还需要比较其他RTOS如RT-Thread的性能。RT-Thread可能无需改造就能达到微秒级别的延迟。因此,在选择RTOS时,我们需要根据应用的延迟要求来权衡。对于200微秒以内的延迟要求,改造后的Linux可能是一个合适的选择;但对于微秒级别的要求,Linux可能就不是最佳选择。

另外,关于RTOS和Linux在实时性方面的差异,我们需要澄清一个误解。并非在RTOS中随意编写代码就能满足硬实时的要求,同样,在Linux中也并非无法实现实时性。RTOS由于其设计特点和调度机制,通常更容易实现硬实时,但这并不意味着在Linux中就无法实现。Linux通过特定的配置和优化,也可以提供一定程度的实时性,尽管可能无法与专门的RTOS相媲美。

因此,在选择操作系统时,我们需要根据应用的具体需求和场景来权衡。对于需要高实时性的应用,RTOS可能是更好的选择;而对于一些对实时性要求不那么严格的应用,Linux则可能是一个更经济、更灵活的选择。

Linux为什么不硬实时?


我们首先看一下,Linux为什么不能提供硬实时能力。我们认为Linux主要有如下问题(你站在硬实时的角度看它是问题,你换个角度看,它就反而是正确的地方):

1. spinlock是一个随处可见被内核、驱动使用的API

Linux内核与驱动开发人员对自旋锁(spinlock)的运用可谓是热衷至极。每当遇到无需睡眠且时间较短的临界区保护场景时,他们几乎都会优先考虑使用自旋锁。可以说,如果不了解自旋锁,那么即便在内核与驱动开发领域有所建树,也称不上是真正的英雄。

自旋锁的魅力在于其高效性。当两个或多个执行单元(如线程、中断等)竞相获取同一锁时,自旋锁允许失败的执行单元不是立即进行上下文切换,而是原地自旋等待。这种机制避免了因上下文切换而带来的额外开销,特别是在锁持有时间较短的情况下,自旋等待的代价往往低于上下文切换的代价。

然而,自旋锁也并非完美无缺。它有一个显著的副作用,即当某个执行单元持有锁时,会禁止该CPU核上的抢占调度。这意味着即使存在更高优先级的任务等待执行,也必须等待当前持有锁的任务释放锁后才能获得执行机会。

在Linux内核中,自旋锁的实现主要侧重于核间自旋。当多个核上的执行单元尝试获取同一锁时,它们会在各自的核上进行自旋等待。而在核内,则是通过禁止抢占来实现临界区的保护,确保在持有锁期间不会有其他任务打断当前任务的执行。

综上所述,自旋锁在Linux内核与驱动开发中扮演着重要角色,其高效性使得它在特定场景下成为首选的同步机制。然而,我们也需要认识到它带来的副作用,并在使用时权衡其优缺点。

假设T1、T2、T3和T4这四个任务都在同一个CPU核上运行。当T1成功获取到一个自旋锁(spinlock)时,该CPU核上的抢占调度机制就会被临时禁用。这样做的目的是为了保护临界区内的代码和数据,避免在T1执行关键任务时被其他任务打断。

然而,如果在T1持有自旋锁的过程中,T2作为一个高优先级的实时任务被唤醒并准备执行,由于抢占调度被禁止,T2无法立即打断T1的执行。即使T2的优先级高于T1,它也必须耐心地等待T1释放自旋锁。

这里的问题在于,我们无法精确预知T1将会持有自旋锁多久。这完全取决于T1在临界区内执行的具体任务(即“做xxxx”)的复杂性和耗时情况。由于这种不确定性,T2需要等待的具体时间也变得不可预测。这种不确定性对于实时任务来说是非常不利的,因为它破坏了实时系统所追求的决定性时延。

决定性时延是指在实时系统中,任务能够在预定的、可预测的时间范围内完成。然而,由于T1持有自旋锁的时间不可知,T2的执行被延迟了多久也变得未知,这就破坏了实时系统的决定性时延特性。

因此,在使用自旋锁时,需要仔细考虑其对实时任务调度和时延的影响。在实时性要求非常高的系统中,可能需要考虑其他同步机制或调度策略,以确保实时任务能够得到及时的响应和执行。

2. Linux的中断执行时间可能过长且不可嵌套

众所周知,早期的Linux版本有个标记叫IRQF_DISABLED,标记本中断在执行的时候,其他所有中断都被禁止进入;而后Linux内核实际去掉了这个申请flags,其实就是都是IRQF_DISABLED了,总体可认为Linux内核不支持中断的嵌套。

int request_irq(unsigned int irq, irq_handler_t handler,

unsigned long irqflags, const char *devname, void *dev_id);

中断在执行的时候,所有的中断都进不来,这个设计本身简化了内核,但是对于硬实时的打击是致命的,前面的中断不执行完成,优先级再高的中断也得给我等着。

比如中断1在执行的过程中,来了中断2,而中断2对应的事情是必须要决定性时延的,由于IRQ1的中断服务程序也是码农写的,我们无法确定这个中断服务程序要执行多久。这显然让高优先级中断2的进入延迟不再具备可预期性。

3. 软中断(softirq)是一个比进程上下文优先级更高的上下文

我们设想一个场景,哪怕Linux解决了问题2,就是Linux的中断变地可嵌套,高优先级的中断可以打断低优先级的中断,并且高优先级的中断2唤醒了一个用户写的实时线程。

IRQ2唤醒了实时任务T1,但是T1必须等待IRQ1唤起的软中断(也包括使用软中断上下文的tasklet等)被执行完,T1才能投入执行。IRQ1唤起的softirq的代码是码农写的,这个码农写多久,鬼都不知道,这显然破坏了实时任务T1得以调度执行的确定性时延。

4. 内核里面会屏蔽中断的API如local_irq_disable、spin_lock_irqsave等

前文已经多次指出,在驱动程序中调用local_irq_disable()函数往往被视为一个潜在的问题或者说是bug。原因在于这个函数会禁用本地CPU的中断,但它并不能解决其他CPU核上运行的线程或中断服务程序与当前核上线程之间的竞态条件。尽管在只有一个CPU核的系统中调用此API通常是安全的,但我们在编写Linux内核代码时,应当始终假设我们是在多核环境下工作,这是Linux内核编程跨平台的基本常识。

大部分有经验的开发者都明白,在编写驱动程序时应当避免使用local_irq_disable()这样的API。然而,spin_lock_irqsave()这样的API在内核编程中却非常常见。它通常用于一个特定的场景,即当中断服务程序与线程之间存在潜在的竞态条件时。作为内核程序员,我相信你已经非常熟悉这样的经典用法了,这已经成为了内核编程中的常规操作,体现出了内核编程的严谨性和技巧性。

它把T1、T2、T3、T4、IRQ1、IRQ2这6者之间的竞争消灭于无形。T1如果持有了spin_lock_irqsave,本核上的T2、IRQ1显然进不来,CPU1上面的T3、T4、IRQ2想访问T1访问的临界资源必须spin。IRQ1如果持有了spin_lock, CPU1上面的T3、T4、IRQ2想访问IRQ1访问的临界资源必须spin。

那么,问题又来了,spin_lock_irqsave既屏蔽了抢占,又屏蔽了中断,这会导致中断和实时任务的确定性时延造成不可预期的破坏。因为spin_lock_irqsave和spin_lock_irqrestore是码农写的,鬼都不知道它要多久。

当然,历史上,粗犷的大内核锁(Big Kernel Lock,BKL)也是一个问题。由于晶晶姑娘不喜欢内核粗犷的一面,BKL在如今的内核里面已经烟消云散。

在Linux的世界里,这些锁当然都没有一个锁牛逼,就是RCU,尤其是面对这个世界符合阿姆达尔定律(Amdahl's law)定律的情况下,我们既要保证临界资源访问的被保护,又要尽一切可能地让多个线程同时狂奔。关于RCU的细节,谢神医已经有多篇文章论述。

Linux的世界大概是这样的:中断、软中断、线程(包括ksoftirqd线程)。我们都清楚地知道,软中断大量陷入的情况下,内核会将后续的软中断投入ksoftirqd内核线程执行,所以软中断还有一个可能的执行时机是在内核线程里面。

5. Linux用户空间内存的lazy分配机制与交换swap

对于喜欢在RTOS写程序的童鞋来说,Linux的世界一时半会难以理解,但是对于写Linux的童鞋来说,绝大多数的RTOS简直就是在裸奔。

我们都知道,在Linux里面,用户空间的内存都执行lazy的分配机制。比如你malloc一个内存

char *p = malloc(1024*1024);

这个时候Linux忽悠你说拿到了内存并且p获得了地址,但是实际的拿到却是在你写的时候,以page fault缺页中断的形式获得的。比如你写p[0]=1就拿到了第一页,你写p[4096]就拿到了第2页。这个lazy的分配机制,也同样适用于栈、代码段等。

你是一个实时的线程,你被唤醒得以执行,你执行的时候,发现你访问的临时变量还没有获得内存,你的代码段可能还特马在硬盘里,请问你实时个什么鬼?你执行到函数b的时候,去访问d[1000],结果发现这个栈的这页内存还要通过page fault来通过内核buddy去申请,你的确定性延迟还如何满足?

main()

{

a();

}

a()

{

b();

}

b()

{

int d[1024];

d[1000]=100;

c();

}

当然,已经进入内存的东西,也由于内核的swap机制,会与磁盘进行交换。

绝大多数的RTOS都没有这个“问题”,这也恰恰是他们不够“牛逼”的地方。对于手机、电脑这种富应用的系统而言,你不能用资源已经被确定性分配的思维模式来思考。

Linux preempt-rt如何解决这些问题?

前段时间,这篇文章刷屏了:《Linux实时补丁即将合并进Linux 5.3》 ,许多童鞋都说活久见,实际是活久了也特么没见到。我进内核搜索,发现没有一个体系架构到目前真地使能了支持。

到今天为止,ARCH_SUPPORTS_RT谁他么都不是真:

barry@barryUbuntu:~/develop/linux$ git grep ARCH_SUPPORTS_RT

arch/Kconfig:config ARCH_SUPPORTS_RT

kernel/Kconfig.preempt: depends on EXPERT && ARCH_SUPPORTS_RT

所以,你要真地在mainline见到PREEMPT_RT开花结果,还必须活地更久一点。

当你提到preempt-rt补丁时,强调了Linux的特性和它在实时性方面的考量,这是非常准确的。Linux作为一个功能丰富的操作系统,其设计初衷是支持多样化的应用和场景,包括用户空间的各种进程和线程。

preempt-rt补丁是Linux内核的一个实时性增强补丁,它旨在提升Linux在实时任务调度方面的性能。通过改进内核的调度策略和中断处理机制,preempt-rt使得Linux能够更好地满足实时应用的需求。

相对于其他RTOS,Linux在处理实时任务时确实有其独特之处。RTOS通常更强调高优先级中断的确定性时延,因为它们通常将整个系统编译在一起,可以在中断处理程序中直接嵌入策略。然而,Linux作为一个通用的操作系统,其内核与用户空间之间有着明确的分离。用户空间的应用无法直接访问或修改内核代码,只能通过系统调用等接口与内核进行交互。

因此,在Linux中,实现实时任务的确定性调度时延就显得尤为重要。通过preempt-rt补丁,Linux内核提供了更好的实时调度能力,使得高优先级的RT线程能够得到及时的处理和调度。同时,由于Linux内核提供了丰富的操作接口,开发者可以在用户空间编写应用,通过调用这些接口来利用内核提供的实时功能。

总的来说,Linux不是一个简单的裸机操作系统,它有着复杂的内核架构和用户空间应用。在实现实时性时,需要充分考虑到这种架构的特点,并通过适当的补丁和配置来优化实时性能。而preempt-rt补丁正是为了提升Linux在实时任务调度方面的能力而设计的。

推荐器件

更多器件
器件型号 数量 器件厂商 器件描述 数据手册 ECAD模型 风险等级 参考价格 更多信息
88E1512-A0-NNP2C000 1 Marvell Technology Group Ltd Ethernet Transceiver, QFN-56
暂无数据 查看
DSC1101DL5-020.0000 1 Microchip Technology Inc CMOS Output Clock Oscillator

ECAD模型

下载ECAD模型
$2.06 查看
FOD420SDV 1 Fairchild Semiconductor Corporation Triac Output Optocoupler, 1-Element, 5000V Isolation, LEAD FREE, SURFACE MOUNT PACKAGE-6
$4.23 查看

相关推荐

电子产业图谱