Kernel Preempt
Linux 内核提供了多种内核抢占模式,用于在吞吐和延迟之间 tradeoff。
内核抢占模型的一个重要概念是抢占点(preemption points),它指的是代码中发生内核抢占的位置(更直观粗糙一些的说就是调用 __schedule()
的位置),一共有以下几类:
- 常规:硬件中断返回内核处、内核返回用户空间处
- 启发式:调用
cond_sched()
或者might_sleep()
处,这两个函数类似于yield()
- 条件变化:开启抢占、释放自旋锁、开软中断
并不是每个抢占点都真的会发生抢占,这大概取决于几种因素:
- 当前内核是否是抢占内核
- 当前有无高特权级的 RT Task
- 当前的进程是否可被抢占,这取决于这些进程在是不是持有一些锁、是不是有一些原子性的语义,有没有特殊的事件在阻止抢占内核用
preempt_counter
变量来描述是否满足抢占条件(基本包含后 2 点),当preempt_counter == 0
的时候,就是可以抢占的了(但是抢占后可能并不满足效果,比如被抢占的进程拿着锁,抢占进程拿不到锁,依然只能死等)。
Linux Kenerl 本身的抢占模型分为 4 种:
PREEMPT_NONE
:并不抢占,一个内核线程只会在时间片耗尽、发生阻塞和调用cond_sched
时自愿放弃 CPUPREEMPT_VOLUNTARY
:和PREEMPT_NOE
一样,只是有更多的might_sleep
来自愿放弃 CPUPREEMPT_FULL
:基本上在任何会导致preempt_counter
变化的时候都会进行抢占,只要preempt_counter == 0
就立刻抢占。PREEMPT_RT
:类似于PREEMPT_FULL
,为 RT workload 设计,所以条件判断会松一些。
每次内核在 reboot 后只能是其中的一种模型,并不能动态调整。这就导致对于非抢占内核(NONE,VOLUNTARY),它面对那种会耗尽整个时间片的内核任务,就只能使用 conds_ched
来自愿放弃 CPU 来提高系统的实时性。
这种“启发式的自愿放弃法”会导致非抢占的内核中弥散着各种 cond_sched
函数,这些函数的维护性非常差,因为我们需要解释为什么在这个点可以自愿放弃 CPU,当这个点附近的代码发生变化的时候,这个启发式的抢占点的位置也需要调整。而且这种启发式的方法,可能导致有一些可能潜在的自愿放弃点没有被发现。