在面试的时候我们常常被问及一个问题:几种中断下半部机制 softirq、tasklet、workqueue 有什么区别?Linux 为什么要设计这几种机制?真正能够回答清楚的人还是少数的。下面我们就详细分析一下这其中的区别。本文的代码分析基于 Linux kernel 3.18.22 和 arm64 架构,最好的学习方法还是 “RTFSC”
1. Linux 中断
arm64 和其他所有 CPU 架构的中断处理流程都是一样:正常执行流程被打断进入中断服务程序,保护现场、处理中断、恢复现场:
在整个中断处理过程中,arm64 的 CPU 全局中断是自动 disable 的 (PSTATE 寄存器中的 interrupt bit 被 masks)。如果用户想支持 interrupt nested,需要自己在中断服务程序中使能中断。Linux 现在是不使用中断嵌套的。
1.1 CPU 中断打开 / 关闭
arm64 关闭和打开本地 CPU 的全局中断的方法,是操作 SPSR(Saved Process Status Register) 寄存器 IRQ mask bit。
Linux 中 arm64 关闭和打开本地 CPU 中断的函数实现。arch/arm64/include/asm/irqflags.h:
local_irq_disable() -> raw_local_irq_disable() -> arch_local_irq_disable()
local_irq_enable() -> raw_local_irq_enable() -> arch_local_irq_enable()
static inline void arch_local_irq_enable(void)
{
asm volatile(
// (1) 清除 DAIF 中的 bit2 I 标志位,打开中断"msrdaifclr, #2// arch_local_irq_enable"
:
:
: "memory");
}
static inline void arch_local_irq_disable(void)
{
asm volatile(
// (2) 设置 DAIF 中的 bit2 I 标志位,关闭中断"msrdaifset, #2// arch_local_irq_disable"
:
:
: "memory");
}
static inline unsigned long arch_local_irq_save(void)
{
unsigned long flags;
asm volatile(
// (3) 备份 DAIF 标志"mrs%0, daif// arch_local_irq_save\n"
"msrdaifset, #2"
: "=r" (flags)
:
: "memory");
return flags;
}
static inline unsigned long arch_local_save_flags(void)
{
unsigned long flags;
asm volatile(
// (4) 恢复 DAIF 标志"mrs%0, daif// arch_local_save_flags"
: "=r" (flags)
:
: "memory");
return flags;
}
1.2 中断控制器 GIC
上面描述了 CPU 对全局中断的处理,但是还有一个工作需要有人去做:就是把外部中断、内部中断、CPU 间中断等各种中断按照优先级、亲和力、私有性等发送给多个 CPU。负责这个工作的就是中断控制器 GIC(Generic Interrupt Controller)。
从软件角度上看,GIC 可以分成两个功能模块:Distributor。负责连接系统中所有的中断源,通过寄存器可以独立的配置每个中断的属性:priority、state、security、outing information、enable status。定义哪些中断可以转发到 CPU core。
CPU Interface。CPU core 用来接收中断,寄存器主要提供的功能:mask、 identify 、control states of interrupts forwarded to that core。每个 CPU core 拥有自己的 CPU interface。
对 GIC 来说,中断可以分成以下几种类型:SGI(Software Generated Interrupt),Interrupt IDs 0-15。系统一般用其来实现 IPI 中断。
PPI(Private Peripheral Interrupt),Interrupt IDs16-31。私有中断,这种中断对每个 CPU 都是独立一份的,比如 per-core timer 中断。
SPI(Shared Peripheral Interrupt),Interrupt numbers 32-1020。最常用的外设中断,中断可以发给一个或者多个 CPU。
LPI(Locality-specific Peripheral Interrupt)。基于 message 的中断,GICv2 和 GICv1 中不支持。
GIC 从原理上理解并不难,但是如果涉及到级联等技术细节,整个初始化过程还是比较复杂。大家可以自行下载 GIC 手册:GIC-400、GIC-500 学习,GIC 代码分析 也是一篇很不错的分析文章。
一款 GIC 相关的操作函数都会集中到 irq_chip 数据结构中,以 GIC-400 为例,它的相关操作函数如下:drivers/irqchip/irq-gic.c:
static struct irq_chip gic_chip = {
.name= "GIC",
.irq_mask= gic_mask_irq,
.irq_unmask= gic_unmask_irq,
.irq_eoi= gic_eoi_irq,
.irq_set_type= gic_set_type,
.irq_retrigger= gic_retrigger,
#ifdef CONFIG_SMP.irq_set_affinity= gic_set_affinity,
#endif.irq_set_wake= gic_set_wake,
};
1.3 Linux 中断处理流程
从代码上看 Linux 中断的处理流程大概是这样的:
从处理流程上看,对于 gic 的每个中断源,Linux 系统分配一个 irq_desc 数据结构与之对应。irq_desc 结构中有两个中断处理函数 desc->handle_irq() 和 desc->action->handler(),这两个函数代表中断处理的两个层级:desc->handle_irq()。第一层次的中断处理函数,这个是系统在初始化时根据中断源的特征统一分配的,不同类型的中断源的 gic 操作是不一样的,把这些通用 gic 操作提取出来就是第一层次的操作函数。具体实现包括: handle_fasteoi_irq()
handle_simple_irq()
handle_edge_irq()
handle_level_irq()
handle_percpu_irq()
handle_percpu_devid_irq()
desc->action->handler() 第二层次的中断处理函数,由用户注册实现具体设备的驱动服务程序,都是和 GIC 操作无关的代码。同时一个中断源可以多个设备共享,所以一个 desc 可以挂载多个 action,由链表结构组织起来。
1.4 中断服务注册
从上一节的中断二层结构中可以看到第二层的中断处理函数 desc->action->handler 是由用户来注册的,下面我们来分析具体注册过程:kernel/irq/manage.c:
request_irq() -> request_threaded_irq() -> __setup_irq()
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
| →
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)
{
struct irqaction *action;
struct irq_desc *desc;
int retval;
/*
* Sanity-check: shared interrupts must pass in a real dev-ID,
* otherwise we'll have trouble later trying to figure out
* which interrupt is which (messes up the interrupt freeing
* logic etc).
*/
if ((irqflags & IRQF_SHARED) && !dev_id)
return -EINVAL;
// (1) 根据中断号找到对应的 desc 结构desc = irq_to_desc(irq);
if (!desc)
return -EINVAL;
if (!irq_settings_can_request(desc) ||
WARN_ON(irq_settings_is_per_cpu_devid(desc)))
return -EINVAL;
// (2) 如果 action->handler 为空,那么用户是想创建一个线程化中断// 将线程化中断的 action->handler 初始化为 irq_default_primary_handler()// irq_default_primary_handler() 非常简单,只是返回一个 IRQ_WAKE_THREAD 值if (!handler) {
if (!thread_fn)
return -EINVAL;
handler = irq_default_primary_handler;
}
// (3) 分配新的 action 数据结构action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
chip_bus_lock(desc);
// (4) 将新的 action 结构安装到 desc 中retval = __setup_irq(irq, desc, action);
chip_bus_sync_unlock(desc);
if (retval)
kfree(action);
#ifdef CONFIG_DEBUG_SHIRQ_FIXMEif (!retval && (irqflags & IRQF_SHARED)) {
/*
* It's a shared IRQ -- the driver ought to be prepared for it
* to happen immediately, so let's make sure....
* We disable the irq to m