Linux kernel的中断子系统

发布时间:2022-06-27 发布网站:脚本宝典
脚本宝典收集整理的这篇文章主要介绍了Linux kernel的中断子系统脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。
@H_304_0@

Linux kernel的中断子系统

文章目录

  • Linux kernel的中断子系统
    • 1、GICV3简介
      • 1.1 GICv3定义的4种中断类型:
      • 1.2 中断号
      • 1.3 GICV3编程模型
      • 1.4 GICV3中断亲和度路由
    • 2、GIC的设备树描述
    • 3、ARM GICV3代码分析
      • 3.1 irq chip driver声明
      • 3.2 gic_of_inIT流程
      • 3.3 gic_init_bases流程
    • 4、中断域irq_domain以及中断映射
      • 4.1 irq_domain
      • 4.2 中断映射的完整过程
    • 5、ARM64中断处理过程
      • 5.1 ARM64底层中断处理流程(GICV3和汇编部分)
        • 5.1.1 异常向量表
        • 5.1.2 linux内核底层中断处理流程(汇编部分)
          • 5.1.2.1 保存中断上下文
          • 5.1.2.2 恢复中断上下文
      • 5.2 ARM64高层中断处理(c语言部分)
        • 5.2.1 el1_irq
        • 5.2.2 gic_handle_irq
    • 6、中断函数的注册
    • 7、中断的上下部机制(TODO)
    • 8、ITS介绍以及代码分析
      • 8.1 ITS概述
      • 8.2 The ITS table
      • 8.3 The ITS Command
      • 8.4 ITS代码分析
        • 8.4.1 ITS数据结构介绍
        • 8.4.2 ITS的初始化
          • 8.4.2.1 its初始化函数its_init
          • 8.4.2.2 its_cpu_init
        • 8.4.3 its中断上报
    • 参考资料
内核版本:5.10

中断控制器:gicv3

1、GICV3简介

1.1 GICv3定义的4种中断类型:

SPI,共享外设中断,该中断来于外设,但是该中断可以对所有的cpu core有效。

PPI,私有外设中断,中断来源于外设,但是该中断只对指定的core有效。

SGI,软中断,用于给其它的core发送中断信号。

LPI,(Locality-sPEcific Peripheral Interrupt) ,自定义外设中断,这个是gicv3特有的中断。

In particular, LPIs are always message-based interrupts, and their configuration is held in tables in memory rather than registers.

NOTE: LPIs are only supported when GICD_CTLR.ARE_NS==1.

1.2 中断号

Linux kernel的中断子系统

1.3 GICV3编程模型

Linux kernel的中断子系统

对应的寄存器编程接口描述如下:(参考GICv3_Software_Overview_Official_Release_B)

Distributor (GICD_*) The Distributor registers are memory-mapped, and contain global settings that affect all PEs connected to the interrupt controller. The Distributor PRovides a programming interface for:  Interrupt prioritization and distribution of SPIs.  Enabling and disabling SPIs.  Setting the priority level of each SPI.  Routing information for each SPI.  Setting each SPI to be level-sensitive or Edge-triggered.  Generating message-based SPIs.  Controlling the active and pending state of SPIs.  Controls to determine the programmers’ model that is used in each Security state (affinity routing or legacy).

Redistributors (GICR_*) For each connected PE there is a Redistributor. The Redistributors provides a programming interface for:  Enabling and disabling SGIs and PPIs.  Setting the priority level of SGIs and PPIs.  Setting each PPI to be level-sensitive or edge-triggered.  Assigning each SGI and PPI to an interrupt group.  Controlling the state of SGIs and PPIs.  Base address control for the data structures in memory that support the associated interrupt properties and pending state for LPIs.  Power management support for the connected PE.

CPU interfaces (ICC_*_ELn)—(non memory-mapped) Each Redistributor is connected to a CPU interface. The CPU interface provides a programming interface for:  General control and configuration to enable interrupt handling.  Acknowledging an interrupt.  Performing a priority drop and deactivation of interrupts.  Setting an interrupt priority mask for the PE.  Defining the preemption policy for the PE.  Determining the highest priority pending interrupt for the PE.

In GICv3 the CPU Interface registers are accessed as System registers (ICC_*_ELn). Software must enable the System register interface before using these registers. This is controlled by the SRE bit in the ICC_SRE_ELn registers, where “n” specifies the Exception level (EL1-EL3). (这里表明GICV3,CPU interfaces对应的寄存器是作为CPU内部的系统寄存器去访问的,而不是通过memory mapped去访问的)

NOTE: In GICv1 and GICv2 the CPU Interface registers were memory mapped (GICC_*).

NOTE: Software can check for GIC System register support by reading ID_AA64PFR0_EL1 for the PE, see ARM® Architecture Reference Manual, ARMv8, for ARMv8-A architecture profile for details.

1.4 GICV3中断亲和度路由

Linux kernel的中断子系统

gicv3使用hierarchy来标识一个具体的cpu core,类似于ipv4,<affinity level 3>.<affinity level 2>.<affinity level 1>.<affinity level 0> 组成一个PE的路由。

每一个core的affinity值可以通过MPDIR_EL1寄存器获取, 每一个affinity占用8bit。 配置对应core的MPIDR值,可以将中断路由到该core上。

四个等级affinity的定义是根据SOC自己的定义。 比如可能affinity3代表socketid,affinity2 代表clusterid, affnity1代表coreid, affnity0代表thread id。

2、GIC的设备树描述

(1)参考rk3399 SDK下面的interrupt controller描述文档

rk3399_kernel/kernel/Documentation/devicetree/bindings/interrupt-controller/arm,gic-v3.txt

(2)参考标准内核里面的interrupt controller描述文档

kernel/Documentation/devicetree/bindings/interrupt-controller/arm,gic-v3.yaML

一个gicv3定义的例子如下:

gic: interrupt-controller@2c010000 {
                compatible = "arm,gic-v3";               
                #interrupt-cells = <4>;                   
                #address-cells = <2>;
                #size-cells = <2>;
                ranges;
                interrupt-controller;
                redistributor-stride = <0x0 0x40000>;   // 256kB stride
                #redistributor-regions = <2>;
                reg = <0x0 0x2c010000 0 0X10000>,       // GICD
                      <0x0 0x2d000000 0 0x800000>,      // GICR 1: CPUs 0-31
                      <0x0 0x2e000000 0 0x800000>;      // GICR 2: CPUs 32-63
                      <0x0 0x2c040000 0 0x2000>,        // GICC
                      <0x0 0x2c060000 0 0x2000>,        // GICH
                      <0x0 0x2c080000 0 0x2000>;        // GICV
                interrupts = <1 9 4>;

                gic-its@2c200000 {
                        compatible = "arm,gic-v3-its";
                        msi-controller;
                        #msi-cells = <1>;
                        reg = <0x0 0x2c200000 0 0x20000>;
                };

                gic-its@2c400000 {
                        compatible = "arm,gic-v3-its";
                        msi-controller;
                        #msi-cells = <1>;
                        reg = <0x0 0x2c400000 0 0x20000>;
                };
        };
  • compatible: 用于匹配GICv3驱动
  • #interrupt-cells: 这是一个中断控制器节点的属性。它声明了该中断控制器的中断指示符(-interrupts)中 cell 的个数
  • #address-cells , #size-cells, ranges:用于寻址, #address-cells表示reg中address元素的个数,#size-cells用来表示length元素的个数
  • interrupt-controller: 表示该节点是一个中断控制器
  • redistributor-stride: 一个GICR的大小
  • #redistributor-regions: GICR域个数。
  • **reg :**GIC的物理基地址,分别对应GICD,GICR,GICC…
  • interrupts: 分别代表中断类型,中断号,中断类型, PPI中断亲和, 保留字段。 a为0表示SPI,1表示PPI;b表示中断号(注意SPI/PPI的中断号范围);c为1表示沿中断,4表示平中断。
  • msi-controller: 表示节点是MSI控制器

3、ARM GICV3代码分析

基于内核版本5.10

代码路径:

kernel/drivers/irqchip/irq-gic-v3.c

GICV3的初始化流程:

3.1 irq chip driver声明

IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gic_of_init);

定义IRQCHIP_DECLARE之后,相应的内容会保存到__irqchip_of_table里边。

#define IRQCHIP_DECLARE(name, compat, fn) OF_DECLARE_2(irqchip, name, compat, fn)

#define OF_DECLARE_2(table, name, compat, fn)  
        _OF_DECLARE(table, name, compat, fn, of_init_fn_2)

#define _OF_DECLARE(table, name, compat, fn, fn_type)             
    static const struct of_device_id __of_table_##name         
        __used __section(__##table##_of_table)             
         = { .compatible = compat,                 
             .data = (fn == (fn_type)NULL) ? fn : fn  }
宏展开过程如下:
--IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gic_of_init);
--OF_DECLARE_2(irqchip, gic_v3, "arm,gic-v3", gic_of_init)--_OF_DECLARE(irqchip, gic_v3, "arm,gic-v3", gic_of_init, of_init_fn_2)--
   static const struct of_device_id __of_table_gic_v3         
        __used __section(__irqchip_of_table)             
         = { .compatible = "arm,gic-v3",                 
             .data = gic_of_init  } 

展开以后可以看到,在vmlinux的__irqchip_of_table段中保存了一个of_device_id类型的结构体,其compatible字段为"arm,gic-v3",data指针指向gic_of_init函数。

_irqchip_of_table在vmlinux.lds文件里边被放到了 _irqchip_begin和__irqchip_of_end之间

#ifdef CONFIG_IRQCHIP
    #define IRQCHIP_OF_MATCH_TABLE()                    
        . = ALIGN(8);                           
        VMLINUX_SYMBOL(__irqchip_begin) = .;                
        *(__irqchip_of_table)                       
        *(__irqchip_of_end)
#endif

在内核启动初始化中断的函数中,of_irq_init 函数会去查找设备节点信息,该函数的传入参数就是 __irqchip_of_table 段,由于 IRQCHIP_DECLARE 已经将信息填充好了,of_irq_init 函数会根据 “arm,gic-v3” 去查找对应的设备节点,并获取设备的信息。or_irq_init 函数中,如果检测到设备树中存在和"arm,gic-v3"匹配的节点,并且节点下面存在interrupt-controller字段,那么最终会回调 IRQCHIP_DECLARE 声明的回调函数,也就是data指向的 gic_of_init函数,而这个函数就是 GIC 驱动的初始化入口。of_irq_init函数在drivers/of/irq.c中实现。

--drivers/irqchip/irqchip.c
    
void __init irqchip_init(void)
{
	of_irq_init(__irqchip_of_table);
	acpi_probe_device_table(irqchip);
}
对应arm64架构,在arch/arm64/kernel/irq.c的init_IRQ函数中会调用irqchip_init函数:
void __init init_IRQ(void)
{
     init_irq_stacks();
     irqchip_init();
     if (!handle_arch_irq)
         panic("No interrupt controller found.");

     if (system_uses_irq_prio_masking()) {
         /*
          * Now that we have a stack for our IRQ handler, set
          * the PMR/PSR pair to a consistent state.
         */
        WARN_ON(read_sysreg(daif) &amp; PSR_A_BIT);
        local_daif_reStore(DAIF_PROCCTX_NOIRQ);
     }
}
init_IRQ则是在内核启动流程中调用,init/main.c的main函数如下:
/* init some links before init_ISA_irqs() */
early_irq_init();
init_IRQ();
tick_init();
rcu_init_nohz();
init_timers();
hrtimers_init();
softirq_init();
timekeeping_init();

