首先看看维基百科对实时操作系统的定义:
实时操作系统(Real-time operating system, RTOS),又称即时操作系统,它会按照排序运行、管理系统资源,并为开发应用程序提供一致的基础。实时操作系统与一般的操作系统相比,最大的特色就是“实时性”,如果有一个任务需要执行,实时操作系统会马上(在较短时间内)执行该任务,不会有较长的延时。这种特性保证了各个任务的及时执行。
实时操作系统 (Real-time OS) 是相对于分时操作系统 (Time-Sharing OS) 的一个概念。在一个分时操作系统中,计算机资源会被平均地分配给系统内所有的工作。在分时系统中,各项任务需要花多长时间来完成,这一点并不重要。
所以在一 个实时操作系统之中,最关注的是每个任务在多长时间内可以完成。简单地说,实时和分时操作系统最大的不同在于 时限(deadline)”这个概念。 在一个特定任务的执行时间内必须是确定的并且可预测的,在任何情况下都能保证任务的最大执行时间限制,通常实时分为软实时和硬实时。
- 软实时: 仅仅要求事件的响应是实时的,并不要求任务必须在多长的时间内完成,大多数情况下要求的时统计意义上的实时,而不需要100%达到实时。在许多情况下,这种软性正确性已经达到了用户期望的水平。比如用户在操作DVD播放的时候,偶尔不能在限定的时间内完成任务也是可以接受的,它可以容忍偶然的超时错误,失败造成的后果并不严重
- 硬实时: 在任务的执行时间的要求是非常严格的,无论在什么情况下,任务的执行时间必须要得到绝对的保证,否则将会产生灾难性的后果。比如,汽车碰撞后,必须在X时间内弹开安全气囊,你弹开晚了,人已经挂了。
1 为什么linux不是硬实时
Linux系统一开始就被按照GPOS(通用操作系统)来设计的,它所追求的是尽量缩短系统的平均响应时间,提高吞吐量,达到更好的平均性能。在这个背景下,Linux无法达到强实时性的因素是多方面的,比如虚拟内存管理、共享资源互斥访问机制等等,但最重要的因素是进程调度以及内核抢占机制,这也是本文讨论的重点。
我们首先来看看,为什么Linux不是一个硬实时的操作系统,其主要有以下几个原因
spinlock是一个随处可见被内核和驱动使用的API
首先,我们来看看spinlock的实现,spin_lock()会调用preempt_disable() 导致本核的 抢占调度 被关闭(preempt_disable函数实际增加preempt_count来达到此效果),其次我们理解spin_lock_irq()是local_irq_disable()+preempt_disable()的合体。
对于两个接口,大家是不是很熟悉,我们在linux内核和驱动程序中随处可见,在不用睡眠,时间较短的临界区的场景,我们都会第一时间想到spinlock。自旋锁的优点是,在两个人(这两个人可能是线程与线程,中断与线程,中断与中断等)同时竞争一个锁的时候,防止出现失败的那一方出现上下文切换,所以希望在原点等待。
但是这样的自旋锁本身也会导致一些副作用,它导致了持有该锁的CPU核的抢占被禁止,所以内核自旋锁的实现,是通过禁止抢占来实现临界区的保护,如下图的例子
假设T1, T2, T3, T4运行在一个核上面,当T1拿到spinlock后,这个核上的抢占调度被禁止
- 如果在T1持有spinlock的时间内,T2是一个高优先级的实时任务,尽管T2被唤醒,它也不可能立即打断T1的执行,必须等待T1释放spinlock
- 由于T1究竟会持有spinlock多久做什么,这个鬼都不知道 ,所以T2究竟要等多久,也未可知,这显然破坏了决定性的时延。
- 假设T0时刻,系统正在执行自旋锁进入临界区,此时在T1时刻,系统唤醒了一个高优先级的任务,而此时抢占调度是关闭的,所以RT实时进程是无法得以调度的
- 而当中断T2时刻,发生了一个中断,就会导致进入中断处理,这段时间发生了多久,鬼也不知道,当中断处理完后,返回到自旋锁的临界区,当退出临界区时候,会打开内核抢占并且尝试抢占当前的进程,此时RT实时任务将会得以调度,但是此时的延时有太多的不确定性
Linux的中断执行时间可能过长且不可嵌套
当中断发生后,一般硬件会自动屏蔽当前CPU对于中断的响应,而软件层面上,直到IRQ HANDLER做完,才会重新开启中断,比如,对于ARM处理器而言,exception进来的时候,硬件都会自动屏蔽中断
也就是说,当ARM处理器收到中断的时候,它进入中断模式,同时ARM处理器的CPSR寄存器的IRQ位会被硬件设置为屏蔽IRQ。
Linux内核会在如下2个时候重新开启CPSR对IRQ的响应:
- 从IRQ HANDLER返回中断底半部的SOFTIRQ
- 从IRQ HANDLER返回一个线程上下文
中断在执行的时候,所有的中断都进不来,这个设计本身简化了内核,但是对于硬实时的打击是致命的,前面的中断不执行完成,优先级再高的中断也得给我等着。
比如中断1在执行的过程中,来了中断2,而中断2对应的事情是必须要决定性时延的, 由于IRQ1的中断服务程序也是码农写的,我们无法确定这个中断服务程序要执行多久 。这显然让高优先级中断2的进入延迟不再具备可预期性。
软中断(softirq)是一个比进程上下文优先级更高的上下文
我们设想一个场景,哪怕Linux解决了问题2,就是Linux的中断变地可嵌套,高优先级的中断可以打断低优先级的中断,并且高优先级的中断2唤醒了一个用户写的实时线程。
IRQ2唤醒了实时任务T1,但是T1必须等待IRQ1唤起的软中断(也包括使用软中断上下文的tasklet等)被执行完,T1才能投入执行。 IRQ1唤起的softirq的代码是码农写的,这个码农写多久,鬼都不知道 ,这显然破坏了实时任务T1得以调度执行的确定性时延。
内核里面会屏蔽中断的API如local_irq_disable、spin_lock_irqsave等
那么,问题又来了,spin_lock_irqsave既屏蔽了抢占,又屏蔽了中断,这会导致中断和实时任务的确定性时延造成不可预期的破坏。 因为spin_lock_irqsave和spin_lock_irqrestore是码农写的,鬼都不知道它要多久。
所有针对这个问题,我们回顾linux中的四类区间:
- 1 中断
- 2 软中断
- 3 进程上下文中的spin_lock
- 4 进程上下文中的其他区域
上述四类区间中,只有第四类区间支持抢占调度。当可以调度的事情发生在前3类区间中,即如果在这3类区间中唤醒了高优先级的可以抢占的task,实际上却不能抢占,直到这3类区间结束。
用一个例子说明如下:
如上图所示:
- T0时刻
Normal task
因为syscall
陷入内核 - T1时刻
CPU
拿到spin lock
,进入Critical section
- T2时刻系统产生中断
IRQ1
,进入IRQ handler
- T3时刻系统唤醒了高优先级的
RT task
,但由于此时系统处于不可调度区域,所以RT task
无法立即运行 - T4时刻
IRQ1
结束,但接着产生中断IRQ2
,进入IRQ handler
- T5时刻,中断都结束,但
spin lock
仍然没有释放,系统仍然处于不可调度区域 - T6时刻,
spin lock
释放,高优先级的RT task
立马得到调度 - T7时刻
RT task
运行结束,Normal task
再一次被调度到 - T8时刻从内核态返回
从T1到T6,这个区间的时间是不可预测的,因此通用的Linux系统无法达到硬实时的标准。
2 linux内核实时性改进
早在2001年时,内核就开始打上抢占补丁,回顾之前的知识点
- 如果linux内核不支持抢占,那么进程要么主动要求调度,入调用schedule()或者cond_resched()等,要么在系统调用、异常处理和中断处理完成后返回用户空间强进行调度,上述都会导致早期linux内核调度时延非常大
- 如果linux内核支持抢占,如果唤醒动作发生在系统调用或者异常处理上下文,在下一次调用preempt_enable时会检查调度标志位,是否需要抢占调度。同时对于中断处理返回内核态,也会检查调度,而preempt_enable函数会主动调用__preempt_schedule来判断是否需要抢占当前的进程。
主要是用thread_info数据结构中的一个preempt_count计数
- 当preempt_count为0时,表示内核可以被抢占
- 当preempt_count大于0时,则禁止抢占
preempt_count包含preempt, softirq,hardirq,nim以及need_resched几个域
内核提供preempt_disable来关闭抢占,之后preempt_count会加1,preempt_enable函数用于打开抢占,preempt_count计数会减1,程序会判断当前是否为0,如果为0,就调用___preempt_schedule
preempt_count本质上是一个per-CPU的32位变量,它在各种处理器架构下的存放位置和命名不尽相同,但其值都可以使用preempt_count()函数统一获取。preempt_count逻辑相关的核心代码位于include/linux/preempt.h,虽然只是一个32位变量,但由于其和中断、调度/抢占密切相关,因此在系统中发挥的作用不容小觑。
- preemption count占8个bits,因此一共可以表示最多256层调度关闭的嵌套
- preempt_count中的第8到15个bit表示softirq count,它记录了进入softirq的嵌套次数,如果softirq count的值为正数,说明现在正处于softirq上下文中。由于softirq在单个CPU上是不会嵌套执行的,因此和hardirq count一样,实际只需要一个bit(bit 8)就可以了。但这里多出的7个bits并不是因为历史原因多出来的,而是另有他用。
- preempt_count中的第16到19个bit表示hardirq count,它记录了进入hardirq/top half的嵌套次数,在do_IRQ()中,irq_enter()用于标记hardirq的进入,此时hardirq count的值会加1。irq_exit()用于标记hardirq的退出,hardirq count的值会相应的减1。如果hardirq count的值为正数,说明现在正处于hardirq上下文中,代码中可借助**in_irq()**宏实现快速判断。注意这里的命名是"in_irq"而不是"in_hardirq"。
对于内核仅仅的支持抢占调度,要达到硬实时系统的要求还远远的达不到要求,为此社区一直致力于linux内核实时性优化和改进工作,最近几年有很多优化的补丁和方法,
PREEMPT_RT
补丁可以通过以下方面对kernel进行源码级的改造:
- spinlock迁移为可调度的mutex
- 中断线程化
- 软中断线程化
从而将Linux内核中的1/2/3类区间都改造成4类区间,大大提高了系统的实时性。对于linux也提供了很多工具用于检查哪些地方有比较大的调度延时
- 对于可以通过工具
cyclictest
工具来测试Linux实时性能。该工具的使用方法已经安装方法这里不做过多介绍。 - ftrace工具是一个很多好的追踪跟踪器,如preemptirqsoff跟踪器可以跟踪中断并禁止抢占代码的延迟,同时记录关闭的最大时延
- linux内核还继承了latencytop工具,它在内核上下文切换时被记录进程的内核栈,然后通过匹配内核栈函数来判断导致上下文奇幻的原因。这个不仅方便判断系统出现了哪些方面的延时,还有助于查看某个进程或线程的延迟情况。详细的用法见https://blog.yufeng.info/archives/1239
参考文档
https://blog.csdn.net/juS3Ve/article/details/81437432
http://howar.cn/2020/03/15/embedded-linux-real-time/
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。