1. 概述
从本文开始,我们来谈谈中断子系统。中断是处理器异步处理外围设备要求的机制。可以说,中断处理是操作系统管理外围设备的基石。此外,系统调度和核间交互离不开中断,其重要性不言而喻。
一般分层图:

-
硬件层:最下层为硬件连接层,对应于特定的外设和SoC物理连接,中断信号从外设到中断控制器,由中断控制器统一管理,然后路由到处理器;
-
硬件相关层:该层包括两部分代码,一部分与架构相关,如ARM另一部分是中断控制器的驱动代码;
-
一般层:这部分也可以认为是框架层,是硬件无关层,这部分代码在所有硬件平台上都是通用的;
-
用户层:本部分为中断用户,主要由各种设备驱动,通过中断相关界面进行申请和注册,最终在外设触发中断时进行相应的回调;
中断系统系列文章将包括硬件相关、中断框架层、上半部和下半部分、Softirq、Workqueue本文将首先介绍硬件相关的原理和驱动,前戏结束,直奔主题。
2. GIC硬件原理
-
ARM该公司提供了一个通用的中断控制器
GIC(Generic Interrupt Controller),GIC的版本包括V1 ~ V4,因为我用的SoC中断控制器是V2本文将围绕版本GIC-V2展开介绍;
来一张功能版的框图:
-
GIC-V2在功能方面,除了常用的中断使能、中断屏蔽、优先管理等功能外,还支持安全扩展、虚拟化等; -
GIC-V2从组成上说,主要分为Distributor和CPU Interface两个模块,Distributor主要负责中断源的管理,包括优先处理、屏蔽、抢占等,并分发最高优先级的中断CPU Interface,CPU Interface主要用于连接处理器,与处理器进行交互; -
Virtual Distributor和Virtual CPU Interface与虚拟化有关,本文不深入分析;
再看一张细节图Distributor和CPU Interface的功能:
-
GIC-V2支持三种类型的中断:-
SGI(software-generated interrupts):软件中断主要用于核间交互和内核IPI:inter-processor interrupts就是基于SGI,中断号ID0 - ID15用于SGI; -
PPI(Private Peripheral Interrupt):中断私人外设,每一个CPU都有自己的私有中断,典型的应用有local timer,中断号ID16 - ID31用于PPI; -
SPI(Shared Peripheral Interrupt):共享外设中断,产生中断后,可以分发到某个CPU上,中断号ID32 - ID1019用于SPI,ID1020 - ID1023特殊用途保留;
-
-
Distributor功能:-
全局开关控制
Distributor分发到CPU Interface; -
打开或关闭每个中断;
-
设置每个中断的优先级;
-
设置每个中断路由CPU列表;
-
设置各外设中断的触发方法:电平触发和边缘触发;
-
设置每个中断Group:Group0或Group1,其中Group0用于安全中断,支持FIQ和IRQ,Group1用于非安全中断,只支持IRQ;
-
将
SGI中断分发到目标CPU上; -
可见每个中断状态;
-
为设置和清除外设中断提供软件机制pending状态;
-
-
CPU Interface功能:-
中断请求信号CPU上;
-
中断确认;
-
完成标识中断处理;
-
为处理器设置中断优先级掩码;
-
设置处理器中断抢占策略;
-
确定处理器的最高优先级pending中断;
-
如下图所示:
-
Inactive:无中断状态; -
Pending:硬件或软件触发中断,但尚未传递到目标CPU,在电平触发模式下保持中断pending状态; -
Active:中断并将其传递给目标CPU,并且目标CPU中断可以处理; -
Active and pending:中断并将其传递给目标CPU,同时发生同样的中断,中断正在等待处理;
GIC检测中断过程如下:
-
GIC捕获中断信号,中断信号assert,标记为pending状态;
-
Distributor确定好目标CPU之后,将中断信号发送到目标CPU同时,对每一个CPU,Distributor会从pending最高优先级中断发送到信号中CPU Interface; -
CPU Interface来决定是否将中断信号发送至目标CPU; -
CPU中断处理完成后,发送完成信号
EOI(End of Interrupt)给GIC;
3. GIC驱动分析
3.1 添加设备信息
ARM平台的设备信息通过Device Tree添加设备树,放置设备树信息arch/arm64/boot/dts/下
下图为中断控制器设备树信息:
-
compatible字段:用于匹配特定的驱动,如图片arm, gic-400,相应的驱动程序可以根据此名称匹配; -
interrupt-cells字段:用于指定中断源所需的单元数,该值为3。例如,在设备树上添加中断信号时,通常可以看到类似的信号interrupts = <0 23 4>;第一单元0的信息表示中断类型(1:PPI,0:SPI),第二单元23表示中断号,第三单元4表示中断触发类型; -
reg字段:描述中断控制器的地址信息和地址范围,如图片GIC Distributor(GICD)和GIC CPU Interface(GICC)地址信息; -
interrupt-controller字段:表示设备为中断控制器,外设可连接到中断控制器; -
详情请参考设备数量各字段的含义
Documentation/devicetree/bindings相应的对应信息;
如何将设备树的信息添加到系统中?Device Tree最后编译成dtb并通过文件Uboot内核启动后会传递给内核dtb文件解析成code>device_node结构。关于设备树的相关知识,本文先不展开,后续再找机会补充。来一张图,先简要介绍下关键路径:
-
设备树的节点信息,最终会变成
device_node结构,在内存中维持一个树状结构; -
设备与驱动,会根据
compatible字段进行匹配;
3.2 驱动流程分析
GIC驱动的执行流程如下图所示:
-
首先需要了解一下链接脚本
vmlinux.lds,脚本中定义了一个__irqchip_of_table段,该段用于存放中断控制器信息,用于最终来匹配设备; -
在GIC驱动程序中,使用
IRQCHIP_DECLARE宏来声明结构信息,包括compatible字段和回调函数,该宏会将这个结构放置到__irqchip_of_table字段中; -
在内核启动初始化中断的函数中,
of_irq_init函数会去查找设备节点信息,该函数的传入参数就是__irqchip_of_table段,由于IRQCHIP_DECLARE已经将信息填充好了,of_irq_init函数会根据arm,gic-400去查找对应的设备节点,并获取设备的信息。中断控制器也存在级联的情况,of_irq_init函数中也处理了这种情况; -
or_irq_init函数中,最终会回调IRQCHIP_DECLARE声明的回调函数,也就是gic_of_init,而这个函数就是GIC驱动的初始化入口函数了; -
GIC的工作,本质上是由中断信号来驱动,因此驱动本身的工作就是完成各类信息的初始化,注册好相应的回调函数,以便能在信号到来之时去执行;
-
set_smp_process_call设置__smp_cross_call函数指向gic_raise_softirq,本质上就是通过软件来触发GIC的SGI中断,用于核间交互; -
cpuhp_setup_state_nocalls函数,设置好CPU进行热插拔时GIC的回调函数,以便在CPU热插拔时做相应处理; -
set_handle_irq函数的设置很关键,它将全局函数指针handle_arch_irq指向了gic_handle_irq,而处理器在进入中断异常时,会跳转到handle_arch_irq执行,所以,可以认为它就是中断处理的入口函数了; -
驱动中完成了各类函数的注册,此外还完成了
irq_chip,irq_domain等结构体的初始化,这些结构在下文会进一步分析; -
最后,完成GIC硬件模块的初始化设置,以及电源管理相关的注册等工作;
3.3 数据结构分析
先来张图:
-
GIC驱动中,使用
struct gic_chip_data结构体来描述GIC控制器的信息,整个驱动都是围绕着该结构体的初始化,驱动中将函数指针都初始化好,实际的工作是由中断信号触发,也就是在中断来临的时候去进行回调; -
struct irq_chip结构,描述的是中断控制器的底层操作函数集,这些函数集最终完成对控制器硬件的操作; -
struct irq_domain结构,用于硬件中断号和Linux IRQ中断号(virq,虚拟中断号)之间的映射;
还是上一下具体的数据结构代码吧,关键注释如下:
struct irq_chip {struct device *parent_device; //指向父设备const char *name; // /proc/interrupts中显示的名字unsigned int (*irq_startup)(struct irq_data *data); //启动中断,如果设置成NULL,则默认为enablevoid (*irq_shutdown)(struct irq_data *data); //关闭中断,如果设置成NULL,则默认为disablevoid (*irq_enable)(struct irq_data *data); //中断使能,如果设置成NULL,则默认为chip->unmaskvoid (*irq_disable)(struct irq_data *data); //中断禁止void (*irq_ack)(struct irq_data *data); //开始新的中断void (*irq_mask)(struct irq_data *data); //中断源屏蔽void (*irq_mask_ack)(struct irq_data *data); //应答并屏蔽中断void (*irq_unmask)(struct irq_data *data); //解除中断屏蔽void (*irq_eoi)(struct irq_data *data); //中断处理结束后调用int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force); //在SMP中设置CPU亲和力int (*irq_retrigger)(struct irq_data *data); //重新发送中断到CPUint (*irq_set_type)(struct irq_data *data, unsigned int flow_type); //设置中断触发类型int (*irq_set_wake)(struct irq_data *data, unsigned int on); //使能/禁止电源管理中的唤醒功能void (*irq_bus_lock)(struct irq_data *data); //慢速芯片总线上的锁void (*irq_bus_sync_unlock)(struct irq_data *data); //同步释放慢速总线芯片的锁void (*irq_cpu_online)(struct irq_data *data);void (*irq_cpu_offline)(struct irq_data *data);void (*irq_suspend)(struct irq_data *data);void (*irq_resume)(struct irq_data *data);void (*irq_pm_shutdown)(struct irq_data *data);void (*irq_calc_mask)(struct irq_data *data);void (*irq_print_chip)(struct irq_data *data, struct seq_file *p);int (*irq_request_resources)(struct irq_data *data);void (*irq_release_resources)(struct irq_data *data);void (*irq_compose_msi_msg)(struct irq_data *data, struct msi_msg *msg);void (*irq_write_msi_msg)(struct irq_data *data, struct msi_msg *msg);int (*irq_get_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool *state);int (*irq_set_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool state);int (*irq_set_vcpu_affinity)(struct irq_data *data, void *vcpu_info);void (*ipi_send_single)(struct irq_data *data, unsigned int cpu);void (*ipi_send_mask)(struct irq_data *data, const struct cpumask *dest);unsigned long flags;};struct irq_domain {struct list_head link; //用于添加到全局链表irq_domain_list中const char *name; //IRQ domain的名字const struct irq_domain_ops *ops; //IRQ domain映射操作函数集void *host_data; //在GIC驱动中,指向了irq_gic_dataunsigned int flags;unsigned int mapcount; //映射中断的个数/* Optional data */struct fwnode_handle *fwnode;enum irq_domain_bus_token bus_token;struct irq_domain_chip_generic *gc;#ifdef CONFIG_IRQ_DOMAIN_HIERARCHYstruct irq_domain *parent; //支持级联的话,指向父设备#endif#ifdef CONFIG_GENERIC_IRQ_DEBUGFSstruct dentry *debugfs_file;#endif/* reverse map data. The linear map gets appended to the irq_domain */irq_hw_number_t hwirq_max; //IRQ domain支持中断数量的最大值unsigned int revmap_direct_max_irq;unsigned int revmap_size; //线性映射的大小struct radix_tree_root revmap_tree; //Radix Tree映射的根节点unsigned int linear_revmap[]; //线性映射用到的查找表};struct irq_domain_ops {int (*match)(struct irq_domain *d, struct device_node *node,enum irq_domain_bus_token bus_token); // 用于中断控制器设备与IRQ domain的匹配int (*select)(struct irq_domain *d, struct irq_fwspec *fwspec,enum irq_domain_bus_token bus_token);int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw); //用于硬件中断号与Linux中断号的映射void (*unmap)(struct irq_domain *d, unsigned int virq);int (*xlate)(struct irq_domain *d, struct device_node *node,const u32 *intspec, unsigned int intsize,unsigned long *out_hwirq, unsigned int *out_type); //通过device_node,解析硬件中断号和触发方式#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY/* extended V2 interfaces to support hierarchy irq_domains */int (*alloc)(struct irq_domain *d, unsigned int virq,unsigned int nr_irqs, void *arg);void (*free)(struct irq_domain *d, unsigned int virq,unsigned int nr_irqs);void (*activate)(struct irq_domain *d, struct irq_data *irq_data);void (*deactivate)(struct irq_domain *d, struct irq_data *irq_data);int (*translate)(struct irq_domain *d, struct irq_fwspec *fwspec,unsigned long *out_hwirq, unsigned int *out_type);#endif};
3.3.1 IRQ domain
IRQ domain用于将硬件的中断号,转换成Linux系统中的中断号(virtual irq, virq),来张图:
-
每个中断控制器都对应一个IRQ Domain;
-
中断控制器驱动通过
irq_domain_add_*()接口来创建IRQ Domain; -
IRQ Domain支持三种映射方式:linear map(线性映射),tree map(树映射),no map(不映射);
-
linear map:维护固定大小的表,索引是硬件中断号,如果硬件中断最大数量固定,并且数值不大,可以选择线性映射;
-
tree map:硬件中断号可能很大,可以选择树映射;
-
no map:硬件中断号直接就是Linux的中断号;
-
三种映射的方式如下图:
-
图中描述了三个中断控制器,对应到三种不同的映射方式;
-
各个控制器的硬件中断号可以一样,最终在Linux内核中映射的中断号是唯一的;
4. Arch-speicific代码分析
-
中断也是异常模式的一种,当外设触发中断时,处理器会切换到特定的异常模式进行处理,而这部分代码都是架构相关的;ARM64的代码位于
arch/arm64/kernel/entry.S。 -
ARM64处理器有四个异常级别Exception Level:0~3,EL0级对应用户态程序,EL1级对应操作系统内核态,EL2级对应Hypervisor,EL3级对应Secure Monitor;
-
异常触发时,处理器进行切换,并且跳转到异常向量表开始执行,针对中断异常,最终会跳转到
irq_handler中;
代码比较简单,如下:
/** Interrupt handling.*/.macro irq_handlerldr_l x1, handle_arch_irqmov x0, spirq_stack_entryblr x1irq_stack_exit.endm
来张图:
-
中断触发,处理器去异常向量表找到对应的入口,比如EL0的中断跳转到
el0_irq处,EL1则跳转到el1_irq处; -
在GIC驱动中,会调用
set_handle_irq接口来设置handle_arch_irq的函数指针,让它指向gic_handle_irq,因此中断触发的时候会跳转到gic_handle_irq处执行; -
gic_handle_irq函数处理时,分为两种情况,一种是外设触发的中断,硬件中断号在16 ~ 1020之间,一种是软件触发的中断,用于处理器之间的交互,硬件中断号在16以内; -
外设触发中断后,根据
irq domain去查找对应的Linux IRQ中断号,进而得到中断描述符irq_desc,最终也就能调用到外设的中断处理函数了;
GIC和Arch相关的介绍就此打住,下一篇文章会接着介绍通用的中断处理框架,敬请期待。