3.2 gic_of_init流程

static int __init gic_of_init(struct device_node *node, struct device_node *parent)
{
	void __iomem *dist_base;
	struct redist_region *rdist_regs;
	u64 redist_stride;
	u32 nr_redist_regions;
	int err, i;

	dist_base = of_iomap(node, 0);//index为0,映射GICD(GIC Distributor)的寄存器地址空间 ------------- (1)
	if (!dist_base) {
		pr_err("%pOF: unable to map gic dist registersn", node);
		return -ENXIO;
	}

	err = gic_validate_dist_version(dist_base);//检测gic的版本是否是v3或者v4,读GICD_PIDR2寄存器 --------------- (2)
	if (err) {
		pr_err("%pOF: no distributor detected, giving upn", node);
		goto out_unmap_dist;
	}

	if (of_property_read_u32(node, "#redistributor-regions", &nr_redist_regions))
        //读取设备树中#redistributor-regions的值,3个cpu clusters对应3个GICR域? --------------- (3)
		nr_redist_regions = 1;

	rdist_regs = kcalloc(nr_redist_regions, sizeof(*rdist_regs),
			     GFP_KERNEL);
	if (!rdist_regs) {
		err = -ENOMEM;
		goto out_unmap_dist;
	}

	for (i = 0; i < nr_redist_regions; i++) {
		struct resource res;
		int ret;

		ret = of_address_to_resource(node, 1 + i, &res);
		rdist_regs[i].redist_base = of_iomap(node, 1 + i);
		if (ret || !rdist_regs[i].redist_base) {
			pr_err("%pOF: couldn't map region %dn", node, i);
			err = -ENODEV;
			goto out_unmap_rdist;
		}
		rdist_regs[i].phys_base = res.start;
	}//映射每一个GICR的基地址 --------- (4)

	if (of_property_read_u64(node, "redistributor-stride", &redist_stride))
		redist_stride = 0;//读取DTS中redistributor-stride的值,redistributor-stride代表GICR域中每一个GICR的大小,正常情况下一个CPU core对应一个GICR(redistributor-stride必须是64KB的倍数),一个cpu cluster中cpu core的个数乘以redistributor-stride的值等于一个GICR域的大小。----------- (5)

	gic_enable_of_quirks(node, gic_quirks, &gic_data);

	err = gic_init_bases(dist_base, rdist_regs, nr_redist_regions,
			     redist_stride, &node->fwnode);//gic初始化函数 ------------- (6)
	if (err)
		goto out_unmap_rdist;

	gic_populate_ppi_partitions(node);

	if (static_branch_likely(&supports_deactivate_key))
		gic_of_SETUP_KVM_info(node);//虚拟化相关设置
	return 0;

out_unmap_rdist:
	for (i = 0; i < nr_redist_regions; i++)
		if (rdist_regs[i].redist_base)
			iounmap(rdist_regs[i].redist_base);
	kfree(rdist_regs);
out_unmap_dist:
	iounmap(dist_base);
	return err;
}

(1)映射GICD的寄存器地址空间。 通过设备结点直接进行设备内存区间的 ioremap(),index是内存段的索引。若设备结点的reg属性有多段,可通过index标示要ioremap的是哪一段,只有1段的情况, index为0。采用Device Tree后,大量的设备驱动通过of_iomap()进行映射,而不再通过传统的ioremap。

(2) 验证GICD的版本是否为GICv3 or GICv4。 主要通过读GICD_PIDR2寄存器bit[7:4]. 0x1代表GICv1, 0x2代表GICv2…以此类推。

(3) 通过DTS读取redistributor-regions的值。redistributor-regions代表GICR独立的区域数量(地址连续)。 假设一个64核的arm64 服务器,redistributor-regions=2, 那么64个核可以用2个连续的GICR连续空间表示。

(4) 为一个GICR域 分配基地址。

(5) 通过DTS读取redistributor-stride的值. redistributor-stride代表GICR域中每一个GICR的大小,正常情况下一个CPU对应一个GICR(redistributor-stride必须是64KB的倍数)

(6) 主要处理流程,下面介绍。

(7) 可以设置一组PPI的亲和性。(TODO:分析PPI亲和度的设置过程)

3.3 gic_init_bases流程

static int __init gic_init_bases(void __iomem *dist_base,
				 struct redist_region *rdist_regs,
				 u32 nr_redist_regions,
				 u64 redist_stride,
				 struct fwnode_handle *handle)
{
	u32 typer;
	int err;

	if (!is_hyp_mode_available())
		static_branch_disable(&supports_deactivate_key);

	if (static_branch_likely(&supports_deactivate_key))
		pr_info("GIC: Using split EOI/Deactivate moden");

	gic_data.fwnode = handle;
	gic_data.dist_base = dist_base;
	gic_data.redist_regions = rdist_regs;
	gic_data.nr_redist_regions = nr_redist_regions;
	gic_data.redist_stride = redist_stride; //初始化全局结构体static struct gic_chip_data gic_data

	/*
	 * Find out how many interrupts are supported.
	 */
	typer = readl_relaxed(gic_data.dist_base + GICD_TYPER);//读取GICD_TYPER寄存器的值,后面可以根据typer计算得到所支持的SPI中断号最大值为多少 ------------- (1)
	gic_data.rdists.gicd_typer = typer;

	gic_enable_quirks(readl_relaxed(gic_data.dist_base + GICD_IIDR),
			  gic_quirks, &gic_data);

	pr_info("%d SPIs implementedn", GIC_LINE_NR - 32);
    //展开GIC_LINE_NR可以计算得到SPI中断号的最大值,GICD_TYPER寄存器bit[4:0], 如果该字段的值为N,则最大SPI INTID为32(N + 1)-1
	pr_info("%d Extended SPIs implementedn", GIC_ESPI_NR);

	/*
	 * ThunderX1 explodes on reading GICD_TYPER2, in violation of the
	 * architecture spec (which says that reserved registers are RES0).
	 */
	if (!(gic_data.flags & FLAGS_WORKAROUND_CAVIUM_ERRATUM_38539))
		gic_data.rdists.gicd_typer2 = readl_relaxed(gic_data.dist_base + GICD_TYPER2);

	gic_data.domain = irq_domain_create_tree(handle, &gic_irq_domain_ops,
						 &gic_data);//向系统中注册一个irq domain数据结构 ------------- (2) 
	gic_data.rdists.rdist = alloc_percpu(typeof(*gic_data.rdists.rdist));
	gic_data.rdists.has_rvpeid = true;
	gic_data.rdists.has_vlpis = true;
	gic_data.rdists.has_direct_lpi = true;
	gic_data.rdists.has_vpend_valid_dirty = true;

	if (WARN_ON(!gic_data.domain) || WARN_ON(!gic_data.rdists.rdist)) {
		err = -ENOMEM;
		goto out_free;
	}

	irq_domain_update_bus_token(gic_data.domain, DOMAIN_BUS_WIRED);// ------------- (3)

	gic_data.has_rss = !!(typer & GICD_TYPER_RSS);// ------------- (4)
	pr_info("Distributor has %sRange Selector supportn",
		gic_data.has_rss ? "" : "no ");

	if (typer & GICD_TYPER_MBIS) {
		err = mbi_init(handle, gic_data.domain);// ------------- (5)
		if (err)
			pr_err("Failed to initialize MBIsn");
	}

	set_handle_irq(gic_handle_irq);// 设定arch相关的irq handler。gic_irq_handle是内核gic中断处理的入口函数 ------------- (6)

	gic_update_rdist_properties();

	gic_dist_init();
	gic_cpu_init();
	gic_smp_init();
	gic_cpu_pm_init();

	if (gic_dist_supports_lpis()) {
		its_init(handle, &gic_data.rdists, gic_data.domain);
		its_cpu_init();
	} else {
		if (IS_ENABLED(CONFIG_ARM_GIC_V2M))
			gicv2m_init(handle, gic_data.domain);
	}

	gic_enable_nmi_support();

	return 0;

out_free:
	if (gic_data.domain)
		irq_domain_remove(gic_data.domain);
	free_percpu(gic_data.rdists.rdist);
	return err;
}

(1) 确认支持SPI 中断号最大的值为多少,GICv3最多支持1020个中断(SPI+SGI+SPI).GICD_TYPER寄存器bit[4:0], 如果该字段的值为N,则最大SPI INTID为32(N + 1)-1。 例如,0x00011指定最大SPI INTID为127。

(2) 向系统中注册一个irq domain的数据结构. irq_domain主要作用是将硬件中断号映射到IRQ number。 参考第4节:中断域irq_domain以及中断映射

(3) 主要作用是给irq_find_host()函数使用,找到对应的irq_domain。 这里使用 DOMAIN_BUS_WIRED,主要作用就是区分其他domain, 如MSI。

(4) 判断GICD 是否支持rss, rss(Range Selector Support)表示SGI中断亲和性的范围 GICD_TYPER寄存器bit[26], 如果该字段为0,表示中断路由(IRI) 支持affinity 0-15的SGI,如果该字段为1, 表示支持affinity 0 - 255的SGI。

(5) 判断是否支持通过写GICD寄存器生成消息中断。GICD_TYPER寄存器bit[16]。

(6) 设定arch相关的irq handler。gic_irq_handle是内核gic中断处理的入口函数。

(7) 更新vlpi相关配置。gic虚拟化相关。

(8) 初始化ITS。 Interrupt Translation Service, 用来解析LPI中断。 初始化之前需要先判断GIC是否支持LPI,该功能在ARM里是可选的。

(9) 该函数主要包含两个作用。 1.设置核间通信函数。当一个CPU core上的软件控制行为需要传递到其他的CPU上的时候,就会调用这个callback函数(例如在某一个CPU上运行的进程调用了系统调用进行reboot)。对于GIC v3,这个callback定义为gic_raise_softirq. 2. 设置CPU 上下线流程中和GIC相关的状态机。

(10) 初始化GICD。

(11) 初始化CPU interface。

(12) 初始化GIC电源管理。

4、中断域irq_domain以及中断映射

gic的中断处理程序是从ack一个硬件中断开始的, 在gic的中断处理过程中,会根据中断的映射去寻找对应的虚拟中断号, 再去进行后续的中断处理。

那么问题来了,为什么要有一个虚拟中断号的概念? 当前的SOC,通常内部会有多个中断控制器(比如gic interrupt controller, gpio interrupt controller), 每一个中断控制器对应多个中断号, 而硬件中断号在不同的中断控制器上是会重复编码的, 这时仅仅用硬中断号已经不能唯一标识一个外设中断。 对于软件工程师而言,我们不需要care是中断哪个中断控制器的第几个中断号, 因此linux kernel提供了一个虚拟中断号的概念。

4.1 irq_domain

linux kernel提供irq_domain的管理框架, 将hwirq映射到虚拟中断号上。每一个中断控制器都需要注册一个irq_domain。

irq_domian数据结构:

/**
 * struct irq_domain - Hardware interrupt number translation object
 * @link: Element in global irq_domain list.
 * @name: Name of interrupt domain
 * @ops: pointer to irq_domain methods
 * @host_data: private data pointer for use by owner.  Not touched by irq_domain
 *             core code.
 * @flags: host per irq_domain flags
 * @;mapcount: The number of mapped interrupts
 *
 * Optional elements
 * @fwnode: Pointer to firmware node associated with the irq_domain. Pretty easy
 *          to swap it for the of_node via the irq_domain_get_of_node accessor
 * @gc: Pointer to a list of generic chips. There is a helper function for
 *      setting up one or more generic chips for interrupt controllers
 *      drivers using the generic chip library which uses this pointer.
 * @parent: Pointer to parent irq_domain to support hierarchy irq_domains
 * @debugfs_file: dentry for the domain debugfs file
 *
 * revmap data, used internally by irq_domain
 * @revmap_direct_max_irq: The largest hwirq that can be set for controllers that
 *                         support direct mapping
 * @revmap_Size: Size of the linear map table @linear_revmap[]
 * @revmap_tree: Radix map tree for hwirqs that don't fit in the linear map
 * @linear_revmap: Linear table of hwirq->virq reverse mappings
 */
