来看看linux中断分析

前言

这研究了勉强算是一个周的gicv3中断,但是这个玩意说到底还是一个IP,一个驱动。我们最后其实大多数的时候还是要站在比较高的维度去看这个中断。

那么我们不用大跃进,我们今天站在Linux操作系统的角度来看看这个中断。

内容还是来自前辈的文章,可以看一下前辈更多的精彩内存,链接在文末。

一、内核中断概述

与ATF相比,linux内核中断子系统为了适配不同的cpu架构与中断处理器

将与体系结构和硬件无关的部分抽象出来,作为通用的中断处理框架,而与硬件相关的部分作为驱动层,以实现对中断资源的管理和中断事件的处理。

(硬件驱动-HAL-内核驱动)

由于linux支持中断控制器的级联,因此多个中断控制器就可能包含相同的硬件中断号。试想如果不做进一步的转换,则通用中断处理框架代码中引用一个中断,则必须要首先要获取该中断对应的中断控制器,然后再获取其在中断控制器上的硬件中断号。

显然这会导致通用中断处理框架与硬件之间存在耦合关系,不符合linux内核的分层抽象机制。为了可以通过中断号唯一地标识一个中断,linux引入了irq domain机制以实现将硬件中断号映射为全局唯一的逻辑中断号

中断触发后,软件需要与中断控制器交互以完成中断应答、中断结束等操作。对于特定的中断控制器,这些操作除了与中断类型有关外,对于所有的同类型中断都是相同的,而与实际的业务无关,因此可以抽象出来统一处理,内核将这部分操作独立为high level中断处理函数。

中断的目的一般是为了处理一些实际业务,如某次DMA传输已结束,通知cpu可以获取相关数据,或者某一个网络数据包已到达,网卡驱动可以处理该数据了等。这部分业务对于取决于中断发送方的定义,因此一般来说不同的中断都不同。因此除了high level中断处理函数以外,还需要执行该中断特定的处理函数,这个函数就是我们在驱动中注册中断时指定的回调函数,在内核中通过irqaction描述该函数。

由于内核允许多个外设共享一根中断线,因此对于共享中断,每次中断可能是由不同的外设触发的,也相应地需要执行不同的irqaction。**故在中断描述符中含有一个irqaction的链表,该中断触发时会依次执行该链表中的不同函数,每个函数都先判断该中断是否属于自身。**若属于自己则执行相应的处理,否则快速返回以让正确的设备处理。

内核通过一些关键的数据结构描述以上这些组件,如irq_desc,irq_domain等,它们之间的关系如下图:

在这里插入图片描述

  • (1)irq_desc:描述一个特定的中断信息,如其所属的中断控制器,中断domain,处理该中断的high level中断处理函数以及irqactions等

  • (2)irq_common_data:该结构所有中断控制器共享,而与特定中断控制器无关的信息,如numa的node,msi描述符,affinity信息等

  • (3)irq_data:该结构主要包含了irq_desc中与硬件相关的一些成员

  • (4)irq_chip:描述一个中断控制器,它主要包含了中断控制器的名字以及回调函数

  • (5)irq_domain:描述一个特定中断控制器的中断号转换信息

  • (6)irq_flow_handler_t:high level中断处理函数,该函数主要用于处理与中断控制器相关的一些底层操作

  • (7)irqactions:该函数为驱动注册的中断处理函数,用于处理实际的业务

二、GICv3驱动

下面来看看Gic在内核中是怎么玩的。

2.1 GICv3驱动注册与调用

GICv3驱动通过下述宏注册到内核中:

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

其中IRQCHIP_DECLARE的定义如下(irqchip.h):

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

该宏实际的作用是定义了一个struct of_device_id类型的__of_table_gic_v3的变量,将compatible成员初始化为arm,gic-v3,data成员初始化为gic_of_init。

并且指示链接器在链接时将该变量保存在__irqchip_of_table的段中,其中段的信息在arch/arm64/kernel/ vmlinux.lds中定义,其定义如下:

. = ALIGN(8); __irqchip_of_table = .; KEEP(*(__irqchip_of_table)) KEEP(*(__irqchip_of_table_end))

即所有通过IRQCHIP_DECLARE定义的中断控制器相关of_device_id结构数据都被保存在__irqchip_of_table段中,该段以__irqchip_of_table地址开头,以__irqchip_of_table_end地址结束。

