浅入浅出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 的硬件设计也分成相应的两部分:
distributor 翻译过来就是分发器,负责将中断源传递过来的中断进行分发,而 CPU interface 不言而喻,则是针对 CPU 的配置接口,在多核架构中,每个 CPU 对应一个 CPU interface,负责将 distributor 传递过来的中断传递给 CPU,同时和 CPU 进行系列的交互。因此,在 GIC 中,一个外部中断向上传递的流程为:中断产生源 -> GIC distributor -> GIC CPU interface -> CPU。
GIC 中的 distributor 是属于所有 CPU 共享的,主要控制中断的收集与分发,其具体实现的接口为:
对于每个连接到 GIC 的 CPU 核,都会存在一个对应的 CPU interface,它主要提供以下的接口:
在中断发生时,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>, /* GIC Dist */
<0x0 0x03022000 0 0x2000>, /* GIC CPU */
<0x0 0x03024000 0 0x2000>, /* GIC VCPU Control */
<0x0 0x03026000 0 0x2000>; /* GIC VCPU */
interrupts = <GIC_PPI 9 0xf04>; /* GIC Maintenence IRQ */
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;
/* 通过这个代码,简单可以知道,gic的信息都是通过gic_data这个
* 全局数组进行保存的,每增加一个gic控制器的时候,gic_cnt将
* 会加1 */
if (WARN_ON(gic_cnt >= CONFIG_ARM_GIC_MAX_NR))
return -EINVAL;
gic = &gic_data[gic_cnt];
/* 从dts获取gic dist和gic cpu的寄存器地址信息 */
ret = gic_of_setup(gic, node);
if (ret)
return ret;
/*
* Disable split EOI/Deactivate if either HYP is not available
* or the CPU interface is too small.
*/
/* 检查是否使能EOI mode(全局变量supports_deactivate表示),默认为true,一般是false */
if (gic_cnt == 0 && !gic_check_eoimode(node, &gic->raw_cpu_base))
static_key_slow_dec(&supports_deactivate); /* 不使能EOI则将supports_deactivate置false */
/* gic初始化 */
ret = __gic_init_bases(gic, -1, &node->fwnode);
if (ret) {
gic_teardown(gic);
return ret;
}
if (!gic_cnt) {
/* root gic,如果配置了gic_dist_physaddr,则设置 */
gic_init_physaddr(node);
/* 完成逻辑irq和hwirp的映射,下面介绍 */
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; /* gic硬件控制器相关信息,使能/关闭某中断 */
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 - interrupt descriptor
* @irq_common_data: per irq and chip data passed down to chip functions
* @kstat_irqs: irq stats per cpu
* @handle_irq: highlevel irq-events handler
* @preflow_handler: handler called before the flow handler (currently used by sparc)
* @action: the irq action chain
* @status: status information
* @core_internal_state__do_not_mess_with_it: core internal status information
* @depth: disable-depth, for nested irq_disable() calls
* @wake_depth: enable depth, for multiple irq_set_irq_wake() callers
* @irq_count: stats field to detect stalled irqs
* @last_unhandled: aging timer for unhandled count
* @irqs_unhandled: stats field for spurious unhandled interrupts
* @threads_handled: stats field for deferred spurious detection of threaded handlers
* @threads_handled_last: comparator field for deferred spurious detection of theraded handlers
* @lock: locking for SMP
* @affinity_hint: hint to user space for preferred irq affinity
* @affinity_notify: context for notification of affinity changes
* @pending_mask: pending rebalanced interrupts
* @threads_oneshot: bitfield to handle shared oneshot threads
* @threads_active: number of irqaction threads currently running
* @wait_for_threads: wait queue for sync_irq to wait for threaded handlers
* @nr_actions: number of installed actions on this descriptor
* @no_suspend_depth: number of irqactions on a irq descriptor with
* IRQF_NO_SUSPEND set
* @force_resume_depth: number of irqactions on a irq descriptor with
* IRQF_FORCE_RESUME set
* @rcu: rcu head for delayed free
* @kobj: kobject used to represent this struct in sysfs
* @dir: /proc/irq/ procfs entry
* @name: flow handler name for /proc/interrupts output
*/
struct irq_desc {
struct irq_common_data irq_common_data; /* 由所有irqchip共享的irq数据 */
struct irq_data irq_data; /* 中断相关的数据,逻辑irq,物理irq,irq_domain等 */
unsigned int __percpu *kstat_irqs; /* 每个CPU的irq统计 */
irq_flow_handler_t handle_irq; /* 高级irq事件处理程序 */
/* action 应用中将会是一个链表,我们的irq是支持共享的 */
struct irqaction *action; /* IRQ action list */
....
wait_queue_head_t wait_for_threads; /* 等待队列头,中断同步的时候使用,比如进程在disable或free某个irq,需要等待irq执行完成 */
...
} ____cacheline_internodealigned_in_smp;
在__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
/* 设置PPI/SGI中断处理函数,实际上就是写gic的PPI/SGI中断寄存器,
* 多核通信将会调用到gic_raise_softirq */
set_smp_cross_call(gic_raise_softirq);
#endif
/* 设置CPU热插拔时执行的回调函数 */
cpuhp_setup_state_nocalls(CPUHP_AP_IRQ_GIC_STARTING,
"AP_IRQ_GIC_STARTING",
gic_starting_cpu, NULL);
/* 设置中断发生时higher level回调函数,gic_handle_irq是中断处理程序的汇编代码调用,
* 这里实际上就是将gic_handle_irq赋值给平台的handle_arch_irq函数指针 */
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_chip_data中的chip变量,irq CPU亲和设置函数 */
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 实现,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 相关的初始化函数,这两个函数对应的设置项为:
配置完成之后,GIC就开始工作。
中断域,负责gic中hwirq和逻辑irq的映射。hwirq即hardware irq,git硬件上的irq id,查看相应平台的gic介绍就有:
逻辑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)
{
...
/*
* Find out how many interrupts are supported.
* The GIC only supports up to 1020 interrupt sources.
*/
/* 获取gic支持的irq数量 */
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) { /* DT/ACPI */
/* 创建一个domain,并添加到irq_domain_list */
gic->domain = irq_domain_create_linear(handle, gic_irqs,
&gic_irq_domain_hierarchy_ops,
gic);
} else { /* Legacy support */
/*
* For primary GICs, skip over SGIs.
* For secondary GICs, skip over PPIs, too.
*/
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; /* calculate # of irqs to allocate */
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,实现的逻辑如下:
实际上映射之前,都是先通过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_desc的handle_irq,指向handle_fasteoi_irq */
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又是怎么处理的。
日常内核驱动中,外设使用的irq都是在dts中配置,驱动通过dts获取irq之后,再通过request_irq()函数进行申请,最终来到request_threaded_irq。request_threaded_irq实质完成下面的几个逻辑:
__setup_irq逻辑:
irq/%d-%s
名称的内核线程,就是这里创建的;上面在 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跳转到相应的处理函数。
/*
* Exception vectors.
*/
.pushsection ".entry.text", "ax"
.align 11
ENTRY(vectors)
kernel_ventry 1, sync_invalid // Synchronous EL1t
kernel_ventry 1, irq_invalid // IRQ EL1t
kernel_ventry 1, fiq_invalid // FIQ EL1t
kernel_ventry 1, error_invalid // Error EL1t
kernel_ventry 1, sync // Synchronous EL1h
kernel_ventry 1, irq // IRQ EL1h
kernel_ventry 1, fiq_invalid // FIQ EL1h
kernel_ventry 1, error_invalid // Error EL1h
kernel_ventry 0, sync // Synchronous 64-bit EL0
kernel_ventry 0, irq // IRQ 64-bit EL0
kernel_ventry 0, fiq_invalid // FIQ 64-bit EL0
kernel_ventry 0, error_invalid // Error 64-bit EL0
#ifdef CONFIG_COMPAT
kernel_ventry 0, sync_compat, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_compat, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid_compat, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_invalid_compat, 32 // Error 32-bit EL0
#else
kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_invalid, 32 // Error 32-bit EL0
#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] // get preempt count
cbnz w24, 1f // preempt count != 0
ldr x0, [tsk, #TSK_TI_FLAGS] // get flags
tbz x0, #TIF_NEED_RESCHED, 1f // needs rescheduling?
bl el1_preempt
1:
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
bl trace_hardirqs_on
#endif
kernel_exit 1
ENDPROC(el1_irq)
/*
* Interrupt handling.
*/
.macro irq_handler
ldr_l x1, handle_arch_irq
mov x0, sp
irq_stack_entry
blr x1 //跳转到上面赋值到x1的handle_arch_irq函数
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;
}
/* 中断号小于16则是SGI,软中断,CPU间的异常通信 */
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
/*
* Ensure any shared data written by the CPU sending
* the IPI is read after we've read the ACK register
* on the GIC.
*
* Pairs with the write barrier in gic_raise_softirq
*/
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异常向量表解读-中断处理解读