内核处理中断的基本过程

最近阅读了一些中断相关的代码,趁热打铁,把自己的浅显的理解记录下来。
我们知道,使用中断的驱动程序或者内核模块在初始化的时候通常会调用request_irq或者request_threaded_irq之类的函数来向内核请求中断,并注册中断处理回调。当中断发生后,内核就会调用驱动或者内核注册的回调函数。在我们能够请求中断前,内核会执行许多中断相关的初始化动作,比如扫描设备树中的中断控制器节点,初始化中断控制器,创建数据结构用于管理控制器下的中断。另外内核还会扫描设备树中的各个节点,解析它们使用的中断,映射中断创建中断描述符用于记录中断相关的信息。

内核在初始化中断控制器的过程中,会为每个中断控制器创建一个数据结构,它就是irq_domain,它主要的作用是记录当前中断控制器下硬件中断号与虚拟中断号的映射关系。为什么会存在虚拟中断号呢?因为我们的芯片中可能存在多个中断控制器,每个中断控制器下都可能有相同硬件中断号,所以内核需要一个具有全局唯一性的虚拟中断号用来识别每个中断。

内核扫描设备树节点,为它们映射中断的过程中,就会根据设备使用的中断来分配虚拟中断号,并将虚拟中断号和硬件中断号的映射关系记录到irq_domain中。除了分配虚拟中断号,内核还会为每个中断分配一个中断描述符(irq_desc),中断描述符中会用于记录该中断相关的信息,比如它所属的irq_domain、硬件中断号、软件中断号、highlevel中断处理回调(澄清一下,这个回调并不是指request_irq注册的回调)、irqaction链表。

前面突然说到了irqacttion链表,irqaction是用来做什么的呢?irqaction就是我们调用request_irq或者request_threaded_irq请求中断后生成的一个结构体变量。irqaction会把request_irq或者request_threaded_irq调用者提供的中断处理函数记录下来,irqaction会被链接到irq_desc(中断描述符)的irqaction链表上。当中断发生后,内核(准确的说是中断控制器的驱动)会先调用irq_desc的highlevel中断处理回调,highlevel中断处理回调遍历irqaction,然后再调用驱动程序通过request_irq或者request_threaded_irq注册的中断处理函数。irq_descirqaction是一对多的关系,也就是说多个驱动可以对同一个中断调用request_irq,当然,共享中断的驱动们会受到额外的一些限制,比如它们请求中断时必须传递IRQF_SHARED标记,必须请求相同的触发类型等等。

前面我们大概了解了内核初始化中断控制器、映射中断、驱动请求中断的过程中会发生些什么,接下来就分别看看这些部分的代码。最后我们在看看中断发生后,内核是怎样一步一步调用到驱动注册的处理函数的。

初始化中断控制器

内核在初始化过程中会扫描设备树中的中断控制器节点,为中断控制器寻找合适的控制器驱动并调用控制器驱动的初始化函数。不同中断控制器驱动的初始化动作是不同的,但它们大多会做这两个工作:

  • 初始化irq_chipirq_chip是一个回调函数集,它里面包含了很多操作中断控制器的回调,比如屏蔽中断、使能中断、ack中断的函数。这些回调函数由中断控制器驱动来提供,稍后中断处理流程中就会调用到这些函数。
  • 分配irq_domain。正如开头所说的,irq_domain主要用于记录当前中断控制器下硬件中断号与虚拟中断号的映射关系。

此外,中断控制器驱动在执行初始化时,还会设置一个全局的中断处理入口函数。中断发生后,cpu会先跳转到中断向量表,然后执行一些汇编代码完成一些中断现场保存工作,随后再通过中断控制器驱动设置的入口处理函数将中断传递到中断控制器的驱动。
前面我们提到了芯片中可能存在多个中断控制器,这些中断控制器的拓扑结构就像这样:
中断控制器拓扑.drawio

只有根控制器的驱动会设置全局的中断处理入口函数,child控制器驱动不需要设置。对于parent控制器而言,child控制器就相当于一个普通中断设备,内核调用child控制器驱动的初始化函数时,初始化函数中除了初始化irq_chip、分配irq_domain外,还会映射child控制器所使用的中断,为它创建中断描述符,并记录下它特殊的highlevel中断处理回调。

中断控制器和控制器驱动的匹配过程

内核中断控制器的驱动大多位于linux_*/driver/irqchip/目录下,在这些中断控制器驱动代码中常可以看到这样类似的定义:

IRQCHIP_DECLARE(gic_400, "arm,gic-400", gic_of_init);
IRQCHIP_DECLARE(arm11mp_gic, "arm,arm11mp-gic", gic_of_init);
IRQCHIP_DECLARE(arm1176jzf_dc_gic, "arm,arm1176jzf-devchip-gic", gic_of_init);
IRQCHIP_DECLARE(cortex_a15_gic, "arm,cortex-a15-gic", gic_of_init);
IRQCHIP_DECLARE(cortex_a9_gic, "arm,cortex-a9-gic", gic_of_init);

IRQCHIP_DECLARE宏会定义一个struct of_device_id类型的结构体变量,它会被用于匹配中断控制器和中断控制器驱动。当我们编译内核镜像时,由IRQCHIP_DECLARE宏定义的of_device_id会被链接到__irqchip_of_table这个段中。内核启动过程中扫描设备树中的中断控制器节点时,便会在__irqchip_of_table段中查找与中断控制器匹配的of_device_id,随后调用中断控制器驱动的初始化函数(IRQCHIP_DECLARE宏第2个参数用于匹配,第3个参数是驱动的初始化函数)。
内核匹配中断控制器与驱动的函数调用过程大致如下:

start_kernel
	init_IRQ
		irqchip_init
			of_irq_init(__irqchip_of_table) // of_irq_init函数会遍历设备树中的中断控制器节点,从__irqchip_of_table中查找合适的驱动

of_irq_init函数代码大致如下:

// linux_5.10.97/driver/of/irq.c
void __init of_irq_init(const struct of_device_id *matches) // 入参matches是指向 __irqchip_of_table的指针
{
	const struct of_device_id *match;
	struct device_node *np, *parent = NULL;
	struct of_intc_desc *desc, *temp_desc;
	struct list_head intc_desc_list, intc_parent_list;

	INIT_LIST_HEAD(&intc_desc_list);
	INIT_LIST_HEAD(&intc_parent_list);
	
	// 1 遍历设备书中的中断控制器节点,在__irqchip_of_table中查找合适的 of_device_id
	for_each_matching_node_and_match(np, matches, &match) {
		if (!of_property_read_bool(np, "interrupt-controller") ||
				!of_device_is_available(np))
			continue;

		......
		desc = kzalloc(sizeof(*desc), GFP_KERNEL);
		......
		
		// 1.1 将中断控制器节点信息和驱动的初始化函数暂时记录下来,链接到intc_desc_list链表
		desc->irq_init_cb = match->data; // match->data 是驱动的初始化函数
		desc->dev = of_node_get(np); // np是中断控制器设备树节点
		desc->interrupt_parent = of_irq_find_parent(np);
		if (desc->interrupt_parent == np)
			desc->interrupt_parent = NULL;
		list_add_tail(&desc->list, &intc_desc_list);
	}

	// 2 从根控制器节点开始,按照先parent后child顺序调用中断控制器驱动的初始化函数
	while (!list_empty(&intc_desc_list)) {
		
		list_for_each_entry_safe(desc, temp_desc, &intc_desc_list, list) {
			int ret;

			if (desc->interrupt_parent != parent)
				continue;

			list_del(&desc->list);

			of_node_set_flag(desc->dev, OF_POPULATED);

			......
			ret = desc->irq_init_cb(desc->dev, desc->interrupt_parent); // 调用中断控制器驱动初始化函数,初始化函数第一个入参为中断控制器的设备树节点,第二个入参为中断控制器的parent节点
			......
			list_add_tail(&desc->list, &intc_parent_list);
		}

		/* Get the next pending parent that might have children */
		desc = list_first_entry_or_null(&intc_parent_list,
						typeof(*desc), list);
		......
		list_del(&desc->list);
		parent = desc->dev;
		kfree(desc);
	}

	list_for_each_entry_safe(desc, temp_desc, &intc_parent_list, list) {
		list_del(&desc->list);
		kfree(desc);
	}
err:
	......
}

中断控制器驱动的初始化函数

接下来以arm的gic v1(generic interrupt controller)驱动为例来进行分析。gic v1的驱动代码位于linux_5.10.97/driver/irqchip/irq_gic.c,它的初始化函数为gic_of_initgic_of_init调用的一些关键函数大致如下:

gic_of_init(struct device_node *node, struct device_node *parent)
	gic = &gic_data[gic_cnt]; // irq_gic.c中定义了一个全局的数组gic_data[]和一个全局变量gic_cnt,它们被用来记录已初始化gic中断控制器的数据和数量。 因为根中断控制器是最先初始化的,所以gic_data[0]中记录的是根中断控制器信息。
	__gic_init_bases(gic, &node->fwnode);
		if (gic == &gic_data[0]){
			// 1 如果正在初始化的是根控制器,将全局的中断处理入口函数设置为 gic_handle_irq
			set_handle_irq(gic_handle_irq);
				handle_arch_irq = handle_irq; // 中断发生后,cpu执行一部分汇编代码后就会调用handle_arch_irq
		}
		// 2 初始化irq_chip
		gic_init_chip(gic, NULL, name, true);
		// 3 计算当前中断控制器支持的中断数目,创建irq_domain
		gic_init_bases(gic, handle);
			gic->domain = irq_domain_create_linear(handle, gic_irqs, &gic_irq_domain_hierarchy_ops, gic); // 入参中handle记录的是设备树节点信息,gic_irqs是中断控制器支持的中断数目,gic_irq_domain_hierarchy_ops是一个操作函数集,稍后映射中断的过程中会调用这个函数集里面的函数
	if (parent){
		// 4 parent不为空,表示当前初始化的是child中断控制器,对于parent控制器而言,child控制器本身也是一个中断设备。
		irq = irq_of_parse_and_map(node, 0);
			of_irq_parse_one(dev, index, &oirq) // 解析child控制器设备树节点中的中断信息 
			irq_create_of_mapping(&oirq) // 映射该中断,为其创建中断描述符
		gic_cascade_irq(gic_cnt, irq); // 将child控制器的highlevel中断处理回调设置为gic_handle_cascade_irq,当child控制下的设备产生了中断时,它的parent就会通过gic_handle_cascade_irq函数把中断处理请求传递给它
			irq_set_chained_handler_and_data(irq, gic_handle_cascade_irq, &gic_data[gic_nr]);
	}

中断处理的入口函数

通常根控制器驱动执行初始化动作时会向内核设置一个全局的中断处理入口函数,所有的中断发生后,都会先传递到这个入口函数里面来,由它来把中断处理请求分发到各个驱动或者下级中断控制器驱动。从上面代码中的注释1处可以看到,gic会把入口函数设置为gic_handle_irq,稍后分析中断处理流程时再来细看这个函数。

gic的irq_chip

每个中断控制器实现的irq_chip都不一样,笔者对中断控制器操作寄存器的细节不了解,只大概知道它是干嘛的。对于gic而言,irq_chip的内容大致如下:

// linux_5.10.97/driver/irqchip/irq_gic.c
static const struct irq_chip gic_chip = {
	.irq_mask		= gic_mask_irq, // 屏蔽中断线
	.irq_unmask		= gic_unmask_irq, // unmask中断线
	.irq_eoi		= gic_eoi_irq, // end of interrupt
	.irq_set_type		= gic_set_type,
	.irq_get_irqchip_state	= gic_irq_get_irqchip_state,
	.irq_set_irqchip_state	= gic_irq_set_irqchip_state,
	.flags			= IRQCHIP_SET_TYPE_MASKED |
				  IRQCHIP_SKIP_SET_WAKE |
				  IRQCHIP_MASK_ON_SUSPEND,
};
irq_domain

正如文章开头说的,irq_domain会记录当前中断控制器下硬件中断号和虚拟中断号的映射关系,它有三种方式来记录映射关系:

  • 使用数组来记录,数组下标就代表硬件中断号,数组成员就是虚拟中断号
  • 使用radix tree来记录,树节点的key就是硬件中断号,value是一个struct irq_data类型的结构体变量,irq_data结构体中有成员记录虚拟中断号
  • 不记录,当硬件中断号直接映射到虚拟中断号时,硬件中断号和虚拟中断号是相同的,所以irq_domain不用记录这样的映射关系

irq_domain可以使用上述三种方式的任意一种来记录映射关系,也可以混搭。具体使用哪种方式,取决于中断控制器驱动如何初始化irq_domain。先来简单看下irq_domian的结构成员,稍微留意一下revmap_direct_max_irqrevmap_size这两个成员:

// linux_5.10.97/include/linux/irqdomain.h
struct irq_domain {
	struct list_head link; // 所有的irq_domain都会链接到一个全局链表irq_domain_list,link是链表节点
	const char *name;
	const struct irq_domain_ops *ops; // 由irq_domain创建者提供的操作函数集,映射中断的过程中会调用它们
	void *host_data; // irq_domian创建者的私有数据
	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; // 指向parent中断控制器的irq_domain
#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; // 硬件中断号小于这个值,irq_domain就认为它使用了直接映射,就不会记录映射关系
	unsigned int revmap_size; // 硬件中断号小于这个值,irq_domian就用数组来记录映射关系
	struct radix_tree_root revmap_tree; // 用于记录映射关系的radix tree
	struct mutex revmap_tree_mutex;
	unsigned int linear_revmap[]; // 用于记录映射关系的变长数组,irq_domain初始化过程中会根据revmap_size的值为这个数组分配足够的空间
};

从前面的gic_of_init函数可以看到,它间接调用了一个名为irq_domain_create_linear函数来创建irq_domainirq_domain_create_linear会将irq_domain::revmap_direct_max_irq设置为0,将irq_domain::revmap_sizeirq_domain::hwirq_max设置为相同值,这样irq_domain就只会用数组来记录映射关系了。除了irq_domain_create_linear函数外,内核还提供了其它的函数用来创建irq_domain,比如irq_domain_create_treeirq_domain_create_tree创建的irq_domain会使用radix tree来记录映射关系。

需要注意的是,刚创建完irq_domain时,irq_domain是没有记录任何映射关系的,在稍后解析设备树中断设备的节点并为其映射中断的过程中才会真正的创建映射关系。

irq_domain_ops

gic_of_init函数间接调用irq_domain_create_linear函数时,传递了一个名为gic_irq_domain_hierarchy_ops的函数集作为入参,它会被保存到irq_domain::opsgic_irq_domain_hierarchy_ops定义如下:

// linux_5.10.97/driver/irqchip/irq_gic.c
static const struct irq_domain_ops gic_irq_domain_hierarchy_ops = {
	.translate = gic_irq_domain_translate, // 从设备树节点interrupts属性中提取硬件中断号
	.alloc = gic_irq_domain_alloc, // 设置irq_desc的highlevel中断处理回调,初始化irq_data中的某些数据
	.free = irq_domain_free_irqs_top,
};

irq_domain_ops中的函数通常会在映射中断过程中被调用。比如,映射中断的过程中内核需要知道中断设备的硬件中断号,而设备树中使用中断设备节点通常长这样:

ap2woccif: ap2woccif@151A5000 {
		compatible = "xxxxxx,xxxxx";
		......
		interrupt-parent = <&gic>;
		interrupts = <GIC_SPI 211 IRQ_TYPE_LEVEL_HIGH>,
			     <GIC_SPI 212 IRQ_TYPE_LEVEL_HIGH>;
        };

interrupts属性用于描述了设备的中断信息,比如触发方式是什么,硬件中断号是什么,这部分信息只有中断控制器的驱动能解析出来,中断控制器驱动初始化时提供的irq_domain_ops::translate就会被用于解析设备节点的中断信息,提取硬件中断号。有的中断控制器会提供irq_domain_ops::xlate,它也是用来解析硬件中断号的。

映射中断

内核初始化过程中扫描设备树中的设备节点时,如果该设备节点中有中断属性(interrupts),内核就会解析它携带的中断信息,并映射该中断。大致的函数调用过程如下:

// 内核初始化过程中执行machine相关的初始化动作时,通常会调用of_platform_default_populate来扫描设备树节点,为它们生成platform_device
of_platform_default_populate
	of_platform_populate(root, of_default_bus_match_table, lookup, parent);
		root = root ? of_node_get(root) : of_find_node_by_path("/"); // 找到设备树根节点
		for_each_child_of_node(root, child) { // 遍历根节点下所有节点,调用of_platform_bus_create为其创建platform_device,其实不止根节点下的节点会被创建platform_device,位于simple-bus,simple-mfd,isa这些bus下的节点也会被创建platform_device(详情见of_default_bus_match_table数组和of_platform_bus_create函数)
			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); // 创建platform_device,同时解析设备树节点中的信息
						num_irq = of_irq_count(np); // 获取设备树节点中的中断属性个数 
						of_irq_to_resource_table(np, res, num_irq) // 如果中断属性个数大于0,调用of_irq_to_resource_table来映射中断
							of_irq_to_resource(dev, i, res)
								// 1 映射中断
								irq = of_irq_get(dev, index)
									of_irq_parse_one(dev, index, &oirq); // 提取设备树节点中的interrupt-parent,interrupts等属性,这些属性会放在oirq中
									irq_create_of_mapping(&oirq); // 映射中断
								// 2 将虚拟中断号存储到`struct resource`类型的结构体变量,设备的驱动程序在请求中断时,通过platform_device::resource便可以访问虚拟中断号
								r->start = r->end = irq;
		}

