• linux-gic中断分析


    linux-gic中断分析

    这里主要分析 linux kernel 中 GICv3 中断控制器的代码(drivers/irqchip/irq-gic-v3.c)。

    一、设备树

    先来看下中断控制器的设备树信息:

    	gic: interrupt-controller@3400000 {
    		compatible = "arm,gic-v3";
    		#interrupt-cells = <3>;
    		#address-cells = <0>;
    		ranges;
    		interrupts = ;  /* GIC Maintenence IRQ */
    		interrupt-controller;
    		reg = <0x0 0x03400000 0 0x10000>, /* GIC Dist */
    		      <0x0 0x03460000 0 0xFF004>; /* GIC Re */
    		interrupt-parent = <&gic>;
    	};
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    二、初始化

    1. irq chip driver 的声明

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

    定义 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")		\
    		__aligned(__alignof__(struct of_device_id))		\
    		 = { .compatible = compat,				\
    		     .data = (fn == (fn_type)NULL) ? fn : fn  } /* 存放回调函数gic_of_init */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这里展开后,会定义一个__of_table_gic_v3结构体,存放在 __irqchip_of_table段里面,该段用于存放中断控制器信息:

    /* linux-5.15/include/asm-generic/vmlinux.lds.h */
    #define _OF_TABLE_1(name)                                               \
            . = ALIGN(8);                                                   \
            __##name##_of_table = .;                                        \
            KEEP(*(__##name##_of_table))                                    \
            KEEP(*(__##name##_of_table_end))
    	
    #define OF_TABLE(cfg, name)     __OF_TABLE(IS_ENABLED(cfg), name)
    
    #define IRQCHIP_OF_MATCH_TABLE() OF_TABLE(CONFIG_IRQCHIP, irqchip)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在内核启动初始化中断的函数中,of_irq_init 函数会去查找设备节点信息,该函数的传入参数就是 __irqchip_of_table 段,由于 IRQCHIP_DECLARE 已经将信息填充好了,of_irq_init 函数会根据 “arm,gic-v3” 去查找对应的设备节点,并获取设备的信息。or_irq_init 函数中,最终会回调 IRQCHIP_DECLARE 声明的回调函数,也就是 gic_of_init,而这个函数就是 GIC 驱动的初始化入口。

    start_kernel
    	init_IRQ
    		init_irq_stacks /* 为每个CPU分配中断栈 */
    		irqchip_init
    			of_irq_init(__irqchip_of_table);
    				desc->irq_init_cb = match->data;
    				desc->irq_init_cb(desc->dev, desc->interrupt_parent); /* 指向回调函数gic_of_init */
    				--->gic_of_init
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2. gic_of_init 流程

    static struct gic_chip_data gic_data[CONFIG_ARM_GIC_MAX_NR] __read_mostly;
    
    gic_of_init
    --->dist_base = of_iomap(node, 0); /* 映射distributor地址空间 */
    	if (of_property_read_u32(node, "#redistributor-regions", &nr_redist_regions)) 
    		nr_redist_regions = 1; /* sunxi的设备树没有“#redistributor-regions”属性,所以这里默认就是1 */
    	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); /* 映射redistributor地址空间 */
    		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;
    	}
    --->gic_init_bases(dist_base, rdist_regs, nr_redist_regions, redist_stride, &node->fwnode);
    		typer = readl_relaxed(gic_data.dist_base + GICD_TYPER);
    		gic_data.rdists.gicd_typer = typer; /* 获取支持的中断数量 */
    		/* 向系统中注册一个 irq domain 的数据结构,irq_domain 主要作用是将硬件中断号映射到 irq number */
    		gic_data.domain = irq_domain_create_tree(handle, &gic_irq_domain_ops, &gic_data);
    		gic_data.rdists.rdist = alloc_percpu(typeof(*gic_data.rdists.rdist));
    		......
            set_handle_irq(gic_handle_irq);
    			handle_arch_irq = handle_irq; /* 设置arm中断的全局入口函数为gic_handle_irq */
    		gic_update_rdist_properties(); /* 获取rdist的基本信息,填充至gic_data结构体 */
            gic_dist_init(); /* 初始化 Distributor */
    		--->writel_relaxed(0, base + GICD_CTLR); /* Disable the distributor */
                for (i = 32; i < GIC_LINE_NR; i += 32) /* 将 SPI 配置为非安全组 1 */
                    writel_relaxed(~0, base + GICD_IGROUPR + i / 8);
                for (i = 0; i < GIC_ESPI_NR; i += 32) {
                    writel_relaxed(~0U, base + GICD_ICENABLERnE + i / 8); /* 禁止将扩展SPI转发到CPU接口 */
                    writel_relaxed(~0U, base + GICD_ICACTIVERnE + i / 8); /* 停用扩展SPI中断 */
                }
                for (i = 0; i < GIC_ESPI_NR; i += 32)
                    writel_relaxed(~0U, base + GICD_IGROUPRnE + i / 8); /* 控制扩展SPI范围中对应的SPI在组1 */
                for (i = 0; i < GIC_ESPI_NR; i += 16)
                    writel_relaxed(0, base + GICD_ICFGRnE + i / 4); /* 设置扩展SPI中断是电平触发 */
                for (i = 0; i < GIC_ESPI_NR; i += 4)
                    writel_relaxed(GICD_INT_DEF_PRI_X4, base + GICD_IPRIORITYRnE + i); /* 设置扩展SPI的中断优先级 */
    			gic_dist_config(base, GIC_LINE_NR, gic_dist_wait_for_rwp);
    			--->for (i = 32; i < gic_irqs; i += 16)
                    	writel_relaxed(GICD_INT_ACTLOW_LVLTRIG, base + GIC_DIST_CONFIG + i / 4); /* 设置SPI中断为电平触发,低电平有效 */
                    for (i = 32; i < gic_irqs; i += 4)
                        writel_relaxed(GICD_INT_DEF_PRI_X4, base + GIC_DIST_PRI + i); /* 设置SPI的中断优先级 */
                    for (i = 32; i < gic_irqs; i += 32) {
                        writel_relaxed(GICD_INT_EN_CLR_X32, base + GIC_DIST_ACTIVE_CLEAR + i / 8); /* 禁止将SPI转发到CPU接口 */
                        writel_relaxed(GICD_INT_EN_CLR_X32, base + GIC_DIST_ENABLE_CLEAR + i / 8); /* 停用SPI中断 */
                    }
    			val = GICD_CTLR_ARE_NS | GICD_CTLR_ENABLE_G1A | GICD_CTLR_ENABLE_G1;
    			writel_relaxed(val, base + GICD_CTLR); /* 为非安全状态启用关联路由,启用非安全组1中断 */
    			/* 将所有spi中断路由到当前启动的cpu */
                affinity = gic_mpidr_to_affinity(cpu_logical_map(smp_processor_id()));
                for (i = 32; i < GIC_LINE_NR; i++) {
                    trace_android_vh_gic_v3_affinity_init(i, GICD_IROUTER, &affinity);
                    gic_write_irouter(affinity, base + GICD_IROUTER + i * 8);
                }
                for (i = 0; i < GIC_ESPI_NR; i++) {
                    trace_android_vh_gic_v3_affinity_init(i, GICD_IROUTERnE, &affinity);
                    gic_write_irouter(affinity, base + GICD_IROUTERnE + i * 8);
                }
            gic_cpu_init(); /* 初始化cpu interface */
    		--->gic_populate_rdist();
    			gic_enable_redist(true);
    				rbase = gic_data_rdist_rd_base();
                    val = readl_relaxed(rbase + GICR_WAKER);
                    if (enable)
                        /* Wake up this CPU redistributor */
                        val &= ~GICR_WAKER_ProcessorSleep; /* 此PE没有处于,也没有进入低功耗模式 */
                    else
                        val |= GICR_WAKER_ProcessorSleep;
                    writel_relaxed(val, rbase + GICR_WAKER);
                rbase = gic_data_rdist_sgi_base();
    
                /* Configure SGIs/PPIs as non-secure Group-1 */
                for (i = 0; i < gic_data.ppi_nr + 16; i += 32)
                    writel_relaxed(~0, rbase + GICR_IGROUPR0 + i / 8);
    			gic_cpu_sys_reg_init();
    				gic_enable_sre(); /* 使能系统寄存器接口 */
                    if (static_branch_likely(&supports_deactivate_key)) { /* 设置EOI模式,默认是mode 1 */
                        /* EOI drops priority only (mode 1) */
                        gic_write_ctlr(ICC_CTLR_EL1_EOImode_drop);
                    } else {
                        /* EOI deactivates interrupt too (mode 0) */
                        gic_write_ctlr(ICC_CTLR_EL1_EOImode_drop_dir);
                    }
            gic_smp_init(); /* Register all 8 non-secure SGIs */
    			/* 设置cpu热拔插gic的回调函数 */
    			cpuhp_setup_state_nocalls(CPUHP_AP_IRQ_GIC_STARTING, "irqchip/arm/gicv3:starting", gic_starting_cpu, NULL);
    			base_sgi = __irq_domain_alloc_irqs(gic_data.domain, -1, 8, NUMA_NO_NODE, &sgi_fwspec, false, NULL);
    			set_smp_ipi_range(base_sgi, 8);
            gic_syscore_init();
    			register_syscore_ops(&gic_syscore_ops);
    		......
            gic_enable_nmi_support();
    --->gic_populate_ppi_partitions(node); /* 设置一组 PPI 的亲和性 */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99

    3. 初始化流程总结

    见下图:

    在这里插入图片描述

    三、数据结构

    1. struct irq_chip

    描述的是中断控制器的底层操作函数集,这些函数集最终完成对控制器硬件的操作:

    struct irq_chip {
    	struct device	*parent_device;
    	const char	*name;
    	unsigned int	(*irq_startup)(struct irq_data *data);
    	void		(*irq_shutdown)(struct irq_data *data);
    	void		(*irq_enable)(struct irq_data *data);
    	void		(*irq_disable)(struct irq_data *data);
    
    	void		(*irq_ack)(struct irq_data *data);
    	void		(*irq_mask)(struct irq_data *data);
    	void		(*irq_mask_ack)(struct irq_data *data);
    	void		(*irq_unmask)(struct irq_data *data);
    	void		(*irq_eoi)(struct irq_data *data);
    
    	int		(*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);
    	int		(*irq_retrigger)(struct irq_data *data);
    	int		(*irq_set_type)(struct irq_data *data, unsigned int flow_type);
    	int		(*irq_set_wake)(struct irq_data *data, unsigned int on);
    
    	void		(*irq_bus_lock)(struct irq_data *data);
    	void		(*irq_bus_sync_unlock)(struct irq_data *data);
    
    	void		(*irq_cpu_online)(struct irq_data *data);
    	void		(*irq_cpu_offline)(struct irq_data *data);
    
    	void		(*irq_suspend)(struct irq_data *data);
    	void		(*irq_resume)(struct irq_data *data);
    	void		(*irq_pm_shutdown)(struct irq_data *data);
    
    	void		(*irq_calc_mask)(struct irq_data *data);
    
    	void		(*irq_print_chip)(struct irq_data *data, struct seq_file *p);
    	int		(*irq_request_resources)(struct irq_data *data);
    	void		(*irq_release_resources)(struct irq_data *data);
    
    	void		(*irq_compose_msi_msg)(struct irq_data *data, struct msi_msg *msg);
    	void		(*irq_write_msi_msg)(struct irq_data *data, struct msi_msg *msg);
    
    	int		(*irq_get_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool *state);
    	int		(*irq_set_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool state);
    
    	int		(*irq_set_vcpu_affinity)(struct irq_data *data, void *vcpu_info);
    
    	void		(*ipi_send_single)(struct irq_data *data, unsigned int cpu);
    	void		(*ipi_send_mask)(struct irq_data *data, const struct cpumask *dest);
    
    	int		(*irq_nmi_setup)(struct irq_data *data);
    	void		(*irq_nmi_teardown)(struct irq_data *data);
    
    	unsigned long	flags;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    2. struct irq_domain

    irq_domain用于将硬件的中断号,转换成Linux系统中的中断号(virtual irq, virq

    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
    
    	ANDROID_KABI_RESERVE(1);
    	ANDROID_KABI_RESERVE(2);
    	ANDROID_KABI_RESERVE(3);
    	ANDROID_KABI_RESERVE(4);
    
    	/* reverse map data. The linear map gets appended to the irq_domain */
    	irq_hw_number_t hwirq_max;
    	unsigned int revmap_size;
    	struct radix_tree_root revmap_tree;
    	struct mutex revmap_mutex;
    	struct irq_data __rcu *revmap[];
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 每个中断控制器都对应一个irq_domain
    • 中断控制器驱动通过irq_domain_add_*()接口来创建irq_domain
    • irq_domain支持三种映射方式:
      • linear map:维护固定大小的表,索引是硬件中断号,如果硬件中断最大数量固定,并且数值不大,可以选择线性映射
      • tree map:硬件中断号可能很大,可以选择树映射
      • no map:硬件中断号直接就是Linux的中断号

    irq_domain 的引入相当于一个中断控制器就是一个 irq_domain,这样一来所有的中断控制器就会出现级联的布局。利用树状的结构可以充分的利用 irq 数目,而且每一个 irq_domain 区域可以自己去管理自己 interrupt 的特性。

    每一个中断控制器对应多个中断号, 而硬件中断号在不同的中断控制器上是会重复编码的, 这时仅仅用硬中断号已经不能唯一标识一个外设中断,因此 linux kernel 提供了一个虚拟中断号(virq)的概念。

    3. struct irq_data

    中断描述符中应该会包括底层irq chip相关的数据结构,linux kernel中把这些数据组织在一起,形成struct irq_data。

    struct irq_data {
    	u32			mask;
    	unsigned int		irq;
    	unsigned long		hwirq;
    	struct irq_common_data	*common;
    	struct irq_chip		*chip;
    	struct irq_domain	*domain;
    #ifdef	CONFIG_IRQ_DOMAIN_HIERARCHY
    	struct irq_data		*parent_data;
    #endif
    	void			*chip_data;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • irq:虚拟中断号

    • hwirq:硬件中断号

    • chip:对应的 irq_chip 数据结构

    • domain:对应的 irq_domain 数据结构

    4. struct irq_desc

    描述一个外设的中断,称之中断描述符

    struct irq_desc {
    	struct irq_common_data	irq_common_data;
    	struct irq_data		irq_data;
    	unsigned int __percpu	*kstat_irqs;
    	irq_flow_handler_t	handle_irq;
    	struct irqaction	*action;	/* IRQ action list */
    	unsigned int		status_use_accessors;
    	unsigned int		core_internal_state__do_not_mess_with_it;
    	unsigned int		depth;		/* nested irq disables */
    	unsigned int		wake_depth;	/* nested wake enables */
    	unsigned int		tot_count;
    	unsigned int		irq_count;	/* For detecting broken IRQs */
    	unsigned long		last_unhandled;	/* Aging timer for unhandled count */
    	unsigned int		irqs_unhandled;
    	atomic_t		threads_handled;
    	int			threads_handled_last;
    	raw_spinlock_t		lock;
    	struct cpumask		*percpu_enabled;
    	const struct cpumask	*percpu_affinity;
    #ifdef CONFIG_SMP
    	const struct cpumask	*affinity_hint;
    	struct irq_affinity_notify *affinity_notify;
    	......
    	int			parent_irq;
    	struct module		*owner;
    	const char		*name;
    } ____cacheline_internodealigned_in_smp;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    下面这张是总的数据结构关联图,核心是围绕着struct irq_desc来展开的:

    在这里插入图片描述

    • Linux内核的中断处理,围绕着中断描述符结构struct irq_desc展开,内核提供了两种中断描述符组织形式:

      1. 打开CONFIG_SPARSE_IRQ宏(中断编号不连续),中断描述符以radix-tree来组织,用户在初始化时进行动态分配,然后再插入radix-tree中。

      2. 关闭CONFIG_SPARSE_IRQ宏(中断编号连续),中断描述符以数组的形式组织,并且已经分配好。

      3. 不管哪种形式,最终都可以通过linux irq号来找到对应的中断描述符。

    四、关键流程分析

    1. 创建映射关系

    驱动中通常会使用platform_get_irq或irq_of_parse_and_map接口去根据设备树的信息创建映射关系(硬件中断号到linux irq中断号映射),然后调用request_irq()接口或者request_threaded_irq()接口来注册设备的中断处理函数。

    unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
    --->if (of_irq_parse_one(dev, index, &oirq)) /* 分析device node中的interrupt相关属性,存放到oirq->args[]中 */
    		return 0;
    
    --->return irq_create_of_mapping(&oirq); /* 创建映射,并返回对应的IRQ number */
    	--->irq_create_fwspec_mapping(&fwspec);
    			/* 根据给定的fwspec找到一个合适的domain,这是根据传递进来的参数irq_data的np成员来寻找的 */
    		--->domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_WIRED);
            --->irq_domain_translate(domain, fwspec, &hwirq, &type);
    				/* 这里调用gic_irq_domain_translate,对硬中断号进行转换保存在hwirq中,GIC硬中断号=dts硬中断号+32 */
                    d->ops->translate(d, fwspec, hwirq, type);
    					*hwirq = fwspec->param[1] + 32; /* SPI */
            --->virq = irq_find_mapping(domain, hwirq); /* 通过hwirq去查询,这个hwirq是否已经映射,如果已经映射,就返回virq */
    		--->if (virq) {
                    if (type == IRQ_TYPE_NONE || type == irq_get_trigger_type(virq))
    					return virq;
                }
    		--->if (irq_domain_is_hierarchy(domain)) /* 中断级联 */
                --->virq = irq_domain_alloc_irqs(domain, 1, NUMA_NO_NODE, fwspec);
    				--->__irq_domain_alloc_irqs(domain, -1, nr_irqs, node, arg, false, NULL);
    					--->virq = irq_domain_alloc_descs(irq_base, nr_irqs, 0, node, affinity); /* 申请一个虚拟中断号virq */
    					--->irq_domain_alloc_irq_data(domain, virq, nr_irqs);
    					--->irq_domain_alloc_irqs_hierarchy(domain, virq, nr_irqs, arg);
    							domain->ops->alloc(domain, irq_base, nr_irqs, arg); /* 执行gic_irq_domain_alloc函数 */
    								gic_irq_domain_translate(domain, fwspec, &hwirq, &type);
    								for (i = 0; i < nr_irqs; i++) {
    									gic_irq_domain_map(domain, virq + i, hwirq + i);
                                        	switch (__get_intid_range(hw)) {
                                            case SGI_RANGE:
                                                irq_domain_set_info(d, irq, hw, chip, d->host_data, handle_percpu_devid_fasteoi_ipi, NULL, NULL); /* 如果中断是属于SGI,则入口函数是handle_percpu_devid_fasteoi_ipi */
                                            case PPI_RANGE:
                                                irq_domain_set_info(d, irq, hw, chip, d->host_data, handle_percpu_devid_irq, NULL, NULL); /* 如果中断是属于PPI,则入口函数是handle_percpu_devid_irq */
                                            case SPI_RANGE:
                                                irq_domain_set_info(d, irq, hw, chip, d->host_data,  handle_fasteoi_irq, NULL, NULL); /* 如果中断是属于SPI,则入口函数是handle_fasteoi_irq */
                                                    irq_set_chip_and_handler_name(virq, chip, handler, handler_name);
                                                        __irq_set_handler(irq, handle, 0, name);
                                                            __irq_do_set_handler(desc, handle, is_chained, name);
                                                    			desc->handle_irq = handle; /* 设置highlevel的中断处理函数:desc->handle_irq = handle_fasteoi_irq */
                                                                desc->action = &chained_action; /* action链表,成员handler存放中断服务函数 */
                                            }
                        --->for (i = 0; i < nr_irqs; i++)
                                irq_domain_insert_irq(virq + i);
    								for (data = irq_get_irq_data(virq); data; data = data->parent_data)
                                        /* 通过线性映射或radix tree映射,以硬中断号为索引,建立硬中断与软中断的映射关系 */
                                        irq_domain_set_mapping(domain, data->hwirq, data);
    										radix_tree_insert(&domain->revmap_tree, hwirq, irq_data);
    		--->else /* 非中断级联 */
                	virq = irq_create_mapping(domain, hwirq); /* hwirq没有映射,跟据硬件中断号,分配软件虚拟中断号, 期间会创建irq_desc */
                    --->irq_create_mapping_affinity(host, hwirq, NULL); /* 将硬件中断映射到 linux irq 空间 */
                        --->virq = irq_domain_alloc_descs(-1, 1, hwirq, of_node_to_nid(of_node), affinity); /* 申请一个虚拟中断号virq */
                                if (virq >= 0) /* 指定了virq号 */
                                    virq = __irq_alloc_descs(virq, virq, cnt, node, THIS_MODULE, affinity);
                                else /* 没有指定virq号 */
                                    virq = __irq_alloc_descs(-1, hint, cnt, node, THIS_MODULE, affinity);
                                        /* 从全局allocated_irqs,从from开始查找cnt个为0的bit位置返回到start */
                                        start = bitmap_find_next_zero_area(allocated_irqs, IRQ_BITMAP_BITS, from, cnt, 0);
                                        alloc_descs(start, cnt, node, affinity, owner);
                                            for (i = 0; i < cnt; i++) {
                                                desc = alloc_desc(start + i, node, flags, mask, owner); /* 分配中断描述符 */
                                                irq_insert_desc(start + i, desc); /* 中断描述符插入到全局radix tree :irq_desc_tree */
                                                    radix_tree_insert(&irq_desc_tree, irq, desc);
                                            }
                                            bitmap_set(allocated_irqs, start, cnt); /* 根据已经分配的中断描述符置位allocated_irqs */
                            /* 通过线性映射或radix tree映射,以硬中断号为索引,建立硬中断与软中断的映射关系 */	
                        --->irq_domain_associate(domain, virq, hwirq);
                                struct irq_data *irq_data = irq_get_irq_data(virq); /* 根据virq找到对应的irq_data */
                                if (domain->ops->map) /* gicv3的gic_irq_domain_ops没有实现map */
                                    domain->ops->map(domain, virq, hwirq);
                                irq_domain_set_mapping(domain, hwirq, irq_data);
                                /* 区分硬中断号是否大于domain->revmap_size,如果小于domain->revmap_size,则插入线性映射数组,数组索引为硬中断号 */
                                if (hwirq < domain->revmap_size)
                                    rcu_assign_pointer(domain->revmap[hwirq], irq_data);
                                        domain->revmap[hwirq] = irq_data;
                                else
                                    /* 将virq对应的irq_data链入到radix tree,硬中断号为索引 */
                                    radix_tree_insert(&domain->revmap_tree, hwirq, irq_data);				
            --->irq_data = irq_get_irq_data(virq);
            --->irqd_set_trigger_type(irq_data, type); /* 设定触发类型 */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78

    2. 中断注册

    设备驱动在获取到了irq中断号后,通常就会采用request_irq/request_threaded_irq来注册中断,其中request_irq用于注册普通处理的中断,request_threaded_irq用于注册线程化处理的中断。线程化中断的主要目是把中断上下文的任务迁移到线程中,减少系统关中断的时间,增强系统的实时性。

    ps:request_irq也是调用request_threaded_irq,只是在传参的时候,线程处理函数thread_fn函数设置成NULL。

    static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, 												const char *name, void *dev)
        request_threaded_irq(irq, handler, NULL, flags, name, dev);
    
    • 1
    • 2

    下面来分析一下request_threaded_irq函数:

    参数描述:

    输入参数描述
    irq要注册handler的那个IRQ number。这里要注册的handler包括两个,一个是传统意义的中断handler,我们称之primary handler,另外一个是threaded interrupt handler
    handlerprimary handler。需要注意的是primary handler和threaded interrupt handler不能同时为空,否则会出错
    thread_fnthreaded interrupt handler。如果该参数不是NULL,那么系统会创建一个kernel thread,调用的function就是thread_fn
    irqflags见下面的表格
    devname申请的中断对应的设备名称
    dev_id对应的设备描述符指针

    常见irqflags种类:(include/linux/interrupt.h)

    flag定义描述
    IRQF_TRIGGER_XXX描述该interrupt line触发类型的flag
    IRQF_SHARED允许在多个设备之间共享 irq
    IRQF_PERCPU在SMP的架构下,中断有两种mode,一种中断是在所有processor之间共享的,也就是global的,一旦中断产生,interrupt controller可以把这个中断送达多个处理器。当然,在具体实现的时候不会同时将中断送达多个CPU,一般是软件和硬件协同处理,将中断送达一个CPU处理。但是一段时间内产生的中断可以平均(或者按照既定的策略)分配到一组CPU上。这种interrupt mode下,interrupt controller针对该中断的operational register是global的,所有的CPU看到的都是一套寄存器,一旦一个CPU ack了该中断,那么其他的CPU看到的该interupt source的状态也是已经ack的状态。
    和global对应的就是per cpu interrupt了,对于这种interrupt,不是processor之间共享的,而是特定属于一个CPU的。例如GIC中interrupt ID等于30的中断就是per cpu的(这个中断event被用于各个CPU的local timer),这个中断号虽然只有一个,但是,实际上控制该interrupt ID的寄存器有n组(如果系统中有n个processor),每个CPU看到的是不同的控制寄存器。在具体实现中,这些寄存器组有两种形态,一种是banked,所有CPU操作同样的寄存器地址,硬件系统会根据访问的cpu定向到不同的寄存器,另外一种是non banked,也就是说,对于该interrupt source,每个cpu都有自己独特的访问地址
    IRQF_NO_THREAD有些low level的interrupt是不能线程化的(例如系统timer的中断),这个flag就是起这个作用的。另外,有些级联的interrupt controller对应的IRQ也是不能线程化的(例如secondary GIC对应的IRQ),它的线程化可能会影响一大批附属于该interrupt controller的外设的中断响应延迟
    IRQF_NOBALANCING这也是和multi-processor相关的一个flag。对于那些可以在多个CPU之间共享的中断,具体送达哪一个processor是有策略的,我们可以在多个CPU之间进行平衡。如果你不想让你的中断参与到irq balancing的过程中那么就设定这个flag
    IRQF_ONESHOTone shot本身的意思是只有一次的,结合到中断这个场景,则表示中断是一次性触发的,不能嵌套
    IRQF_NO_SUSPEND这个flag比较好理解,就是说在系统suspend的时候,不用disable这个中断,如果disable,可能会导致系统不能正常的resume
    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)
    --->desc = irq_to_desc(irq); /* 根据linux中断号,获取中断描述符irq_desc */
    	/* 分配新的action数据结构,填充里面的相关成员 */
    --->action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
    --->action->handler = handler;
    --->action->thread_fn = thread_fn;
    --->action->flags = irqflags;
    --->action->name = devname;
    --->action->dev_id = dev_id;
    
    --->retval = __setup_irq(irq, desc, action); /* 用于完成中断的相关设置,包括中断线程化的处理 */
    		struct irqaction *old, **old_ptr;
    		......
    		nested = irq_settings_is_nested_thread(desc); /* 检查中断是否嵌套在另外一个中断线程中 */
    		if (nested) {
                new->handler = irq_nested_primary_handler;
            }
    		else {
                if (irq_settings_can_thread(desc) /* 中断没有嵌套,检查能否线程化 */
                    irq_setup_forced_threading(new); /* 中断强制线程化设置 */
            }
            if (new->thread_fn && !nested) /* 设置了线程化处理函数且没有嵌套 */
            	setup_irq_thread(new, irq, false); /* 为该中断创建一个内核线程 */
                    t = kthread_create(irq_thread, new, "irq/%d-s-%s", irq, new->name);
            if (!desc->action)
            	irq_request_resources(desc);
            old_ptr = &desc->action; /* old_ptr指向desc->action */
    		old = *old_ptr; /* old指向注册之前的action list,如果不是NULL,那么说明需要共享interrupt line */
            if (old)
            {
                ......
                /*
                    desc->action非空的话,代表中断需要共享。中断共享,irqaction会连成链表,每个irqaction有thread_mask
                    位图,当所有共享中断都处理完才能unmask中断。
                */    
                do {
                    /*
                     * Or all existing action->thread_mask bits,
                     * so we can find the next zero bit for this
                     * new action.
                     */
                    thread_mask |= old->thread_mask;
                    old_ptr = &old->next;
                    old = *old_ptr;
                } while (old);
                shared = 1;
            }
            ......
            if (!shared) { /* 中断非共享 */
                if (new->flags & IRQF_TRIGGER_MASK)
                    __irq_set_trigger(desc, new->flags & IRQF_TRIGGER_MASK); /* 设置中断触发类型 */
            	irq_activate(desc);
                	struct irq_data *d = irq_desc_get_irq_data(desc);
                	irq_domain_activate_irq(d, false);
                		ret = __irq_domain_activate_irq(irq_data, reserve);
                        if (!ret)
                            irqd_set_activated(irq_data);
                				__irqd_to_state(d) |= IRQD_ACTIVATED;
                irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);
            }
            /* 中断服务函数已经赋值:desc->action->handler = action->handler,这里将该irqaction(new)挂入队列的尾部 */
            *old_ptr = new;
            ......
            __enable_irq(desc);
            --->irq_startup(desc, IRQ_RESEND, IRQ_START_FORCE);
                --->irq_enable(desc);
                    --->unmask_irq(desc);
                    	--->desc->irq_data.chip->irq_unmask(&desc->irq_data); /* 调用gicv3的api */
                    		--->gic_unmask_irq
                    			--->gic_poke_irq(d, GICD_ISENABLER); /* 修改GICD_ISENABLER寄存器,使能对应位的中断 */
            ......
            register_irq_proc(irq, desc);
            register_handler_proc(irq, new);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74

    3. 中断响应

    当完成中断的注册后,所有结构的组织关系都已经建立好,剩下就是等信号来临时,进行中断的处理工作。

    假设当前在 EL0 运行一个应用程序,触发了一个 EL0 的 irq 中断,则处理器会做如下的操作:

    在这里插入图片描述

    先跳到 arm64 对应的异常向量表:(arch/arm64/kernel/entry.S)

    SYM_CODE_START(vectors)
    	kernel_ventry   1, t, 64, sync          // Synchronous EL1t
    	kernel_ventry   1, t, 64, irq           // IRQ EL1t
    	kernel_ventry   1, t, 64, fiq           // FIQ EL1h
    	kernel_ventry   1, t, 64, error         // Error EL1t
    
    	kernel_ventry   1, h, 64, sync          // Synchronous EL1h
    	kernel_ventry   1, h, 64, irq           // IRQ EL1h
    	kernel_ventry   1, h, 64, fiq           // FIQ EL1h
    	kernel_ventry   1, h, 64, error         // Error EL1h
    
    	kernel_ventry   0, t, 64, sync          // Synchronous 64-bit EL0
    	kernel_ventry   0, t, 64, irq           // IRQ 64-bit EL0
    	kernel_ventry   0, t, 64, fiq           // FIQ 64-bit EL0
    	kernel_ventry   0, t, 64, error         // Error 64-bit EL0
    
    	kernel_ventry   0, t, 32, sync          // Synchronous 32-bit EL0
    	kernel_ventry   0, t, 32, irq           // IRQ 32-bit EL0
    	kernel_ventry   0, t, 32, fiq           // FIQ 32-bit EL0
    	kernel_ventry   0, t, 32, error         // Error 32-bit EL0
    SYM_CODE_END(vectors)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    问:这里的后缀t和h分别是什么意思?

    答:这是用来标记是否使用共享栈SP_EL0:

    在这里插入图片描述

    arm64 的异常向量表 vectors 中设置了各种异常的入口。kernel_ventry 展开后,可以看到有效的异常入口有两个同步异常 el0_sync,el1_sync 和两个异步异常 el0_irq,el1_irq,中断属于异步异常。

    kernel_ventry   0, t, 64, irq
    --->el0t_64_irq
        --->kernel_entry 0, 64 /* 保存现场:将 CPU 寄存器按照 pt_regs 结构体的定义将现场保存到栈上 */
        --->el0t_64_irq_handler
                __el0_irq_handler_common(regs);
                    el0_interrupt(regs, handle_arch_irq);
                        enter_from_user_mode(regs);
                        write_sysreg(DAIF_PROCCTX_NOIRQ, daif);
                        if (regs->pc & BIT(55))
                            arm64_apply_bp_hardening();
                        do_interrupt_handler(regs, handler);
                            handle_arch_irq(); /* 该函数在gicv3驱动初始化的时候有设置:gic_handle_irq */
                        exit_to_user_mode(regs);
    	--->ret_to_user
            	......
            	kernel_exit 0 /* 恢复现场:恢复 pt_regs 中的寄存器上下文 */
            		.....
    				eret /* 将SPSR_ELn寄存器的值恢复到PSTATE中 */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    中断处理最终会进入 gic_handle_irq:

    static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
        irqnr = do_read_iar(regs); /* 读取中断控制器的寄存器ICC_IAR1_EL1,并获取 hwirq */
    	if (static_branch_likely(&supports_deactivate_key))
    		gic_write_eoir(irqnr);
    			write_sysreg_s(irq, SYS_ICC_EOIR1_EL1); /* 对ICC_EOIR1_EL1寄存器的写操作表示中断的结束 */			------------ (note)
    	handle_domain_irq(gic_data.domain, irqnr, regs);
    		struct pt_regs *old_regs = set_irq_regs(regs); /* 保存被中断的线程的上下文 */
            irq_enter(); /* 进入中断上下文 */
    		desc = irq_resolve_mapping(domain, hwirq); /* 根据hwirq去查找linux中断号 */
            handle_irq_desc(desc);
    			generic_handle_irq_desc(desc);
    				/*
    					highlevel irq-events handler,desc->handle_irq还不是指向真正的处理函数
    					spi中断的话,指向handle_fasteoi_irq
    				*/
    				desc->handle_irq(desc);
    				--->handle_fasteoi_irq
                        --->handle_irq_event(desc);
    							handle_irq_event_percpu(desc);
                        			__handle_irq_event_percpu(desc, &flags);
    									for_each_action_of_desc(desc, action) {
                                        	res = action->handler(irq, action->dev_id); /* 这里执行irqnum对应的中断处理函数 */
                                            switch (res) {
                                            	case IRQ_WAKE_THREAD: /* 中断线程化处理 */
                                                    __irq_wake_thread(desc, action);
                                                case IRQ_HANDLED:
                                                    *flags |= action->flags;
                                                    break;
                                            }
                                        }
    					--->cond_unmask_eoi_irq(desc, chip);
    							chip->irq_eoi(&desc->irq_data); /* 执行gic_eoimode1_eoi_irq */
    								gic_write_dir(gic_irq(d));
    									write_sysreg_s(irq, SYS_ICC_DIR_EL1); /* 对ICC_DIR_EL1寄存器的写操作表示中断的deactivation */
    							unmask_irq(desc); /* 根据不同条件执行unmask_irq()解除中断屏蔽 */
            irq_exit();/* 退出中断上下文 */
    		set_irq_regs(old_regs); /* 恢复被中断的线程的上下文 */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    handle_fasteoi_irq:处理共享中断,并且遍历 irqaction 链表,逐个调用 action->handler() 函数,这个函数正是设备驱动程序调用 request_irq/request_threaded_irq 接口注册的中断处理函数,此外如果中断线程化处理的话,还会调用 __irq_wake_thread 唤醒内核线程。

    ------------ (note):

    问:假设读取到了一个SPI中断, 为什么一开始就写EOI表示中断结束,此时中断处理不是还没有执行么?
    答:在GIC v3协议中定义, 处理完中断后,软件必须通知中断控制器已经处理了中断,以便状态机可以转换到下一个状态。
    GICv3架构将中断的完成分为2个阶段:

    1. Priority Drop:将运行优先级降回到中断之前的值。
    2. 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不是代表中断结束。

    在这里插入图片描述

    4. 中断线程化

    __irq_wake_thread
        wake_up_process(action->thread); /* 唤醒中断处理线程irq_thread */
    
    irq_thread
        if (force_irqthreads() && test_bit(IRQTF_FORCED_THREAD, &action->thread_flags)) /* 强制线程化 */
            handler_fn = irq_forced_thread_fn;
    	else
        	handler_fn = irq_thread_fn;			
    	......
        while (!irq_wait_for_interrupt(action)) {
            action_ret = handler_fn(desc, action);
            action->thread_fn(action->irq, action->dev_id); /* 运行最终的线程化处理函数 */
            if (action_ret == IRQ_WAKE_THREAD)
                irq_wake_secondary(desc, action);          
        }
    
    irq_wait_for_interrupt
        for (;;) {
        	set_current_state(TASK_INTERRUPTIBLE);
            ......
    		if (test_and_clear_bit(IRQTF_RUNTHREAD, &action->thread_flags)) { /* 测试中断线程的唤醒条件是否满足 */
    			__set_current_state(TASK_RUNNING); /* 满足条件,设置当前任务为TASK_RUNNING,并返回0 */
    			return 0;
    		}
            schedule(); /* 不满足唤醒条件,执行调度,让出cpu */
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
  • 相关阅读:
    【PyTorch】深度学习实践之反向传播 Back Propagation
    [MAUI]写一个跨平台富文本编辑器
    css:移动端实现1px、0.5px的细线
    【Python基础教程】类的定义和使用
    docker实战学习2022版本(六)之Dockerfile整合微服务实战
    ElasticSearch学习(五): 分词器
    easycms v5.5 分析 | Bugku S3 AWD排位赛
    Prerender.io 配置过程 给你的VUE单页面网站增加一点seo吧~
    rsync
    六月集训(第28天) —— 动态规划
  • 原文地址:https://blog.csdn.net/u012010054/article/details/132957744