一、背景
之前的博客 中断上下文及抢占标志位的检查------基于调度及锁举例 里,我们讲到了一个影响系统调度和运行软中断行为和其他调度行为的关键的一个标志位数据。另外,在之前的博客 有关arm64下arch_local_irq_disable里中断优先级逻辑 和 arm64下的nmi的相关逻辑 里,我们介绍了硬中断及nmi中断相关的底层实现原理,在之前的博客 rcu_read_lock_bh及软中断处理的相关细节介绍 里,我们介绍了软中断相关的一些实现细节,在这篇博客里,我们针对内核代码里常见的一些宏,来判断当前执行上下文的状态的这些宏进行详细展开介绍,并针对普通linux和rt-linux分别进行介绍。
这些宏里最重要的一个函数是preempt_count(),我们先针对preempt_count在第二章里进行介绍,然后再在第三章里介绍其他的宏或函数。
二、preempt_count()的相关细节及x86下的优化
preempt_count()的实现与具体的架构相关,默认有一份实现,各个arch可以进行各自arch对应的重写实现,默认的这份实现定义在include/asm-generic/preempt.h里,如下图:

具体架构,如x86平台和arm64平台都有对其进行重写。要注意这个preempt.h也就只能使用一份,一旦需要重写preempt.h里的任意一个函数,就需要重新写一份完整的preempt.h里的所有函数的实现。
如果arch有自己实现的preempt.h放在了arch/**/include/asm/下的话,那么include/linux/preempt.h在include时如下图就会找到arch定义的preempt.h而不用默认的include/asm-generic/preempt.h:

preempt.h里除了定义了获取抢占标志位的preempt_count函数,还定义了是否需要重新调度的标志位,在内核代码里,有现有的设置task的need reschedule标志位的函数,定义在include/linux/sched.h里,如下图set_tsk_need_resched:

这个实现是不区分平台的,而真正在判断是否需要进行重新调度的函数,则是定义在include/asm-generic/preempt.h里的,默认的实现使用了tif_need_resched()函数:

而tif_need_resched是定义在include/linux/thread_info.h里的:

要注意,这个各个arch平台也有一个thread_info.h,在包含时是用的:
#include <asm/thread_info.h>
而各个arch定义的thread_info.h只是定义标志位枚举值,thread_info结构体定义这些信息。
而公共使用的include/linux/thread_info.h定义的是调度相关的一些static inline的常用函数实现。
回到刚提到的一个公共使用的set_tsk_need_resched函数,为什么已经有这么一个set_tsk_need_resched的函数,还需要每次如下图的在调用set_tsk_need_resched的同时还去调用set_preempt_need_resched函数呢?

上图里的这两句set_tsk_need_resched和set_preempt_need_resched已经在最新版本内核里合并成一句函数调用了:


为什么已经有这么一个set_tsk_need_resched的函数,还需要每次如下图的在调用set_tsk_need_resched的同时还去调用set_preempt_need_resched函数呢?
这是因为set_preempt_need_resched函数提供另一种途径在设置need resched的标志位,从而配合判断需要重新调度的接口should_resched,should_resched函数不仅要检查是否要重新调度还得检查当前的抢占计数情况(意思就是当前是否是禁用抢占的、是否是禁用软中断的、是否是禁用硬中断的),下图是没有实现arch自己的preempt.h时用默认的实现:

这就可以让不同的架构去一定程度上去优化这个should_resched函数,比如在arm64里就直接用一个current_thread_info里的单一变量,一并检查了抢占计数和need resched标志位:

arm64下的相关的结构体定义:

这样检查是否抢占,可以规定need_resched是0才表示需要重新调度,这样可以配合count抢占计数,达到检查是否是0,表示需要进行重新调度。
x86下也有相似的优化:

只是x86下的preempt_count的抢占计数和need_resched标志位是放在per-cpu变量里的,而不是放在current里,这是有一笔优化补丁,如下:

而为什么arm64下不进行这样的per-cpu的优化,是因为arm64下的current的指针是直接存在特殊寄存器里的,不需要做这样的一个优化。
三、in_interrupt()、irq_count()、in_task()等上下文判断的宏介绍
我们从检查范围大致地由小到大来排序介绍(下面不是严格的范围由小变大,一层层的包含关系):
常用宏里最小的一个检查范围是in_serving_softirq(),它是检查是否在处理一个软中断,无论是在ksoftirqd%u上下文、还是在rt-linux里的ktimers%u上下文、还是在进程上下文的local_bh_disable时,只要是运行软中断的注册的回调函数,就是认为是serving softirq状态。

要注意,rt-linux里的软中断的相关标志位不是存放在preempt_count里的,这是因为rt-linux里的软中断disable的状态,不像普通linux里的软中断disable状态那样禁用了抢占,rt-linux里软中断虽然被禁用,仍然还是可以被别的任务抢占的。所以,要进行软中断标记得用别的一个地方来存放,放在了current里的softirq_disable_cnt里,要注意,具体是哪些bit表示软中断禁用的次数,哪个bit表示软中断是否在serving,这些offset的情况,是和普通linux里的preempt_count里的位置是一样的。
接下来范围大一些的宏是softirq_count宏:
这个宏不仅包含了软中断是否是serving状态,还包含了软中断是否禁用状态:

要注意SOFTIRQ_MASK包含了softirq serving的bit也包含了软中断禁用的计数bits。
接下来范围大一些的宏是in_task宏:

可以看到in_task里也检查了是否在in_serving_softirq的状态,如果是in_serving_softirq的状态,那么in_task也是false,所以in_task的意思是不在执行硬中断、NMI中断、软中断的逻辑,也就是纯纯的进程上下文。但是如果仅仅是bh disable,但是不在bh disable的最后可能调用的软中断回调里,那也算是in_task状态。
接下来是irq_count,irq_count比in_task多了一个软中断disable的计数,也就是说如果task调用了bh变种的锁,它禁用了软中断,那么irq_count也是true。

而in_interrupt和irq_count是一样的:

还有一个常用的宏是preemptible,这个宏用来判断是否允许抢占,也就是上面第二章里讲到的preempt_count,但是除了preempt_count还要检查是否禁用了硬中断,禁用硬中断的常用宏是irqs_disabled,这个是arch有关的实现,要切记这个标志位在preempt_count里是没有的,preempt_count里的硬中断相关的标志位表示的是硬中断是否在serving,也就是是否在硬中断处理过程中,并没有标记上硬中断是否被关闭。当然如果硬中断在serving一般也是硬中断关闭状态,但是用irq_save的变种的锁就不会标记上preempt_count计数,而用irqs_disable的宏就可以判断。

有关preemptible相比刚才介绍过的softirq_count的对比:
