深入分析Linux中断子系统之中断控制器及驱动
说明:
Kernel版本:4.14
ARM64处理器,Contex-A53,双核
使用工具:Source Insight 3.5, Visio
1. 概述
从这篇文章开始,来聊一聊中断子系统。中断是处理器用于异步处理外围设备请求的一种机制,可以说中断处理是操作系统管理外围设备的基石,此外系统调度、核间交互等都离不开中断,它的重要性不言而喻。
来一张概要的分层图:

硬件层:最下层为硬件连接层,对应的是具体的外设与SoC的物理连接,中断信号是从外设到中断控制器,由中断控制器统一管理,再路由到处理器上;
硬件相关层:这个层包括两部分代码,一部分是架构相关的,比如ARM64处理器处理中断相关,另一部分是中断控制器的驱动代码;
通用层:这部分也可以认为是框架层,是硬件无关层,这部分代码在所有硬件平台上是通用的;
用户层:这部分也就是中断的使用者了,主要是各类设备驱动,通过中断相关接口来进行申请和注册,最终在外设触发中断时,进行相应的回调处理;
中断子系统系列文章,会包括硬件相关、中断框架层、上半部与下半部、Softirq、Workqueue等机制的介绍,本文会先介绍硬件相关的原理及驱动,前戏结束,直奔主题。
【文章福利】小编推荐自己的Linux内核技术交流群:【749907784】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!(含视频教程、电子书、实战项目及代码)


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
文件解析成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,虚拟中断号)之间的映射;
还是上一下具体的数据结构代码吧,关键注释如下:
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
中;
代码比较简单,如下:
来张图:

中断触发,处理器去异常向量表找到对应的入口,比如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
,最终也就能调用到外设的中断处理函数了;
原文作者:LoyenWang
