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_PPI 9 0xf04>;  /* GIC Maintenence IRQ */
		interrupt-controller;
		reg = <0x0 0x03400000 0 0x10000>, /* GIC Dist */
		      <0x0 0x03460000 0 0xFF004>; /* GIC Re */
		interrupt-parent = <&gic>;
	};

二、初始化

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")		\
		__aligned(__alignof__(struct of_device_id))		\
		 = { .compatible = compat,				\
		     .data = (fn == (fn_type)NULL) ? fn : fn  } /* 存放回调函数gic_of_init */

这里展开后,会定义一个__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)

在内核启动初始化中断的函数中,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

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 的亲和性 */

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;
};

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[];
};
  • 每个中断控制器都对应一个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;
};
  • 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;

下面这张是总的数据结构关联图,核心是围绕着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); /* 设定触发类型 */

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);

下面来分析一下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);

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)

问:这里的后缀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中 */

中断处理最终会进入 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); /* 恢复被中断的线程的上下文 */

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 */
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值