Linux kernel的中断子系统

Posted V一叶知秋

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux kernel的中断子系统相关的知识,希望对你有一定的参考价值。

Linux kernel的中断子系统


内核版本: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 中断号

1.3 GICV3编程模型

对应的寄存器编程接口描述如下:(参考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中断亲和度路由

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) & 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 registers\\n", 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 up\\n", 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 %d\\n", 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 mode\\n");

	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 implemented\\n", 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 implemented\\n", 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 support\\n",
		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 MBIs\\n");
	

	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

以上是关于Linux kernel的中断子系统的主要内容,如果未能解决你的问题,请参考以下文章

Linux kernel的中断子系统之:综述

Linux kernel的中断子系统之:ARM中断处理过程

linux kernel的中断子系统之:IRQ number和中断描述符

linux kernel的中断子系统之:IRQ number和中断描述符转

linux kernel的中断子系统之:softirq

Linux kernel中断子系统之:驱动申请中断API