Linux 实时性措施探讨

Linux 实时性措施探讨

全篇的内容来自NASA的文章《Challenges Using Linux as a Real-Time Operation System》,觉得内容实在太干货了,所以在自己理解的基础上总结提炼了一下,分享给大家。鉴于个人水平有限,有些专业的场景由于自己没有接触过所以可能存在误解,有疑问的可以看原文。

内核抢占

主线版本和CONFIG_PREEMPT_RT补丁

  • CONFIG_PREEMPT_NONE:最常见模式,内核无法抢占,在现代ARM处理器上的典型切换延时低于200us,但是同时内核无法保证调度的最大延迟,通常在几百ms左右

  • CONFIG_PREEMPT_VOLUNTARY:内核仍就无法抢占,但是提供了某些状态下自动放弃处理器的机制,添加了一些重调度点,减少调度延时

  • CONFIG_PREEMPT:内核支持抢占,调度延迟远小于非抢占内核。现代ARM处理器的开销在10us,最大到100us左右

Linux发行版中,服务器版本通常使用前两个选项,为了达到最大的性能。桌面版则通常使用后两个选项,为了达到更好的用户体验

CONFIG_PREEMPT_RT项目是一个基于Linux主线的补丁,它使内核支持完全抢占,事实上CONFIG_PREEMPTCONFIG_PREEMPT_RT的一个子集

CONFIG_PREEMPT_RT补丁主要做了以下几点:

  • 用实时互斥锁(rtmutex)代替内核原有的锁方案,使内核锁支持抢占
  • 自旋锁和读写锁保护的临界区也支持抢占
  • 对于内核的自旋锁和信号量,支持优先级继承
  • 将中断处理过程移入可抢占的内核线程
  • 内核定时器进行分频,提高内核定时器的精度,副作用是用户态的POSIX定时器精度也会一起提高

CONFIG_PREEMPT_RT补丁下,内核的一些资源管理函数,尤其是内存管理,还是会包含不确定的开销。所以,CONFIG_PREEMPT_RT会假设实时应用在开始实时任务之前,已经获取了想要的内存资源。在ARM处理器平台上评估结果显示,CONFIG_PREEMPT_RT下最大的中断响应时间比CONFIG_PREEMPT快了37%~72%;任务切换时间则差不多。

OSADL组织测试了不同稳定版本linux内核打了CONFIG_PREEMPT_RT补丁之后的系统延迟,根据硬件性能的不同,从40us-400us不等

硬件选型和实时性

尽管CONFIG_PREEMPT_RT补丁提供了一个支持完全抢占的内核,但是系统的实时性依旧受硬件平台、硬件配置以及驱动的影响。这些措施大部分都适用于所有的实时系统。举一些例子:

  • 电源管理功能,如:CPU睿频,超线程技术等,都会使得系统的运行时间和响应时间变得不确定。
  • 如果硬件设备偶发的独占一个资源,比如PCI总线,那也会使得系统的实时性下降。
  • 固件,例如BIOS,也会无法预估的挂起操作系统来做一些底层的操作。最常见的就是X86处理器是系统管理模式,很多功能都会牵扯到系统管理模式,比如电源管理,设备控制,事务管理,设备仿真,软件纠错等。这些调用的开销经常需要100us左右。
  • 设备驱动不能包含禁止系统抢占的代码,庆幸的是现在的Linux内核驱动都会设计成上半部和下半部两部分,上半部会尽快释放资源,下半部则放在内核线程中去处理,去处理一些更占用资源的任务。这样的设计完美契合了CONFIG_PREEMPT_RT理念

内存管理

一个实时的应用程序常常需要在初始化阶段就获取和锁定它需要的内存资源。虽然linux也提供类上述两个功能,但是linux的两个功能使得它们的工作方式和其他操作系统不同,严格限制了预分配内存。这两个功能一个是内存映射和按需分页,另一个就是过需分配内存

内存映射和按需分页(Demand Paging)和内存锁定(Memory Locking)