上面的函数调用过程有点复杂,大概知道这个过程中断被映射了以及虚拟中断号被记录到了platform_device::resource就行。
接下来详细看看中断映射,中断映射的过程中大概会做这几件事情:

  • 分配虚拟中断号,分配中断描述符(irq_desc)
  • 分配irq_data
  • 调用irq_domainirq_domain_ops::alloc回调来设置irq_data和highlevel中断处理回调
  • 将硬件中断号和虚拟中断号的映射关系记录到irq_domain

中断映射大致的函数调用过程如下:

irq_create_of_mapping
	irq_create_fwspec_mapping(&fwspec);
		// 使用设备树节点的interrupt-parent属性来查找irq_domain (从全局的irq_domain_list链表上查找)
		domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_WIRED);
		// 调用irq_domain_ops::translate来把设备树节点的interrupts翻译为硬件中断号
		irq_domain_translate(domain, fwspec, &hwirq, &type)
		// 使用硬件中断号在irq_domain中查找虚拟中断号
		virq = irq_find_mapping(domain, hwirq);
		if (virq) {
			// 如果虚拟中断号不为0,表示该硬件中断号已经映射过了,不能再映射了
		}else{
			// 虚拟中断为0,映射中断
			virq = irq_domain_alloc_irqs(domain, 1, NUMA_NO_NODE, fwspec);
		}

irq_domain_alloc_irqs函数的大致内容如下:

irq_domain_alloc_irqs
	__irq_domain_alloc_irqs(domain, -1, nr_irqs, node, arg, false, NULL);
		// 1 分配虚拟中断号,分配irq_desc
		virq = irq_domain_alloc_descs(irq_base, nr_irqs, 0, node, affinity);
			 __irq_alloc_descs(-1, hint, cnt, node, THIS_MODULE, affinity);
				start = bitmap_find_next_zero_area(allocated_irqs, IRQ_BITMAP_BITS, from, cnt, 0); // 从bitmap中查找空闲的虚拟中断号
				alloc_descs(start, cnt, node, affinity, owner); // 分配irq_desc并给irq_desc设置一些初始值
					alloc_desc(start + i, node, flags, mask, owner);
		// 2 分配额外的irq_data
		irq_domain_alloc_irq_data(domain, virq, nr_irqs)
		// 3 调用irq_domain的irq_domain_ops::alloc回调来设置irq_data和highlevel中断处理回调
		irq_domain_alloc_irqs_hierarchy(domain, virq, nr_irqs, arg);
		// 4 将硬件中断号和虚拟中断号的映射关系记录到irq_domain
		irq_domain_insert_irq(virq + i);

内核通过一个全局的位图来管理虚拟中断号,每一个bit都代表一个虚拟中断号,正如上面注释1下面的代码所示,分配irq_desc前要先去位图上找到空闲的虚拟中断号,每分配一个虚拟中断号,就置位对应的bit。

中断描述符

内核会为每个中断分配一个中断描述符(irq_desc),内核使用一个全局的radix tree来管理所有的irq_desc,虚拟中断号是radix tree树节点的key,而irq_desc的地址是radix tree树节点的value,通过虚拟中断号就可以在radix tree上找到irq_desc

irq_desc有几个比较关键的结构体成员,如下:

struct irq_desc {
	struct irq_data		irq_data; // irq_data记录了虚拟中断、硬件中断号、irq_domain地址、irq_chip地址等信息
	......
	irq_flow_handler_t	handle_irq; // 该中断的highlevel处理回调,中断控制器的驱动会通过这个处理回调将中断请求分发给irq_desc,这个回调通常在调用irq_domain_ops::alloc的期间由中断控制器的驱动设置
	struct irqaction	*action; // 中断设备驱动程序调用request_irq函数请求中断后,就会生成一个irqaction挂于这个action链表上,irqaction会记录驱动提注册中断处理函数,通常控制器驱动将中断分发到irq_desc::handle_irq后,irq_desc::handle_irq会挨个去执行action链表上的irqaction,调用中断设备驱动注册的中断处理函数
	......
	unsigned int		nr_actions; // irqaction的数目
	......
}

irq_data

irq_data记录了虚拟中断、硬件中断号、irq_domain地址、irq_chip地址等信息,它的结构体成员大致如下:

struct irq_data {
	u32			mask;
	unsigned int		irq; // 虚拟中断号
	unsigned long		hwirq; // 硬件中断号
	struct irq_common_data	*common;
	struct irq_chip		*chip; // 指向irq_chip的指针
	struct irq_domain	*domain; // 指向irq_domain的指针
#ifdef	CONFIG_IRQ_DOMAIN_HIERARCHY
	struct irq_data		*parent_data; // 指向parent irq_data的指针
#endif
	void			*chip_data;
};

irq_desc的结构体成员可以看到它内部有一个irq_datairq_data会随着irq_desc的分配而分配。但若是系统中存在级联的中断控制器,那么就还需要分配额外的irq_data来记录该中断与parent中断控制器的关系。比如有两个级联的中断控制器,child控制器下的某个中断在映射时会产生两个irq_data,如下图所示:
在这里插入图片描述

无耐笔者功力太浅,暂未弄清parent irq_data的作用。

设置irq_data和中断处理回调

irq_domain_alloc_descs函数和irq_domain_alloc_irq_data函数分配irq_data时会给irq_data::irq赋值,而接下来的irq_domain_ops::alloc又会给irq_data::hwirq,irq_data::chip,irq_data::chip_datad等成员赋值,同时还会为当前中断设置highlevel中断处理回调。
gic在初始化irq_domain时,将irq_domain_ops::alloc设置为gic_irq_domain_allocgic_irq_domain_alloc调用的函数大致如下:

gic_irq_domain_alloc
	// 获取硬件中断号
	gic_irq_domain_translate(domain, fwspec, &hwirq, &type)
	// 调用gic_irq_domain_map来设置irq_data
	gic_irq_domain_map(domain, virq + i, hwirq + i);

gic_irq_domain_map函数代码如下:

static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq,
				irq_hw_number_t hw)
{
	struct gic_chip_data *gic = d->host_data;
	// 获取irq_data
	struct irq_data *irqd = irq_desc_get_irq_data(irq_to_desc(irq));
	
	// 调用irq_domain_set_info来设置irq_data,其中第6个入参是一个函数,这个函数就是中断的highlevel处理回调,irq_domain_set_info函数会把它赋给irq_desc::handle_irq这个结构体成员
	// 对于gic而言,0-15号中断是SGI,16-31号中断是PPI,它们的中断处理回调与普通的SPI中断不一样
	switch (hw) {
	case 0 ... 15:
		......
		irq_domain_set_info(d, irq, hw, &gic->chip, d->host_data, handle_percpu_devid_fasteoi_ipi, NULL, NULL);
		break;
	case 16 ... 31:
		......
		irq_domain_set_info(d, irq, hw, &gic->chip, d->host_data, handle_percpu_devid_irq, NULL, NULL);
		break;
	default:
		irq_domain_set_info(d, irq, hw, &gic->chip, d->host_data, handle_fasteoi_irq, NULL, NULL);
		......
		break;
	}
	......
}

irq_domain_set_info函数调用过程大致如下:

irq_domain_set_info
	// 给irq_data的部分成员赋值
	irq_domain_set_hwirq_and_chip(domain, virq, hwirq, chip, chip_data);
		struct irq_data *irq_data = irq_domain_get_irq_data(domain, virq);
		irq_data->hwirq = hwirq;
		irq_data->chip = chip ? chip : &no_irq_chip;
		irq_data->chip_data = chip_data;
	// 把中断处理回调赋值给irq_desc::handle_irq
	__irq_set_handler(virq, handler, 0, handler_name);
		struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, 0);
		__irq_do_set_handler(desc, handle, is_chained, name);
			desc->handle_irq = handle;

对于一个SPI中断的映射过程而言,内核调用gic_irq_domain_alloc后,irq_desc::irq_handler被设gic的驱动置为了handle_fasteoi_irq函数,稍后该中断触发时,gic的驱动就会调用handle_fasteoi_irq函数处理该中断。

记录映射关系

irq_descirq_data都初始化完成后,内核会调用irq_domain_insert_irq函数来记录映射关系:

irq_domain_insert_irq
	irq_domain_set_mapping(domain, data->hwirq, data);
		if (hwirq < domain->revmap_size) { // 硬件中断号小于irq_domai::revmap_size时,使用数组来记录映射关系
			domain->linear_revmap[hwirq] = irq_data->irq;
		} else { // 硬件中断号大于irq_domai::revmap_size时,使用radix tree来记录映射关系
			radix_tree_insert(&domain->revmap_tree, hwirq, irq_data); // 树节点key值是hwirq,value是irq_data的地址
		}

