操作系统的实时性是指执行一个特定任务的时间是确定的和可预测的,这个任务执行时限需要考虑任何的情况,包括最恶劣的情况。或者说操作系统能够在规定的时间点内完成指定的任务操作,一旦超过这个时间点会对整个系统带来不可估量的后果,也就是指任务执行的及时性。这里以车辆紧急制动为例,这个任务的响应时间就是车载系统收到紧急制动指令到真正实施紧急制动这个动作所需的时间,我们当然希望这个时间是确定的而且是及时的,即需要在最恶劣的情况下也能保证在一定的时间内完成,否则刹车失灵车企可能就要上热搜了。
实时操作系统有很多,国外的有μC/OS、FreeRTOS、SafeRTOSVxworks等,国内的代表有RT-Thread和LiteOS。本文主要讲一下Linux的实时性。
不同的应用场景对操作系统的要求是不一样的,比如在车载领域追求的是实时性,任务执行的确定性和可预测性;而服务器系统追求的是吞吐量,相同的时间内执行的任务越多越好,越快越好;而在嵌入式领域追求的则是低功耗。实时性,吞吐量,低功耗是鱼与熊掌不可兼得。
业界一般是通过延迟来衡量一个操作系统的实时性,主要包括中断延迟和抢占延迟
这里主要从中断延迟和抢占延迟两个主要方面来分析影响Linux实时性的一些因素。
Linux默认是不支持中断嵌套的,假设cpu在执行一个中断A, Linux会先屏蔽中断,意味着这个CPU在执行完中断A之前是不会响应其它的中断。假设这时候又来了一个优先级更高的,更紧急的中断B, cpu也不得不等到中断A执行完才能去执行中断B, 但中断A什么时候执行完可能是不确定的,这就导致了中断B的中断延迟的不确定性。这一点对Linux的实时性影响是非常大的
Linux的可抢占性有以下四种:
自旋锁是内核中常用的同步机制,它的特点是如果申请失败不会陷入休眠,这样可以减少任务切换context switch的次数,有利于提高系统的吞吐量。但是在自旋锁持期间会禁止抢占,有的接口可能还会禁止中断,这会给抢占延迟和中断延迟带来很大的不确定性。假设cpu A持有一个自旋锁,同时cpu B也尝试申请该自旋锁,那么cpu B就会自旋,不会主动让出CPU刚给其它线程,这也会引入一定的抢占延时。这个问题可以通过CONFIG_PREEMPT_RT把自旋锁配置成睡眠可抢占来解决。这里需要注意的是,如果自旋锁设置为休眠可抢占之后,要注意优先级反转问题。
优先级反转
可以简单理解为操作系统每隔多久会触发一次任务调度,通常用赫兹hz来表示,即一秒会触发多少次调度,系统任务调度的赫兹数越高实时性越好。一般配置为100-1000hz,比如100hz就是10ms触发一次调度,1000hz就是1ms触发一次调度。如果hz配置太大会导致频繁触发调度会占据很大的cpu资源(或者叫浪费cpu资源,光任务切来切去就够了)。
如果使用spin_lock_irqsave, spin_lock_irq, spin_lock_bh, spin_lock_disable, preempt_disable, migrate_disable等禁止抢占api,会对中断延迟和调度延时带来很大的不确定性,会对系统的实时性产生影响
假设一个高优先级的中断产生时,唤醒了一个实时线程,那么这个实现线程需要等到中断执行完才能执行。但是如果中断执行的过程中需要处理软中断,这可能会导致实时线程的执行被延迟很久,也就是说会造成抢占延迟的不确定性
用户空间申请一块内存时,得到的只是一个虚拟的起始地址,内核并没有马上给这段虚拟空间分配物理内存。当程序真正访问这段虚拟空间之后会触发一个缺页异常,然后在缺页异常的处理程序中为这段虚拟空间分配内存。这也会对应用程序的执行时间上的不确定性和及时性有很大影响。
当内存不够时,Linux会把一些内存swap到硬盘或者对内存进行压缩(zram),当程序访问的堆栈不在内存上时就会触发缺页异常,将数据又从硬盘swap到内存,或者是解压缩,这也会引入不确定性。
出于省电的目的,当cpu没事干的时候会进入idle状态. 例如ARM64架构中,当CPU Idle时,会调用WFI指令(wait for interrupt),关掉CPU的Clock以便降低功耗,当有外设中断触发时,CPU又会恢复回来。按照cpu睡眠的深度分为以下这几个C-State,睡眠越浅越耗电,但是唤醒就越快,睡眠越深越省电,唤醒也就越慢,这对中断延迟和抢占延迟也有很大影响。
影响Linux实时性的因素还有:CPU频率切换(DVFS), DMA, IOBuffer, cache, branch-prediction, TrustZone, hypervisor,NMI等等因素,这里先不展开细讲。
linux提供一个实时补丁来提高Linux的实时性,这个patch主要采取的措施有以下几种
这个补丁中内核可抢占性配置为CONFIG_PREEMPT_RT模式