内存映射和按需分页是很多操作系统都用到的技术,原理是只有当应用程序实际访问到对应的页时,才会将虚拟地址装载到对应的物理地址。然而,在按需分页的早期的实现机制中,应用程序中不在物理地址上的那部分虚拟地址,是映射到文件系统的swap文件的。而其他系统也会用swap技术来将可执行页(主要是堆栈)映射到文件系统。Linux的按需分页做的更进一步,它将堆栈页映射到一个单独的只写页(初始化为0)。应用程序的虚拟内存区只有当发生了写请求之后,才会映射到对应的物理地址或者swap文件。这个过程就会产生一个缺页中断,Linux接收到中断后就会做出响应,去重新建立虚拟地址到物理地址的映射(原先是映射到一个全0的区域),但是与此同时,这个虚拟页也有了重新映射到swap文件的权限。这样一来,当应用程序需要申请一大片内存时,它就不能立马消费对应的物理内存或者swap文件区,不管它是否马上就要用到。这种策略应对大部分应用场景有很好的效果,但是对于实时应用程序却不友好。

所以,一个实时应用程序在设计的时候,就需要尽可能的在初始化阶段就分配好它所需要的内存资源,这样就避免在实时任务运行时去动态请求内存,因为这种操作的开销往往不可预计

然而这还远远不够,上面的措施只能保证初始化阶段获取的虚拟内存有确切的物理内存的映射,而一个使用按需分页的实时系统需要提供一套机制,保证应用程序中所有的虚拟内存都有完整的物理内存映射。换句话说,实时系统需要保证程序运行起来之后,不会重新将虚拟地址映射回文件系统,这种机制就叫内存锁定

Linux提供了对应的系统调用mlockall(),一个实时应用应该在刚启动进程或者线程的时候就调用mlockall(),同时要配合MCL_CURRENT|MCL_FUTURE参数,这两个参数保证当前和未来分配到当前进程(或线程)的页都会锁定到实际的物理内存。注意,即使系统被配置成不支持swap,也是需要调用这个函数的,因为Linux任然会把可执行文件镜像swap到文件系统。

在2.6.9内核之前,只有超级权限的进程可以锁定内存,而较新的内核版本支持普通权限的进程可以锁定一定数量的内存,支持的大小由宏RLIMIT_MEMLOCK来约定,缺省值是64K字节。

尽管如此,mlockall()也只能锁定由Linux分配的页,而最开始那些映射到只读的0页的虚拟内存,仍旧会第一次写需求的时候产生缺页中断,此时Linux的系统开销就会由页分配和内存锁定同时影响。

Linux不提供用来关闭按需分页的内核参数或者选项。与此同时,实时程序必须做到以下三点,来保证在任务真正开始前,获取到的内存都是可用的:

  • 应用在堆上分配内存时,必须向请求的内存中的每一页都主动进行一次写操作
  • 应用必须包含一个函数,这个函数调用时需要把整个任务周期内需要用到的最大的栈空间给用满
  • 应用程序需要充分使用内核提供给用户空间的buffer,比如socket通信用到的buffer

PS:说实话这三点我没有理解的特别深刻,因为我没有考虑过也没有见过其他人做过这种操作,所以我怀疑自己可能翻的有误,有疑问的建议看原文

过需分配内存(overcommit memory)

通过内存映射技术,Linux可以承诺给应用程序分配比实际操作系统能提供的更多的虚拟内存,这个功能就叫做过需分配内存。因为操作系统假设所有的应用程序不会同时用尽他们所请求的所有内存。但是这样一来,过需分配内存实际上对于实时程序是有负面影响的,这增加了判断内存是否过度分配的难度。这样一来,当内存第一次超额申请的时候,申请的结果仍然能成功返回。

默认配置下,Linux允许分配的虚拟内存大小等于swap分区的大小加上1.5倍的物理内存大小

当一个内存写操作发生时,如果此时系统的虚拟内存用完了,才会出现内存过度分配的现象。一旦这种情况出现,那么内核就会开始kill进程,当然这个进程不会是正在执行写操作的进程。因此这种操作通常不会发生在root权限下的进程,更多会发生在其他进程。