struct irq_domain {
	struct list_head link;
	const char *name;
	const struct irq_domain_ops *ops;
	void *host_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;
	unsigned int revmap_direct_max_irq;
	unsigned int revmap_size;
	struct radix_tree_root revmap_tree;
	struct mutex revmap_tree_mutex;
	unsigned int linear_revmap[];
};

link: 用于将irq domain连接到全局链表irq_domain_list中; name: irq domain的名称; ops: irq domain映射操作使用方法的集合; mapcount: 映射好的中断的数量; fwnode: 对应中断控制器的device node; parent: 指向父级irqdomain的指针,用于支持级联irq_domain; hwirq_max: 该irq domain支持的中断最大数量; revmap_tree: Radix Tree 映射的根节点; linear_revmap: hwirq->virq 反向映射的线性表;

从该结构体中我们可以看出irq_domain支持多种类型的映射。

irq_domain映射类型:

(1) 线性映射 线性映射保留一张固定的表,通过hwirq number来索引.当hwirq被映射后, 会相应地分配一个irq_desc, IRQ number就被存在表中。当hwirqs是固定的而且小于256, 用线性映射更好。它的优势是寻找时间固定,并且irq_descs只在in-use IRQs分配。缺点是表格和hwirq 最大numbers一样大.

irq_domain_add_linear

(2) 树映射 此种方法使用radix tree来维护映射, 通过key来查找此方法适合hwirq number非常大的时候, 因为它不需要分配和hwirq一样大的table。 缺点是查表效率依赖table里的entries数量。

irq_domain_add_tree

(3) 不映射 当有些硬件可以对hwirq number编程时,IRQ number被编进硬件寄存器里,那么就不需要映射了。这种情况下通过irq_create_direct_mapping()实现。

irq_domain_add_nomap()

4.2 中断映射的完整过程

(1)interrupt controller初始化的过程中,注册irq domain

在前面介绍的gic_of_init函数中,gic会去注册irq_domain(申请一个irq_domain数据结构,并且添加到全局链表irq_domain_list):

-->gic_of_init
    -->gic_init_bases
    	-->irq_domain_create_tree
    		-->__irq_domain_add
__irq_domain_add(fwnode, 0, ~0, 0, ops, host_data);//第二个参数size=0,第四个参数direct_max=0,表明gic-v3的irq_domain的映射类型为radix tree mapping

__irq_domain_add的函数实现在kernel/irq/irqdomain.c中,如下:

/**
 * __irq_domain_add() - Allocate a new irq_domain data structure
 * @fwnode: firmware node for the interrupt controller
 * @size: Size of linear map; 0 for radix mapping only
 * @hwirq_max: Maximum number of interrupts supported by controller
 * @direct_max: Maximum value of direct maps; Use ~0 for no limit; 0 for no
 *              direct mapping
 * @ops: domain callbacks
 * @host_data: Controller private data pointer
 *
 * Allocates and initializes an irq_domain structure.
 * Returns pointer to IRQ domain, or NULL on failure.
 */
struct irq_domain *__irq_domain_add(struct fwnode_handle *fwnode, int size,
				    irq_hw_number_t hwirq_max, int direct_max,
				    const struct irq_domain_ops *ops,
				    void *host_data)
{
	struct irqchip_fwid *fwid;
	struct irq_domain *domain;

	static atomic_t unknown_domains;

	domain = kzalloc_node(sizeof(*domain) + (sizeof(unsigned int) * size),
			      GFP_KERNEL, of_node_to_nid(to_of_node(fwnode)));
	if (!domain)
		return NULL;

	if (is_fwnode_irqchip(fwnode)) {
		fwid = container_of(fwnode, struct irqchip_fwid, fwnode);

		switch (fwid->type) {
		case IRQCHIP_FWNODE_NAMED:
		case IRQCHIP_FWNODE_NAMED_ID:
			domain->fwnode = fwnode;
			domain->name = kstrdup(fwid->name, GFP_KERNEL);
			if (!domain->name) {
				kfree(domain);
				return NULL;
			}
			domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
			break;
		default:
			domain->fwnode = fwnode;
			domain->name = fwid->name;
			break;
		}
	} else if (is_of_node(fwnode) || is_acpi_device_node(fwnode) ||
		   is_software_node(fwnode)) {
		char *name;

		/*
		 * fwnode paths contain '/', which debugfs is legitiMATEly
		 * unhappy about. Replace them with ':', which does
		 * the trick and is not as offensive as ''...
		 */
		name = kasprintf(GFP_KERNEL, "%pfw", fwnode);
		if (!name) {
			kfree(domain);
			return NULL;
		}

		strreplace(name, '/', ':');

		domain->name = name;
		domain->fwnode = fwnode;
		domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
	}

	if (!domain->name) {
		if (fwnode)
			pr_err("Invalid fwnode type for irqdomainn");
		domain->name = kasprintf(GFP_KERNEL, "unknown-%d",
					 atomic_inc_return(&unknown_domains));
		if (!domain->name) {
			kfree(domain);
			return NULL;
		}
		domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
	}

	fwnode_handle_get(fwnode);

	/* Fill structure */
	INIT_RADIX_TREE(&domain->revmap_tree, GFP_KERNEL);
	mutex_init(&domain->revmap_tree_mutex);
	domain->ops = ops;
	domain->host_data = host_data;
	domain->hwirq_max = hwirq_max;
	domain->revmap_size = size;
	domain->revmap_direct_max_irq = direct_max;
	irq_domain_check_hierarchy(domain);

	mutex_lock(&irq_domain_mutex);
	debugfs_add_domain_dir(domain);
	list_add(&domain->link, &irq_domain_list);//将irq_domain添加到全局链表irq_domain_list
	mutex_unlock(&irq_domain_mutex);

	pr_debug("Added domain %sn", domain->name);
	return domain;
}
export_SYMBOL_GPL(__irq_domain_add);

从函数的注释我们也可以看出来,__irq_domain_add用于申请并且初始化一个irq_domain结构体。

irq_domain分配的内存大小为sizeof(*domain) + (sizeof(unsigned int) * size), (sizeof(unsigned int) * size)大小的空间是用于linear_revmap[]成员。最后,irq_domain添加到全局的链表irq_domain_list中。

(2)内核在设备初始化过程中(解析设备树创建device的过程),创建硬中断号和虚拟中断号的映射关系

内核启动过程在进行系统初始化时,do_initcall()函数会调用系统中所有的initcall回调函数进行初始化,其中of_platform_default_populate_init()函数定义为arch_initcall_sync类型的初始化函数。

--drivers/of/platform.c
static int __init of_platform_default_populate_init(void)
{
	struct device_node *node;

	device_links_supplier_sync_state_pause();

	if (!of_have_populated_dt())
		return -ENODEV;

	/*
	 * Handle certain compatibles explicitly, since we don't want to create
	 * platform_devices for every node in /reserved-memory with a
	 * "compatible",
	 */
	for_each_matching_node(node, reserved_mem_matches)
		of_platform_device_create(node, NULL, NULL);

	node = of_find_node_by_path("/firmware");
	if (node) {
		of_platform_populate(node, NULL, NULL, NULL);
		of_node_put(node);
	}

	/* Populate everything else. */
	fw_devlink_pause();
	of_platform_default_populate(NULL, NULL, NULL);
	fw_devlink_resume();

	return 0;
}

of_platform_default_populate()函数会枚举并且初始化总线上的设备,比如一般设备树中soc节点的compatiable为"simple-bus",那么该函数匹配到这个字段以后就会去枚举soc下面所有的设备。最终会解析总线下面每个device node的设备树信息,填充device结构体,并完成device的注册。我们关注这个过程当中的各个device的中断映射。

-->do_one_initcall()
   -->of_platform_default_populate(NULL, NULL, NULL);
	  -->of_platform_populate(root, of_default_bus_match_table, lookup, parent);
		 -->of_platform_bus_create(child, matches, lookup, parent, true);
			-->of_platform_device_create_pdata(bus, bus_id, platform_data, parent);
			  -->of_device_alloc(np, bus_id, parent);
				 -->of_irq_to_resource_table(np, res, num_irq)
                    -->of_irq_to_resource(dev, i, res)
   					   -->of_irq_get(dev, index);
//在of_irq_get函数中会完成该device node中和中断有关的内容的解析,并且建立映射关系

/**
 * of_irq_get - Decode a node's IRQ and return it as a Linux IRQ number
 * @dev: pointer to device tree node
 * @index: zero-based index of the IRQ
 *
 * Returns Linux IRQ number on success, or 0 on the IRQ mapping failure, or
 * -EPROBE_DEFER if the IRQ domain is not yet created, or error code in case
 * of any other failure.
 */
int of_irq_get(struct device_node *dev, int index)
{
	int rc;
	struct of_phandle_args oirq;
	struct irq_domain *domain;

	rc = of_irq_parse_one(dev, index, &oirq);//解析device node中和中断相关的信息
	if (rc)
		return rc;

	domain = irq_find_host(oirq.np);//oirq.np指向interrupt-controller,通过这个函数拿到前面gic的初始化过程中已经注册的irq_domain
	if (!domain)
		return -EPROBE_DEFER;

	return irq_create_of_mapping(&oirq);
}
EXPORT_SYMBOL_GPL(of_irq_get);

of_irq_parse_one()函数用于解析dts文件中device node和中断有关的属性,比如interrupt属性。irq_create_of_mapping函数完成具体的硬件中断到虚拟中断号的映射过程。

--> irq_create_of_mapping
   -->irq_create_fwspec_mapping
	  --> irq_find_matching_fwspec // 找到device node对应的irq_domain, 每一个irq_domain都定义了一系列的映射相关的方法
	  --> irq_domain_translate //解析中断信息,如硬件中断号, 中断触发类型
		  --> domain->ops->translate (gic_irq_domain_translate)//在这里回调irq_domain ops提供的translate方法,这个方法在gic-v3的驱动代码中定义
	  --> irq_domain_alloc_descs // 映射硬件中断号到虚拟中断号
		 -->__irq_domain_alloc_irqs
    		--> irq_domain_alloc_descs//分配一个虚拟中断号 从allocated_irqs位图中取第一个空闲的bit位作为虚拟中断号
    		-->irq_domain_alloc_irqs_hierarchy
			   --> domain->ops->alloc (gic_irq_domain_alloc)
				   --> gic_irq_domain_map //gic创建硬中断和虚拟中断号的映射,并且根据中断类型设置struct irq_desc->handle_irq处理函数
    			      -->irq_domain_set_info
    					 -->irq_domain_set_hwirq_and_chip
			-->irq_domain_insert_irq(virq + i);//在irq_domain的radix tree中插入hwirq和irq_data的映射关系
			   -->irq_domain_set_mapping(domain, data->hwirq, data);
				  -->radix_tree_insert(&domain->revmap_tree, hwirq, irq_data);
/**
 * irq_domain_set_hwirq_and_chip - Set hwirq and irqchip of @virq at @domain
 * @domain:	Interrupt domain to match
 * @virq:	IRQ number
 * @hwirq:	The hwirq number
 * @chip:	The associated interrupt chip
 * @chip_data:	The associated chip data
 */
int irq_domain_set_hwirq_and_chip(struct irq_domain *domain, unsigned int virq,
				  irq_hw_number_t hwirq, struct irq_chip *chip,
				  void *chip_data)
{
	struct irq_data *irq_data = irq_domain_get_irq_data(domain, virq);

	if (!irq_data)
		return -ENOENT;

	irq_data->hwirq = hwirq;
	irq_data->chip = chip ? chip : &no_irq_chip;
	irq_data->chip_data = chip_data;

	return 0;
}
EXPORT_SYMBOL_GPL(irq_domain_set_hwirq_and_chip);

在irq_domain_set_hwirq_and_chip函数中,通过虚拟中断号virq获取irq_data数据结构,并把硬件中断号hwirq设置给irq_data->hwirq,就完成了硬件中断号到软件虚拟中断号的映射。参数chip在这里指的就是在gic-v3驱动中定义的gic-v3底层操作相关的方法集合。

static struct irq_chip gic_chip = {
	.name			= "GICv3",
	.irq_mask		= gic_mask_irq,
	.irq_unmask		= gic_unmask_irq,
	.irq_eoi		= gic_eoi_irq,
	.irq_set_type		= gic_set_type,
	.irq_set_affinity	= gic_set_affinity,
	.irq_retrigger          = gic_retrigger,
	.irq_get_irqchip_state	= gic_irq_get_irqchip_state,
	.irq_set_irqchip_state	= gic_irq_set_irqchip_state,
	.irq_nmi_setup		= gic_irq_nmi_setup,
	.irq_nmi_teardown	= gic_irq_nmi_teardown,
	.ipi_send_mask		= gic_ipi_send_mask,
	.flags			= IRQCHIP_SET_TYPE_MASKED |
				  IRQCHIP_SKIP_SET_WAKE |
				  IRQCHIP_MASK_ON_SUSPEND,
};

中断映射过程数据结构之间的关系如下:

Linux kernel的中断子系统

这里注意一下gic_irq_domain_translate函数通过设备树参数计算得到硬件中断号的代码,设备树中分配的中断号加上offset才是真正的硬件中断号。

		switch (fwspec->param[0]) {
		case 0:			/* SPI */
			*hwirq = fwspec->param[1] + 32;
			break;
		case 1:			/* PPI */
			*hwirq = fwspec->param[1] + 16;
			break;
		case 2:			/* ESPI */
			*hwirq = fwspec->param[1] + ESPI_BASE_INTID;
			break;
		case 3:			/* EPPI */
			*hwirq = fwspec->param[1] + EPPI_BASE_INTID;
			break;
		case GIC_IRQ_TYPE_LPI:	/* LPI */
			*hwirq = fwspec->param[1];
			break;
		case GIC_IRQ_TYPE_PARTITION:
			*hwirq = fwspec->param[1];
			if (fwspec->param[1] >= 16)
				*hwirq += EPPI_BASE_INTID - 16;
			else
				*hwirq += 16;
			break;
		default:
			return -EINVAL;

完整的映射过程总结如下:

1、gic的初始化流程中申请并且注册一个irq_domain,每一个intr controller都对应一个irq_domain
--gic_of_init
  --gic_init_bases
    --irq_domain_create_tree
    
2、内核的启动流程中,会解析并且遍历设备树。在解析总线下面每个device node的设备树信息,填充device结构体,并完成device注册的过程中,会建立硬件中断号和虚拟中断号之间的映射关系
----of_device_alloc(np, bus_id, parent);
  ----of_irq_to_resource_table(np, res, num_irq)// kernel/drivers/of/irq.c
    ----of_irq_to_resource(dev, i, res)
   	  ----of_irq_get(dev, index)
        ----irq_create_of_mapping  // kernel/irq/irqdomain.c 在这里完成具体的映射过程
		  ----irq_create_fwspec_mapping(&fwspec); //struct irq_fwspec fwspec通过解析设备树得到
			----domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_WIRED);// (1)找到该设备中断对应的intr controller的irq domain
			----irq_domain_translate(domain, fwspec, &hwirq, &type)// (2)回调irq_domain ops提供的translate方法,这个方法在gic-v3的驱动代码中定义,解析中断信息,如硬件中断号, 中断触发类型等
            ----virq = irq_domain_alloc_irqs(domain, 1, NUMA_NO_NODE, fwspec);// (3)分配一个虚拟中断号,从allocated_irqs位图中取第一个空闲的bit位作为虚拟中断号virq,分配一个irq_desc结构体,通过radix_tree建立virq和irq_desc的映射关系
			  ----virq = irq_domain_alloc_descs(irq_base, nr_irqs, 0, node, affinity);
 				----virq = __irq_alloc_descs(virq, virq, cnt, node, THIS_MODULE,affinity);// kernel/kernel/irq/irqdesc.c
			      ----start = bitmap_find_next_zero_area(allocated_irqs, IRQ_BITMAP_BITS,From, cnt, 0);
				  ----ret = alloc_descs(start, cnt, node, affinity, owner);
					----desc = alloc_desc(start + i, node, flags, mask, owner);// 为irq_desc分配内存,申请一个irq_desc结构体
					----irq_insert_desc(start + i, desc); //将virq和irq_desc插入radix tree,建立两者的映射关系
			  ----irq_domain_alloc_irq_data(domain, virq, nr_irqs)// (4)现在已经分配好virq和virq对应的irq_desc,在这里为其对应的irq_data分配内存并且申请一个irq_data结构体
              ----ret = irq_domain_alloc_irqs_hierarchy(domain, virq, nr_irqs, arg); // (5)回调irq_domain ops提供的alloc方法,填充irq_data结构体,irq_data->hwirq = hwirq;,irq_data->chip = chip;chip指向在gic-v3驱动中定义的gic-v3底层操作相关的方法集合。
				----gic_irq_domain_alloc
                  ----gic_irq_domain_map(domain, virq + i, hwirq + i);
					----irq_domain_set_info
                      ----irq_domain_set_hwirq_and_chip(domain, virq, hwirq, chip, chip_data);
              ----irq_domain_insert_irq(virq + i);// (6)将hwirq和irq_data插入irq_domain下revmap_tree指针指向的radix_tree,建立硬件中断号和irq_data之间的映射关系
				----irq_domain_set_mapping(domain, data->hwirq, data);
				  ----radix_tree_insert(&domain->revmap_tree, hwirq, irq_data);

1、在枚举设备树中的设备时,会去解析设备对应的中断信息。首先根据设备树里面中断的拓扑关系得到该设备中断对应的中断控制器的irq_domain。

2、回调irq_domain ops提供的translate方法,完成该设备中断的具体解析工作,得到硬件中断号hwirq,中断触发类型irq type等信息。

3、从allocated_irqs位图中取第一个空闲的bit位作为虚拟中断号virq,申请内存并且分配一个irq_desc结构体,通过radix_tree建立virq和irq_desc的映射关系。

4、申请内存并且分配一个irq_data结构体,前面分配的irq_desc结构体中的irq_data指针指向这个irq_data。

5、回调irq_domain ops提供的alloc方法,填充irq_data结构体,irq_data->hwirq = hwirq;,irq_data->chip = chip;chip指向在gic-v3驱动中定义的gic-v3底层操作相关的方法集合。

6、将hwirq和irq_data插入irq_domain下revmap_tree指针指向的radix_tree,建立硬件中断号和irq_data之间的映射关系。

可以看到在映射过程中,涉及到一个位图数据结构,allocated_irqs位图用于空闲虚拟中断号的分配,还涉及到两个radix tree数据结构,一个radix tree用于保存virq和irq__desc的映射关系,另外一个radix tree (domain->revmap_tree)用于保存hwirq和irq_data的映射关系。

至此每个设备的device node和irq_domain、virq、hwirq、irq_desc、irq_data、chip之间的链接关系已经全部确立。

5、ARM64中断处理过程

5.1 ARM64底层中断处理流程(GICV3和汇编部分)

当GIC接收到一个中断信号以后,处理流程如下:

1、进入pending状态 (参考GICv3_Software_Overview_Official_Release_B第五节Handling Interrupts)

  • Group enables
  • Interrupt enables
  • Routing controls
  • Interrupt priority & priority mask
  • Running priority

2、cpu应答中断以后 (参考Arm_Architecture_Reference_Manual_Armv8_for Armv8-A_architecture_profile D.1.10节)

  • 处理器的状态保存在对应的异常等级的SPSR_ELx中
  • 返回地址保存在对应的异常等级
  • PSTATE寄存器里的DAIF域都设置为1,相当于把调试异常、系统错误(SError)、IRQ以及FIQ都关闭了
  • 设置栈指针,指向对应异常等级里的栈
  • 处理器等级切换到对应的异常等级,然后跳转到异常向量表执行

5.1.1 异常向量表

异常向量表存放的基地址可以通过向量基址寄存器(Vcetor Base Address Register,vbAR)来设置。VBAR是异常向量表的基地址寄存器。

ARMv8架构针对不同的运行状态定义了4种不同类型的异常向量表,如下:

Linux kernel的中断子系统

Linux kernel的中断子系统

Linux kernel的中断子系统

当前异常等级指的是系统中当前最高等级的异常等级。假设当前系统只运行linux内核并且不包含虚拟化和安全特性,那么当前系统最高的异常等级就是EL1,在EL1下运行linux内核程序,在更低一级的EL0下运行用户态程序。基于这种假设,对于上面异常向量表中的4种运行状态可以做以下说明:

  • **使用SP0寄存器的当前异常等级:**表示当前系统运行在EL1(内核态)时使用EL0的栈指针SP,这是一种错误类型。
  • **使用SPx寄存器的当前异常等级:**表示当前系统运行在EL1时使用EL1的SP,这说明系统在内核态发生了异常,这是很常见的场景。
  • **在AArch64执行环境下的低异常等级:**表示当前系统运行在EL0(用户态)并且执行ARM64指令集的程序时发生了异常。
  • **在AArch32执行环境下的低异常等级:**表示当前系统运行在EL0并且执行ARM32指令集程序时发生了异常。

与上表对应,linux内核中关于异常向量表的定义在arch/arm64/kernel/entry.S中,如下:

/*
 * Exception vectors.
 */
	.pushsection ".entry.text", "ax"

	.align	11
SYM_CODE_START(vectors)
	kernel_ventry	1, sync_invalid			// Synchronous EL1t
	kernel_ventry	1, irq_invalid			// IRQ EL1t
	kernel_ventry	1, fiq_invalid			// FIQ EL1t
	kernel_ventry	1, error_invalid		// Error EL1t

	kernel_ventry	1, sync				// Synchronous EL1h
	kernel_ventry	1, irq				// IRQ EL1h
	kernel_ventry	1, fiq_invalid			// FIQ EL1h
	kernel_ventry	1, error			// Error EL1h

	kernel_ventry	0, sync				// Synchronous 64-bit EL0
	kernel_ventry	0, irq				// IRQ 64-bit EL0
	kernel_ventry	0, fiq_invalid			// FIQ 64-bit EL0
	kernel_ventry	0, error			// Error 64-bit EL0

#ifdef CONFIG_COMPAT
	kernel_ventry	0, sync_compat, 32		// Synchronous 32-bit EL0
	kernel_ventry	0, irq_compat, 32		// IRQ 32-bit EL0
	kernel_ventry	0, fiq_invalid_compat, 32	// FIQ 32-bit EL0
	kernel_ventry	0, error_compat, 32		// Error 32-bit EL0
#else
	kernel_ventry	0, sync_invalid, 32		// Synchronous 32-bit EL0
	kernel_ventry	0, irq_invalid, 32		// IRQ 32-bit EL0
	kernel_ventry	0, fiq_invalid, 32		// FIQ 32-bit EL0
	kernel_ventry	0, error_invalid, 32		// Error 32-bit EL0
#endif
SYM_CODE_END(vectors)

5.1.2 linux内核底层中断处理流程(汇编部分)

假设IRQ发生在内核态,也就是CPU正在EL1下执行内核程序的时候发生了外设中断,下面分析linux内核在汇编部分的中断处理流程。

kernel_ventry是一个宏,这里不给出完整的展开代码,简化后的部分代码如下:

.macro kernel_ventry, el, label, regsize = 64 //el, label是宏的参数
.align 7 //align是一条伪指令 align 7表示按照2^7字节对齐
sub	sp, sp, #S_FRAME_SIZE
b	el()el()_label 

/*
S_FRAME_SIZE表示栈框的大小--sizeof(struct pt_regs);
sub	sp, sp, #S_FRAME_SIZE指令让栈针sp下移S_FRAME_SIZE;

b	el()el()_label指令的说明如下:
第一个“el”表示el字符,“()”表示宏参数的结束字符,第二个“el”表示宏的参数el,“label”表示参数label
*/

当IRQ发生在内核态时,CPU会跳转到异常向量表对应的表项,也就是

kernel_ventry	1, irq				// IRQ EL1h

这个时候展开

b	el()el()_label /*  el=1 label=irq  */
---->
    b el1_irq

最终会跳转到el1_irq标签中。

/*
 * EL1 mode handlers.
 */
	.align	6
SYM_CODE_START_LOCAL_NOALIGN(el1_irq)
	kernel_entry 1
	el1_interrupt_handler handle_arch_irq
	kernel_exit 1
SYM_CODE_END(el1_irq)

el1_irq是中断处理的核心模块。

  • kernel_entry是一个宏,用来保存中断上下文。
  • el1_interrupt_handler同样是一个宏,用来处理中断,最终目的是调用handle_arch_irq函数,跳转到c语言的中断处理部分。
  • kernel_exit宏和kernel_entry宏是成对出现的,用来恢复中断上下文。
5.1.2.1 保存中断上下文

1、栈框

Linux内核中定义了一个pt_regs数据结构来描述内核栈上寄存器的排列信息。

arch/arm64/include/asm/ptrace.h
/*
 * This struct defines the way the registers are stored on the stack during an
 * exception. Note that sizeof(struct pt_regs) has to be a multiple of 16 (for
 * stack alignment). struct user_pt_regs must form a prefix of struct pt_regs.
 */
struct pt_regs {
	union {
		struct user_pt_regs user_regs;
		struct {
			u64 regs[31];
			u64 sp;
			u64 pc;
			u64 pstate;
		};
	};
	u64 orig_x0;
#ifdef __AARCH64EB__
	u32 unused2;
	s32 syscallno;
#else
	s32 syscallno;
	u32 unused2;
#endif

	u64 orig_addr_limit;
	/* Only valid when ARM64_HAS_IRQ_PRIO_MASKING is enabled. */
	u64 pmr_save;
	u64 stackframe[2];

	/* Only valid for some EL1 exceptions. */
	u64 locKDEp_hardirqs;
	u64 exit_rcu;
};

pt_regs数据结构定义了34个寄存器,分别代表x0~x30、SP寄存器、PC寄存器以及PSTATE寄存器,另外还包括了stackframe等信息。

Linux内核定义了很多宏来访问pt_regs数据结构对应的栈框,如前面使用到的S_FRAME_SIZE,有很多汇编代码会直接使用这些宏。

arch/arm64/kernel/asm-offsets.c
    
DEFINE(S_FRAME_SIZE,		sizeof(struct pt_regs));

2、保存中断上下文

