什么是实时操作系统(RTOS),linux 为什么无法保证实时性
实时操作系统的定义
实时操作系统是保证在一定时间限制内完成特定功能的操作系统(执行时间的确定性是实时操作系统最根本的,其代价往往就是吞吐量低)。
实时操作系统有硬实时和软实时之分,软实时仅仅要求事件的响应是实时的,并不要求任务必须在多长的时间内完成。硬实时不但要考虑响应速度(即从一个事件发生到系统对此作出反应,并开始执行有关程序之间所需的时间),还要考虑有关程序能否在规定时间内执行完,比如,汽车碰撞后,必须在X时间内弹开安全气囊,弹开晚了,人已经挂了。
实时操作系统为了实时性而做的努力
实时操作系统的任务调度是居于优先级,高优先级从阻塞变为就绪后,立马执行任务切换。
linux普通进程的任务调度算法是居于CFS,当高优先级从阻塞变为就绪,也不一定执行任务切换(1、有一个最小调度粒度 sysctl_sched_min_granularity,2、调用 wake_up_process() 只是触发抢占,执行抢占大多在 tick中断返回前)。由于vruntime还受运行时间的影响,运行时间越长,vruntime越大,所以高优先级进程可能会等低优先级任务运行一段时间再运行。
linux 自旋锁是通过关本核抢占实现临界区,临界区执行期间即使实时任务被唤醒(比如被中断唤醒),也得等到释放自旋锁后才能得到运行,无法保证实时性。
PREEMPT_RT
补丁可以通过以下方面对kernel进行源码级的改造来优化实时性:
- spinlock迁移为可调度的mutex
- 中断线程化
- 软中断线程化
实时任务调度器是实时操作系统的一个必选项,但不代表只要设计出来一个实时调度器就足够了。实时操作系统的特性是在整个操作系统的设计思路上都要时刻关注实时性。
消息事件处理机制(优先级)
常规的操作系统中,消息队列都是按照FIFO(先进先出)的方式进行调度,如果有多个接受者,那么接受者也是按照FIFO的原则接受消息(数据),但实时操作系统会提供基于优先级的处理方式:两个任务优先级是分别是10和20,同时等待一个信号量,如果按照优先级方式处理(数值越低优先级越高),则优先级为10的任务会优先收到信号量。
提供内核级的优先级翻转处理方式(优先级)
实时操作系统调度器最经常遇到的问题就是优先级翻转,互斥锁提供防止优先级翻转的机制。
减少粗粒度的锁和长期关中断的使用(中断)
这里的锁主要是指自旋锁(spinlock)一类会关闭中断的锁,也包括任何关中断的操作。在Windows和Linux的驱动中,为了同步的需要,可能会长期关闭中断,这里的长期可能是毫秒到百微秒级。但实时操作系统通常不允许长期关中断。
对于非实时操作系统来说,如果收到一个外部中断,那么操作系统在处理中断程序的整个过程中可能会一直关中断,不支持中断嵌套。实时系统执行中断函数不关中断,允许中断嵌套。
对于非实时系统比如中断1在执行的过程中,来了中断2,对于实时系统中断2对应的事情是必须要确定性时延的, 由于IRQ1的中断服务程序也是码农写的,我们无法确定这个中断服务程序要执行多久 。这显然让高优先级中断2延迟不再具备可预期性。
系统级的服务也要保证实时性(优先级)
对于一些系统级的服务,比如文件系统操作,非实时系统会缓存用户请求,并不直接把数据写入设备,或者建立一系列的线程池,分发文件系统请求。但实时系统中允许高优先级的任务优先写入数据,在文件系统提供服务的整个过程中,高优先级的请求被优先处理,这种高优先级策略直到操作完成,这种设计实际上会牺牲性能。
由于应用场景的差异,会出现有些用户需要实时性的驱动,有些用户需要高性能的驱动,因此实时操作系统实际上要提供多种形式的配置以满足不同实时性需求的用户。
避免提供执行时间不确定的API(确定性)
无论什么时候执行API,执行时间都是相同的。多数实时操作系统都不支持虚拟内存(page file/swap area),主要原因是缺页中断(page fault)会导致任务调度的不确定性增加。实时操作系统很多都支持分页,但很少会使用虚拟内存,因为一次缺页中断的开销十分巨大(通常都是毫秒级),波及的代码很多,导致用户程序执行的不确定性增加。
实时操作系统的确定性是一个很重要的指标,在某些极端场景下,甚至会禁用动态内存分配(malloc/free),来保证系统不受到动态的任务变化的干扰。
针对实时性设计的SMP和虚拟化技术(确定性)
SMP(多核)场景的实时调度是很困难的,这里还涉及到任务核间迁移的开销。针对SMP场景,多数实时操作系统的设计都不算十分优秀,但比起普通操作系统来说,其实时性已经好很多了。
同时实时操作系统的虚拟化能从hypervisor层面上提供虚拟机级别的实时调度,虚拟机上可以是另外一个实时系统,也可以是一个非实时系统。