文章目录
- 1.1 背景
- 1.1.1 in_interrupt 定义
- 1.1.2 irq_count 定义
- 1.1.3 preempt_count 各域含义
- 1.1.4 ARMv8 中断处理流程回顾
1.1 背景
在 Linux 代码中经常会看到 WARN_ON(in_interrupt())
; 或者 BUG_ON(in_interrupt())
; 从名字可以看出这两句的含义是:如果当前处在 中断上下文 那么就会报 错误,并输出错误信息,那么系统时如何判断处当前运行环境是处于中断上下文的的呢?这里就需要跟踪下 in_interrupt
的函数的实现了。
1.1.1 in_interrupt 定义
in_interrupt 的定义位于preempt.h文件中
include/linux/preempt.h
/*
* Are we doing bottom half or hardware interrupt processing?
*
* in_irq() - We're in (hard) IRQ context
* in_softirq() - We have BH disabled, or are processing softirqs
* in_interrupt() - We're in NMI,IRQ,SoftIRQ context or have BH disabled
* in_serving_softirq() - We're in softirq context
* in_nmi() - We're in NMI context
* in_task() - We're in task context
*
* Note: due to the BH disabled confusion: in_softirq(),in_interrupt() really
* should not be used in new code.
*/
#define in_irq() (hardirq_count())
#define in_softirq() (softirq_count())
#define in_interrupt() (irq_count())
#define in_serving_softirq() (softirq_count() & SOFTIRQ_OFFSET)
#define in_nmi() (preempt_count() & NMI_MASK)
#define in_task() (!(preempt_count() & \
(NMI_MASK | HARDIRQ_MASK | SOFTIRQ_OFFSET)))
注释可以看到 in_interrupt()
的含义是:判断当前进程是否处于当前处于 NMI(不可屏蔽中断) ,IRQ 中断 , 软中断 或者 软中断被屏蔽 的环境中。
线程被硬件中断打断后,如果希望执行完hardirq后就直接返回原来的线程,而不去执行pending的softirq,那么可以选择“屏蔽”softirq。
由于 in_interrupt 时 irq_count的宏定义, 接下还需要继续跟踪 irq_count
的实现。
1.1.2 irq_count 定义
include/linux/preempt.h
#define hardirq_count() (preempt_count() & HARDIRQ_MASK)
#define softirq_count() (preempt_count() & SOFTIRQ_MASK)
#define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK \
| NMI_MASK))
從上面的代码中可以看到 irq_count
的值是 preempt_count()
“与” 硬中断,软中断,不可屏蔽中断的域获得到的。那么preempt_count 是如何实现的呢?
preempt_count 被定义在进程 struct task_struct
的 thread_info
域中 ,也就是线程描述符中,在内核中可以通过 current_thread_info()
接口来获取进程的 thread_info
:
/*
* low level task data that entry.S needs immediate access to.
*/
struct thread_info {
unsigned long flags; /* low level flags */
mm_segment_t addr_limit; /* address limit */
#ifdef CONFIG_ARM64_SW_TTBR0_PAN
u64 ttbr0; /* saved TTBR0_EL1 */
#endif
int preempt_count; /* 0 => preemptable, <0 => bug */
};
include/asm-generic/preempt.h
static __always_inline int preempt_count(void)
{
return READ_ONCE(current_thread_info()->preempt_count);
}
從上面 struct thread_info
结构体定义可以看到 preempt_count 是一个 32
位变量,虽然只是一个32位变量,但由于其和中断、调度/抢占密切相关,因此该变量的作用还是十分重要的。
在 arm 中,preempt_count 是 per task 的变量
接下里继续跟踪 preempt_count
的域构成及对应域的含义。
1.1.3 preempt_count 各域含义
Linux 一共把 32bits 的 preempt_count 分成了 3个域,也就是代码中对应的:
- HARDIRQ_MASK;
- SOFTIRQ_MASK;
- PREEMPT_MASK。
接下来看看各个域的作用:
HARDIRQ 域:
preempt_count 中的第 16
到 19
个 bit 表示 hardirq count
,它记录了进入hardirq/top half 的嵌套次数,此时 hardirq count的值会加 1。如果 hardirq count 的值为正数,说明现在正处于hardirq上下文中。
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。
PREEMPT 域:
在中断上下文中,调度是关闭的,不会发生进程的切换,这属于一种隐式的禁止调度,而在代码中,也可以使用 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()
宏给出判断。
1.1.4 ARMv8 中断处理流程回顾
最后我们再回顾下 ARMv8 中断处理流程,如下:
gic_handle_irq(struct pt_regs *regs)
-->handle_domain_irq(struct irq_domain *domain, unsigned int hwirq, struct pt_regs *regs)
--> __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq
-->irq_enter(); ------------------------------>(1)
-->generic_handle_irq(irq);
-->irq = irq_find_mapping(domain, hwirq); //根据硬件中断号找到对应的软件中断号
-->struct irq_desc *desc = irq_to_desc(irq); //根据i rq 号找到对应的struct irq_desc
--> generic_handle_irq_desc(desc); //进入具体中断处理
-->irq_exit();--------------------------------->(2)
(1) irq_enter
显式告诉 Linux 内核现在要进入中断上下文了;
(2) 在处理完中断后调用 irq_exit
告诉 Linux 已经完成中断处理过程。
接下来看下 irq_enter
的实现:
linux/kernel/softirq.c
/*
* Enter an interrupt context.
*/
void irq_enter(void)
{
rcu_irq_enter();
if (is_idle_task(current) && !in_interrupt()) {
/*
* Prevent raise_softirq from needlessly waking up ksoftirqd
* here, as softirq will be serviced on return from interrupt.
*/
local_bh_disable();------->(1)
tick_irq_enter();
_local_bh_enable();------->(2)
}
__irq_enter();
}
(1) 关闭软中断并将 preempt_count 加1;
(2) 打开软中断并将 preempt_count 减 1。
linux/include/linux/hardirq.h
/*
* It is safe to do non-atomic ops on ->hardirq_context,
* because NMI handlers may not preempt and the ops are
* always balanced, so the interrupted value of ->hardirq_context
* will always be restored.
*/
#define __irq_enter() \
do { \
account_irq_enter_time(current); \
preempt_count_add(HARDIRQ_OFFSET); \ ---->(1)
trace_hardirq_enter(); \
} while (0)
(1) 在硬中断环境中将 preempt_count 加1;
推荐阅读:
https://zhuanlan.zhihu.com/p/88883239
https://zhuanlan.zhihu.com/p/80680484