在内核启动时,通过遍历该段的数据,查找与device tree中匹配的驱动,并执行其data成员指向的回调函数,以完成中断控制器驱动的加载。该流程如下:

在这里插入图片描述

其中在irqchip_init中指定了设备需要匹配的id为__irqchip_of_table地址指定的数据,其代码如下:

extern struct of_device_id __irqchip_of_table[];

void __init irqchip_init(void)
{
	of_irq_init(__irqchip_of_table);
	acpi_probe_device_table(irqchip);
} 

of_irq_init函数则遍历devicetree,并查找与__irqchip_of_table匹配的中断控制器,若匹配成功,则调用由struct of_device_id结构体中data字段设定的中断控制器初始化函数。

显然,为了能正确调用GICv3的驱动,在devicetree中需要添加其设备节点,且其compatible属性需要设置为arm,gic-v3。如arm fvp平台devicetree中中断控制器的设置如下:

gic: interrupt-controller@2f000000 {
		compatible = "arm,gic-v3";
		#interrupt-cells = <3>;
		#address-cells = <2>;
		#size-cells = <2>;
		ranges;
		interrupt-controller;
		reg = <0x0 0x2f000000 0 0x10000>,	// GICD
		      <0x0 0x2f100000 0 0x200000>,	// GICR
		      <0x0 0x2c000000 0 0x2000>,	// GICC
		      <0x0 0x2c010000 0 0x2000>,	// GICH
		      <0x0 0x2c02f000 0 0x2000>;	// GICV
		interrupts = <GIC_PPI 9 IRQ_TYPE_LEVEL_HIGH>;

		its: msi-controller@2f020000 {
			#msi-cells = <1>;
			compatible = "arm,gic-v3-its";
			reg = <0x0 0x2f020000 0x0 0x20000>; // GITS
			msi-controller;
		};
	};

我们知道设备驱动一般使用module_init等宏将自身注册到内核,在内核加载设备时通过总线的match方法查找匹配的驱动,或在驱动加载时从总线上查找匹配的设备。

由于中断控制器的初始化在总线初始化之前,因此无法将其挂载到任何总线上,故其注册方式和匹配方式需要采用上面的特殊处理。

2.2 GICv3驱动架构

GICv3驱动包括驱动初始化流程和回调函数定义等,其主要的初始化流程如下:
在这里插入图片描述

  • (1)gic_validate_dist_version:gic版本校验

  • (2)gic_enable_of_quirks:对于有些平台可能需要加入一些其特定的workaround接口,该函数即用于调用相关的回调

  • (3)gic_init_bases:该函数是gic初始化的主函数,后面再做进一步分析

  • (4)gic_populate_ppi_partitions:由于ppi是每个PE私有的中断,但一般相同中断号的私有中断都是用于同一目的的,因此可以通过相同中的percpu变量管理该中断。
      但随着架构的演进,也出现了一些架构可能需要将不同PE上同一个中断号的ppi用于不同的功能。因此,内核通过在软件层面加入ppi partition机制以支持这一特性。其主要思路是通过引入affinity机制,将系统中的cpu进行分组,每一组含有若干个ppi功能相同的cpu,当中断发生时根据affinity的配置来判断该中断的目的。

  • (5)gic_of_setup_kvm_info:设置kvm中与中断控制器相关的信息

2.2.1 GIC硬件初始化

gic_init_bases主要包括两部分功能,gic硬件初始化,smp相关中断初始化和irqdomain初始化,其主要流程如下:

在这里插入图片描述
gic硬件初始化主要为初始化每个中断的group、priority、affinity等信息,以及一些cpu interface,redistributor使能等的配置。其主流程与bl31中断控制器初始化流程类似,具体流程可参考<bl31中断初探>一文。

有一点需要注意的是在内核对每个中断做初始化时会初始化所有中断的属性信息,包括在bl31中被配置为group 0和secure group 1的中断都会执行相应的初始化操作。为了防止在bl31中配置好的中断被改写,GICv3在寄存器访问级别做了security扩展。其定义如下:

在这里插入图片描述
即在non secure状态下读取secure中断的信息会返回0,而在non secure状态下写secure状态寄存器,写入值会被忽略。因此,内核在初始化所有中断信息时,对secure中断的配置是无效的。

