Kernel / Linux / 软件积累 · 2023年8月9日

Linux中断子系统(二)-通用框架处理

主要问题:

1. 用户怎样使用中断 即中断注册

2.外设触发中断信号时,最终是怎么调用到中断handler的   即中断处理                                                                                      

一. 数据结构分析

数据结构 ,核心围绕struct irq_desc来展开:

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

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

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

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

图的左侧灰色部分,主要在中断控制器驱动中进行初始化设置,包括各个结构中函数指针的指向等,其中struct irq_chip用于对中断控制器的硬件操作,struct irq_domain与中断控制器对应,完成的工作是硬件中断号到linux irq的映射。

图的上侧灰色部分,中断描述符的创建,这里指CONFIG_SPARSE_IRQ,主要在获取设备中断信息的过程中完成的,从而让设备树中的中断能与具体的中断描述符 irq_desc匹配。

图中剩余部分,在设备申请注册中断的过程中进行设置,比如struct irqaction中handler的设置,这个用于指向我们设备驱动程序中的中断处理函数。

中断的处理主要有以下几个功能模块:

1。硬件中断号到linux irq中断号的映射,并创建好irq_desc中断描述符

2。中断注册时,先获取设备的中断号,根据中断号找到对应的irq_desc,并将设备的中断处理函数添回到irq_desc中

3。设备触发中断信号时,根据硬件中断号得到linux irq中断号,找到对应的irq_desc,最终调用到设备的中断处理函数。

三。流程分析

这一次,让我们以问题的方式来展开:

先来让我们回答第一个问题:用户是怎么使用中断的?

  1. 熟悉设备驱动的同学应该都清楚,经常会在驱动程序中调用request_irq()接口或者request_threaded_irq()接口来注册设备的中断处理函数;
  2. request_irq()/request_threaded_irq接口中,都需要用到irq,也就是中断号,那么这个中断号是从哪里来的呢?它是Linux irq,它又是如何映射到具体的硬件设备的中断号的呢?

先来看第二个问题:设备硬件中断号到Linux irq中断号的映射

  1. 硬件设备的中断信息都在设备树device tree中进行了描述,在系统启动过程中,这些信息都已经加载到内存中并得到了解析;
  2. 驱动中通常会使用platform_get_irq或irq_of_parse_and_map接口,去根据设备树的信息去创建映射关系(硬件中断号到linux irq中断号映射);
  3. Linux中断子系统(一)-中断控制器及驱动分析提到过struct irq_domain用于完成映射工作,因此在irq_create_fwspec_mapping接口中,会先去找到匹配的irq domain,再去回调该irq domain中的函数集,通常irq domain都是在中断控制器驱动中初始化的,以ARM GICv2为例,最终回调到gic_irq_domain_hierarchy_ops中的函数;
  4. 如果已经创建好了映射,那么可以直接进行返回linux irq中断号了,否则的话需要irq_domain_alloc_irqs来创建映射关系;
  5. irq_domain_alloc_irqs完成两个工作:
  6. 针对linux irq中断号创建一个irq_desc中断描述符;
  7. 调用domain->ops->alloc函数来完成映射,在ARM GICv2驱动中对应gic_irq_domain_alloc函数,这个函数很关键,所以下文介绍一下;

gic_irq_domain_alloc函数如下:

  • gic_irq_domain_translate:负责解析出设备树中描述的中断号和中断触发类型(边缘触发、电平触发等);
  • gic_irq_domain_map:将硬件中断号和linux中断号绑定到一个结构中,也就完成了映射,此外还绑定了irq_desc结构中的其他字段,最重要的是设置了irq_desc->handle_irq的函数指针,这个最终是中断响应时往上执行的入口,这个是关键,下文讲述中断处理过程时还会提到;
  • 根据硬件中断号的范围设置irq_desc->handle_irq的指针,共享中断入口为handle_fasteoi_irq,私有中断入口为handle_percpu_devid_irq;

上述函数执行完成后,完成了两大工作:

  1. 硬件中断号与Linux中断号完成映射,并为Linux中断号创建了irq_desc中断描述符;
  2. 数据结构的绑定及初始化,关键的地方是设置了中断处理往上执行的入口;

再看第一个问题:中断是怎么来注册的?

设备驱动 中,获取到irq中断号后,通常就用采用requset_irq/request_thread_irq来注册中断,其中request_irq用于注册普通处理的中断,request_threaded_irq用于注册线程化处理的中断;

在讲具体的注册流程前,先看一下主要的中断标志位:

#define IRQF_SHARED                0x00000080              //多个设备共享一个中断号,需要外设硬件支持

#define IRQF_PROBE_SHARED        0x00000100              //中断处理程序允许sharing mismatch发生