此外,在检验这个状态时,需要重新检验一下所有被应用程序分配的内存。这样一来会导致系统会低估内存的利用率。系统预估应用的内存使用率时,只计算实际分配的页,不会考虑稍后延迟分配的页,那么最高的内存使用率就很明显只会出现在压力测试的时候。

幸运的是,提供了一个参数overcommit_memory,可以选择关闭过需分配内存,在实时应用中这个选项是需要关闭的

系统调用和缺页异常

有些系统调用会执行内存分配,这会导致缺页异常,在实时应用中,这些调用是必须避免的。很不幸,linux没有一份完整的清单来列举所有会造成缺页中断的系统调用,函数的使用手册也多半不会提及。只能从表面来推测一个系统调用是否会造成缺页中断,比如通过它执行的功能,或者使用的参数项。最常见的一个例子就是基于IP通信的相关调用。

因此,实时应用应该避免使用IP通信,或者在初始化阶段去执行通信功能。

定时器

软件时钟和高性能时钟

Linux定义了一个软件时钟来管理系统事件,例如调度,休眠,超时以及测量CPU时间。从2.6.20开始,这个软时钟的周期支持100hz(也就是10ms),250hz,300hz,1000hz。在X86或者PowerPC上,这个周期是1ms。所以任何和时间定时相关的系统调用,时间粒度都不能超过这个周期。

然而,从2.6.1开始,Linux加入了高性能定时器的支持,它支持纳秒级别的精度,但是实际的性能还是依赖于硬件。可以通过内核参数CONFIG_HIGH_RES_TIMERS来使能高性能定时器,当开启时,内核就使用高性能定时器来管理大部分事件。并且像selectpollepollnanosleepfutex等这种系统调用默认就是使用高性能定时器的。他们的精度都远高于1ms。

定时器懒惰(timer slack)

引入高性能定时器会更频繁的唤醒CPU,增加系统开销,增加功耗。为了平衡这点,2.6.22之后,引入了timer slack机制,slack表示了有一小段时间,默认是50us,每个线程可单独设置。当线程中有个定时器任务时,内核检查定时器是否已经在设定时间+slack值的范围内结束,如果是,就会在设定时间之后一小段时间内执行定时器事件,因此,timer slack会增加时钟周期的计数次数。

不过实时线程的调度策略是不支持timer slack的,只有普通线程支持。尽管如此它还是会影响普通线程的异步性,如果有地方因为等待普通线程的异步资源而受到阻塞,那么应考虑改用实时线程。

进程调度和CPU隔离

对应Linux 调度器来说,进程和线程是一样的,所以下面统一以任务来描述所有的进程和线程。

调度策略和优先级

Linux调度策略分实时策略和普通策略

实时策略

SHCED_FIFOSCHED_RR,3.14之后引入了SCHED_DEADLINE。前两者使用静态优先级,1最小,99最大,而普通任务的静态优先级是0。所以实时任务的优先级总是比普通任务高。SCHED_FIFO策略只有当任务主动结束或者遇到更高优先级的任务抢占时才会终止并让出CPU,换句话说,SCHED_FIFO没有时间片的概念。所以这个策略下一个任务可以永久占用CPU(当然linux有机制避免这种情况发送),哪怕有一个通优先级的任务进来,也无法抢占它。

SCHED_RR则是在SCHED_FIFO的基础上加入了时间片的概念,每个任务只能在自己的时间片内占用CPU,时间片用完后就会被同优先级的任务抢占。在3.9内核之前,默认的时间片大小固定是100ms,之后可以通过内核参数sched_rr_timeslice_ms来修改。并且,Linux还提供了nice值用来动态调整实时任务的时间片。动态调整范围从+19(10ms)到-20(200ms)

SCHED_DEADLINE策略给在调度周期的范围内,每个任务设定一个相对的deadline期限。然后调度器需要评估一个周期内所有任务可以运行的最长时间,然后运行时在每个周期,先运行最早到期限的任务。并且每次插入新任务的时候,也会执行一次检查,看看有没有任务需要调度。

所有使用SCHED_DEADLINE策略的任务都被当成一个组,比其他调度策略的任务优先级更高,以确保到deadline时任务可以被调度。

