Read the fucking source code!
--By 鲁迅A picture is worth a thousand words.
--By 高尔基说明:node
从这篇文章开始,来聊一聊中断子系统。
中断是处理器用于异步处理外围设备请求的一种机制,能够说中断处理是操做系统管理外围设备的基石,此外系统调度、核间交互等都离不开中断,它的重要性不言而喻。linux
来一张概要的分层图:安全
中断子系统系列文章,会包括硬件相关、中断框架层、上半部与下半部、Softirq、Workqueue等机制的介绍,本文会先介绍硬件相关的原理及驱动,前戏结束,直奔主题。数据结构
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
的功能:app
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
功能:dom
Distributor
分发到CPU Interface
;SGI
中断分发到目标CPU上;CPU Interface
功能:异步
中断处理的状态机以下图:函数
Inactive
:无中断状态;Pending
:硬件或软件触发了中断,但还没有传递到目标CPU,在电平触发模式下,产生中断的同时保持pending
状态;Active
:发生了中断并将其传递给目标CPU,而且目标CPU能够处理该中断;Active and pending
:发生了中断并将其传递给目标CPU,同时发生了相同的中断而且该中断正在等待处理;GIC检测中断流程以下:
Distributor
肯定好目标CPU后,将中断信号发送到目标CPU上,同时,对于每一个CPU,Distributor
会从pending信号中选择最高优先级中断发送至CPU Interface
;CPU Interface
来决定是否将中断信号发送至目标CPU;EOI(End of Interrupt)
给GIC;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
文件解析成device_node
结构。关于设备树的相关知识,本文先不展开,后续再找机会补充。来一张图,先简要介绍下关键路径:
device_node
结构,在内存中维持一个树状结构;compatible
字段进行匹配;GIC驱动的执行流程以下图所示:
vmlinux.lds
,脚本中定义了一个__irqchip_of_table
段,该段用于存放中断控制器信息,用于最终来匹配设备;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驱动的初始化入口函数了;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
等结构体的初始化,这些结构在下文会进一步分析;先来张图:
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,则默认为enable void (*irq_shutdown)(struct irq_data *data); //关闭中断,若是设置成NULL,则默认为disable void (*irq_enable)(struct irq_data *data); //中断使能,若是设置成NULL,则默认为chip->unmask void (*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); //从新发送中断到CPU int (*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_data unsigned 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_HIERARCHY struct irq_domain *parent; //支持级联的话,指向父设备 #endif #ifdef CONFIG_GENERIC_IRQ_DEBUGFS struct 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 };
IRQ domain用于将硬件的中断号,转换成Linux系统中的中断号(virtual irq, virq
),来张图:
irq_domain_add_*()
接口来建立IRQ Domain;三种映射的方式以下图:
arch/arm64/kernel/entry.S
。irq_handler
中;代码比较简单,以下:
/* * Interrupt handling. */ .macro irq_handler ldr_l x1, handle_arch_irq mov x0, sp irq_stack_entry blr x1 irq_stack_exit .endm
来张图:
el0_irq
处,EL1则跳转到el1_irq
处;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相关的介绍就此打住,下一篇文章会接着介绍通用的中断处理框架,敬请期待。
ARM Generic Interrupt Controller Architecture version 2.0
欢迎关注公众号,不按期更新Linux内核机制相关文章,谢谢。