中断控制器(GIC)

中断控制器(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接口。分发器对中断的控制包括:

  1. 中断使能或禁能控制。分发器对中断的控制分成两个级别,一个是全局中断的控制(GIC_DIST_CTRL),一旦禁止了全局的中断,那么任何的中断源产生的中断事件都不会被传递到CPU接口;另外一个级别是对针对各个中断源进行控制(GIC_DIST_ENABLE_CLEAR),禁止某一个中断源会导致该中断事件不会分发到CPU接口,但不影响其他中断源产生中断事件的分发。
  2. 控制将当前优先级最高的中断事件分发到一个或者一组CPU接口。
  3. 优先级控制。
  4. 中断属性设定,例如是电平触发还是边沿触发。
  5. 中断的设定。
  6. 分发器可以管理若干个中断源,这些中断源用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)

中断状态机

在这里插入图片描述

  1. 添加挂起状态(A1、A2)
    对于一个 SGI,发生以下 2 种情况的 1 种:

    • 软件写 GICD_SGIR 寄存器,指定目标处理器
    • 目标处理器上软件写GICD_SPENDSGIRn 寄存器

    对于一个 SPI 或 PPI,发生以下 2 种情况的 1 种:
    * 外设发出一个中断请求信号
    * 软件写 GICD_ISPENDRn 寄存器

  2. 删除挂起状态(B1、B2)
    对于 SGI,目标处理器写 GICD_CPENDSGIRn 寄存器,对于一个 SPI 或 PPI,发生以下 2 种情况的 1 种:
    * 电平触发类型中断,信号取消
    * 边沿触发类型中断,软件写 GICD_ICPENDRn 寄存器

  3. 挂起到激活(C)
    如果中断使能,并且高优先级,软件从 GICC_IAR 寄存器读取时发生状态改变。

  4. 挂起到激活和挂起(D)
    对于 SGI,这种转变发生在以下任一情况下:
    * 将 SGI 状态设置为挂起的写入操作与读取 GICC_IAR 几乎同时发生
    * 当多个挂起的 SGI 具有相同 ID 时,并且它们来自同一个源处理器并指向同一个处理器。其中一个 SGI 状态变为激活(C),其他 SGI 状态变为激活和挂起(D)。
    对于 SPI 或 PPI,满足以下所有条件,则发生这种转换
    * 中断开启
    * 软件读取 GICC_IAR,读操作将激活状态添加到中断中。
    此外,还应满足以下条件之一:
    * 对于电平触发中断,中断信号保持。通常都是这样,因为外设直到处理器处理完中断后才会取消触发信号。
    * 对于边沿触发中断,是否发生此转换取决于读取 GICC_IAR 的时间(中断再次触发,上一次未处理),读取 GICC_IAR 可能会转换到 C,后面可能会转换到 A2。

  5. 删除激活状态(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(不映射);

  1. linear map:维护固定大小的表,索引是硬件中断号,如果硬件中断最大数量固定,并且数值不大,可以选择线性映射;
  2. tree map:硬件中断号可能很大,可以选择树映射;
  3. 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;
}

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

苦梨甜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值