irq_desc
irq_desc 确实是中断系统中最核心的数据结构。您可以把它想象成内核里的一份档案柜,每一个硬件中断号(IRQ Number)都对应一个抽屉,每个抽屉里都存放着一份详细的“中断处理档案”,这份档案就是irq_desc 结构体。
- 在内核中,所有这些“档案”通常被组织成一个大数组,即irq_desc 数组。比如,当中断号为 75 的中断发生时,内核就会直接去查找 irq_desc[75] 这个数组元素,获取处理该中断所需的所有信息。
- 注意:为了节省内存,如果内核配置了 CONFIG_SPARSE_IRQ(稀疏中断),系统就会用一种叫做“基数树 (radix tree)”的数据结构来代替数组。这么做的原因是,有些系统可能定义了大量中断号,但实际只用了其中几个,用数组会浪费大量空间。
- 这份“档案”里最重要的内容有两项:一个是“总负责人”,另一个是“具体行动方案列表”
- handle_irq 函数指针 (总负责人/第一响应人)
- handle_irq 是 irq_desc 结构体中的一个函数指针,当中断发生时,内核首先调用的就是它。
- 对于顶层中断(级联中断):handle_irq 扮演的是一个“分发者”的角色。
- 比如,一个通用的GPIO中断控制器(GIC)可能会占用一个CPU中断号,我们称之为中断A 。当中断 A 发生时,内核调用 irq_desc[A].handle_irq。这个函数的任务不是处理最终的设备,而是去查询GIC的内部寄存器,搞清楚到底是哪个具体的GPIO引脚产生了中断,假设是中断 B。然后,它会接着调用下一级的处理函数,也就是 irq_desc[B].handle_irq,把任务分发下去。
- 对于具体的设备中断:handle_irq 通常指向一个内核预设好的标准处理流程函数。
- 比如对于GPIO中断 B,它的 irq_desc[B].handle_irq 常常被设置为 handle_level_irq(处理电平触发)或 handle_edge_irq(处理边沿触发)这样的通用函数。这些通用函数的任务就是去执行下面要讲的 action 链表。
- action 链表 (具体行动方案列表)
- action 是一个指向 irqaction 结构体链表的指针。这是处理共享中断的关键。
- 什么是共享中断?
- 想象一下,一个GPIO引脚(中断B)上可能连接了多个外部设备(比如两个不同的按键)。当中断B发生时,内核只知道是这个引脚有信号了,但不知道具体是哪个设备触发的。
- action 链表的作用:
- 此时,irq_desc[B] 中的 action 链表就派上用场了。每一个连接到该引脚的设备,在注册中断时(调用request_irq),都会将自己的“行动方案”(一个irqaction结构体)挂载到这个链表上。
- 当中断B的handle_irq(比如handle_edge_irq)被调用时,它就会遍历 action 链表,依次执行链表上每一个irqaction中定义的处理函数(handler)。
- 每个设备自己的处理函数都有责任判断:“这次中断是不是我产生的?”如果是,就进行处理;如果不是,就直接返回。
- handle_irq 函数指针 (总负责人/第一响应人)
irqaction
irqaction 是中断处理的最小执行单元,它详细定义了“做什么”(handler/thread_fn)和“是谁”(name/dev_id),并通过挂载到 irq_desc 的 action 链表上,将具体的设备驱动与内核的中断处理流程无缝地连接了起来。
- 核心执行人:handler 和 thread_fn (中断的上半部与下半部)
- handler (中断处理函数 - 上半部)
- 当中断发生时,CPU会立刻放下手头所有的事情,跳转到这个 handler 函数来执行。它必须在尽可能短的时间内完成工作,因为它执行期间会屏蔽掉同级别的其他中断。
- 它运行在特殊的“中断上下文”中,不能执行任何可能导致睡眠或阻塞的操作(比如内存分配、获取信号量等)。
- 通常只做最紧急、最核心的事情,比如:
- 读取硬件寄存器,判断中断来源。
- 清除中断标志位。
- 将获取到的数据拷贝到内存缓冲区。
- thread_fn (中断线程处理函数 - 下半部)
- 执行环境正常:thread_fn 实际上是一个普通的内核线程。它不像 handler 那样有诸多限制,可以执行可能会睡眠的操作(比如进行复杂的计算、与用户空间进行数据交换、写文件等)。
- 延时执行:它不会立即执行,而是由 handler 执行完毕后唤醒,然后等待内核调度器在合适的时机来运行它。
- handler (中断处理函数 - 上半部)
- 身份标识:name 和 dev_id
- name (中断名称)
- 作用:这是一个字符串,用来给这个中断起一个有意义的名字,比如 “my-keyboard-irq”。
- 用途:当您查看 /proc/interrupts 文件时,您会看到这个名字,这对于调试和系统分析非常有用,让您能一眼看出哪个中断属于哪个设备。
- dev_id (设备ID)
- 作用:这是一个 void * 类型的指针,通常用来指向设备自己的私有数据结构(比如 platform_device 结构体指针)。
- 用途:
- 传递上下文:dev_id 会作为参数传递给您的 handler 和 thread_fn 函数。这样,在处理函数内部,您就可以通过这个指针访问到您设备的所有信息。
- 区分共享中断:这是最重要的用途!当多个设备共享同一个中断号时,它们的 irqaction 会被挂在同一个 action 链表上。
- 卸载时:当您调用 free_irq() 卸载中断时,您必须提供注册时用的那个 dev_id。内核会根据这个 dev_id,在链表中准确地找到属于您设备的那个 irqaction 并将其移除,而不会影响其他设备。
- 因此,对于共享中断,dev_id 必须是唯一的,且不能为NULL。
irq_domain 和 irq_data
“硬件与软件的翻译官”——irq_domain 和 irq_data
- 之所以需要“翻译官”,是因为硬件和软件在描述同一个中断时,说的“语言”不一样。
- 硬件的语言(本地语言):硬件中断号(hwirq)是局部的、相对的。比如,GPIO 控制器内部有 0-31 号中断引脚,I2C 控制器内部也可能有 0-7 号中断。它们的“1号中断”是完全不同的东西。
- 软件的语言(通用语言):Linux 内核需要一个全局统一的、唯一的中断号(irq)来管理所有中断。内核里的 irq_desc 数组就是用这个全局 irq 号来索引的。
- irq_data - “双语对照卡”
- 您可以把 irq_data 结构体想象成一张“双语对照卡”或者一个“中转站”。每一张卡都只描述一个具体的中断,上面记录了关于这个中断的所有关键信息,最重要的就是两种“语言”的对照。
- hwirq (硬件中断号): 记录了这个中断在它所属的硬件控制器内部的本地编号。比如,在 GPIO 控制器里的第 5 号中断引脚,那么 hwirq 就是 5。
- irq (软件中断号): 记录了这个中断在整个 Linux 内核中全局唯一的编号。比如,内核可能会给 GPIO 的 5 号中断分配一个全局编号 137。这个 137 就是您在 request_irq() 函数中实际使用的中断号。
- chip 指针: 指向我们稍后会讲的 irq_chip 结构体,也就是这个中断的“硬件遥控器”。
- domain 指针: 指向管理这张卡的“首席翻译官”,也就是 irq_domain。
- irq_domain - “首席翻译官”
- irq_domain 结构体就是负责创建和管理上述“双语对照卡”的“首席翻译官”。它的存在,完美地解决了硬件中断的“方言”问题。
- 解决“方言”冲突 (Namespace)
- 每个中断控制器(比如一个 GIC、一个 GPIO 控制器、一个 I2C 控制器)都有自己的一套本地中断号,这就是一种“方言”。如果没有 irq_domain,内核就无法区分 GPIO 的“1号中断”和 UART 的“1号中断”。
- irq_domain 的做法是,为每一个中断控制器都分配一个独立的 irq_domain 实例。这样,每个“翻译官”都只负责自己“域”内的翻译工作,互不干扰。
- 核心工作:执行翻译 (Mapping)
- irq_domain 的主要工作流程,尤其是在现代基于设备树的系统中,是这样的:
- 解读“申请材料”(解析设备树)
- 当内核解析设备树时,会看到类似这样的描述:
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_RISING>;- 这段话的意思是“我需要一个中断,它来自 gpio1 控制器,是这个控制器里的第 5 号中断”。 此时,gpio1 所对应的 irq_domain 就会被激活。它内部有一个 irq_domain_ops 结构体,里面有一个 xlate 函数,这个函数专门负责读懂 interrupts = <5 …> 这句话,从中提取出 hwirq 是 5,触发方式是上升沿等关键信息。
- 当内核解析设备树时,会看到类似这样的描述:
- 分配“通用编号”并制作“对照卡”(映射)
- 当 xlate 函数成功解读出 hwirq = 5 之后,irq_domain 的 map 函数就会被调用。map 函数的核心任务是:
- 向内核申请一个尚未被使用的、全局唯一的 irq 号(比如内核分配了 138)。
- 创建一个 irq_data 结构体(制作一张新的“双语对照卡”)。
- 在这张卡上填写好:hwirq = 5, irq = 138。
- 同时,将这张卡和对应的 irq_chip(硬件操作方法)等信息关联起来。
- 当 xlate 函数成功解读出 hwirq = 5 之后,irq_domain 的 map 函数就会被调用。map 函数的核心任务是:
irq_chip
irq_chip 是Linux中断系统硬件抽象层(HAL)的完美体现。它通过一套标准的函数指针接口,将千差万别的硬件操作统一起来。
- 世界上有成百上千种不同的中断控制器芯片(比如 ARM 的 GIC、MIPS 的内建中断控制器、各种GPIO控制器等)。它们实现“屏蔽中断”这个功能的具体方法(即需要读写哪个寄存器的哪一位)是完全不同的。
- 如果让内核的中断核心代码去适配每一种芯片,那代码将会变得无比臃肿和混乱。
- 于是,Linux内核的开发者们设计了 irq_chip 这个绝妙的抽象层。它的工作模式是:
- 内核核心代码(上层):只下达标准化的命令,比如“屏蔽掉75号中断”。它不关心具体怎么做。
- irq_chip(遥控器):接收到“屏蔽”这个标准命令。
- 芯片驱动(下层):为这个“遥控器”的“屏蔽”按键 (irq_mask) 接上具体的电路,也就是实现了irq_mask这个函数,函数内部去操作特定芯片的寄存器。
- irq_chip 是如何工作的?
- “装配遥控器”: 当中断控制器的驱动(比如GIC或GPIO驱动)初始化时,它会创建一个irq_chip结构体的实例,并将自己实现的底层硬件操作函数(比如gic_mask_irq, gpio_unmask_irq)的地址,赋值给irq_chip里的相应函数指针(.irq_mask, .irq_unmask)。
- “绑定遥控器”: 在 irq_domain 将 hwirq 映射为 irq 的过程中,这个“装配”好的irq_chip遥控器会被和每一个具体的irq_data(双语对照卡)绑定在一起。
- “使用遥控器”:
- 当您调用 request_irq() 时,内核中断核心代码不需要知道您用的是什么芯片,它只会执行类似 irq_startup(irq_data) 这样的标准操作。
- 这个标准操作会通过 irq_data->chip 指针,找到您芯片专属的那个“遥控器”,然后调用遥控器上的 .irq_startup 按键,最终执行到您芯片驱动里写的具体硬件操作函数。
- 同样,当您的中断处理函数执行完毕后,内核会自动调用 irq_eoi(irq_data),这也将通过遥控器调用到您芯片的 .irq_eoi 函数,完成中断结束的硬件操作。

729

被折叠的 条评论
为什么被折叠?