smp中断初始化主要为sgi分配irq_desc,建立逻辑中断号到物理中断号的映射,以及使能中断等。中断号映射接口__irq_domain_alloc_irqs的详细流程可参看下一节的中断号映射流程。

2.2.2 Irq domain初始化和中断号映射

Irq domain用于硬件中断号和逻辑中断号的映射,其映射方式可以采用线性映射或radix tree映射。线性映射直接将映射表保存在一个数组中,而radix tree映射将映射表保存在radix tree中。对于中断较多,且中断号不连续的系统若采用线性映射,则数组中可能会含有较多的空洞,比较浪费内存空间。Irq domain初始化流程见上一节,主要是分配一个domain结构体,为其初始化一个radix tree结构,并设置该domain的ops回调函数和私有数据等。

设备硬件中断号到逻辑中断号的映射是在设备模型初始化device时执行的。内核向总线上添加设备时,从devicetree中读取该设备中断相关的resource,解析处硬件中断号,并通过中断号映射机制将其映射为逻辑中断号。设备添加完成后,系统就可以通过设备模型接口获取逻辑中断号,如我们在驱动中通过platform_get_resource获取到的中断号即是已经映射过的逻辑中断号。以下是内核中断号映射的流程:
在这里插入图片描述

  • (1)of_irq_parse_one用于从devicetree中读取包括中断号等于中断相关的信息,并在irq_create_of_mapping函数中将其保存到struct irq_fwspec结构体中

  • (2)irq_domain_translate:该函数将struct irq_fwspec结构体中的devicetree信息转换为实际的硬件中断号和中断触发类型

  • (3)bitmap_find_next_zero_area:在内核中通过一个bitmap表来维护已分配和空闲逻辑irq信息,若某个irq号已被分配,则其对应的bit也将被置位。此函数用于查找空闲的bit以为该设备的中断分配逻辑中断号

  • (4)irq_domain_set_info:设置irq_desc的信息,包括其所属的domain,硬件irq号,中断控制器的回调函数,以及high level中断处理函数等

  • (5)radix_tree_insert:将该中断相关信息插入到radix tree中,以完成中断号的映射

上面step 4中的中断控制器回调函数定义如下:

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

其包含了对中断控制器的各种操作,如mask/unmask中断,设置中断类型,亲和性,中断处理完成操作,以及重新触发一个中断等。其被注册到irq_desc中后,此后中断处理流程中就可以方便地通过该回调操作中断控制器相关接口。

三、中断处理流程

中断触发后硬件将执行必要的上下文保存操作,如将PSATE保存到SPSR寄存器,将返回地址保存到ELR寄存器,然后跳转到中断入口函数。

与bl31一样,根据中断触发时的运行状态不同,ARMv8一共有四张中断向量表,它们位于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)

由于上面四张表中不同中断向量的处理方式类似,后面我们将以IRQ EL1t类型的SPI中断为例介绍linux中的中断处理流程,其基本流程如下图:
在这里插入图片描述

ARMv8架构中断处理流程中一共包含了四层处理逻辑,中断入口函数、由中断控制器注册的与架构相关的处理函数、high level中断处理函数、以及设备注册的中断action处理函数。它们分别位于上图不同的虚线框中,具体如下:

  • (1)异常向量中的异常入口,它在系统初始化时将被保存到VBAR_EL1寄存器中。中断触发后,硬件根据中断发生时的系统状态,自动跳转到该表的特定entry中。如对于IRQ EL1t类型的SPI中断,则会跳转到kernel_ventry 1, t, 64, irq入口

  • (2)该函数为中断控制器初始化流程中通过set_handle_irq接口设置的,用于处理架构相关的一些操作,如step 3的中断控制器操作,以及获取该中断对应的irq_desc,并调用high level中断处理函数等

  • (3)这两个函数用于向中断控制器应答中断以及执行中断的priority drop和deactivate操作

  • (4)调用high level中断处理函数,该函数是在irq domain映射流程中通过__irq_set_handler接口设置的,它将会遍历irqaction链表,并执行对应的中断处理函数

  • (5)执行驱动注册的中断处理函数

  • (6)由于内核中断处理过程是关中断的,若中断处理流程较长(包括软中断和tasklet),就会造成较大的中断延迟,从而影响系统的实时性,因此内核支持了中断线程化。中断注册时,可以选择是否将中断处理逻辑放在内核线程中,从而消除中断延迟的不确定性。本函数就是用于唤醒线程化中断相关内核线程的