映射关系记录好后,在irq_domain上通过硬件中断号便可以获取虚拟中断号,获取虚拟中断号后,又可以使用虚拟中断号从全局的树结构中找到irq_desc

请求中断

内核初始完中断控制器并映射中断后,我们的设备驱动就可以请求中断了。对于platform_driver而言,它们可以从platform_device::resource获取虚拟中断号,然后调用request_irq或者request_threaded_irq函数即可请求中断,request_irq函数其实也是通过调用request_threaded_irq来实现的,我们直接来看request_threaded_irq函数的调用流程:

// linux_5.10.97/kernel/irq/manage.c
// 入参中irq是虚拟中断号,handler是中断处理函数,thread_fn也是中断处理函数(它不为空的时候内核会线程化中断),irqflags是一些标记
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)
	// 获取irq_desc
	desc = irq_to_desc(irq);
	
	// handler和thread_fn不能都为空
	if (!handler) {
		if (!thread_fn)
			return -EINVAL;
		handler = irq_default_primary_handler; // handler为空,thread_fn不为空,这种情况下内核会线程化中断。这里设置了一个特殊的handler,irq_default_primary_handler会返回一个特殊值,highlevel中断处理回调会根据这个特殊的返回值唤醒中断线程。
	}
	
	// 分配irqaction
	action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
	action->handler = handler;
	action->thread_fn = thread_fn;
	action->flags = irqflags;
	
	// 初始化irqaction
	__setup_irq(irq, desc, action);

request_threaded_irqhandler入参和thread_fn入参都是中断处理函数,它两应当至少有一个不为NUL。当我们传递的thread_fn不为NULL,或者中断没有设置IRQ_NOTHREAD之类的标记时(gic映射中断时是设置了的),内核都会尝试把我们的中断处理函数放到线程里去执行,这种情况下,内核会为该irqaction创建一个内核线程。

接下来看看__setup_irq函数的调用流程如下:

// linux_5.10.97/kernel/irq/manage.c
static int __setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new)
	// 判断IRQ_NOTHREAD标志,如果没有设置该标志,即便传递的`thread_fn`为NULL,也要尝试线程化中断
	if (irq_settings_can_thread(desc)) { 
		ret = irq_setup_forced_threading(new); // 从这个函数里面出来后 handler会被赋值给irqaction::thread_fn,具体的实现要稍稍复杂一些,感兴趣可以看一看
	}
	
	// 创建线程
	if (new->thread_fn && !nested) {
		ret = setup_irq_thread(new, irq, false);
		......
	}
	
	// 判断是否有别人请求过该中断
	old_ptr = &desc->action;
	old = *old_ptr;
	if (old) {
		// 如果是,就还需要做一些额外的判断,内核的注释解释的挺清楚
		/*
		 * Can't share interrupts unless both agree to and are
		 * the same type (level, edge, polarity). So both flag
		 * fields must have IRQF_SHARED set and the bits which
		 * set the trigger type must match. Also all must
		 * agree on ONESHOT.
		 * Interrupt lines used for NMIs cannot be shared.
		 */
		 ......
		 // 把新建的irqaction放到irq_desc::action链表尾部
		 do {
			thread_mask |= old->thread_mask;
			old_ptr = &old->next;
			old = *old_ptr;
		} while (old);
		shared = 1;
	}
	...... // 剩余的一大部分代码理解不了,省略

中断处理流程

在描述中断的处理流程前,先简单回顾一下初始化中断控制器、映射中断以及请求中断过程中发生的事情:

  • gic根控制器驱动设置了全局的中断处理入口函数gic_handle_irq并创建了irq_domain
  • 内核为中断设备映射了中断,创建了中断描述符(irq_desc),将highlevel中断处理回调设置为handle_fasteoi_irq(对于child控制器而言是gic_handle_cascade_irq)。此外,内核还将硬件中断和软件中断的映射关系记录到了irq_domain
  • 驱动请求中断后,内核使用irqaction记录的中断处理函数,并将irqaction链接到了irq_desc::action链表

不难想象,当中断发生后,内核的调用流程大概就像这样:
中断 --》保存中断现场 --》调用根控制器设置的入口函数(gic_handle_irq) --》gic_handle_irq通过寄存器获取硬件中断号,通过irq_domain记录的映射关系获取虚拟中断号 --》 通过虚拟中断号获取irq_desc --》调用irq_desc记录的highlevel中断回调

