转载自https://zhuanlan.zhihu.com/p/88883239 作者:兰新宇
前面介绍中断的文章中,多次提到了一个叫做"preempt_count"的概念,由于无法用简单的几句话解释清楚,所以放到本文单独地进行介绍。
preempt_count本质上是一个per-CPU的32位变量,它在各种处理器架构下的存放位置和命名不尽相同,但其值都可以使用preempt_count()函数统一获取。preempt_count逻辑相关的核心代码位于include/linux/preempt.h,虽然只是一个32位变量,但由于其和中断、调度/抢占密切相关,因此在系统中发挥的作用不容小觑。
来看下preempt_count是怎样构成的:
hardirq相关
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"。
#define hardirq_count() (preempt_count() & HARDIRQ_MASK)
#define in_irq() (hardirq_count())
hardirq count占据4个bits,理论上可以表示16层嵌套,但现在Linux系统并不支持hardirq的嵌套执行,所以实际使用的只有1个bit。
之所以采用4个bits,一是历史原因,因为早期Linux并不是将中断处理的过程分为top half和bottom half,而是将中断分为fast interrupt handler和slow interrupt handler,而slow interrupt handler是可以嵌套执行的,二是某些 driver 代码可能在top half中重新使能hardirq。
softirq相关
preempt_count中的第8到15个bit表示softirq count,它记录了进入softirq的嵌套次数,如果softirq count的值为正数,说明现在正处于softirq上下文中。由于softirq在单个CPU上是不会嵌套执行的,因此和hardirq count一样,实际只需要一个bit(bit 8)就可以了。但这里多出的7个bits并不是因为历史原因多出来的,而是另有他用。
这个"他用"就是表示在进程上下文中,为了防止进程被softirq所抢占,关闭/禁止softirq的次数,比如每使用一次local_bh_disable(),softirq count高7个bits(bit 9到bit 15)的值就会加1,使用local_bh_enable()则会让softirq count高7个bits的的值减1。
代码中可借助in_softirq()宏快速判断当前是否在softirq上下文:
#define softirq_count() (preempt_count() & SOFTIRQ_MASK)
#define in_softirq() (softirq_count())
这篇文章曾提到:进入softirq是在softirq上下文,关闭softirq抢占也是在softirq上下文,但还是有办法区分的。办法就是使用in_serving_softirq()宏来确切地表示现在是在处理softirq。
#define SOFTIRQ_OFFSET (1UL << 8)
#define in_serving_softirq() (softirq_count() & SOFTIRQ_OFFSET)
上下文
不管是hardirq上下文还是softirq上下文,都属于我们俗称的中断上下文(interrupt context)。
为此,有一个名为in_interrupt()的宏专门用来判断当前是否在中断上下文中。
#define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK))
#define in_interrupt() (irq_count())
与中断上下文相对应的就是俗称的进程上下文(process context)
#define in_task() (!(preempt_count() & (HARDIRQ_MASK | SOFTIRQ_OFFSET | NMI_MASK)))
需要注意的是,并不是只有进程才会处在process context,内核线程依然可以运行在process context。
在中断上下文中,调度是关闭的,不会发生进程的切换,这属于一种隐式的禁止调度,而在代码中,也可以使用preempt_disable()来显示地关闭调度,关闭次数由第0到7个bits组成的preemption count(注意不是preempt count)来记录。每使用一次preempt_disable(),preemption count的值就会加1,使用preempt_enable()则会让preemption count的值减1。preemption count占8个bits,因此一共可以表示最多256层调度关闭的嵌套。
处于中断上下文,或者显示地禁止了调度,preempt_count()的值都不为0,都不允许睡眠/调度的发生,这两种场景被统称为atomic上下文,可由in_atomic()宏给出判断。
#define in_atomic() (preempt_count() != 0)
中断上下文、进程上下文和atomic上下文的关系大概可以表示成这样:
评论