2.6.12之前,只有特定的任务可以执行实施策略,之后的版本中,普通的任务也可以使用SCHED_RRSCHED_FIFO策略,但是优先级的范围不能超过RLIMIT_RTPRIOSCHED_DEADLINE只支持特定的任务。

普通策略

有三种普通调度策略SCHED_OTHERSCHED_BATCHSCHED_IDLE

SCHED_IDLE策略只有在处理器空闲的时候才会运行,该策略下所有任务轮转运行,无优先级

SCHED_OTHERSCHED_BATCH都受动态优先级影响,动态优先级是调度器根据任务的响应性和公平性来估算的,并且受nice值的影响。两者的区别主要在于,SCHED_BATCH会被调度器默认为非交互式的任务,并且被认为是CPU密集型任务。在大部分调度算法下,SCHED_OTHER更受欢迎,因为大部分调度算法是为交互型任务而设计的。

实时系统中的周期性任务和时间敏感性任务应该使用实时调度策略,下面介绍一下调度器的工作原理和注意细节

CFS调度器

2.6.23之后,普通优先级的任务使用的默认调度器就是CFS调度器,设计目的是完全公平调度。简单概括一下就是对于所有同nice值的任务,CFS调度器会把处理器的时间片均匀的分给这些任务。

CFS会取一个周期来分配时间片,这个周期被认为是CFS的调度延时,你也可以认为这是一个任务调度需要等待的最长时间。随着内核版本的演变,CFS分配的默认时间片值也不断的在改变。在3.2和3.14版本中,默认值是基于CPU数量的计算公式6ms*(1+log2(cpu数量))。当然,为了避免产生过多的上下文切换的开销,CFS约定了一个最小运行时间,这段时间内任务是无法被抢占的。这个值的计算公式是0.75ms*(1+log2(cpu数量))

当然如果参与调度的任务超过了8个,那默认用来分割的周期也会增大。为了保证每个任务公平的享用处理器,CFS追踪每个人任务的运行时间,并且用红黑树进行排序。

为了确保引入nice值并且保证公平性,CFS定义了一个虚拟运行时间

nice值会影响一个任务的虚拟时间,nice值更小的任务(优先级更高)的虚拟运行时间会增长的比实际运行时间慢。相反,nice值越大的任务虚拟运行时间增长的比实际运行时间更快。

尽管CFS让每个任务公平的访问处理器资源,但是CFS会隐式的提升那些经常阻塞休眠的任务的优先级,用一个现象来描述就是一个I/O密集型任务在唤醒时会抢占一个CPU密集型任务。

异步任务应该用普通还是实时策略?

异步任务使用实时策略有以下优势:

  • 使用实时策略可以增加任务运行的可重复性(我的理解是周期的稳定性)
  • 调度效率比CFS高
  • 实时策略不倾向于公平分配CPU,所以它更青睐CPU密集型任务
  • 实时策略允许用户选择时间片调度和非时间片调度,CFS必须依赖时间片
  • 实时策略可以更好的控制任务的运行顺序
  • 实时任务不受timer slack影响,定时精度也更高

