浅入浅出linux中断子系统,如需深入,直接跳转重要参考章节。
什么是中断?
当CPU被某些信号触发,CPU暂停当前工作,转而处理信号的事件,简单的称它为中断,这个信号可以是系统外设的信号,也可能是芯片内部的一些信号。CPU支持的中断很多,如果每一个中断都直连CPU,那么又有点不现实,所以现在的更多是将中断信号连接到一个中断控制器,然后再由它使用一根中断线触发CPU中断,CPU响应中断后,再根据寄存器信息判断是哪一个中断信号。此处不展开中断的硬件逻辑介绍,下面以ARM架构为例介绍linux的中断子系统。
ARM有两根低电平触发的中断线:IRQ和FIQ,对linux而言,并没有使用FAQ,只是使用了IRQ一根中断线并处理中断请求。
IRQ(Interrupt Request):指中断模式;
FIQ(Fast Interrupt Request):指快速中断模式;
IRQ与FIQ是ARM处理器的两种不同编程模式.
IRQ和FIQ的区别:
1. 对FIQ你必须进快处理中断请求,并离开这个模式;
2. IRQ可以被FIQ所中断,但FIQ不能被IRQ所中断,在处理FIQ时必须要关闭中断;
3. FIQ的优先级比IRQ高;
4. FIQ模式下,比IRQ模式多了几个独立的寄存器,这就导致可能FIQ中断处理程序不需要通用寄存器压栈;
5. FIQ的中断向量地址在0x0000001C,而IRQ的在0x00000018(也有的在FFFF001C以及FFFF0018),所以FIQ中断处理程序可以一直往下跑而不用跳转;
6. IRQ和FIQ的响应延迟有区别(?????好像中断都是完成一个总线周期才会响应的?????????);
对于 arm 处理器,提供了专属的 arm 中断控制器,即 GIC(Generic Interrupt Controller)。GIC 的硬件设计也分成相应的两部分:
- GIC distributor
- GIC CPU interface
distributor 翻译过来就是分发器,负责将中断源传递过来的中断进行分发,而 CPU interface 不言而喻,则是针对 CPU 的配置接口,在多核架构中,每个 CPU 对应一个 CPU interface,负责将 distributor 传递过来的中断传递给 CPU,同时和 CPU 进行系列的交互。因此,在 GIC 中,一个外部中断向上传递的流程为:中断产生源 -> GIC distributor -> GIC CPU interface -> CPU。
distributor
GIC 中的 distributor 是属于所有 CPU 共享的,主要控制中断的收集与分发,其具体实现的接口为:
- 全局地控制是否将中断源传递到 CPU interface.
- 控制单个中断线是否使能,如果不使能,对应中断线上产生的中断自然不会传递到 CPU interface.
- 设置每个中断的优先级,当出现中断源的并发时,向 CPU interface 传递优先级更高的中断
- 设置所有中断源传递的目标 CPU,可以指定某些中断只传递到特定的 CPU interface.
- 设置中断触发模式,是电平触发还是边沿触发
- 为每个中断配置为 Group0 或者 Group1,通常只有在实现了 secure 模式的处理器上才会区分 Group0 或者 Group1
- 传递 SGI 到特定的 CPU 核,可以是单个,也可以是多个.
- 软件可以直接设置和清除外部中断的 pending bit,也就是软件也可以触发外部中断.
CPU interface
对于每个连接到 GIC 的 CPU 核,都会存在一个对应的 CPU interface,它主要提供以下的接口:
- 是否将中断信号发送给 CPU 中断引脚的 enable 控制。
- ack 一个中断信号
- 标识某个中断处理的完成
- 设置中断的优先级
- 确定最高优先级的中断并将其传递给 CPU 核
- 定义中断抢占的策略
在中断发生时,distributor 根据 target CPU mask 的设置将中断发送给指定的 CPU interface,此时的 CPU interface 并不会直接将中断传递给 CPU,一方面,该中断号需要被使能,再者,该中断源需要具有足够的优先级。当 CPU 并没有在处理中断时,这时候优先级自然是最低的,一旦 CPU 正在处理中断时,优先级就变成了正在处理中断的优先级,如果该优先级比正在处理的优先级高,那么就可以根据中断的抢占策略决定是否让当前中断抢占之前的中断。
在 linux 中,没有使用 FIQ 中断信号,并不支持中断的抢占,因此,中断的执行都是顺序的,或者说即使在 CPU 执行中断的过程中 gic 重新发送了更高优先级的中断信号,CPU 也并不会处理,因为 linux 在中断处理中屏蔽了中断。
**bank寄存器概念:**指一个地址对应多个寄存器的副本,不同CPU访问的结果是不同的,是各自的寄存器。
**EOI:**当 CPU 中断处理完之后,需要将中断处理完的消息通知 GIC,这个消息通知包含两个部分:降低 CPU interface 的上报优先级、deactivate 处理完的中断,切换中断的状态。这两个过程可配置为统一操作和分开操作的模式,由寄存器 GICC_CTLR 的 EOI 配置位进行配置,在 GIC 中被称为 EOI(end of interrupt) mode,当 EOI mode 被使能时,写寄存器 GICC_EOIR 将会触发中断的 priority drop,而 deactivate 操作需要通过写 GICC_DIR 寄存器实现,当 EOI mode 被 disable 时,直接写 GICC_EOIR 将会同时触发 priority drop 和中断的 deactivate。所以使能EOI需要两步操作,不使能则一步操作。
代码流程
在代码中,主要是gic的初始化和配置,将一些中断的配置信息和不同中断号的处理函数注册好,下面一起来跟代码,代码是linux4.9内核,arm64架构。
从设备端的dts可以看到以下信息:
gic: interrupt-controller@03020000 {
compatible = "arm,cortex-a15-gic", "arm,cortex-a9-gic";
#interrupt-cells = <3>;
#address-cells = <0>;
device_type = "gic";
interrupt-controller;
reg = <0x0 0x03021000 0 0x1000>,
<0x0 0x03022000 0 0x2000>,
<0x0 0x03024000 0 0x2000>,
<0x0 0x03026000 0 0x2000>;
interrupts = <GIC_PPI 9 0xf04>;
interrupt-parent = <&gic>;
};
实际上,这个配置就是root gic,通过在内核搜索,可以知道是drivers/irqchip/irq-gic.c 匹配上这个dts配置。
int __init
gic_of_init(struct device_node *node, struct device_node *parent)
{
struct gic_chip_data *gic;
int irq, ret;
if (WARN_ON(!node))
return -ENODEV;
if (WARN_ON(gic_cnt >= CONFIG_ARM_GIC_MAX_NR))
return -EINVAL;
gic = &gic_data[gic_cnt];
ret = gic_of_setup(gic, node);
if (ret)
return ret;
if (gic_cnt == 0 && !gic_check_eoimode(node, &gic->raw_cpu_base))
static_key_slow_dec(&supports_deactivate);
ret = __gic_init_bases(gic, -1, &node->fwnode);
if (ret) {
gic_teardown(gic);
return ret;
}
if (!gic_cnt) {
gic_init_physaddr(node);
gic_of_setup_kvm_info(node);
}
if (parent) {
irq = irq_of_parse_and_map(node, 0);
gic_cascade_irq(gic_cnt, irq);
}
if (IS_ENABLED(CONFIG_ARM_GIC_V2M))
gicv2m_init(&node->fwnode, gic_data[gic_cnt].domain);
gic_cnt++;
return 0;
}
IRQCHIP_DECLARE(cortex_a15_gic, "arm,cortex-a15-gic", gic_of_init);
重要的数据结构
重要的数据结构:gic_chip_data 和 irq_desc。
struct gic_chip_data {
struct irq_chip chip;
union gic_base dist_base;
union gic_base cpu_base;
void __iomem *raw_dist_base;
void __iomem *raw_cpu_base;
u32 percpu_offset;
struct irq_domain *domain;
unsigned int gic_irqs;
...
};
static struct irq_chip gic_chip = {
.irq_mask = gic_mask_irq,
.irq_unmask = gic_unmask_irq,
.irq_eoi = gic_eoi_irq,
.irq_set_type = gic_set_type,
.irq_get_irqchip_state = gic_irq_get_irqchip_state,
.irq_set_irqchip_state = gic_irq_set_irqchip_state,
.flags = IRQCHIP_SET_TYPE_MASKED |
IRQCHIP_SKIP_SET_WAKE |
IRQCHIP_MASK_ON_SUSPEND,
};
对于日常驱动,经常接触到的是irq_desc,描述单个irq信息:
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq;
struct irqaction *action;
....
wait_queue_head_t wait_for_threads;
...
} ____cacheline_internodealigned_in_smp;
__gic_init_bases
在__gic_init_bases中,主要针对root gic和非root git处理。针对 root gic,中断输出引脚直接连接到 CPU 的 IRQ line,和 secondary gic 的区别在于 root gic 需要处理 SGI 和 PPI,而 secondary git 不需要,同时,在多核环境中,还需要配置相应的 CPU interface:
static int __init __gic_init_bases(struct gic_chip_data *gic,
int irq_start,
struct fwnode_handle *handle)
{
...
#ifdef CONFIG_SMP
set_smp_cross_call(gic_raise_softirq);
#endif
cpuhp_setup_state_nocalls(CPUHP_AP_IRQ_GIC_STARTING,
"AP_IRQ_GIC_STARTING",
gic_starting_cpu, NULL);
set_handle_irq(gic_handle_irq);
}
if (static_key_true(&supports_deactivate) && gic == &gic_data[0]) {
name = kasprintf(GFP_KERNEL, "GICv2");
gic_init_chip(gic, NULL, name, true);
} else {
name = kasprintf(GFP_KERNEL, "GIC-%d", (int)(gic-&gic_data[0]));
gic_init_chip(gic, NULL, name, false);
}
ret = gic_init_bases(gic, irq_start, handle);
if (ret)
kfree(name);
return ret;
}
gic_init_bases
gic_init_bases函数主要完成下面的几个操作:
- 不带bank寄存器的GIC特殊处理
- irq domain与irq映射关系的建立
- gic寄存器的初始化配置
对于不带 bank 寄存器的 GIC 实现,GIC 中有部分寄存器是和 CPU 相关的,比如大部分 CPU interface 相关的寄存器,多个 CPU 核自然不能使用同一套寄存器,需要在内核中为每个 CPU 分配对应的存储空间,因此需要使用到 percpu 相关的数据结构和变量,gic 相关数据结构中的 percpu 变量大多都是和这种情况相关的。
比如 struct gic_chip_data类型的 gic_data 数组中的每个成员,不带 bank 寄存器的 GIC 驱动中使用 gic->dist_base.percpu_base 记录 distributor 的基地址,否则使用 gic->dist_base.common_base。
大多数的 GIC 实现都支持 bank 寄存器,因此说它是正常的 GIC 实现。
初始化则主要是gic_dist_init和gic_cpu_init函数完成。这两个函数分别针对 GIC 的 distributor 和 CPU 相关的初始化函数,这两个函数对应的设置项为:
- 全局地使能中断从 distributor 到 CPU interface 的传递
- 为每个中断设置传递的目标 CPU mask,通常就是一个特定的 CPU
- 为每个中断设置默认的触发方式,默认为低电平触发(具体平台取决于 GIC 的驱动实现)
- 为每个中断设置优先级
- 初始化复位所有的中断线
- 为每个 CPU interface 记录对应的 CPU mask,当然,这个 mask 只对应一个 CPU
- 设置 CPU interface 的中断屏蔽 threshold
- 其它的 CPU interface 的一些初始化工作
配置完成之后,GIC就开始工作。
irq domain与hwirq
中断域,负责gic中hwirq和逻辑irq的映射。hwirq即hardware irq,git硬件上的irq id,查看相应平台的gic介绍就有:
- 0~15 对应 SGI 中断,该中断是 percpu 的
- 16~31 对应 PPI 中断,该中断是 percpu 的
- 32~1020 对应 SPI 中断,即共享中断,是所有 CPU 共享的,这些中断会被分发到哪个 CPU 根据配置决定
逻辑irq
当某个中断源产生中断时,我只要能够根据中断号的映射找到该中断对应的中断资源就好了,这里的中断资源包括中断回调函数/中断执行对应的参数等。
但问题是,对于级联的 gic,不同中断对应的 hwirq 可能是相同的,因此,需要对硬件上的中断号做一层映射,也就是软件上维护一个全局且唯一的逻辑 irq 映射表,每一个 GIC 的每一个 ID 都有一个对应的唯一的逻辑 irq,然后大可以通过唯一的逻辑 irq 来匹配对应的中断资源。于是一个完整的映射表就完成了: gic的 hwirq -> 逻辑 irq -> 对应的中断 resource。
gic_init_bases
static int gic_init_bases(struct gic_chip_data *gic, int irq_start,
struct fwnode_handle *handle)
{
...
gic_irqs = readl_relaxed(gic_data_dist_base(gic) + GIC_DIST_CTR) & 0x1f;
gic_irqs = (gic_irqs + 1) * 32;
if (gic_irqs > 1020)
gic_irqs = 1020;
gic->gic_irqs = gic_irqs;
if (handle) {
gic->domain = irq_domain_create_linear(handle, gic_irqs,
&gic_irq_domain_hierarchy_ops,
gic);
} else {
if (gic == &gic_data[0] && (irq_start & 31) > 0) {
hwirq_base = 16;
if (irq_start != -1)
irq_start = (irq_start & ~31) + 16;
} else {
hwirq_base = 32;
}
gic_irqs -= hwirq_base;
irq_base = irq_alloc_descs(irq_start, 16, gic_irqs,
numa_node_id());
if (irq_base < 0) {
WARN(1, "Cannot allocate irq_descs @ IRQ%d, assuming pre-allocated\n",
irq_start);
irq_base = irq_start;
}
gic->domain = irq_domain_add_legacy(NULL, gic_irqs, irq_base,
hwirq_base, &gic_irq_domain_ops, gic);
}
...
}
gic_of_setup_kvm_info
通过上面gic_init_bases,看到只是完成了irq domain的创建,但是hwirq和逻辑irq的映射还没有完成,实际上是gic_of_setup_kvm_info完成相应的映射建立。
gic_of_setup_kvm_info通过调用irq_of_parse_and_map—>irq_create_fwspec_mapping,实现的逻辑如下:
- 通过irq_find_matching_fwspec调用到 irq_domain_create_linear 函数传递进来的ops->match找到相应的irq domain;
- irq_domain_translate获取irq相关的硬件描述信息;
- irq_domain_alloc_irqs通过irq_domain_alloc_irqs_recursive调用到irq domain alloc(gic_irq_domain_alloc)完成映射;
- 申请irq_data等资源;
实际上映射之前,都是先通过gic_irq_domain_translate获取硬件信息,了解hwirq的起始是多少,一般在gic驱动中,会忽略前面16个SGI信号,如果dts配置了interrupts字段,它的第0个字段信息则是代表需要跳过的,我们一般配置为GIC_PPI(实际为1)所以将会忽略PPI信号(+16),所以我们的hwirq将会忽略前面的32个信号。*irq dts的解析在of_irq_parse_one也有进行。*而映射则是根据逻辑irq获取各种irq资源并完成相应的赋值。逻辑irq是由hwirq决定的,从hwirq的开始查找一个空闲的逻辑irq并返回(BITMAP)。
static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq,
irq_hw_number_t hw)
{
struct gic_chip_data *gic = d->host_data;
if (hw < 32) {
irq_set_percpu_devid(irq);
irq_domain_set_info(d, irq, hw, &gic->chip, d->host_data,
handle_percpu_devid_irq, NULL, NULL);
irq_set_status_flags(irq, IRQ_NOAUTOEN);
} else {
irq_domain_set_info(d, irq, hw, &gic->chip, d->host_data,
handle_fasteoi_irq, NULL, NULL);
irq_set_probe(irq);
}
return 0;
}
start_kernel
通过跟踪代码发现,在start_kernel的时候,会调用到early_irq_init和init_IRQ函数。在early_irq_init中,将会创建irq_desc结构体,并将其加入到静态全局红黑树链表irq_desc_tree。接着在init_IRQ中,将会调用irqchip_init函数,在该函数中,通过of_irq_init()触发调用上面说的gic_of_init( desc->irq_init_cb函数指针调用)等。
回到gic_of_init,如果不是root gic,将会特殊处理,在gic_cascade_irq中,将设置某irq的中断处理函数中,查看第二级gic对应的中断配置。
上面只是一个gic的初始化,但是实际中断是怎么处理的,还不是很清晰,下面继续看看驱动是怎么申请中断,中断来了,CPU又是怎么处理的。
request_irq
日常内核驱动中,外设使用的irq都是在dts中配置,驱动通过dts获取irq之后,再通过request_irq()函数进行申请,最终来到request_threaded_irq。request_threaded_irq实质完成下面的几个逻辑:
- 通过irq_to_desc,在全局静态链表irq_desc_tree中查找到irq_desc;
- 创建一个irqaction,并完成中断处理函数等信息的初始化;
- 通过__setup_irq函数完成irq初始化;
__setup_irq逻辑:
- 检查该中断是否需要创建中断线程,如果需要,将创建相应的内核线程,所以有时候ps可以一些
irq/%d-%s 名称的内核线程,就是这里创建的; - 如果是共享中断,而且之前已经有其他设备已经申请了,那么就是将irqaction添加到irq_desc中;
- 如果是第一次申请的,则调用到irq_chip的irq_request_resources函数申请资源,设置触发方式(电平/边缘),CPU亲和度,使能中断等;
FAQ
为什么我们在写外设驱动的时候,从芯片手册得到中断号,还需要-32才填入dts呢?
上面在 gic_of_setup_kvm_info 小章节这里介绍到,gic驱动会忽略前面16个SGI信号,同时dts中的gic: interrupt-controller节点配置了interrupts的参数0,参数0则表示irq domain在跳过SGI的基础上再跳过interrupts[0] * 16的中断,而dts中的interrupts参数0一般配置为GIC_PPI,GIC_PPI在ARM平台定义为1,则是跳过前面的16个SGI和16个PPI中断,所以dts填写hwirq时,需要-32。
中断发生后,软件发生了什么事情?
外部中断信号过来,经过gic的distributor后,在中断使能、CPU没有处于中断处理中,CPU响应了该中断,则进入中断处理。ARM linux是怎么处理中断的呢?
中断之后进入CPU将会跳转到中断向量表(中断向量表的介绍参考文末链接),使用哪个中断向量表依赖当前CPU的状态以及栈指针状态。
arm64的中断向量表定义在arch/arm64/kernel/entry.S的vectors中,通过kernel_ventry跳转到相应的处理函数。
.pushsection ".entry.text", "ax"
.align 11
ENTRY(vectors)
kernel_ventry 1, sync_invalid
kernel_ventry 1, irq_invalid
kernel_ventry 1, fiq_invalid
kernel_ventry 1, error_invalid
kernel_ventry 1, sync
kernel_ventry 1, irq
kernel_ventry 1, fiq_invalid
kernel_ventry 1, error_invalid
kernel_ventry 0, sync
kernel_ventry 0, irq
kernel_ventry 0, fiq_invalid
kernel_ventry 0, error_invalid
#ifdef CONFIG_COMPAT
kernel_ventry 0, sync_compat, 32
kernel_ventry 0, irq_compat, 32
kernel_ventry 0, fiq_invalid_compat, 32
kernel_ventry 0, error_invalid_compat, 32
#else
kernel_ventry 0, sync_invalid, 32
kernel_ventry 0, irq_invalid, 32
kernel_ventry 0, fiq_invalid, 32
kernel_ventry 0, error_invalid, 32
#endif
END(vectors)
linux是没有使能fiq中断的,而invalid字段的则是未实现的异常向量,所以当发生irq中断时,将会从中断向量表中跳转到el1_irq:
el1_irq:
kernel_entry 1
enable_dbg
#ifdef CONFIG_TRACE_IRQFLAGS
bl trace_hardirqs_off
#endif
irq_handler
#ifdef CONFIG_PREEMPT
ldr w24, [tsk, #TSK_TI_PREEMPT]
cbnz w24, 1f
ldr x0, [tsk, #TSK_TI_FLAGS]
tbz x0, #TIF_NEED_RESCHED, 1f
bl el1_preempt
1:
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
bl trace_hardirqs_on
#endif
kernel_exit 1
ENDPROC(el1_irq)
.macro irq_handler
ldr_l x1, handle_arch_irq
mov x0, sp
irq_stack_entry
blr x1
irq_stack_exit
.endm
从汇编跳转到了handle_arch_irq,handle_arch_irq是个函数指针,在__gic_init_bases函数的时候,将其指向为gic_handle_irq函数。
static void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
u32 irqstat, irqnr;
struct gic_chip_data *gic = &gic_data[0];
void __iomem *cpu_base = gic_data_cpu_base(gic);
do {
irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK);
irqnr = irqstat & GICC_IAR_INT_ID_MASK;
if (likely(irqnr > 15 && irqnr < 1020)) {
if (static_key_true(&supports_deactivate))
writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI);
handle_domain_irq(gic->domain, irqnr, regs);
continue;
}
if (irqnr < 16) {
writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI);
if (static_key_true(&supports_deactivate))
writel_relaxed(irqstat, cpu_base + GIC_CPU_DEACTIVATE);
#ifdef CONFIG_SMP
smp_rmb();
handle_IPI(irqnr, regs);
#endif
continue;
}
break;
} while (1);
}
而在handle_domain_irq中,根据hwirq找到逻辑irq,接着就是 generic_handle_irq—>generic_handle_irq_desc—>desc->handle_irq(desc)。
而desc->handle_irq是指向什么函数呢?回到irq domain的ops->alloc函数,在进行hwirq和逻辑irq映射时,将 desc->handle_irq 指向 handle_fasteoi_irq 函数,而hwirq小于32的,则指向handle_percpu_devid_irq函数。
而 handle_fasteoi_irq 的流向则是:handle_fasteoi_irq—>handle_irq_event—>handle_irq_event_percpu—>__handle_irq_event_percpu,在这里,将从desc->action将各个handler逐个处理。action->handler是上面request_irq时传递进来的处理函数,至此,完成基本的中断处理。
重要参考
linux中断子系统-arm-gic 介绍
linux 中断子系统 - GIC 驱动源码分析
Linux Kernel 5.14 arm64异常向量表解读-中断处理解读
|