#define __IRQF_TIMER                0x00000200              //时钟中断

#define IRQF_PERCPU                0x00000400              //属于特定CPU的中断

#define IRQF_NOBALANCING        0x00000800              //禁止在CPU之间进行中断均衡处理

#define IRQF_IRQPOLL                0x00001000              //中断被用作轮训

#define IRQF_ONESHOT                0x00002000              //一次性触发的中断,不能嵌套,1)在硬件中断处理完成后才能打开中断;2)在中断线程化中保持关闭状态,直到该中断源上的所有thread_fn函数都执行完

#define IRQF_NO_SUSPEND                0x00004000              //系统休眠唤醒操作中,不关闭该中断

#define IRQF_FORCE_RESUME        0x00008000              //系统唤醒过程中必须强制打开该中断

#define IRQF_NO_THREAD                0x00010000              //禁止中断线程化

#define IRQF_EARLY_RESUME        0x00020000              //系统唤醒过程中在syscore阶段resume,而不用等到设备resume阶段

#define IRQF_COND_SUSPEND        0x00040000              //与NO_SUSPEND的用户共享中断时,执行本设备的中断处理函数

  • request_irq也是调用request_threaded_irq,只是在传参的时候,线程处理函数thread_fn函数设置成NULL;
  • 由于在硬件中断号和Linux中断号完成映射后,irq_desc已经创建好,可以通过irq_to_desc接口去获取对应的irq_desc;
  • 创建irqaction,并初始化该结构体中的各个字段,其中包括传入的中断处理函数赋值给对应的字段;
  • __setup_irq用于完成中断的相关设置,包括中断线程化的处理:
  • 中断线程化用于减少系统关中断的时间,增强系统的实时性;
  • ARM64默认开启了CONFIG_IRQ_FORCED_THREADING,引导参数传入threadirqs时,则除了IRQF_NO_THREAD外的中断,其他的都将强制线程化处理;
  • 中断线程化会为每个中断都创建一个内核线程,如果中断进行共享,对应irqaction将连接成链表,每个irqaction都有thread_mask位图字段,当所有共享中断都处理完成后才能unmask中断,解除中断屏蔽;

中断处理

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

Arch-specific处理流程:

中断收到之后,首先会跳转到异常向量表的入口处,进而逐级进行回调处理,最终调用到 generic_handle_irq 来进行中断处理

generic_handle_irq 处理如下图:

  • generic_handle_irq函数最终会调用到desc->handle_irq(),这个也就是对应到上文中在建立映射关系的过程中,调用irq_domain_set_info函数,设置好了函数指针,也就是handle_fasteoi_irq和handle_percpu_devid_irq;
  • handle_fasteoi_irq:处理共享中断,并且遍历irqaction链表,逐个调用action->handler()函数,这个函数正是设备驱动程序调用request_irq/request_threaded_irq接口注册的中断处理函数,此外如果中断线程化处理的话,还会调用__irq_wake_thread()唤醒内核线程;
  • handle_percpu_devid_irq:处理per-CPU中断处理,在这个过程中会分别调用中断控制器的处理函数进行硬件操作,该函数调用action->handler()来进行中断处理;

来看看中断线程化处理后的唤醒流程吧__handle_irq_event_percpu->__irq_wake_thread:

  • __handle_irq_event_percpu->__irq_wake_thread将唤醒irq_thread中断内核线程;
  • irq_thread内核线程,将根据是否为强制中断线程化对函数指针handler_fn进行初始化,以便后续进行调用;
  • irq_thread内核线程将while(!irq_wait_for_interrupt)循环进行中断的处理,当满足条件时,执行handler_fn,在该函数中最终调用action->thread_fn,也就是完成了中断的处理;
  • irq_wait_for_interrupt函数,将会判断中断线程的唤醒条件,如果满足了,则将当前任务设置成TASK_RUNNING状态,并返回0,这样就能执行中断的处理,否则就调用schedule()进行调度,让出CPU,并将任务设置成TASK_INTERRUPTIBLE可中断睡眠状态;

中断的处理,总体来说可以分为两部分来看:

  • 从上到下:围绕irq_desc中断描述符建立好连接关系,这个过程就包括:中断源信息的解析(设备树),硬件中断号到Linux中断号的映射关系、irq_desc结构的分配及初始化(内部各个结构的组织关系)、中断的注册(填充irq_desc结构,包括handler处理函数)等,总而言之,就是完成静态关系创建,为中断处理做好准备;
  • 从下到上,当外设触发中断信号时,中断控制器接收到信号并发送到处理器,此时处理器进行异常模式切换,并逐步从处理器架构相关代码逐级回调。如果涉及到中断线程化,则还需要进行中断内核线程的唤醒操作,最终完成中断处理函数的执行。
打赏作者