[原创] Linux 中断子系统 (二)- 通用框架处理
背景
Read the fucking source code!
--By 鲁迅
A picture is worth a thousand words.
--By 高尔基
说明:
Kernel 版本: 4.14
ARM64 处理器, Contex-A53, 双核
使用工具: Source Insight 3.5, Visio
1. 概述
[原创] Linux 中断子系统(一)- 中断控制器及驱动分析讲到了底层硬件 GIC 驱动, 以及 Arch-Specific 的中断代码, 本文将研究下通用的中断处理的过程, 属于硬件无关层. 当然, 我还是建议你看一下上篇文章.
这篇文章会解答两个问题:
用户是怎么使用中断的(中断注册)?
外设触发中断信号时, 最终是怎么调用到中断 handler 的(中断处理)?
2. 数据结构分析
先来看一下总的数据结构, 核心是围绕着 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 的设置, 这个用于指向我们设备驱动程序中的中断处理函数了;
中断的处理主要有以下几个功能模块:
硬件中断号到 Linux irq 中断号的映射, 并创建好 irq_desc 中断描述符;
中断注册时, 先获取设备的中断号, 根据中断号找到对应的 irq_desc, 并将设备的中断处理函数添加到 irq_desc 中;
设备触发中断信号时, 根据硬件中断号得到 Linux irq 中断号, 找到对应的 irq_desc, 最终调用到设备的中断处理函数;
上述的描述比较简单, 更详细的过程, 往下看吧.
3. 流程分析
3.1 中断注册
这一次, 让我们以问题的方式来展开:
先来让我们回答第一个问题: 用户是怎么使用中断的?
熟悉设备驱动的同学应该都清楚, 经常会在驱动程序中调用 request_irq()接口或者
request_threaded_irq()
接口来注册设备的中断处理函数;
request_irq()/request_threaded_irq
接口中, 都需要用到 irq, 也就是中断号, 那么这个中断号是从哪里来的呢? 它是 Linux irq, 它又是如何映射到具体的硬件设备的中断号的呢?
先来看第二个问题: 设备硬件中断号到 Linux irq 中断号的映射
硬件设备的中断信息都在设备树 device tree 中进行了描述, 在系统启动过程中, 这些信息都已经加载到内存中并得到了解析;
驱动中通常会使用 platform_get_irq 或
irq_of_parse_and_map
接口, 去根据设备树的信息去创建映射关系(硬件中断号到 Linux irq 中断号映射);
[原创] Linux 中断子系统(一)- 中断控制器及驱动分析提到过 struct irq_domain 用于完成映射工作, 因此在
irq_create_fwspec_mapping
接口中, 会先去找到匹配的 irq domain, 再去回调该 irq domain 中的函数集, 通常 irq domain 都是在中断控制器驱动中初始化的, 以 ARM GICv2 为例, 最终回调到
gic_irq_domain_hierarchy_ops
中的函数;
如果已经创建好了映射, 那么可以直接进行返回 Linux irq 中断号了, 否则的话需要
irq_domain_alloc_irqs
来创建映射关系;
irq_domain_alloc_irqs
完成两个工作:
针对 Linux irq 中断号创建一个 irq_desc 中断描述符;
调用 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
;
上述函数执行完成后, 完成了两大工作:
硬件中断号与 Linux 中断号完成映射, 并为 Linux 中断号创建了 irq_desc 中断描述符;
数据结构的绑定及初始化, 关键的地方是设置了中断处理往上执行的入口;
再看第一个问题: 中断是怎么来注册的?
设备驱动中, 获取到了 irq 中断号后, 通常就会采用 request_irq/request_threaded_irq 来注册中断, 其中 request_irq 用于注册普通处理的中断, request_threaded_irq 用于注册线程化处理的中断;
在讲具体的注册流程前, 先看一下主要的中断标志位:#defineIRQF_SHARED0x00000080// 多个设备共享一个中断号, 需要外设硬件支持
#defineIRQF_PROBE_SHARED0x00000100// 中断处理程序允许 sharing mismatch 发生
#define__IRQF_TIMER0x00000200// 时钟中断
#defineIRQF_PERCPU0x00000400// 属于特定 CPU 的中断
#defineIRQF_NOBALANCING0x00000800// 禁止在 CPU 之间进行中断均衡处理
#defineIRQF_IRQPOLL0x00001000// 中断被用作轮训
#defineIRQF_ONESHOT0x00002000// 一次性触发的中断, 不能嵌套, 1)在硬件中断处理完成后才能打开中断; 2)在中断线程化中保持关闭状态, 直到该中断源上的所有 thread_fn 函数都执行完
#defineIRQF_NO_SUSPEND0x00004000// 系统休眠唤醒操作中, 不关闭该中断
#defineIRQF_FORCE_RESUME0x00008000// 系统唤醒过程中必须强制打开该中断
#defineIRQF_NO_THREAD0x00010000// 禁止中断线程化
#defineIRQF_EARLY_RESUME0x00020000// 系统唤醒过程中在 syscore 阶段 resume, 而不用等到设备 resume 阶段
#defineIRQF_COND_SUSPEND0x00040000// 与 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 中断, 解除中断屏蔽;
3.2 中断处理
当完成中断的注册后, 所有结构的组织关系都已经建立好, 剩下的工作就是当信号来临时, 进行中断的处理工作.
来回顾一下[原创] Linux 中断子系统(一)- 中断控制器及驱动分析中的 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 可中断睡眠状态;
3.3 总结
中断的处理, 总体来说可以分为两部分来看:
从上到下: 围绕 irq_desc 中断描述符建立好连接关系, 这个过程就包括: 中断源信息的解析 (设备树), 硬件中断号到 Linux 中断号的映射关系, irq_desc 结构的分配及初始化(内部各个结构的组织关系), 中断的注册(填充 irq_desc 结构, 包括 handler 处理函数) 等, 总而言之, 就是完成静态关系创建, 为中断处理做好准备;
从下到上, 当外设触发中断信号时, 中断控制器接收到信号并发送到处理器, 此时处理器进行异常模式切换, 并逐步从处理器架构相关代码逐级回调. 如果涉及到中断线程化, 则还需要进行中断内核线程的唤醒操作, 最终完成中断处理函数的执行.
来源: https://www.cnblogs.com/LoyenWang/p/13052677.html