当然开发者也要注意不要过多的使用实时任务,详细在下面[实时带宽限制](# 实时带宽限制)会介绍。

调度事件

通常情况下Linux内核会在下列情况发生调度或者打断:

  • 时钟计数到期,X86下通常是1ms
  • 调度器调用的高性能定时器到期。如果调度器发现任务即将耗尽自己的时间片,那么会在时间片耗尽时用高性能定时器来触发调度
  • 任务结束,阻塞,休眠或者挂起CPU,此时任务会被移出调度器的运行队列
  • 任务被唤醒或者插入新任务时

只有在上述事件发生时,任务才可以被抢占或者切换到运行状态,这些事件也可以打算当前运行的任务。并且,调度器也会周期性的打算运行任务,内核线程访问时钟计数器的行为大概会消耗CPU1%的性能

3.10内核以后,内核提供了一个选项,可以关闭只运行一个任务的CPU内核的时钟计数,这个宏是CONFIG_NO_HZ_FULL这个选项可以减少时钟周期性打算计算密集型或者实时任务,提升单个处理器上任务的性能

当然开启这个选项也有一些限制:

  • 在启动的cpu上是无法被禁用的,通常是CPU0
  • 只有当cpu的运行队列上只有0或者1个任务时,内核才会关闭时钟计数
  • 这个选项并不是完全关闭时钟计数,而是设置成1HZ,这样是为了维持调度器的正常工作
  • 其他细节可以见内核文档《NO_HZ: Reducing Scheduling-Clock Ticks》

调度延迟

这里讨论的调度延迟主要是指高优先级任务唤醒时和任务开始执行时的开销。对于Linux内核来说,调度延迟主要受硬件影响。现代ARM处理器上评估的最大任务切换的调度延迟在75us左右。OSADL日常检测的数据显示,这个开销在10us~100us左右,支持系统管理模式的硬件这个开销会增大到1ms。所以实时应用的开发者必须测量他们目标硬件上的调度延迟,以此来决定任务的规划策略。调度延迟带来的影响会左右开发者决定实时任务在结束一个周期任务时是使用休眠还是忙等待

休眠还是忙等待?

任务休眠时会挂起CPU,交给其他运行任务,此时当任务唤醒时,就会受到调度延迟的影响。

当然如果处理器上只有一个任务运行的话,这种延迟是可以忽略不计的。

当任务休眠的时间小于调度延迟时,忙等待的性能会比使用nanosleep()要更好。

此外,如果休眠时间较长但是又必须将唤醒的延迟压缩到最小,那么任务可以组合使用sleep和忙等待。任务可以在休眠时间的基础上,减去调度延迟的时间,然后当休眠醒来时,再调用忙等待到目标时间。在这种情况下,如果距离下一次执行的时间大于两倍的调度延迟,那么应该只执行sleep。否则,受到调度延迟的影响,在此期间其他在此休眠期间会被调度的任务,可能在再次被抢占之前什么工作也做不了。

ps:斜体子这段翻译的感觉不是很好,建议对照原文看

然而,用这种忙等待的方式强制占用CPU也不是个好办法,受系统版本,硬件驱动或者其他应用程序的影响,这种强制占用CPU的忙等待会引起不稳定或者异常。所以内核提供了实时带宽来避免这种强制占用CPU的问题

实时带宽限制

默认状态下,Linux内核会将所有实时任务归为一个组,并且限制它们的CPU使用率,这个值默认是95%,每1秒执行一次

内核参数sched_rt_period_ussched_rt_runtime_us可以配置这个实时带宽。sched_rt_period_us设置的是内核执行这种限制的周期(默认1000000us),sched_rt_runtime_us设置的是一个执行周期之内,实时任务的时间带宽上限(默认950000us)。上述参数是作用于所有CPU的。所以在默认情况下,实时任务会发现它们每1秒会被cpu挂起50us。

因此,一个硬实时系统应该关闭这个配置,将sched_rt_runtime_us设置成-1。如果实际运行时出现了不稳定或者异常情况,那么开发者应该确保实时线程会及时的释放cpu,以确保实际的cpu占用率不会超过95%

控制组和实时组调度

控制组是内核提供的一个功能,让超级管理员可以限制某一组任务的资源访问率。上面提到的[实时带宽限制](# 实时带宽限制)和[异步任务应该用普通还是实时策略](# 异步任务应该用普通还是实时策略?)就描述了这种情况,这些机制确保了实时调度组的程序不会时其他普通进程饿死。

内核软中断

软中断主要是为驱动程序的下半部设计的,不过内核也会用它来推迟一些高优先级的任务。由于设计上的原因,软中断必须和触发它的硬件中断运行在同一个CPU上,并且,软中断是不允许休眠的

内核维护了一个容纳32个软中断的数组,软中断号在编译内核时就确定好了,并且是连续的。每个软中断在注册的时候会对应一个函数入口和参数指针。虽然软中断在编译时就规划好了,但是软中断的注册却是在运行时。通常情况下会预留两个软中断号给tasklettasklet机制可以让第三方驱动在不需要单独绑定软中断号的情况下,可以运行它们的中断下半部程序。

内核触发软中断执行的情况包括:

  • 中断服务返回
  • 某些内核子系统,比如网络系统,调用了下半部操作
  • ksoftirqd线程被调度运行

当软中断被触发时,不单单是相关的处理工作会被延迟,而是所有挂起的软中断都会被触发。举个例子,如果串口硬件中断触发,那么在它返回时,可能同时会触发串口驱动程序的下半部和网络堆栈中的某些延时任务

软中断使得实时任务被内核打断的时长变得难以预估

在不可抢占的内核中,软中断的执行有单独的优先级,软中断只能被硬件中断服务程序打断。为了确保每个硬件中断都能被软中断下半部处理程序接收到,内核对每个硬件中断进行了计数。设想一下,高频的硬件中断就会使得CPU的占用率很高。为了避免这种情况,内核限制了高优先级下软中断调用次数,内核会将未执行的软中断处理任务移入内核线程ksoftirqd,这个线程每个cpu都有一个,调度策略是SCHED_OTHER,nice值是19。

而在支持抢占的内核中,处理方式是不一样的。

2.6.x内核中,CONFIG_PREEMPT_RT补丁下,会在每个CPU上,为每个软中断入口创建一个软中断线程,这些线程的调度策略是SCHED_FIFO,优先级根据不同补丁的版本会有区别。通常是1,24或者50。然而,这种线程的激增早期被认为是性能问题。从3.6版本开始,每个软中断入口被放到了软中断最近一次触发的上下文中。

因此,一个用户态的实时程序,如果触发了软中断(比如调用了网络子系统的功能),就会进入到上下文中去处理软中断,或者被其他触发了相同软中断的任务打断。

并且,当不同优先级的任务触发相同的软中断时,软中断就会被修改,防止优先级反转的发生

内核调度策略和优先级

任何用户态的实时任务,即使绑定了cpu亲和性,也会和其他内核任务共享cpu。

并且,虽然CONFIG_PREEMPT_RT保证了linux的实时性,但是毕竟Linux不是按照实时性设计的,所以设备驱动不能以实时任务的思路来写

因此,一些系统配置经常将实时内核任务保持在一个比较高的优先级,当有用户实时任务的优先级等于活大于这些实时内核任务时,他们的调度行为也是类似的。换句话说,任何优先级大于49的用户实时任务都应该确保自己占用CPU的比例在一个很小的比例(比如I/O密集型任务或者休眠比较多的任务)

然而这些建议往往只是理想状态。实际在航天领域中,使用的实时程序往往是CPU密集型并且长时间占用的任务,这些任务中有对时间要求非常高的部分,甚至它们的优先级应该高于包括内核在内的所有任务。在这种情况下,开发者可以用配置了上限的锁来提升它的优先级,确保时间敏感区域不会被抢占。

当然,任何优先级为99(最高)的时间敏感部分的任务,都应该避免或者谨慎设计循环、递归或者其他在发生错误或者中断时可能会无限执行的逻辑,因为这会导致时间敏感区的任务阻塞其他可以恢复处理的任务

将用户实时任务的优先级设置成高于软中断线程或许是“安全”的。在主线内核版本中,高负载会导致软中断经历长延时,直到CPU休眠。这种"安全"也只意味着逻辑上的安全,这种长时间的延迟时间仍然会导致数据丢失。

此外,如果应用程序依赖于软中断来完成某些功能,那一旦软中断被阻塞,那么程序就会出问题,通过以太网进行大数据块的同步传输就是这样一个例子。在这种情况下,如果数据块超过了上下文传输到NIC的限制,那么只有一部分数据会在上下文中传输,内核将会触发NET_TX_SOFTIRQ来传输剩余的数据。和其他所有的软中断一样,被触发的软中断仅限于在提升它的处理器上运行,这样一来,软中断和应用程序就会出现争夺处理器的情况。

在2.6和早期3.x内核版本中,如果应用程序的优先级高于软中断线程,那么软中断不会执行,直到应用程序阻塞、休眠或者调用了其他会触发软中断的系统调用。

事实上,如果应用程序的优先级高于软中断,那么待处理的软中断就会不断积累;相反,而过应用程序在软中断处理完成后就执行,那么增加应用程序的抖动。在上述的例子中,为了保证数据传输,可以将软中断的优先级设置成高于应用程序。

然而,早期的3.x版本的CONFIG_PREEMPT_RT补丁中,NET_TX_SOFTIRQ的优先级并不能单独设置,并且由ksoftirqd执行的软中断线程的优先级都会高于应用程序。这就使得如果系统的中断触发过频繁的话,应用程序就会受到严重的影响。

在3.6版本之后,大部分的软中断处理都是在触发软中断的线程上下文中完成,这使得平衡应用程序和NET_TX_SOFTIRQ的优先级变得更加困难,这需要程序员能够识别所有能够触发NET_TX_SOFTIRQ的线程。

此外当系统遇到大量的外部中断时,内核可能会将所有软中断从IRQ线程移入ksoftirqd任务。因此,如果应用程序为了保证NET_TX_SOFTIRQ的优先级而将优先级设置的比ksoftirqd低,那依然会遭受不可预估的延迟。应用程序需要允许ksoftirqd线程临时提高自己的优先级高于应用程序。一个较为简单的办法就是在执行同步写操作之后,让应用程序短暂的休眠一段时间。这样就能让内核去执行ksoftirqd的任务以及NET_TX_SOFTIRQ下挂起的任务

防止优先级反转

CONFIG_PREEMPT_RT补丁中替换了内核中很多锁的实现,替换后的锁实现了优先级继承以防止优先级反转。通常情况下优先级反转不会有什么问题,在用户程序中,内核提供了一个支持优先级继承的POSIX线程互斥锁作为可选项,使用这个锁可以确保应用程序中资源竞争和代码执行顺序不会受到优先级反转的影响。

CPU隔离

Linux提供了两种CPU隔离性的设置方式。

第一种是使用启动参数isocpus,这个参数为任务设置了亲和性掩码,这样调度器和负载均衡器就不会将任务分配给标识的CPU。

第二种是使用cpuset,这个机制可以将一组控制组的或者一系列进程Id绑定在某几个特定的CPU上(设置亲和性)。cpuset有副作用,最明显的就是,在某些情况下,在创建cpuset之前,cpu上已经存在的进程会一直保留在cpu上运行,这其中包括不断在触发的软中断以及预先设定的定时器.另外,boot之后设置的cpuset,在将进程从排斥的CPU迁移到亲和的CPU上时也会有一定的延迟。

当然也有优点,可以手动修改负载平衡的性能,并且对每个cpu的进程列表进行修改。

isocpuscpuset不是互斥的,一个cpuset可以包括所有被孤立的cpu。用户态的程序只能通过修改亲和性掩码或者设置cpuset来将任务绑定到独立的cpu上。不过在这种情况下,一旦一个进程被分配到一个孤立的CPU,它就不能再迁移到其他CPU了

然而这两种机制只能保证独立的CPU上没有其他应用程序,但是内核可以选择性忽视这两个机制,尤其是下列的场景,内核是一定会忽视的:时钟计数器、调度器、核间中断、全局工作队列、中断向量入口、软中断、per-CPU线程

因此,新版本的内核中,减少了一些中断,下面列举一些内核中断源:

时钟计数器和调度器

这部分在上面[调度事件](# 调度事件)章节已经详细介绍过。这里再补充一些。当计数器被限制在1hz时,调度器仍然是可以用高性能定时器来确保在某个时刻唤醒任务的。正在运行的任务阻塞、休眠、终止、唤醒都会触发调度器执行,这些中断时无法避免的。但是可以通过只为CPU分配一个SCHED_FIFO的任务来减少内核中断的产生

Per-CPU线程

在多核系统中,内核会创建系列Per-CPU线程,来负责每个CPU上的任务管理以及负载均衡。Per-CPU线程可以是实时或者是普通任务。普通策略的任务不会打断实时策略下的用户应用。

然而,很多Per-CPU线程往往具有最高实时优先级,所以强烈建议不要讲实时应用程序设置成最高优先级,因为这些最高优先级的内核任务时很关键的。

因此,每个per-CPU线程偶尔出现中断的情况是无法避免的。并且为了保证内核的稳定性,实时应用程序应该尽可能保证一个较低的CPU使用率,以确保per-CPU线程中较低优先级的线程有机会得以运行。

中断线程和软中断

单独的处理器核是否有权限处理中断是由底层硬件决定的。某些平台上,只有CPU0有权限处理中断。在现代Intel平台上,APIC可以允许内核设置指定的CPU来处理中断。其他平台可能会是两个方案的折中,例如通过固件来配置不同的中断给不同的CPU

Intel平台上,linux通过APIC来实现中断的负载均衡,这和调度器的负载均衡是不同的。只不过Linux实现中断的负载均衡也是通过设置每个中断的CPU亲和性来实现的。所以中断的负载均衡是不违背CPU隔离性的

核间中断和全局工作队列

内核使用核间中断和工作队列来管理一些任务,特别是内存,它保证了多处理器之间资源的一致性。这部分的开销无法避免,也无法配置。

是否需要为操作系统保留CPU0

对于Linux实时系统,常见的做法会避免应用程序在CPU0上运行,并且保留给操作系统。这种建议有两种原因导致,一是由于历史性的原因,很多旧的平台是由CPU0来负责处理中断的。二是有一种猜测,当CPU0运行内核时,如果应用程序也运行在CPU0上,那延迟会更高。

然而,基于CONFIG_PREEMPT_RT补丁的测试已经论证了,及时所有实时程序都运行在CPU0上,延迟方面也并没有显著的区别。当然开发者仍然可以选择将所有的中断放在CPU0上,并将实时程序绑定在其他CPU上,以最大幅度的减少这些中断。

并且,为内核保留CPU0仍然能够提供一些保障措施,防止内核一些低优先级的活动受到实时任务的影响。

所以综上所述:为内核保留CPU0仍然是个比较好的建议,除非其他核上的占用率都达到了预计的最大值。尽管如此,CPU0上的使用率也必须限制在70%以下(假设内核的占用率在30%

CPU亲和性,调度策略和调度优先级

fork()创建的子进程继承了父进程的亲和性、调度策略和优先级,并且这一点调用函数也无法干预,需要留给被创建的子进程自己去修改。Linux POSIX提供的线程操作也会继承这些特性,但是调度策略和优先级可以在调用pthread_create()时通过参数传递来修改,亲和力则仍然需要交给子线程自己去修改。

总结

Linux虽然有实时补丁,但是本质上还是一个非实时系统,无法完美满足硬实时的需求,更多的只是在性能和延迟上的权衡。当然开发者们需要了解哪些条件可能会对系统的实时性产生负面的影响,这样才能做出针对性的优化。下面就是总结出来的几点建议:

  • 使用可抢占内核,可以使用CONFIG_PREEMPT内核选项或者CONFIG_PREEMPT_RT补丁
  • 为了减少分配页时的开销,应用程序在初始化阶段应该对每个在堆内存上请求的页进行一次写操作,运行一次可以用满最大栈空间的函数,并执行依赖内核缓存区的系统调用。
  • 关闭过需分配内存
  • 时间敏感的任务使用实时调度策略。对于没有特殊权限的实时程序可以修改RLIMIT_RTPRIO来提升实时性能
  • 平衡实时任务和内核任务的优先级,包括中断和软中断
  • 关闭实时带宽限制,将sched_rt_runtime_us设置成-1
  • 实时任务在空闲的时候尽量使用休眠而不是忙等待,如果要把开销降到最低,可以使用休眠+短暂的忙等待
  • isocpuscpuset可以设置CPU亲和性
  • 在硬件平台支持的情况下,均衡配置中断负载,将中断处理的CPU和实时任务绑定的CPU尽量隔离开,或者选择性将中断关联到对应设备处理任务的处理器上
  • 禁用单个实时任务处理器上的始终计数器,减少周期性调度的中断。
  • 注意子任务继承的调度属性
  • 关注当前内核版本的软中断机制,当实时任务用到依赖软中断的设备驱动或者服务时,注意实现方式对性能的影响
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值