中断控制器(GIC)
文章目录
前言
上篇大概讲了一下ARM的异常处理,但是处理器是如何知道是哪一种异常或中断的呢?这就需要中断控制器了。
linux中断管理机制
操作系统有个非常重要的部分外设,比如鼠标,键盘,声卡等。处理器与外设计算能力与处理速度上是不在一个数量级的。假设处理器要得到键盘或鼠标的事件,如果处理器发出一个请求信号,它就一直再轮询键盘或鼠标的响应,由于键盘鼠标响应速度慢得多,这就会导致浪费很多资源。但是如果键盘或鼠标产生数据了,去通知处理器,处理器停下当前工作来处理键盘,鼠标事件,处理完后继续原来的工作,这样就会高效很多。
从系统的角度来讲,linux内核中断管理可以分为4层:
- 硬件层面:比如CPU和中断控制器的连接。
- 处理器架构管理:比如CPU中断异常处理。
- 中断控制器管理:比如IRQ中断号映射。
- linux内核通用中断处理层:比如中断注册和中断处理。
不同体系结构中对中断控制器设计理念有所不同,ARM公司提供通用中断控制器(GIC),x86体系架构采用高级可编程中断控制器(PIC).
ARM中断控制器
GIC(Generic Interrupt Controller)作为 ARM 系统中通用中断控制器,目前有四个版本,V1~V4(V2最多支持8个ARM core,V3/V4支持更多的ARM core,主要用于ARM64系统结构)。我们以GIC-400为例:
如图所示,当中断发生的时候,通过中断控制器,发出中断给CPU。GIC-400通过AMBA(Advanced Microcontroller Bus Architecture)片上总线连接到一个或者多个ARM处理器上。从图中我们可以看出GIC 是联系外设中断和 CPU 的桥梁,也是各 CPU 之间中断互联的通道(也带有管理功能),它负责检测、管理、分发中断。简化图,如下:
根据简化图,ARM CPU 对外的连接只有2 个中断: IRQ和FIQ ,相对应的处理模式分别是一般中断(IRQ )处理模式和快速中断(FIQ )处理模式。所以GIC 最后要把中断汇集成2 条线,与CPU 对接。
GIC结构分析
如下图,是中断控制器的结构:
GIC中断控制器可以分为:仲裁单元和CPU接口模块。状态机有:inactive,pending,active,active and pending.
其中有两个重要组成部分分别是分发器(Distributor)与CPU接口单元(CPU Interface):
分发器(Distributor)
系统中的所有中断源都连接到该单元。可以通过仲裁单元的寄存器来控制各个中断源的属性,例如优先级、状态、安全性、路由信息和使能状态。
分发器把中断输出到“CPU接口单元”,后者决定将哪个中断转发给CPU核。
分发器的主要的作用是检测各个中断源的状态,控制各个中断源的行为,分发各个中断源产生的中断事件到指定的一个或者多个CPU接口上。虽然分发器可以管理多个中断源,但是它总是把优先级最高的那个中断请求送往CPU接口。分发器对中断的控制包括:
- 中断使能或禁能控制。分发器对中断的控制分成两个级别,一个是全局中断的控制(GIC_DIST_CTRL),一旦禁止了全局的中断,那么任何的中断源产生的中断事件都不会被传递到CPU接口;另外一个级别是对针对各个中断源进行控制(GIC_DIST_ENABLE_CLEAR),禁止某一个中断源会导致该中断事件不会分发到CPU接口,但不影响其他中断源产生中断事件的分发。
- 控制将当前优先级最高的中断事件分发到一个或者一组CPU接口。
- 优先级控制。
- 中断属性设定,例如是电平触发还是边沿触发。
- 中断的设定。
- 分发器可以管理若干个中断源,这些中断源用ID来标识,我们称之interrupt ID
CPU接口单元(CPU Interface)
CPU核通过控制器的CPU接口单元接收中断。CPU接口单元寄存器用于屏蔽,识别和控制转发到CPU核的中断的状态。系统中的每个CPU核心都有一个单独的CPU接口。分发器对中断的控制包括:
* 使能中断请求信号到CPU上;
* 中断的确认;
* 标识中断处理的完成;
* 为处理器设置中断优先级掩码;
* 设置处理器的中断抢占策略;
* 确定处理器的最高优先级pending中断;
中断类型
SPI:公用外设中断:最多可以支持 988 个外设中断,硬件中断号从 ID32~ID1019。
PPI:私有外设中断:是每个 CPU 私有的中断。最多支持 16 个 PPI 中断,硬件中断号从 ID16~ID31。
SGI:软件触发中断:通常用于多核间通讯,最多支持 16 个 SGI 中断,硬件中断号从 ID0~ID15。
中断状态
- lnactive:中断源没有发送中断;
- Pending:中断源已经发送中断,等待处理器处理;
- Active:处理器已经确认中断,正在处理;
- Active and Pending:处理器正在处理中断,相同的中断源* 又发送了一个中断。
当GIC接收到一个中断请求,将其状态设置为Pending。重新产生一个挂起状态的中断不影响该中断状态。
中断处理顺序
① GIC决定该中断是否使能,若没有被使能对GIC没有影响;
② 对于每个Pending中断,GIC决定目标处理器;
③ 对于每个处理器 ,Distributor根据它拥有的每个中断优先级信息决定最高优先级的挂起中断,将该中断传递给目标CPU Interface;
④ GIC Distributor将一个中断传递给CPU Interface后,该CPU Interface决定该中断是否有足够的优先级将中断请求发给CPU;
⑤ 当CPU开始处理该异常中断,它读取GICC_IAR应答中断。读取的GICC_IAR获取到中断ID,对于SGI,还有源处理器ID。中断ID被用来查找正确的中断处理程序。
GIC识别读过程后,将改变该中断的状态:
a) 当中断状态变为active时,如果该中断挂起状态持续存在或者中断再次产生,中断状态将从Pending转化为pending & active
b) 否则,中断状态将从pending状态变为active
⑥ 当中断完成中断处理后,它需要通知GIC处理已经完成。这个过程称为 priority drop and interrupt deactivation:
a) 总是需要向EOIR寄存器写入一个有效的值(end of interrupt register)
b) 也需要接着向GICC_DIR写入值(deactivate interrupt register)
中断状态机
-
添加挂起状态(A1、A2)
对于一个 SGI,发生以下 2 种情况的 1 种:- 软件写 GICD_SGIR 寄存器,指定目标处理器
- 目标处理器上软件写GICD_SPENDSGIRn 寄存器
对于一个 SPI 或 PPI,发生以下 2 种情况的 1 种:
* 外设发出一个中断请求信号
* 软件写 GICD_ISPENDRn 寄存器 -
删除挂起状态(B1、B2)
对于 SGI,目标处理器写 GICD_CPENDSGIRn 寄存器,对于一个 SPI 或 PPI,发生以下 2 种情况的 1 种:
* 电平触发类型中断,信号取消
* 边沿触发类型中断,软件写 GICD_ICPENDRn 寄存器 -
挂起到激活(C)
如果中断使能,并且高优先级,软件从 GICC_IAR 寄存器读取时发生状态改变。 -
挂起到激活和挂起(D)
对于 SGI,这种转变发生在以下任一情况下:
* 将 SGI 状态设置为挂起的写入操作与读取 GICC_IAR 几乎同时发生
* 当多个挂起的 SGI 具有相同 ID 时,并且它们来自同一个源处理器并指向同一个处理器。其中一个 SGI 状态变为激活(C),其他 SGI 状态变为激活和挂起(D)。
对于 SPI 或 PPI,满足以下所有条件,则发生这种转换
* 中断开启
* 软件读取 GICC_IAR,读操作将激活状态添加到中断中。
此外,还应满足以下条件之一:
* 对于电平触发中断,中断信号保持。通常都是这样,因为外设直到处理器处理完中断后才会取消触发信号。
* 对于边沿触发中断,是否发生此转换取决于读取 GICC_IAR 的时间(中断再次触发,上一次未处理),读取 GICC_IAR 可能会转换到 C,后面可能会转换到 A2。 -
删除激活状态(E1、E2)
软件写入 GICC_EOIR 或 GICC_DIR 来停用中断,
GIC驱动
节点信息
gic: interrupt-controller@2c001000 {
compatible = "arm,cortex-a9-gic";//用于与具体的驱动来进行匹配
#interrupt-cells = <3>;
/*用于指定编码一个中断源所需要的单元个数,这个值为3。比如在外设在设备树中添加中断信号时,通常能看到类似interrupts = <0 23 4>;的信息,第一个单元0,表示的是中断类型(1:PPI,0:SPI),第二个单元23表示的是中断号,第三个单元4表示的是中断触发的类型;*/
#address-cells = <0>;
interrupt-controller;//表示该设备是一个中断控制器,外设可以连接在该中断控制器上;
reg = <0x2c001000 0x1000>,//描述中断控制器的地址信息以及地址范围
<0x2c000100 0x100>;
};
内核启动后会将该节点解析成device_node结构。
驱动流程
GIC的工作,本质上是由中断信号来驱动,因此驱动本身的工作就是完成各类信息的初始化,注册好相应的回调函数,以便能在信号到来之时去执行set_handle_irq函数的设置很关键,它将全局函数指针handle_arch_irq指向了gic_handle_irq,而处理器在进入中断异常时,会跳转到handle_arch_irq执行,所以,可以认为它就是中断处理的入口函数了。驱动中完成了各类函数的注册,此外还完成了irq_chip, irq_domain等结构体的初始化,这些结构在下文会进一步分析。最后,完成GIC硬件模块的初始化设置,以及电源管理相关的注册等工作。
数据结构
GIC驱动中,使用struct gic_chip_data结构体来描述GIC控制器的信息,整个驱动都是围绕着该结构体的初始化,驱动中将函数指针都初始化好,实际的工作是由中断信号触发,也就是在中断来临的时候去进行回调struct irq_chip结构,描述的是中断控制器的底层操作函数集,这些函数集最终完成对控制器硬件的操作struct irq_domain结构,用于硬件中断号和Linux IRQ中断号(virq,虚拟中断号)之间的映射;
每个中断控制器都对应一个IRQ Domain;
中断控制器驱动通过irq_domain_add_*()接口来创建IRQ Domain;
IRQ Domain支持三种映射方式:linear map(线性映射),tree map(树映射),no map(不映射);
- linear map:维护固定大小的表,索引是硬件中断号,如果硬件中断最大数量固定,并且数值不大,可以选择线性映射;
- tree map:硬件中断号可能很大,可以选择树映射;
- no map:硬件中断号直接就是Linux的中断号;
ARM中断架构
中断也是异常模式的一种,当外设触发中断时,处理器会切换到特定的异常模式进行处理,而这部分代码都是架构相关的;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,最终也就能调用到外设的中断处理函数了;
gic_of_init()
static int __init
gic_of_init(struct device_node *node, struct device_node *parent)
{
void __iomem *cpu_base;
void __iomem *dist_base;
u32 percpu_offset;
int irq;
if (WARN_ON(!node))
return -ENODEV;[]
dist_base = of_iomap(node, 0);//映射GIC Distributor的寄存器地址空间
WARN(!dist_base, "unable to map gic dist registers\n");
cpu_base = of_iomap(node, 1);//映射GIC CPU interface的寄存器地址空间
WARN(!cpu_base, "unable to map gic cpu registers\n");
if (of_property_read_u32(node, "cpu-offset", &percpu_offset))//处理cpu-offset属性。
percpu_offset = 0;
gic_init_bases(gic_cnt, -1, dist_base, cpu_base, percpu_offset, node);//主处理过程,后面详述
if (!gic_cnt)
gic_init_physaddr(node);//对于不支持big.LITTLE switcher(CONFIG_BL_SWITCHER)的系统,该函数为空。
if (parent) {//处理interrupt级联
irq = irq_of_parse_and_map(node, 0);//解析second GIC的interrupts属性,并进行mapping,返回IRQ number
gic_cascade_irq(gic_cnt, irq);
}
if (IS_ENABLED(CONFIG_ARM_GIC_V2M))
gicv2m_of_init(node, gic_data[gic_cnt].domain);
gic_cnt++;
return 0;
}
gic_init_bases()函数
void __init gic_init_bases(unsigned int gic_nr, int irq_start,
void __iomem *dist_base, void __iomem *cpu_base,
u32 percpu_offset, struct device_node *node)
{
irq_hw_number_t hwirq_base;
struct gic_chip_data *gic;
int gic_irqs, irq_base, i;
BUG_ON(gic_nr >= MAX_GIC_NR);
gic = &gic_data[gic_nr];
#ifdef CONFIG_GIC_NON_BANKED
if (percpu_offset) { /* Frankein-GIC without banked registers... */
unsigned int cpu;
gic->dist_base.percpu_base = alloc_percpu(void __iomem *);
gic->cpu_base.percpu_base = alloc_percpu(void __iomem *);
if (WARN_ON(!gic->dist_base.percpu_base ||
!gic->cpu_base.percpu_base)) {
free_percpu(gic->dist_base.percpu_base);
free_percpu(gic->cpu_base.percpu_base);
return;
}
for_each_possible_cpu(cpu) {
u32 mpidr = cpu_logical_map(cpu);
u32 core_id = MPIDR_AFFINITY_LEVEL(mpidr, 0);
unsigned long offset = percpu_offset * core_id;
*per_cpu_ptr(gic->dist_base.percpu_base, cpu) = dist_base + offset;
*per_cpu_ptr(gic->cpu_base.percpu_base, cpu) = cpu_base + offset;
}
gic_set_base_accessor(gic, gic_get_percpu_base);
} else
#endif
{ /* Normal, sane GIC... */
WARN(percpu_offset,
"GIC_NON_BANKED not enabled, ignoring %08x offset!",
percpu_offset);
gic->dist_base.common_base = dist_base;
gic->cpu_base.common_base = cpu_base;
gic_set_base_accessor(gic, gic_get_common_base);
}
/*
* Initialize the CPU interface map to all CPUs.
* It will be refined as each CPU probes its ID.
*/
for (i = 0; i < NR_GIC_CPU_IF; i++)
gic_cpu_map[i] = 0xff;
/*
* Find out how many interrupts are supported.
* The GIC only supports up to 1020 interrupt sources.
*/
/*
读取GIC 的最大支持的中断数目,从 GIC_DIST_CTR 寄存器(这是V1版本的寄存器
名字,V2中是GICD_TYPER,Interrupt Controller Type Register,)的低五位
ITLinesNumber获取的。如果ITLinesNumber等于N,那么最大支持的中断数目是
32(N+1)。此外,GIC规范规定最大的中断数目不能超过1020,1020-1023是有特
别用户的interrupt ID。
*/
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;
/*
分配一个 irq_domain 的结构,一个 irq_domain 代表了一个 GIC 控制器
*/
if (node) { /* DT case */
gic->domain = irq_domain_add_linear(node, gic_irqs,
&gic_irq_domain_hierarchy_ops,
gic);
} else { /* Non-DT case */
/*
* For primary GICs, skip over SGIs.
* For secondary GICs, skip over PPIs, too.
*/
if (gic_nr == 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 (IS_ERR_VALUE(irq_base)) {
WARN(1, "Cannot allocate irq_descs @ IRQ%d, assuming pre-allocated\n",
irq_start);
irq_base = irq_start;
}
gic->domain = irq_domain_add_legacy(node, gic_irqs, irq_base,
hwirq_base, &gic_irq_domain_ops, gic);
}
if (WARN_ON(!gic->domain))
return;
if (gic_nr == 0) {//只对root GIC操作,因为设定callback、注册Notifier只需要一次就OK了
#ifdef CONFIG_SMP
set_smp_cross_call(gic_raise_softirq);
register_cpu_notifier(&gic_cpu_notifier);
#endif
set_handle_irq(gic_handle_irq);
}
gic_dist_init(gic);//GIC Distributer 部分初始化
gic_cpu_init(gic);//GIC CPU Interface 部分初始化
gic_pm_init(gic);//GIC PM 部分初始化
}
gic_handle_irq()
static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
u64 irqnr;
do {
irqnr = gic_read_iar();//读取gic寄存器并获取hwirq中断号
if (likely(irqnr > 15 && irqnr < 1020) || irqnr >= 8192) {
int err;
err = handle_domain_irq(gic_data.domain, irqnr, regs);//外设触发的中断
if (err) {
WARN_ONCE(true, "Unexpected interrupt received!\n");
gic_write_eoir(irqnr);
}
continue;
}
if (irqnr < 16) {//软件触发的中断SGI
gic_write_eoir(irqnr);
#ifdef CONFIG_SMP
handle_IPI(irqnr, regs);//处理核间交互
#else
WARN_ONCE(true, "Unexpected SGI received!\n");
#endif
continue;
}
} while (irqnr != ICC_IAR1_EL1_SPURIOUS);
}
__handle_domain_irq()
int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
bool lookup, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
unsigned int irq = hwirq;
int ret = 0;
irq_enter();//进入中断上下文
#ifdef CONFIG_IRQ_DOMAIN
if (lookup)
irq = irq_find_mapping(domain, hwirq);//根据hwirq去查找linux中断号
#endif
/*
* Some hardware gives randomly wrong interrupts. Rather
* than crashing, do something sensible.
*/
if (unlikely(!irq || irq >= nr_irqs)) {
ack_bad_irq(irq);
ret = -EINVAL;
} else {
generic_handle_irq(irq);
/*
根据linux中断号找到对应的中断描述符,从而执行中断服务函数
*/
}
irq_exit();//退出中断上下文
set_irq_regs(old_regs);
return ret;
}