如果该中断不是来自于下级中断控制器,那么剩余的流会像这样:
highlevel中断回调遍历irq_desc::action链表上的irqaction --》调用irq_action::handler记录的中断处理函数,或者唤醒中断线程执行irq_action::thread_fn记录的中断处理函数 --》执行完硬件中断,检查是否需要执行softirq --》恢复中断现场

如果该中断不是来自于下级中断控制器,那么剩余的流程会像这样:
child控制器的highlevel中断处理回调被调用(gic_handle_cascade_irq) --》查寄存器确定硬件中断号,通过自己的irq_domain获取虚拟中断号 --》通过虚拟中断号获取irq_desc -》 调用irq_desc记录的highlevel中断回调 --》highlevel中断回调遍历irq_desc::action链表上的irqaction --》调用irq_action::handler记录的中断处理函数,或者唤醒中断线程执行irq_action::thread_fn记录的中断处理函数 --》执行完硬件中断,检查是否需要执行softirq --》恢复中断现场

接下来看看一些关键函数的调用过程,如下:

// 保存中断现场后调用根中断控制器设置的中断处理入口函数 gic_handle_irq (我们是以gic为例进行的分析,其它类型的中断控制器不是调用这个函数哈)
gic_handle_irq
	// 读取寄存器获取硬件中断号
	irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK);
	irqnr = irqstat & GICC_IAR_INT_ID_MASK;
	// irq_domain处理该硬件中断
	handle_domain_irq(gic->domain, irqnr, regs);
		__handle_domain_irq(domain, hwirq, true, regs);
			// 增加hardirq计数,标志进入硬件中断处理过程
			irq_enter
			// 使用硬件中断号从当前irq_domain查找虚拟中断号
			irq = irq_find_mapping(domain, hwirq);
			// 处理该中断
			generic_handle_irq(irq);
				// 使用虚拟中断号获取irq_desc
				struct irq_desc *desc = irq_to_desc(irq);
				generic_handle_irq_desc(desc);
					// 调用irq_desc::irq_handler
					desc->handle_irq(desc);
			// 减少hardirq计数,标志退出硬件中断处理过程
			irq_exit
				__irq_exit_rcu
					// 减少hardirq计数
					preempt_count_sub(HARDIRQ_OFFSET);
					// 检查是否有softirq被挂起,如果有,处理softirq
					if (!in_interrupt() && local_softirq_pending())
						invoke_softirq();

假如中断产生自根gic中断控制器下的某个中断设备,那么被调用的irq_desc::irq_handler就是handle_fasteoi_irqhandle_fasteoi_irq函数的调用过程如下:

void handle_fasteoi_irq(struct irq_desc *desc)
	handle_irq_event(desc);
		handle_irq_event_percpu(desc);
			_handle_irq_event_percpu(desc, &flags);
				// 遍历irq_desc::action链表上的irqaction,
				for_each_action_of_desc(desc, action){
					// 调用irqaction::handler,如果请求的中断没有被线程化,那么此时中断设备驱动注册的中断处理函数就会被调用了
					res = action->handler(irq, action->dev_id);
					// 如果中断被线程化了,irqaction::handler会被设置为irq_default_primary_handler函数,它会返回 IRQ_WAKE_THREAD
					switch (res) {
					case IRQ_WAKE_THREAD:
						// 唤醒中断线程
						__irq_wake_thread(desc, action); 
					}
				}

假如中断产生自child gic中断控制器下的某个中断设备,那么被调用的irq_desc::irq_handler就是gic_handle_cascade_irqgic_handle_cascade_irq函数的调用过程如下:

gic_handle_cascade_irq
	// 读取寄存器获取硬件中断号
	status = readl_relaxed(gic_data_cpu_base(chip_data) + GIC_CPU_INTACK);
	gic_irq = (status & GICC_IAR_INT_ID_MASK);
	// 使用硬件中断号从当前irq_domain查找虚拟中断号
	cascade_irq = irq_find_mapping(chip_data->domain, gic_irq);
	// 处理该中断
	generic_handle_irq(cascade_irq);
		// 使用虚拟中断号获取irq_desc
		struct irq_desc *desc = irq_to_desc(irq);
		generic_handle_irq_desc(desc);
			// 调用irq_desc::irq_handler,接下来的过程就和上面的handle_fasteoi_irq一样了
			desc->handle_irq(desc);

中断基本的处理过程差不多就这些了,感谢阅读!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值