关于实时系统的调研与扫盲

本文尝试说明白以下问题:

1、什么是硬实时?

2、什么是软实时?

3、衡量实时性的指标是什么?

4、Linux 为什么不能实现硬实时?

5、如何测试RTLinux的实时性?

 

废话不多说,进入正文。

 

1、什么是硬实时?

硬实时的概念,不是越快越好,而是强调可重复的(repeatable)、决定性的时间期限内给予响应(deterministic response time)。 所以硬实时的本质是可预期,实时系统的计算正确性不仅取决于计算的逻辑正确性,还取决于产生结果的时间。比如,汽车碰撞后,必须在X时间内弹开安全气囊,你弹开晚了,人已经挂了。

当然最恶劣场景下的延迟,可能作为一个时间参数,来评估RTOS本身的性能指标。比如你改造了Linux,实现了中断或高优先级任务的100us以内的确定性延迟;但是RT-Thread,可能不改造,就可以到达us以内的延迟。那么,你延迟要求只需要200us以内的,你选改造后的Linux也没有什么毛病,但是如果你要的就是us级,那么对不起,Linux不是你的菜。

众所周知,RT-Thread、FreeRTOS、VxWorks这样的操作系统是硬实时的;Linux这样的操作系统是提供软实时能力的,针对的miss掉截止期限也死不了人的那种应用,比如看电影。

那么,这个时候我们诞生了一个疑问,是不是在RTOS里面随便写代码都能满足硬实时,而在Linux里面无论怎么写代码都满足不了硬实时?我认为这2个问题的答案都是否定的。

 

2、什么是软实时?

软实时能力不像硬实时那样要求100%的可预期。  硬实时与软实时之间最关键的差别在于,软实时只能提供统计意义上的实时。例如,有的应用要求系统在95%的情况下都会确保在规定的时间内完成某个动作,而不一定要求100%。在许多情况下,这样的“软性”正确率已经可以达到用户期望的水平。比如,用户在操作DVD播放机时,只要98%的情况都能正常播放,用户可能就满意了;而发射卫星、控制核反应堆的应用系统,这些系统的实时性必须达到100%,是绝对不允许出现意外。

 

3、衡量实时性的指标?

那么,如何判断一个系统是否是实时的呢?主要有以下两个指标:

(1)中断延迟

中断延迟就是从一个外部事件发生到相应的中断处理函数的第一条指令开始执行所需要的时间。很多实时任务是靠中断驱动的,而且中断事件必须在限定的时限内处理,否则将产生灾难性后果,因此中断延迟对于实时系统来说,是一个非常重要的指标。

(2) 抢占延迟

有时也称调度延迟,抢占延迟就是从一个外部事件发生到相应的处理该事件的任务的第一条命令开始执行的时间。大多数实时系统都是处理一些周期性的或非周期性的重复事件,事件产生的频度就确定了任务的执行时限,因此每次事件发生时,相应的处理任务必须及时响应处理,否则将无法满足时限。抢占延迟就反映了系统的响应及时程度。

如果以上两个指标是确定的,可预测的,那么就可以说系统是实时的。

 

4、Linux 为什么不可以实现硬实时?

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

    在Linux有三类区间是不可以抢占调度的,这三类区间是:

  • 中断

  • 软中断

  • 持有类似spin_lock这样的锁而锁住该CPU核调度的情况(持有spin_lock的进程会进入一个critical section(该核调度被关))

 

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

Linux内核和驱动程序员钟爱spinlock到了如痴如狂的程度,可以说不睡眠、时间较短的critical section场景,都会第一时间想到spinlock。平生不识自旋锁,就称英雄也枉然。自旋锁的优越性在于,在2个人(这2个人可能是线程与线程、中断与线程、中断与中断等)竞争一个锁的时候,避免失败的那一方切换上下文context,所以与其上下文切换,不如原地等。但是自旋锁本身也引起了副作用,它引起了持有锁的CPU核的抢占调度的禁止。内核自旋锁的实现,更多的是核间自旋转而核内是通过禁止抢占来实现临界区保护的。

假设T1, T2, T3, T4运行在一个核上面,当T1拿到spinlock后,这个核上的抢占调度被禁止,如果在T1持有spinlock的时间内,T2是一个高优先级的实时任务,尽管T2被唤醒,它也不可能立即打断T1的执行,必须等待T1释放spinlock。由于T1究竟会持有spinlock多久做xxxx,这个鬼都不知道,所以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,因为它无法修复另外一个核上运行的线程、中断服务程序与本核线程之间的竞争。尽管在单核处理器里面调用这个API是通常安全的,但是我们哪怕是在单核编程,都要假装自己是多核的样子,这个是在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是码农写的,鬼都不知道它要多久。

在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去申请,你的确定性延迟还如何满足?

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

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

 

当提到preempt-rt补丁的时候,我必须强调一点,Linux不是一个裸奔的操作系统。Linux的应用都是在用户空间写的一个个进程、线程。所以相对于其他RTOS可能更加强调高优先级中断的确定性时延(RTOS不太特别强调机制与策略分离的概念,整个东西编译在一起的话,在中断里面放策略也未尝不可),在Linux时间里,用户空间高优先级的RT线程的确定性调度时延就显得更加critical(因为Linux内核里面你不能裸奔地把用户策略的东西放进内核,内核提供的是一些操作接口而已,简单来说,你要做的事情是一个应用,而应用是个用户空间的东西)。

 

5、如何测试RTLinux的实时性?

Linux 实时性能测试工具——Cyclictest https://git.kernel.org/pub/scm/utils/rt-tests/rt-tests.git

可以用来测试 RT线程的确定性调度时延

不过 RT内核的确定性调度时延是 牺牲linux系统的吞吐量 来换取的.

 

 

Ingo Molnar 的实时补丁是完全开源的,它采用的实时实现技术完全类似于Timesys Linux,而且中断线程化的代码是基于TimeSys Linux的中断线程化代码的。这些实时实现技术包括:中断线程化(包括IRQ和softirq)、用Mutex取代spinlock、优先级继承和死锁检测、等待队列优先级化等。

为了能并入主流内核,Ingo Molnar的实时补丁也采用了非常灵活的策略,它支持四种抢占模式:

1.No Forced Preemption (Server),这种模式等同于没有使能抢占选项的标准内核,主要适用于科学计算等服务器环境。

2.Voluntary Kernel Preemption (Desktop),这种模式使能了自愿抢占,但仍然失效抢占内核选项,它通过增加抢占点缩减了抢占延迟,因此适用于一些需要较好的响应性的环境,如桌面环境,当然这种好的响应性是以牺牲一些吞吐率为代价的。

3.Preemptible Kernel (Low-Latency Desktop),这种模式既包含了自愿抢占,又使能了可抢占内核选项,因此有很好的响应延迟,实际上在一定程度上已经达到了软实时性。它主要适用于桌面和一些嵌入式系统,但是吞吐率比模式2更低。

4.Complete Preemption (Real-Time),这种模式使能了所有实时功能,因此完全能够满足软实时需求,它适用于延迟要求为100微秒或稍低的实时系统。

实现实时是以牺牲系统的吞吐率为代价的,因此实时性越好,系统吞吐率就越低。可以看出来,我们的RT内核适用于 延迟要求为100us的实时应用。

 

参考:https://cloud.tencent.com/developer/article/1518255

 

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值