kernel_entry宏用来保存中断上下文。该宏有一个参数el,el=1时会保存发生在EL1的异常现场,当el=0时,保存EL0的异常现场。

	.macro	kernel_entry, el, regsize = 64
	.if	regsize == 32
	mov	w0, w0				// zero upper 32 bits of x0
	.endif
	stp	x0, x1, [sp, #16 * 0]
	stp	x2, x3, [sp, #16 * 1]
	stp	x4, x5, [sp, #16 * 2]
	stp	x6, x7, [sp, #16 * 3]
	stp	x8, x9, [sp, #16 * 4]
	stp	x10, x11, [sp, #16 * 5]
	stp	x12, x13, [sp, #16 * 6]
	stp	x14, x15, [sp, #16 * 7]
	stp	x16, x17, [sp, #16 * 8]
	stp	x18, x19, [sp, #16 * 9]
	stp	x20, x21, [sp, #16 * 10]
	stp	x22, x23, [sp, #16 * 11]
	stp	x24, x25, [sp, #16 * 12]
	stp	x26, x27, [sp, #16 * 13]
	stp	x28, x29, [sp, #16 * 14]

	.if	el == 0
	clear_gp_regs
	mrs	x21, sp_el0
	ldr_this_cpu	tsk, __entry_task, x20
	msr	sp_el0, tsk

	/*
	 * Ensure MDSCR_EL1.SS is clear, since we can unmask debug exceptions
	 * when scheduling.
	 */
	ldr	x19, [tsk, #TSK_TI_FLAGS]
	disable_step_tsk x19, x20

	/* Check for asynchronous tag check faults in user space */
	check_mte_async_tCF x22, x23
	apply_ssbd 1, x22, x23

	ptrauth_keys_install_kernel tsk, x20, x22, x23

	scs_load tsk, x20
	.else
	add	x21, sp, #S_FRAME_SIZE
	get_current_task tsk
	/* Save the task's original addr_limit and set USER_DS */
	ldr	x20, [tsk, #TSK_TI_ADDR_LIMIT]
	str	x20, [sp, #S_ORIG_ADDR_LIMIT]
	mov	x20, #USER_DS
	str	x20, [tsk, #TSK_TI_ADDR_LIMIT]
	/* No need to reset PSTATE.UAO, hardware's already set it to 0 for us */
	.endif /* el == 0 */
	mrs	x22, elr_el1
	mrs	x23, spsr_el1
	stp	lr, x21, [sp, #S_LR]

	/*
	 * In order to be able to dump the contents of struct pt_regs at the
	 * time the exception was taken (in case we attempt to walk the call
	 * stack later), chain it together with the stack frames.
	 */
	.if el == 0
	stp	xzr, xzr, [sp, #S_STACKFRAME]
	.else
	stp	x29, x22, [sp, #S_STACKFRAME]
	.endif
	add	x29, sp, #S_STACKFRAME

#ifdef CONFIG_ARM64_SW_TTBR0_PAN
alternative_if_not ARM64_HAS_PAN
	bl	__swpan_entry_elel
alternative_else_nop_endif
#endif

	stp	x22, x23, [sp, #S_PC]

	/* Not in a syscall by default (el0_svc overwrites for real syscall) */
	.if	el == 0
	mov	w21, #NO_SYSCALL
	str	w21, [sp, #S_SYSCALLNO]
	.endif

	/* Save pmr */
alternative_if ARM64_HAS_IRQ_PRIO_MASKING
	mrs_s	x20, SYS_ICC_PMR_EL1
	str	x20, [sp, #S_PMR_SAVE]
	mov	x20, #GIC_PRIO_IRQON | GIC_PRIO_PSR_I_SET
	msr_s	SYS_ICC_PMR_EL1, x20
alternative_else_nop_endif

	/* Re-enable tag checking (TCO set on exception entry) */
#ifdef CONFIG_ARM64_MTE
alternative_if ARM64_MTE
	SET_PSTATE_TCO(0)
alternative_else_nop_endif
#endif

	/*
	 * Registers that may be useful after this macro is invoked:
	 *
	 * x20 - ICC_PMR_EL1
	 * x21 - aborted SP
	 * x22 - aborted PC
	 * x23 - aborted PSTATE
	*/
	.endm
  • 首先,保存x0~x29寄存器到栈中。在前面介绍的异常向量表项中已经把SP指向了栈框的底部
sub	sp, sp, #S_FRAME_SIZE

因此在栈框的底部保存了x0寄存器的值,依此类推保存了x1-x29寄存器的值。stp指令是多字节存储指令。

  • 然后,处理异常发生在EL0或者EL1的场景

当异常发生在EL0时,执行以下操作:

(1)调用clear_gp_regs宏来清楚x0-x29寄存器的值。

(2)保存sp_el0的值到x21寄存器中。

(3)ldr_this_cpu是一个宏。该宏有3个参数,参数1是task_struct数据结构;参数2是task_struct的Per-CPU变量,用来获取当前cpu的当前进程的数据结构task_struct;参数3是一个临时使用的通用寄存器x20。

(4)把thread_info.flags的值加载到x19寄存器当中,其中TSK_TI_FLAGS是thread_info.flags在task_struct数据结构中的偏移量。

(5)disable_step_tsk是一个宏,如果进程允许单步调试,那么关闭MDSCR_EL1中的软件单步控制功能。

当异常发生在EL1时,执行以下操作:

(1)x21寄存器指向栈顶。

(2)get_thread_info是一个宏,通过sp_el0寄存器来获取task_struct数据结构中的指针。

(3)获取thread_info.addr_limit的值,然后保存在栈框的orig_addr_limit位置上。

(4)设置USER_DS到task_struct的thread_info.addr_limit.

接下来是EL0和EL1都会进行的操作:

把ELR_EL1的值保存到x22寄存器中;

把SPSR_EL1的值保存到x23寄存器中;

把LR和x21寄存器保存到栈框的regs[30]的位置上。

再接下来如果异常发生在EL0,那么把栈框的stackframe[]字段清零。xzr表示64位的零寄存器。如果异常发生在EL1,那么把栈框的stackframe[]字段保存在x29和x22寄存器中。

接下来,x29寄存器指向栈框的stackframe位置。

接下来,把ELR_EL1的值保存到栈框的PC寄存器,把SPSR_EL1的值保存到PSTATE寄存器。(前面已经将这个寄存器的值分别保存到x22、x23寄存器当中)。

如果异常发生在EL0,那么接下来把当前进程的task_struct指针保存到SP_EL0寄存器。

5.1.2.2 恢复中断上下文

kernel_exit宏可以用来恢复中断上下文。

	.macro	kernel_exit, el
	.if	el != 0
	disable_daif

	/* Restore the task's original addr_limit. */
	ldr	x20, [sp, #S_ORIG_ADDR_LIMIT]
	str	x20, [tsk, #TSK_TI_ADDR_LIMIT]

	/* No need to restore UAO, it will be restored from SPSR_EL1 */
	.endif

	/* Restore pmr */
alternative_if ARM64_HAS_IRQ_PRIO_MASKING
	ldr	x20, [sp, #S_PMR_SAVE]
	msr_s	SYS_ICC_PMR_EL1, x20
	mrs_s	x21, SYS_ICC_CTLR_EL1
	tbz	x21, #6, .L__skip_pmr_sync@	// Check for ICC_CTLR_EL1.PMHE
	dsb	sy				// Ensure priority change is seen by redistributor
.L__skip_pmr_sync@:
alternative_else_nop_endif

	ldp	x21, x22, [sp, #S_PC]		// load ELR, SPSR

#ifdef CONFIG_ARM64_SW_TTBR0_PAN
alternative_if_not ARM64_HAS_PAN
	bl	__swpan_exit_elel
alternative_else_nop_endif
#endif

	.if	el == 0
	ldr	x23, [sp, #S_SP]		// load return stack pointer
	msr	sp_el0, x23
	tst	x22, #PSR_MODE32_BIT		// native task?
	b.eq	3f

#ifdef CONFIG_ARM64_ERRATUM_845719
alternative_if ARM64_WORKAROUND_845719
#ifdef CONFIG_PID_IN_CONTEXTIDR
	mrs	x29, contextidr_el1
	msr	contextidr_el1, x29
#else
	msr contextidr_el1, xzr
#endif
alternative_else_nop_endif
#endif
3:
	scs_save tsk, x0

	/* No kernel C function calls after this as user keys are set. */
	ptrauth_keys_install_user tsk, x0, x1, x2

	apply_ssbd 0, x0, x1
	.endif

	msr	elr_el1, x21			// set up the return data
	msr	spsr_el1, x22
	ldp	x0, x1, [sp, #16 * 0]
	ldp	x2, x3, [sp, #16 * 1]
	ldp	x4, x5, [sp, #16 * 2]
	ldp	x6, x7, [sp, #16 * 3]
	ldp	x8, x9, [sp, #16 * 4]
	ldp	x10, x11, [sp, #16 * 5]
	ldp	x12, x13, [sp, #16 * 6]
	ldp	x14, x15, [sp, #16 * 7]
	ldp	x16, x17, [sp, #16 * 8]
	ldp	x18, x19, [sp, #16 * 9]
	ldp	x20, x21, [sp, #16 * 10]
	ldp	x22, x23, [sp, #16 * 11]
	ldp	x24, x25, [sp, #16 * 12]
	ldp	x26, x27, [sp, #16 * 13]
	ldp	x28, x29, [sp, #16 * 14]
	ldr	lr, [sp, #S_LR]
	add	sp, sp, #S_FRAME_SIZE		// restore sp

	.if	el == 0
alternative_insn eret, nop, ARM64_UNMAP_KERNEL_AT_EL0
#ifdef CONFIG_UNMAP_KERNEL_AT_EL0
	bne	4f
	msr	far_el1, x30
	tramp_alias	x30, tramp_exit_native
	br	x30
4:
	tramp_alias	x30, tramp_exit_compat
	br	x30
#endif
	.else
	/* Ensure any device/NC reads complete */
	alternative_insn nop, "dmb sy", ARM64_WORKAROUND_1508412

	eret
	.endif
	sb
	.endm

主要操作如下:

当异常发生在EL1时,恢复task_struct中的thread_info.addr_limit值。然后从栈框的S_PC位置加载ELR和SPSR的值到x21和x22寄存器中。

如果异常发生在EL0,执行以下操作。

(1)从栈框的S_SP位置加载栈框的最高地址(sp_top)到x23寄存器,然后设置到SP_EL0寄存器中。

(2)处理当前进程是32位的应用程序的情况。

接下来,把刚才从栈框中读取的ELR寄存器的值恢复到ELR_EL1中。

接下来,把刚才从栈框中读取的SPSR寄存器的值恢复到SPSR_EL1中。

接下来,从栈框中依此恢复x0-x29寄存器的值。

接下来,恢复LR的地址。

接下来,设置SP指向栈顶。

最后,通过ERET指令从异常现场返回。ERET指令会使用ELR_ELx和SPSR_ELx的值来恢复现场。

在这里注意,整个IRQ处理过程中没有看到关闭IRQ的代码。实际上,当有中断发生时,ARM64会自动把处理器的状态PSTATE保存到SPSR_ELx里。并且会自动设置PSTATE寄存器里的DAIF域为1,相当于把调试异常、系统错误(SError)、IRQ以及FIQ都关闭了。

当中断处理完成以后使用ERET指令来恢复中断现场,把之前保存的SPSR_ELx的值恢复到PSTATE寄存器里,相当于打开了IRQ。

5.2 ARM64高层中断处理(C语言部分)

5.2.1 el1_irq

再回过头来看el1_irq的汇编代码:

SYM_CODE_START_LOCAL_NOALIGN(el1_irq)
	kernel_entry 1
	el1_interrupt_handler handle_arch_irq
	kernel_exit 1
SYM_CODE_END(el1_irq)

el1_interrupt_handler是一个宏,展开后如下:

	.macro el1_interrupt_handler, handler:req
	enable_da_f

	mov	x0, sp
	bl	enter_el1_irq_or_nmi

	irq_handler	handler

#ifdef CONFIG_PREEMPTION
	ldr	x24, [tsk, #TSK_TI_PREEMPT]	// get preempt count
alternative_if ARM64_HAS_IRQ_PRIO_MASKING
	/*
	 * DA_F were cleared at start of handling. If anything is set in DAIF,
	 * we come back from an NMI, so skip preemption
	 */
	mrs	x0, daif
	orr	x24, x24, x0
alternative_else_nop_endif
	cbnz	x24, 1f				// preempt count != 0 || NMI return path
	bl	arm64_preempt_schedule_irq	// irq en/disable is done inside
1:
#endif

	mov	x0, sp
	bl	exit_el1_irq_or_nmi
	.endm

	.macro el0_interrupt_handler, handler:req
	user_exit_irqoff
	enable_da_f

	tbz	x22, #55, 1f
	bl	do_el0_irq_bp_hardening
1:
	irq_handler	handler
	.endm
  • enable_da_f是一个宏,通过msr指令来把pstate寄存器的调试异常(D域)以及SError中断(A域)和FIQ(F域)的掩码位清零,也就是打开上面三种异常和中断功能。但是IRQ此时还是关闭的,因为我们现在正在处理IRQ,打开IRQ会带来复杂的中断嵌套问题,当前的Linux内核不支持中断嵌套。
  • irq_handler同样是一个宏,用来处理中断,后面会详细分析。
  • 如果是在内核空间发生的异常,中断返回之前会检查是否允许抢占。然后再检查是否可以抢占被中断的进程,通过检查当前进程的task_thread_info中的preempt_count字段,当preempt_count为0时,表示当前进程可以被安全抢占,这个时候跳转到arm64_preempt_schedule_irq执行一次抢占调度。(注:如果是在用户空间发生的中断异常,那么返回用户空间前只会检查是否能抢占当前被中断的进程,不会检查是否允许抢占,因为用户态不会被禁用抢占)

继续展开irq_handle宏:

/*
 * Interrupt handling.
 */
	.macro	irq_handler, handler:req
	ldr_l	x1, handler  //参数handler为handle_arch_irq
	mov	x0, sp
	irq_stack_entry
	blr	x1
	irq_stack_exit
	.endm

在前面介绍gicv3的初始化过程时,会设置和arch有关的irq handler:

--gic_of_init
  --gic_init_bases
    --set_handle_irq(gic_handle_irq); 

#ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
int __init set_handle_irq(void (*handle_irq)(struct pt_regs *))
{
	if (handle_arch_irq)
		return -EBUSY;

	handle_arch_irq = handle_irq;
	return 0;
}
#endif

handle_arch_irq是一个全局的函数指针,在gicv3的驱动中会指向gic_handle_irq()函数,这个函数就是gicv3的c语言中断处理的入口函数。我们看下irq_handle的汇编代码:

(1)加载全局函数指针handle_arch_irq的地址到x1寄存器。

(2)保存sp到x0。

(3)irq_stack_entry是一个宏,主要目的是切换进程内核栈为irq栈,irq栈在init_IRQ->init_irq_stacks时初始化,每个cpu一个,同时它也会将进程内核栈指针保存到x19寄存器,便于中断处理完成以后恢复到内核栈。

(4)跳转到x1寄存器中保存的地址,也就是跳转到gic_handle_irq()函数。

(5)irq_stack_exit,前面已经将进程内核栈指针保存在x19寄存器,这里会切换irq栈到进程的内核栈。

5.2.2 gic_handle_irq

static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
	u32 irqnr;

	irqnr = do_read_iar(regs); //cpu通过读gic cpu interface的ICC_IAR1_EL1寄存器来应答该中断并且得到硬件中断号 ----(1)

	/* Check for special IDs First */
	if ((irqnr >= 1020 && irqnr <= 1023))
		return;

	if (gic_supports_nmi() &&
	    unlikely(gic_read_rpr() == GICD_INT_NMI_PRI)) {
		gic_handle_nmi(irqnr, regs);
		return;
	}

	if (gic_prio_masking_enabled()) {
		gic_pmr_mask_irqs();
		gic_arch_enable_irqs();
	}

	if (static_branch_likely(&supports_deactivate_key))
		gic_write_eoir(irqnr); //往ICC_EOIR1_EL1写入硬件中断号,表示中断结束 ----(2)
	else
		isb();

	if (handle_domain_irq(gic_data.domain, irqnr, regs)) {//中断处理主体函数 ----(3)
		WARN_ONCE(true, "Unexpected interrupt received!n");
		gic_deactivate_unhandled(irqnr);//往ICC_DIR_EL1写入硬件中断号,表示deactive该中断 ----(4)
	}
}

(2)和(4),为什么来了一个中断以后就写EOI表示中断结束,此时中断都还没有执行。

在GIC v3协议中定义, 处理完中断后,软件必须通知中断控制器已经处理了中断,以便状态机可以转换到下一个状态。 GICv3架构将中断的完成分为2个阶段

Priority Drop: 将运行优先级降回到中断之前的值。 **Deactivation:**更新当前正在处理的中断的状态机。 从活动状态转换到非活动状态。 这两个阶段可以在一起完成,也可以分为2步完成。 取决于EOImode的值。 如果EOIMode = 0, 对ICC_EOIR1_EL1寄存器的操作代表2个阶段(priority drop 和 deactivation)一起完成。 如果EOImode = 1, 对ICC_EOIR1_EL1寄存器的操作只会导致Priority Drop, 如果想要表示中断已经处理完成,还需要写ICC_DIR_EL1。

所以回答上面的问题, 当前Linux GIC的代码,默认irq chip是EIOmode=1, 所以单独的写EOIR1_EL1不是代表中断结束。

static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq,
			      irq_hw_number_t hw)
{
	if (static_branch_likely(&supports_deactivate_key))
		chip = &gic_eoimode1_chip;
}

继续分析中断控制器中断处理的主体函数handle_domain_irq:

--handle_domain_irq(gic_data.domain, irqnr, regs)
  --__handle_domain_irq(domain, hwirq, true, regs);

/**
 * __handle_domain_irq - Invoke the handler for a HW irq belonging to a domain
 * @domain:	The domain where to perform the lookup
 * @hwirq:	The HW irq number to convert to a LOGical one
 * @lookup:	Whether to perform the domain lookup or not
 * @regs:	Register file coming from the low-level handling code
 *
 * Returns:	0 on success, or -EINVAL if conversion has failed
 */
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();//显式的告诉linux内核现在要进入中断上下文

#ifdef CONFIG_IRQ_DOMAIN
	if (lookup)
		irq = irq_find_mapping(domain, hwirq);//通过硬件中断号hwirq获取虚拟中断号virq ----(1)
#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);//中断处理 ----(2)
	}

	irq_exit();//与irq_enter相反,表示中断已经处理完成
	set_irq_regs(old_regs);
	return ret;
}

#ifdef CONFIG_IRQ_DOMAIN

(1)将硬中断号hwirq作为索引,在domain->revmap_tree指向的radix tree中找到对应的irq_data,返回irq_data数据结构中保存的虚拟中断号virq。

(2)generic_handle_irq(irq);

/**
 * generic_handle_irq - Invoke the handler for a particular irq
 * @irq:	The irq number to handle
 *
 */
int generic_handle_irq(unsigned int irq)
{
	struct irq_desc *desc = irq_to_desc(irq); //以virq作为索引,找到对应的irq_desc ----(1)
	struct irq_data *data;

	if (!desc)
		return -EINVAL;

	data = irq_desc_get_irq_data(desc);
	if (WARN_ON_ONCE(!in_irq() && handle_enforce_irqctx(data)))
		return -EPERM;

	generic_handle_irq_desc(desc);// ----(2)
	return 0;
}

/*
 * Architectures call this to let the generic IRQ layer
 * handle an interrupt.
 */
static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
	desc->handle_irq(desc);//调用desc->handle_irq指向的中断处理回调函数
}

(1)之前在分析中断域的时候已经介绍过,linux内核通过一个radix tree–irq_desc_tree保存虚拟中断号virq和irq_desc之间的映射关系,在这里以virq作为索引,找到对应的irq_desc数据结构。

(2)generic_handle_irq_desc(desc);会调用desc->handle_irq指向的中断处理回调函数完成中断的处理。

--generic_handle_irq_desc(desc);
  --desc->handle_irq(desc);

desc->handle_irq指向的回调函数是在哪里定义的呢?

在这里我们回顾一下硬件中断号到虚拟中断号的映射过程。在枚举设备树中总线下的每一个device node的时候,会去建立该device的中断映射关系。

----of_device_alloc(np, bus_id, parent);
  ----of_irq_to_resource_table(np, res, num_irq)// kernel/drivers/of/irq.c
    ----of_irq_to_resource(dev, i, res)
   	  ----of_irq_get(dev, index)
        ----irq_create_of_mapping  // kernel/irq/irqdomain.c 在这里完成具体的映射过程
		  ----irq_create_fwspec_mapping(&fwspec); //struct irq_fwspec fwspec通过解析设备树得到
            ----virq = irq_domain_alloc_irqs(domain, 1, NUMA_NO_NODE, fwspec);// (3)分配一个虚拟中断号,从allocated_irqs位图中取第一个空闲的bit位作为虚拟中断号virq,分配一个irq_desc结构体,通过radix_tree建立virq和irq_desc的映射关系
              ----ret = irq_domain_alloc_irqs_hierarchy(domain, virq, nr_irqs, arg); // (5)回调irq_domain ops提供的alloc方法,填充irq_data结构体,irq_data->hwirq = hwirq;,irq_data->chip = chip;chip指向在gic-v3驱动中定义的gic-v3底层操作相关的方法集合。
				----gic_irq_domain_alloc
                  ----gic_irq_domain_map
                    ----irq_domain_set_info(d, irq, hw, chip, d->host_data,handle_fasteoi_irq, NULL, NULL);

对应SPI类型和LPI类型的中断,在irq_domain_set_info函数中会将desc->handle()回调函数指向handle_fasteoi_irq();

kernel/irq/chip.c

/**
 *	handle_fasteoi_irq - irq handler for transparent controllers
 *	@desc:	the interrupt description structure for this irq
 *
 *	Only a single callback will be issued to the chip: an ->eoi()
 *	call when the interrupt has been serviced. This enables support
 *	for modern forms of interrupt handlers, which handle the flow
 *	details in hardware, transparently.
 */
void handle_fasteoi_irq(struct irq_desc *desc)
{
	struct irq_chip *chip = desc->irq_data.chip;

	raw_spin_lock(&desc->lock);

	if (!irq_may_run(desc))
		goto out;

	desc->istate &= ~(IRQS_REplay | IRQS_WAITING);

	/*
	 * If its disabled or no action available
	 * then mask it and get out of here:
	 */
	if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {  // ----(1)
		desc->istate |= IRQS_PENDING;
		mask_irq(desc);
		goto out;
	}

	kstat_incr_irqs_this_cpu(desc); //我们一般在终端通过cat /proc/interrupts查看中断计数,这个计数是在这里增加的
	if (desc->istate & IRQS_ONESHOT)//如果该中断的类型是IRQS_ONESHOT,那么调用Mask_irq函数屏蔽该中断源
		mask_irq(desc);

	handle_irq_event(desc);// ----(2)

	cond_unmask_eoi_irq(desc, chip);// ----(3)

	raw_spin_unlock(&desc->lock);
	return;
out:
	if (!(chip->flags & IRQCHIP_EOI_IF_HANDLED))
		chip->irq_eoi(&desc->irq_data);
	raw_spin_unlock(&desc->lock);
}
EXPORT_SYMBOL_GPL(handle_fasteoi_irq);

(1)如果该中断没有指定action描述符或者中断关闭了IRQD_IRQ_DISABLED,那么设置该中断状态为IRQS_PENDING, 并调用irq_mask()函数屏蔽该中断。

(2)handle_irq_event函数是中断处理的核心函数。

(3)当中断处理完成以后需要中断控制器的irq_chip数据结构里的irq_eoi回调函数来发送一个EOI信号,通知中断控制器中断已经处理完毕。

handle_irq_event()->handle_percpu_devid_irq()->__handle_irq_event_percpu()
    
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
{
	irqreturn_t retval = IRQ_NONE;
	unsigned int irq = desc->irq_data.irq;
	struct irqaction *action;

	record_irq_time(desc);

	for_each_action_of_desc(desc, action) { //  for循环用于遍历中断描述符irq_desc中的action链表,在中断函数request_threaded_irq注册的过程中,会将中断对应的irqaction添加到该链表中
        
		irqreturn_t res;

		/*
		 * If this IRQ would be threaded under force_irqthreads, mark it so.
		 */
		if (irq_settings_can_thread(desc) &&
		    !(action->flags & (IRQF_NO_THREAD | IRQF_PERCPU | IRQF_ONESHOT)))
			lockdep_hardirq_threaded();

		trace_irq_handler_entry(irq, action);
		res = action->handler(irq, action->dev_id);// 依次执行回调函数action->handler
		trace_irq_handler_exit(irq, action, res);

		if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pS enabled interruptsn",
			      irq, action->handler))
			local_irq_disable();

		switch (res) {
		case IRQ_WAKE_THREAD:
			/*
			 * Catch drivers which return WAKE_THREAD but
			 * did not set up a thread function
			 */
			if (unlikely(!action->thread_fn)) {
				warn_no_thread(irq, action);
				break;
			}

			__irq_wake_thread(desc, action); // 如果action->handler返回值是IRQ_WAKE_THREAD,说明存在线程化中断函数,在这里会唤醒对应的中断线程进行处理

			fallthrough;	/* to add to randomness */
		case IRQ_HANDLED:
			*flags |= action->flags;
			break;

		default:
			break;
		}

		retval |= res;
	}

	return retval;
}

下面给出ARM64高层中断处理的流程图:

Linux kernel的中断子系统

6、中断函数的注册

在编写外设驱动时通常需要注册中断,在linux2.6.30内核以后新增了线程化的中断注册函数request_threaded_irq(),目的是降低中断处理对系统实时延迟的影响。

kernel/kernel/irq/manage.c
    
/**
 *	request_threaded_irq - allocate an interrupt line
 *	@irq: Interrupt line to allocate
 *	@handler: Function to be called when the IRQ occurs.
 *		  Primary handler for threaded interrupts
 *		  If NULL and thread_fn != NULL the default
 *		  primary handler is installed
 *	@thread_fn: Function called from the irq handler thread
 *		    If NULL, no irq thread is created
 *	@irqflags: Interrupt type flags
 *	@devname: An ascii name for the claiming device
 *	@dev_id: A cookie passed back to the handler function
 *
 *	This call allocates interrupt resources and enables the
 *	interrupt line and IRQ handling. From the point this
 *	call is made your handler function may be invoked. Since
 *	your handler function must clear any interrupt the board
 *	raises, you must take care both to initialise your hardware
 *	and to set up the interrupt handler in the right order.
 *
 *	If you want to set up a threaded irq handler for your device
 *	then you need to supply @handler and @thread_fn. @handler is
 *	still called in hard interrupt context and has to check
 *	whether the interrupt originates from the device. If yes it
 *	needs to disable the interrupt on the device and return
 *	IRQ_WAKE_THREAD which will wake up the handler thread and run
 *	@thread_fn. This split handler design is necessary to support
 *	shared interrupts.
 *
 *	Dev_id must be globally unique. Normally the address of the
 *	device data structure is used as the cookie. Since the handler
 *	receives this value it makes sense to use it.
 *
 *	If your interrupt is shared you must pass a non NULL dev_id
 *	as this is required when freeing the interrupt.
 *
 *	Flags:
 *
 *	IRQF_SHARED		Interrupt is shared
 *	IRQF_TRIGGER_*		Specify active edge(s) or level
 *
 */
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
			 irq_handler_t thread_fn, unsigned long irqflags,
			 const char *devname, void *dev_id)
{
	struct irqaction *action;
	struct irq_desc *desc;
	int retval;

	if (irq == IRQ_NOTCONNECTED)
		return -ENOTCONN;

	/*
	 * Sanity-check: shared interrupts must pass in a real dev-ID,
	 * otherwise we'll have trouble later trying to figure out
	 * which interrupt is which (messes up the interrupt freeing
	 * logic etc).
	 *
	 * Also IRQF_COND_SUSPEND only makes sense for shared interrupts and
	 * it cannot be set along with IRQF_NO_SUSPEND.
	 */
	if (((irqflags & IRQF_SHARED) && !dev_id) ||
	    (!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
	    ((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
		return -EINVAL; //对于那些使用共享中断的外设,这里强制要求传递一个参数dev_id。如果没有额外参数,中断处理程序无法识别究竟是哪个外设产生的中断,通常根据dev_id查询设备寄存器来确定是哪个外设发生的中断

	desc = irq_to_desc(irq);//获取该中断号对应的中断描述符irq_desc
	if (!desc)
		return -EINVAL;

	if (!irq_settings_can_request(desc) ||
	    WARN_ON(irq_settings_is_per_cpu_devid(desc)))
		return -EINVAL;

	if (!handler) {
		if (!thread_fn)
			return -EINVAL;
		handler = irq_default_primary_handler;//handler和thread_fn不能同时为空
	}

	action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
	if (!action)
		return -ENOMEM;

	action->handler = handler;
	action->thread_fn = thread_fn;
	action->flags = irqflags;
	action->name = devname;
	action->dev_id = dev_id;//分配一个irqaction数据结构,并填充相应的成员

	retval = irq_chip_pm_get(&desc->irq_data);
	if (retval < 0) {
		kfree(action);
		return retval;
	}

	retval = __setup_irq(irq, desc, action);// ----(1)

	if (retval) {
		irq_chip_pm_put(&desc->irq_data);
		kfree(action->secondary);
		kfree(action);
	}

#ifdef CONFIG_DEBUG_SHIRQ_FIXME
	if (!retval && (irqflags & IRQF_SHARED)) {
		/*
		 * It's a shared IRQ -- the driver ought to be prepared for it
		 * to happen immediately, so let's make sure....
		 * We disable the irq to make sure that a 'real' IRQ doesn't
		 * run in parallel with our fake.
		 */
		unsigned long flags;

		disable_irq(irq);
		local_irq_save(flags);

		handler(irq, dev_id);

		local_irq_restore(flags);
		enable_irq(irq);
	}
#endif
	return retval;
}
EXPORT_SYMBOL(request_threaded_irq);

(1)request_threaded_irq会继续调用__setup_irq函数完成中断的注册过程。这里会把已经初始化好的irq_action添加到中断描述符irq_desc的irq_action链表中,还会完成一些中断线程化有关的设置。这样每次中断触发以后,会通过virq获取相应的irq_desc然后调用irq_action指向的中断处理函数。

7、中断的上下半部机制(TODO)

8、ITS介绍以及代码分析

8.1 ITS概述

在前面介绍gicv3的时候提到,在gicv3中定义了一个新的中断类型,LPI(locality-specific peripheral interrupts)。LPI是一种基于消息的中断,中断信息不再通过中断线进行传递。

gicv3定义了两种方法实现LPI中断:

  • forwarding方式

    外设可以通过访问redistributor的寄存器GICR_SERLPIR,直接发送LPI中断。

  • 使用ITS方式

    ITS(Interrupt Translation Service)在GICv3中是可选的。ITS负责接收来自外设的中断,并将它们转化为LPI INTID发送到相应的Redistributor。

一般而言比较推荐使用ITS实现LPI,因为ITS提供了很多特性,在中断源比较多的场景,可以更加高效。

外设通过写GITS_TRANSLATER寄存器,发起LPI中断。此时ITS会获得2个信息: EventID: 值保存在GITS_TRANSLATER寄存器中,表示外设发送中断的事件类型 DeviceID: 表示哪一个外设发起LPI中断。 ITS将DeviceID和eventID,通过一系列查表,得到LPI中断号,再使用LPI中断号查表,得到该中断的目标cpu。

8.2 The ITS table

当前,ITS使用三种类型的表来处理LPI的转换和路由: device table: 映射deviceID到中断转换表 interrupt translation table:映射EventID到INTID。以及INTID属于的collection组 collection table:映射collection到Redistributor

Linux kernel的中断子系统

所以一个ITS完整的处理流程是: 当外设往GITS_TRANSLATER寄存器中写数据后(包含device ID和event ID),ITS做如下操作:

1、使用DeviceID,从设备表(device table entry)中选择索引为DeviceID的表项。从该表项中,得到中断 转换表(interrupt translation table)的位置。 2、使用EventID,从中断转换表中选择索引为EventID的表项。得到中断号,以及中断所属的collection号。 3、使用collection号,从collection表格中,选择索引为collection号的表项。得到redistributor的映射信息。 4、根据collection表项的映射信息,将中断信息,发送给对应的redistributor。

Linux kernel的中断子系统

8.3 The ITS Command

its是由its的命令控制的。命令队列是一个循环buffer, 由三个寄存器定义。 GITS_CBASER: 指定命令队列的基地址和大小。命令队列必须64KB对齐,大小必须是4K的倍数。命令队列中的每一个索引是32字节。该寄存器还指定访问命令队列时its的cacheability和shareability的设置。 GITS_CREADR: 指向ITS将处理的下一个命令 GITS_CWRITER: 指向队列中应写入下一个新命令的索引。

Linux kernel的中断子系统

在its的初始化过程以及lpi中断上报等过程中,会涉及到ITS command的发送。 具体的its commad指令参考:GICv3_Software_Overview_Official_Release_B

8.4 ITS代码分析

了解了ITS的具体作用以及处理流程后,下面分析linux内核中ITS相关代码。

its相关的代码位于drivers/irqchip/irq-gic-v3-its.c

8.4.1 ITS数据结构介绍

/*
 * The ITS structure - contains most of the infrastructure, with the
 * top-level MSI domain, the command queue, the collections, and the
 * list of devices writing to it.
 *
 * dev_alloc_lock has to be taken for device allocations, while the
 * spinlock must be taken to parse data structures such as the device
 * list.
 */
struct its_node {
	raw_spinlock_t		lock;
	struct mutex		dev_alloc_lock;
	struct list_head	entry;
	void __iomem		*base;
	void __iomem		*sgir_base;
	phys_addr_t		phys_base;
	struct its_cmd_block	*cmd_base;
	struct its_cmd_block	*cmd_write;
	struct its_baser	tables[GITS_BASER_NR_REGS];
	struct its_collection	*collections;
	struct fwnode_handle	*fwnode_handle;
	u64			(*get_msi_base)(struct its_device *its_dev);
	u64			typer;
	u64			cbaser_save;
	u32			ctlr_save;
	u32			mpidr;
	struct list_head	its_device_list;
	u64			flags;
	unsigned long		list_nr;
	int			numa_node;
	unsigned int		msi_domain_flags;
	u32			pre_its_base; /* for Socionext SynquACER */
	int			vlpi_redist_offset;
};

base : its node的虚拟地址 phys_base: its node的物理地址 cmd_base: 命令队列的基地址 cmd_write: 指向队列中下一个命令的地址 tables[]: 指向device table或vpe table的结构体 collection: 指向its_collection结构体, 主要保存映射到的gicr的地址 cbaser_save: 保存cbaser寄存器的信息 ctlr_save:保存ctlr寄存器的

8.4.2 ITS的初始化

在gic初始化时,会进行ITS的初始化。 its的初始化操作主要是为its的device table以及collection table分配内存,并使能its。

static int __init gic_init_bases(void __iomem *dist_base,
				 struct redist_region *rdist_regs,
				 u32 nr_redist_regions,
				 u64 redist_stride,
				 struct fwnode_handle *handle)
{
	.......
	if (gic_dist_supports_lpis()) {       // ----(1)
		its_init(handle, &gic_data.rdists, gic_data.domain);	// ----(2)
		its_cpu_init();		// ----(3)
}

(1)ITS需要使能内核配置 CONFIG_ARM_GIC_V3_ITS. 如果架构支持LPI, 则进行ITS的初始化。通过读GICD_TYPER(Interrupt Controller Type Register)寄存器的bit17查看架构是否支持LPI。

(2)ts_init是 its的初始化入口。第三个参数需要注意下,它指定了its的parent domain是gic domain。

Linux kernel的中断子系统

(3)its_cpu_init 是在its初始化完成后,进行its的一些额外的配置,如enable lpi以及绑定its collection到its 目的redistributour。

8.4.2.1 its初始化函数its_init
int __init its_init(struct fwnode_handle *handle, struct rdists *rdists,
		    struct irq_domain *parent_domain)
{
	struct device_node *of_node;
	struct its_node *its;
	bool has_v4 = false;
	bool has_v4_1 = false;
	int err;

	gic_rdists = rdists;

	its_parent = parent_domain;
	of_node = to_of_node(handle);
	if (of_node)
		its_of_probe(of_node);  // ----(1)
	else
		its_acpi_probe();

	if (list_empty(&its_nodes)) {
		pr_warn("ITS: No ITS available, not enabling LPIsn");
		return -ENXIO;
	}

	err = allocate_lpi_tables(); // ----(2)
	if (err)
		return err;

	list_for_each_entry(its, &its_nodes, entry) {
		has_v4 |= is_v4(its);
		has_v4_1 |= is_v4_1(its);
	}

	/* Don't bother with inconsistent systems */
	if (WARN_ON(!has_v4_1 && rdists->has_rvpeid))
		rdists->has_rvpeid = false;

	if (has_v4 & rdists->has_vlpis) {
		const struct irq_domain_ops *sgi_ops;

		if (has_v4_1)
			sgi_ops = &its_sgi_domain_ops;
		else
			sgi_ops = NULL;

		if (its_init_vpe_domain() ||
		    its_init_v4(parent_domain, &its_vpe_domain_ops, sgi_ops)) {
			rdists->has_vlpis = false;
			pr_err("ITS: Disabling GICv4 supportn");
		}
	}

	register_syscore_ops(&its_syscore_ops);

	return 0;
}

(1)its_of_probe

--its_of_probe
  --its_probe_one
    --its_force_quiescent   //让ITS处于非活动状态,在非静止状态改变ITS的配置会有安全的风险
    --kzalloc its_node and init   //为its_node分配空间,并对其进行初始化配置
    --its_alloc_tables  //为device table 和 vpe table分配内存
    --its_alloc_collections //为collection table中映射到的gicr 地址分配内存; 每一个its都有一个collection table, ct可以保存在寄存器(GITS_BASER)或者内存(GITS_TYPER.HCC)
    --its_init_domain // its domain初始化,注册its domain相关操作

its probe过程, 主要是初始化its node数据结构, 为its tables分配内存, 初始化its domain并注册its domain相关操作。 its_domain初始化过程中,会指定its irq_domain的host_data为msi_domain_info, 在info->ops.prepare过程中会去创建ITS设备, its translation table会在那个阶段分配内存。

(2)allocate_lpi_tables

--allocate_lpi_tables
  --its_setup_lpi_prop_table
    --its_allocate_prop_table
    --its_lpi_init

ITS 是为LPI服务的,所以在ITS初始化过程中还需要初始化LPI需要的两张表 (LPI configuration table, LPI pending tables ), 然后进行lpi的初始化。

LPI的这两张表就是LPI和其他类型中断的区别所在: LPI的中断的配置,以及中断的状态,是保存在memory的表中,而不是保存在gic的寄存器中的。

LPI 中断配置表: 中断配置表的基地址由GICR_PROPBASER寄存器决定。 对于LPI配置表,每个LPI中断占用1个字节(bit[7:0]),指定了该中断的使能(bit 0)和中断优先级(bit[7:2])。

当外部发送LPI中断给redistributor,redistributor首先要访问memory来获取LPI中断的配置表。为了加速这过程,redistributor中可以配置cache,用来缓存LPI中断的配置信息。

因为有了cache,所以LPI中断的配置信息,就有了2份拷贝,一份在memory中,一份在redistributor的cache中。如果软件修改了memory中的LPI中断的配置信息,需要将redistributor中的cache信息给无效掉。 通过该接口刷相关dcache

gic_flush_dcache_to_poc()

LPI 中断状态表 中单状态表的基地址由GICR_PENDBASER寄存器决定, 该寄存器还可以设置LPI中断状态表memory的属性,如shareability,cache属性等。 该状态表主要用于查看LPI是否pending状态。

该中断状态表由redistributor来设置。每个LPI中断,占用一个bit空间。 0: 该LPI中断,没有处于pending状态 1: 该LPI中断,处于pending状态

8.4.2.2 its_cpu_init
--its_cpu_init
  --its_cpu_init_lpis          //配置lpi 配置表和状态表, 以及使能lpi
  --its_cpu_init_collections   //绑定每一个collection到target redistributor
    --its_cpu_init_collection
      --its_send_mapc     //发送its mapc command, mapc主要用于映射collection到目的redistributor
      --its_send_invall  //指定 memory中的LPI中断的配置信息和cache中保存的必须一致

8.4.3 its中断上报

和gic类似, 在中断上报时,如果设备挂载在its 下, 会调用到its domain的一系列operation

static const struct irq_domain_ops its_domain_ops = {
	.alloc			= its_irq_domain_alloc,
	.free			= its_irq_domain_free,
	.activate		= its_irq_domain_activate,
	.deactivate		= its_irq_domain_deactivate,
};

参考资料

1、GICv3_Software_Overview_Official_Release_B

2、corelink_gic600_generic_interrupt_controller_technical_reference_manual_100336_0106_00_en

3、IHI0069D_gic_architecture_specification

4、ARM GICv3中断控制器

https://blog.csdn.net/yhb1047818384/article/details/86708769#comments_17151284

5、ARM GICv3 GIC代码分析

https://blog.csdn.net/yhb1047818384/article/details/87561438

6、Arm_Architecture_Reference_Manual_Armv8_for Armv8-A_architecture_profile

7、奔跑吧Linux内核

脚本宝典总结

以上是脚本宝典为你收集整理的Linux kernel的中断子系统全部内容,希望文章能够帮你解决Linux kernel的中断子系统所遇到的问题。

如果觉得脚本宝典网站内容还不错,欢迎将脚本宝典推荐好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。