Linux中断实现
- 中断管理
- 2023-03-04
- 130热度
- 0评论
interrupt controller初始化
设备树中对gic-v3的描述如下,其中interrupt-controller标识了该设备是一个中断控制器。
interrupt-controller@3400000 {
compatible = "arm,gic-v3";
#interrupt-cells = <0x03>;
#address-cells = <0x00>;
interrupt-controller;
reg = <0x00 0x3400000 0x00 0x10000 0x00 0x3460000 0x00 0xff004>;
interrupt-parent = <0x18>;
phandle = <0x18>;
};
- compatible:中断设备节点的属性别名
- Interrupt-cells:用来描述子节点interrupts属性的值,为3表明interrupts有3个32bits整数来描述。
- Interrupt-parent:标识此设备节点属于那一个中断控制器,gic可以设置为自己。
驱动匹配compatible的定义在drivers/irqchip/irq-gic-v3.c中,如下:
IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gic_of_init);
展开后为:
#define _OF_DECLARE(table, name, compat, fn, fn_type) \\
static const struct of_device_id __of_table_gic_v3 \\
__used __section("__" #table "_of_table") \\
__aligned(__alignof__(struct of_device_id)) \\
= { .compatible = "arm,gic-v3", \\
.data = gic_of_init }
上图是中断控制器驱动匹配设备树的调用路径,调用到of_irq_init函数,传入的参数是__irqchip_of_table,该table中定义了所有interrupt controller的compatible信息,在一个完整的系统中不仅只有一个gic控制器,还包括一些级联等其他控制器,通过宏定义IRQCHIP_DECLARE的方式会集中在__irqchip_of_table这个段。
IRQ domain
- 硬件中断号:1.1和1.2章节控制器为每个硬件中断源分配了一个唯一编号,用于区分不同的中断源。
- 软件中断号:Linux系统在处理中断过程中使用的编号,也称为虚拟中断号。
为什么要进行中断映射?
上图是一个HW interrupt ID 到Linux IRQ的映射关系图示例。在Linux软件处理过程中,不应该关注中断来自那个中断来源,尤其在中断系统结构中,会出现级联的情况,中断控制器的中断源是另外一个中断控制器的输出,不同的中断控制器会出现重复中断源编号。使用级联这样做的好处就是扩展的中断请求的数量,同时对中断源可以按照控制器分类。如GPIO类型中断控制器,对于Port A(GPIO A1~GPIO A20),这20个GPIO组成的一个中断控制器给到主控制器,也就是说GPIO A1~GPIO A20给到上一级的控制是一个中断信号,系统收到这个信号需要再去读取GPIO Port A的控制器相关掩码进一步判断到是那个GPIO。
Linux系统为了处理硬件中断到虚拟中断的映射关系,引入了Linux IRQ domain。每个中断控制器对应一个IRQ domain,在2.1章节中IRQ domain负责将硬件的中断编号 HW interrupt ID与Linux系统中的IRQ number进行映射。目前IRQ domain支持2中映射方式:linear map,tree map,no map
- Linear map:系统中维护一个数组,硬件中断号的就是数组的索引,取值就是对应的IRQ number。
- Tree map:当HW interrupt数量比较多时,使用Linear map会消耗比较大的内存,选择用树的方式进行映射可以节约内存。
- No map:有些控制器可以支持通过些寄存器配置HW interrupt id而不是由物理连接决定,那么这种情况下就不需要进行映射了。
上图是设备树中级联描述关系,pinctrl设备虽然没有描述interrupt-parent节点,那么就默认使用开头定义的interrupt-parent = <0x01>,因此pinctrl节点的父中断控制器是interrupt-controller@0,而interrupt-contrller@0指明了interrupt-parent=<0x18>,所有其父中断控制器是interrupt-controller@3400000即GIC,Timer_arch的interrupt-parent直接指向的是GIC。
上图为struct irq_domain的数据结构,下面是对关键变量的说明。
- link:所有创建的irq domain链接到全局链表irq_domain_list中。
- ops: 对应irq domain操作使用方法集合。
- revmap_size:映射表的大小。
- revmap_tree:Radix Tree映射的根节点。
- revmap[]:反向映射表,将硬件中断号映射回中断数据结构。
root domain创建(GIC)
在interrupt controller初始化章节,在of_irq_init函数中会调用for_each_matching_node进行遍历设备设备树是否与__irqchip_of_table中的定义的匹配,如果匹配并且是interrupt-controller则获取对应的data(即回调函数),并将其添加intc_desc_list链表中。最后在逐一遍历调用对应的回调函数,gic控制器就对应调用的是gic_of_init。
gic控制器初始化调用gic_of_init函数,在该函数中较关键的是调用gic_init_bases进行了一系列的初始化。在linux系统中,定义了一个struct gic_chip_data gic_data[CONFIG_ARM_GIC_MAX_NR] __read_mostly的全局变量,用于管理gic中断控制器。在gic_init_bases创建了gic irq domain(下一章节描述),设置中断函数的入口gic_handler_irq等等。
在interrupt controller初始化章节,gic_init_bases调用irq_domain_crate_tree创建了一个irq domain,该函数调用__irq_domain_add添加一个irq domain。
首先分配了一个struct domain数据结构并初始化相关成员,其中domain->revmap_tree树用于hwirq和virq直接的映射;接着填充domain的操作函数结合ops;再调用irq_domain_check_hierarchy检测domain是否为级联(有子中断控制器连接?),判断的方式是检测ops->alloc是否为空,这里明显是不为空,所以gic是hierarchy,这里决定着后续在映射中断号时的区别(见2.2.2),最后将domain添加到全局链表irq_domain_list中。
中断号映射(GIC)
中断号的映射就是将硬件中断号与Linux系统中的中断号建立起映射关系。在linux系统中,每个linux中断号都对应struct irq_desc实体,其与各关键数据结构的关系如上图。
Linux系统中大部分在DTS描述的节点是可以转换为platform_device的,如果其节点在DTS中指定了中断属性,那么可以在DTS解析设备树的时候可以获取到设备的中断信息,进而建立起映射关系。而对于如果节点没有转换为platform_device信息的,如I2C,SPI设备,那么需要在驱动中主动调用of_irq_get函数去实现,如下。
static int spi_probe(struct device *dev)
{
const struct spi_driver *sdrv = to_spi_driver(dev->driver);
struct spi_device *spi = to_spi_device(dev);
int ret;
ret = of_clk_set_defaults(dev->of_node, false);
if (ret)
return ret;
if (dev->of_node) {
spi->irq = of_irq_get(dev->of_node, 0);
if (spi->irq == -EPROBE_DEFER)
return -EPROBE_DEFER;
if (spi->irq < 0)
spi->irq = 0;
}
ret = dev_pm_domain_attach(dev, true);
if (ret)
return ret;
if (sdrv->probe) {
ret = sdrv->probe(spi);
if (ret)
dev_pm_domain_detach(dev, true);
}
return ret;
}
下面描述从DTS中解析中断属性建立起映射的过程,下面是DTS中设备对中断相关的描述。
xxx@xxxxx {
...
interrupt-parent = <0x18>;
interrupts = <0x00 0x37 0x04>;
...
};
- interrupt-parent:指向其父节点,表示中断信号由那个中断控制器来处理,这个属性取值是一个整数或引用到其他节点的phandle。也就是说该字段指明了所属的中断控制器。
- interrupts:指定中断的详细信息,<中断类型,中断号,方式>。
内核初始化阶段,会解析DTS中设备,进而解析设备中中断信息,首先调用of_irq_count函数统计设备节点dev中的中断数量num_irq(大多数的设备只有一个中断),接着根据num_irq中断数量调用of_irq_to_resource解析节点的中断信息。在of_irq_to_resource函数中调用of_irq_get函数获取中断号IRQ。
在of_irq_get函数中调用of_irq_parse_one解析中断的描述信息,包括DTS中interrupt-parent和interruts的信息,包含了该中断所属的父节点,中断类型,中断号,中断触发方式等。
获取到中断信息后,调用irq_domain_translate将DTS中描述的中断号(interrupts的第二个cells)转为hwiq,转换方法为如果是SPI类型的中断+32,如果是PPI类型的中断+16。见1.1和1.2章节中,PPI和SGI类型的中断占用了前32号。
获取到hwiq后先调用irq_find_mapping查询是否能获取到virq,如果获取到了则结束整个过程。如果没有获取到中断号,先进行判断domain是否为hierarchy,如果是级联的方式则调用irq_domain_alloc_irqs来进行映射,否则调用irq_carete_mapping来进行映射,对于gic来说这里调用的是irq_domain_alloc_irqs。
无中断控制器映射
中断line接入到的控制器是非级联的,则调用irq_create_mapping进行映射,主要分为两部分,创建desc和建立hwirq和virq的映射。
调用irq_domain_alloc_decs分配一个struct desc描述符,一个virq对应一个struct desc实体。struct desc实体通过virq作为键值插入到irq_desc_tree中,这样就建立起virq与desc直接的联系。
调用irq_domain_associate建立hwirq和virq的映射,会根据hwirq的值来进行判断,如果hwirq比较小采用线性映射的方式即revmap[hwirq]=irq_data,irq_data中存储了irq,hwirq,domain等信息。如果hwirq较大,则使用树映射。
有中断控制器的映射
级联中断控制器通常是中断输入接了有下一级的中断控制器,一般GIC都是级联中断控制器,从上图函数调用关系(黄色部分是与非级联的差异)可以看出级联中断控制器与非级联中断控制器的主要区别是子中断控制器需要为父中断控制器分配irq_data,由其irq_data->parent_data指向其值(有什么用?),调用irq_domain_alloc_irqs_hierarchy创建中断控制器的级联信息(见下)。
以gic_irq_domain_alloc为例,主要的作用先调用gic_irq_domain_translate进行转译hwirq(看起来与irq_domain_translate重复了),接着调用gic_irq_domain_map完成domain信息的设置,根据中断类型设置中断的入口函数,SPIs类型的入口函数为handle_fasteoi_irq,注意2.1章节gic_handle_irq是所有中断的入口,最后再根据中断类型进行分类下陷到各类型入口,之间的关系是gic_handle_irq->......->handle_fasteoi_irq。
小结
(1)解析DTS中断信息,包括父节点,中断号,中断类型,触发类型等。
(2)从allocated_irq位图中获取一个空闲的IRQ中断号。
(3)为IRQ中断号分配一个struct irq_desc实体,并使用IRQ作为键值插入到irq_desc_tree中,以此就建立了IRQ与desc之间的联系。
(4)设置desc->handle_irq的处理函数。
(5)建立IRQ中断与hwirq中断的联系,分为直接映射和树映射。
中断注册
常用申请中断的几个函数如下。
int request_irq(unsigned int irq,irq_handler_t handler,unsigned long flags,const char *name,void *dev)
int devm_request_irq(struct device *dev,unsigned int irq,irq_handler_t handler,unsigned long irqflags,const char *devname,void *dev_id)
int request_threaded_irq(unsigned int irq,irq_handler_t handler,irq_handler_t thread_fn,unsigned long flags,const char *name,void *dev);
中断注册常用的函数是request_irq和request_threaded_irq,request_irq最终也会调用到request_theaded_irq,如上图将中断注册的重要几个阶段进行了描述。
(1)首先调用irq_to_desc获取中断描述符irq_desc,传入的参数是linux 的irq number,前面我们提到每个linux irq都对应一个irq_desc,其中断注册的数据信息将会该结构进行导出。
(2)其次会对用户设置的中断处理函数handler和thread_fn进行判断,如果hanlder传入为空,则设置默认为irq_default_primary_handler,也就是说当中断处理第三级函数将会调用该函数,该函数如下仅仅是返回IRQ_WAKE_THRAED,该返回将会唤醒创建的中断线程,在下章节我们再详细描述。
/*
* Default primary interrupt handler for threaded interrupts. Is
* assigned as primary handler when request_threaded_irq is called
* with handler == NULL. Useful for oneshot interrupts.
*/
static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
{
return IRQ_WAKE_THREAD;
}
(3)接着分配struct irqaction实体,会将用户注册的中断回调等信息进行填充。在中断系统中,会存在irq line不够用的情况,那么就让外设共享一个irq line,也就是说一个irq number对应多个外设,每个外设都对应一个irqaction实体,同故宫irqaction->next链接起来。在中断处理时会遍历这些irqaction进行处理。结合目前这种场景应该时比较少的?
(4)最后调用__setup_irq进行设置,这部分又可以再分为4个重要阶段。
- 首先判断是否设置了中断嵌套,如果设置了中断嵌套则将handler设置为irq_nested_primary_handler,本身linux是不支持中断嵌套的,因此这种场景用途是如何,目前还未遇到?
- 其次判断是否设置了IRQ_NOTHREAD标志,如果没有说明可以进行强制中断线程化。调用irq_setup_forced_threading进行处理,进入函数如果用户handler为irq_default_primary_handler则直接返回,因为中断线程化实际上就是在中断上下文调用irq_default_primary_handler返回IRQ_WAKE_THREAD激活中断线程,通常handler如果被用设置为NULL则会被设置为irq_default_primary_handler。如果不是上述场景,则接着往下,当handler和thread_fn都不为空,那么则相当于需要创建两个中断线程,一个中断线程用于处理handler的回调,另外一个中断线程用于处理thread_fn,因此需要再申请一个secondary action,在实际的中断处理函数中,第一个中断线程函数会回调handler,然后在激活第二线程处理thread_fn,可以看到new->thread_fn=new->handler表示在线程中回调用户注册的handler函数,而实际的new->handler则为irq_default_primary_handler。最后再调用setup_irq_thread创建中断线程。
- 接着获取desc->action,如果该值不为空,说明已经注册过该函数,对应中断共享的场景,需要区分处理了那些外设,因此用thread_mask来进行标识。最后*old_ptr=new用于将当前的action放入链表。
- 最后调用wake_up_and_wait_for_irq_thread_ready来激活中断线程,等待中断函数的触发。
小结,整个中断注册过程中重点需要关注的是否支持强制中断线程化,如果没有强制启动中断线程化,那么用户handler则是中断的顶半部(在中断上下文中处理),而thread_fn则是在线程中处理的。当强制中断线程化的时候,中断的顶半部则为irq_default_primary_handler,该函数直接激活中断线程,用户注册的handler将会变成底半步(在线程中处理用户注册的中断回调),在中断线程中回调处理用户注册的中断服务函数。而当用户同时设置了handler和thread_fn,会创建两个中断线程,第一个线程用于回调处理handler,第二个线程用于处理thread_fn,这种场景应该是比较少的,目前还没有遇到,可以结合下一章节中断处理流程来进行分析理解。
中断处理
中断的处理流程这里分为3级,第一级中断是所有中断的入口gic_handle_irq,对于gic来说中断进行了分配包括PPI,SPI等,每一类注册的中断函数是不一样,因此根据每类注册的中断函数irq desc->handler下陷到第二级中断处理。上面是以SPI类型的中断为例,入口函数为handle_fasteoi_irq,在二级中断处理流程中会最终调用到用户注册的中断回调函数action->handler,下陷到第三级的中断处理。
CPU响应GIC的中断后,会立即关闭本地CPU的中断响应(屏蔽掉CPSR相关的位),只有等中断处理结束后在再次打开才能再次响应中断,这个期间我们称为处理中断的上下文,中断的上下文是禁止调用睡眠的。
上图的第一级和第二级是处于中断上下文中的,而第三级如果没有启用线程化那么也是处于中断上下文(第三级的函数直接在第二级的回调函数中调用),如果启动中断线程化处理,那么用户的中断回调函数即在任务中回调,即脱离了中断上下文。
在中断上下文中处理我们称之为中断上半部(顶半部),在中断线程中处理我们称为下半部(底半部)。因为上半部本地CPU已经关了本地中断响应,无法响应其他中断,需要等待该中断处理完成(发送eoi指令),因此上半部中对于程序的要求比较高,执行程序不能太长,更不能执行睡眠函数,这样会影响其他中断的响应,因此对于程序处理时间太长或有睡眠要求的都启用中断线程化来进行处理,即在下半部运行。
在第三级中启用了中断线程化,需要注意的是oneshot处理,该机制是为了解决中断洪泛引入的。在启用中断线程化是,第一级和第二级中断处理完成后即退出了退出了中断上下文,本地CPU打开了中断响应(上图handle_fasteoi_irq -> chip->irq_eoi),那么之后cpu即可再次响应中断,如果该中断来得非常快,尤其是电平类触发的外设再没有读写数据或清除外设标志时,电平时不会消除的,这就会导致该中断还在线程中没处理完,下一个中断又触发了导致中断洪泛,为了解决这个问题通过oneshot标志来进行判断,在第二级中断处理时如果检测到了oneshot标志,那么就先调用mask_irq设置中断控制器不再响应该中断,这样即使第二级中断处理结束退出打开本地CPU中断响应该中断也不会再触发,因为中断控制器的中断影响被屏蔽了,最后等到该中断在线程中处理完成之后在使能该中断(在irq_finalize_oneshot中unmask_irq)。
级联中断控制器
此前描述了IRQ domain,重点说明了root domain(GIC)创建流程,在实际的架构中,还存在着级联的中断控制器,根中断控制器连接子中断控制器,子中断控制器的输出接入到根中断控制器的输入。根据级联的结构分为两种情况N对1和1对1。
级联N对1是子中断控制器的多个中断共用一个中断输出,如上图的Port B、Port C及Port D各自对应一个中断输出接入到GIC的中断输入,而Port B、Port C及PortD各自对应是Port B1~Port Bn、Port C1~Port Cn及Port D1~Port Dn中断输入,在中断处理上以Port B1为例,linux系统先处理irq number 93的中断(Port B 69的映射),在93号中断处理函数中接着再查询具体是Port B上的那一个端口触发的中断,查询到是PB1且PB1映射号是IRQ 96,则再处理IRQ 96的中断。
级联1对1的子中断控制器每一个中断输入直接对应到根中断控制器的输入,对于这种结构,中断处理流程就与非级联的没有多大区别了,本身就是一对一的关系。
本小结pinctrl为例来说明子中断控制器的创建过程和中断处理流程的差异。Pinctrl是一个中断控制器,对应的就是级联N对1的这种结构。
child domain的创建
pio: pinctrl@xxxx {
#address-cells = <1>;
compatible = "xxx,xxx-pinctrl";
reg = <0x0 0x02000000 0x0 0x800>;
interrupts = <GIC_SPI 69 IRQ_TYPE_LEVEL_HIGH>, /* GPIOB */
<GIC_SPI 71 IRQ_TYPE_LEVEL_HIGH>, /* GPIOC */
<GIC_SPI 73 IRQ_TYPE_LEVEL_HIGH>, /* GPIOD */
<GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>, /* GPIOE */
<GIC_SPI 77 IRQ_TYPE_LEVEL_HIGH>, /* GPIOF */
<GIC_SPI 79 IRQ_TYPE_LEVEL_HIGH>, /* GPIOG */
<GIC_SPI 81 IRQ_TYPE_LEVEL_HIGH>, /* GPIOH */
<GIC_SPI 83 IRQ_TYPE_LEVEL_HIGH>, /* GPIOI */
<GIC_SPI 85 IRQ_TYPE_LEVEL_HIGH>, /* GPIOJ */
<GIC_SPI 140 IRQ_TYPE_LEVEL_HIGH>; /* GPIOK */
clocks = <&ccu CLK_APB1>, <&dcxo24M>, <&rtc_ccu CLK_OSC32K>;
clock-names = "apb", "hosc", "losc";
gpio-controller;
#gpio-cells = <3>;
interrupt-controller;
interrupt-parent = <&gic>;
#interrupt-cells = <3>;
}
Pinctrl的设备树描述如上,interrupt-controller标识了其是一个中断控制器,interrupt-parent为gic。interrupts字段标识了各个bank连接到GIC上的中断号,一共有10个bank,对应的就是gic控制器10个中断输入,在kernel_init阶段会遍历解析interrupts字段逐一建立GIC的中断号映射(2.3章节中的描述),所以Pinctrl的bank的中断映射已经建立完成了,如下图中69->93,71->94,73->95的映射。gpio-controller标识了该节点还是一个gpio的控制器,gpio-cells标识引用的描述方法,这里有3个字段,如下是一个wlan设备节点对GPIO控制器的引用,wlan_regon = <>对应的就是gpio-cells的描述,&pio表示引用了pio这个gpio控制器,PB1表示使用的该GPIO控制器上的GPIO号,GPIO_ACTIVE_HIGH表示该GPIO号默认的电平状态。
wlan {
compatible = "xxx,xxx-wlan";
wlan_en = <&pio PB 1 GPIO_ACTIVE_HIGH>;
};
GIC的驱动匹配compatible的定义使用IRQCHIP_DECLARE(gic_v3, \"arm,gic-v3\", gic_of_init)来实现,在start_kernel的时候进行解析,而pinctrl的驱动则是由各厂商来进行实现,下面是示例。
static int xxx_pinctrl_probe(struct platform_device *pdev)
{
int ret;
return xxx_bsp_pinctrl_init(pdev, &xxxx_pinctrl_data);
}
static struct of_device_id xxx_pinctrl_match[] = {
{ .compatible = "xxx,xxx-pinctrl", }, //与设备树pio: pinctrl@xxxx 对应
{}
};
MODULE_DEVICE_TABLE(of, xxx_pinctrl_match);
static struct platform_driver xxx_pinctrl_driver = {
.probe = xxx_pinctrl_probe,
.driver = {
.name = "xxx-pinctrl",
.pm = PINCTRL_PM_OPS,
.of_match_table = xxx_pinctrl_match,
},
};
static int __init xxx_pio_init(void)
{
return platform_driver_register(&xxx_pinctrl_driver);
}
fs_initcall(xxx_pio_init);
Xxx_pio_init注册一个pinctrl的驱动,驱动的描述在xxx_pinctrl_driver中,其中保活了of_device_id的信息将与设备树进行匹配,最后调用xxx_pinctrl_probe函数进行初始化。
上图只列出pinctrl与中断相关的关键流程,下面进行简要说明。
(1)分配一个struct gpio_chip结构,该结构中存储了gpio的操作函数集合。
(2)分配一个数组pctl->irq,用于存储每个bank对应的irq number。
(3)调用irq_domain_add_linear为pinctrl创建一个中断domain,每个中断控制器都有一个domain。
(4)调用platform_get_irq获取每个bank的irq number(virq),对应gpio控制器中interrupts的描述,该函数会调用of_irq_get来获取中断号,of_irq_get中会判断hwirq是否已经建立了到virq的映射,在kernel_init中会解析DTS所有的interrupts统一建立好根中断控制器的中断号映射关系,因此这里调用of_irq_get中调用virq= irq_find_mapping(domain, hwirq)即可查询到hwirq对应的virq。
(5)遍历每个bank,调用irq_create_mapping将bank上输入的gpio建立起中断映射,即挂接在每个bank上的gpio都用于一个irq desc描述符。
(6)为每个gpio对应的irq设置irq chip和desc->handler,在中断注册请求的时候还会调用.irq_set_type进行设置一次,这个流程看起来有点多余。
(7)调用irq_set_chained_handler_and_data为每个bank的irq注册回调函数,注册了该回调函数,对于GPIO的中断就于直接连接到GIC的中断不同,GPIO的中断会先调用irq_set_chained_handler_and_data注册的中断xxx_pinctrl_irq_handler,而直接连接到GIC的中断调用handle_fasteoi_irq(见2.5章节)。
小结,Pinctrl的初始化中创建了一个domain,先是获取到bank的irq,在建立起bank外接GPIO的中断号映射得到irq,最后会为bank的irq和gpio的irq分别注册回调函数,bank是直接连接到gic上的,所以在中断处理时系统首先响应bank对应的irq中断处理函数,继而再bank 对应的irq处理函数中查询具体是挂在该bank上的那一个gpio,进行处理该gpio的irq中断,可以结合下一章节的中断处理流程就比较清楚初始化的流程。
中断处理流程
与未级联的处理流程相比,多了一级。在第二级中插入了一级响应,第一级响应后下陷不再是调用到handle_fasteoi_irq,而是调用pinctrl初始化是调用irq_set_chained_handler_and_data注册的中断处理函数,因为pinctrl中断控制器是一个bank对应一个gic的中断输入,而bank上又外挂了多个gpio,即多个gpio对应的是一个gic的输入,因此需要插入一级先处理bank的中断响应,在该中断响应中识别出bank上的那一个gpio,获取到该gpio的irq再进行处理第三级和第四级。
对应上述的第1、2、3级是处于中断上下文中。