四、中断控制器的dts配置

以下面的配置为例

gic: interrupt-controller@58000000 {
		status = "okay";
		compatible = "arm,gic-v3";
		#interrupt-cells = <3>;
		#address-cells = <2>;
		#size-cells = <2>;
		ranges;
		interrupt-controller;
		reg =	<0x0 0x58000000 0x0 0x10000>,		// GICD
			<0x0 0x58040000 0x0 0x100000>;		// GICR0~7
		interrupts = <1 9 4>;
	};
  • (1)compatible:指定中断控制器与驱动的匹配字符串

  • (2)interrupt-controller:设置为true。在dts中若某个属性不指定其值,就表示其为bool值,且其值为true

  • (3)#address-cells:指定reg字段中address所占的长度,1表示32bit,2表示64bit

  • (4)#size-cells:指定reg字段中size所占的长度

  • (5)#interrupt-cells:指定interrupts字段所占的长度,至少为3。若支持ppi affinity,则该值至少为4
      + a第一个值表示中断类型,0为spi,1为ppi,2为扩展spi,3为扩展ppi
      + b指定中断号,其中spi类型中断为硬件中断号 – 32,ppi类型中断为硬件中断号 -16,扩展spi为中断号 – xxx,扩展ppi为中断号 –xxx
      + c中断flag,其中bit 0 – bit 3为中断触发类型。
       Gicv2定义如下:
       + 1:上升沿触发,2:下降沿触发
       + 4:高电平触发, 8:低电平触发

Gicv3定义如下:
   + 1:边沿触发
   + 4:电平触发

+ d第4个字段为描述cpu set的phandle,以用于指定该中断的affinity。该值只能用于ppi类型中断,且其指定的node必须为ppi-partitions的子节点。对于不支持ppi-partition的系统或该中断类型不是ppi,该值必须为0

  • (6) ranges:设置为true

  • (7)reg:以以下顺序指定各组件的寄存器基地址,其中后面3个是可选的
       + - GIC Distributor interface (GICD)
       + - GIC Redistributors (GICR), one range per redistributor region
       + - GIC CPU interface (GICC)
       + - GIC Hypervisor interface (GICH)
       + - GIC Virtual CPU interface (GICV)

  • (8)interrupts:指定vgic maintainance中断的中断源

  • (9)redistributor-stride:若使用padding pages,则用于指定连续redistributor的stride。其值至少为64k

  • (10) #redistributor-regions:redistributor连续的独立region数目

五、中断注册接口

(1)int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
	    const char *name, void *dev)
	    
(2)int request_threaded_irq(unsigned int irq, irq_handler_t handler,
		     irq_handler_t thread_fn,
		     unsigned long flags, const char *name, void *dev)
		     
(3)int devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
		 unsigned long irqflags, const char *devname, void *dev_id)

(4)int devm_request_threaded_irq(struct device *dev, unsigned int irq,
			      irq_handler_t handler, irq_handler_t thread_fn,
			      unsigned long irqflags, const char *devname,
			      void *dev_id)
			      
(5)int request_any_context_irq(unsigned int irq, irq_handler_t handler,
			unsigned long flags, const char *name, void *dev_id)
			
(6)int devm_request_any_context_irq(struct device *dev, unsigned int irq,
		 irq_handler_t handler, unsigned long irqflags,
		 const char *devname, void *dev_id)
		 
(7)int request_percpu_irq(unsigned int irq, irq_handler_t handler,
		   const char *devname, void __percpu *percpu_dev_id)
		   
(8)int request_nmi(unsigned int irq, irq_handler_t handler, unsigned long flags,
	    const char *name, void *dev)
	    
(9)int request_percpu_nmi(unsigned int irq, irq_handler_t handler,
		   const char *devname, void __percpu *dev)

原文链接:https://zhuanlan.zhihu.com/p/520171265

在下个雨天再来看一遍。

猜你喜欢

转载自blog.csdn.net/weixin_45264425/article/details/129482662