嵌入式Linux——Linux中断系统中的重要数据结构——最直观的理解

irq_desc

irq_desc 确实是中断系统中最核心的数据结构。您可以把它想象成内核里的一份档案柜,每一个硬件中断号(IRQ Number)都对应一个抽屉,每个抽屉里都存放着一份详细的“中断处理档案”,这份档案就是irq_desc 结构体。

  • 在内核中,所有这些“档案”通常被组织成一个大数组,即irq_desc 数组。比如,当中断号为 75 的中断发生时,内核就会直接去查找 irq_desc[75] 这个数组元素,获取处理该中断所需的所有信息。
  • 注意:为了节省内存,如果内核配置了 CONFIG_SPARSE_IRQ(稀疏中断),系统就会用一种叫做“基数树 (radix tree)”的数据结构来代替数组。这么做的原因是,有些系统可能定义了大量中断号,但实际只用了其中几个,用数组会浪费大量空间。
  • 这份“档案”里最重要的内容有两项:一个是“总负责人”,另一个是“具体行动方案列表”
    1. 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 链表。
    2. action 链表 (具体行动方案列表)
      • action 是一个指向 irqaction 结构体链表的指针。这是处理共享中断的关键。
      • 什么是共享中断?
        • 想象一下,一个GPIO引脚(中断B)上可能连接了多个外部设备(比如两个不同的按键)。当中断B发生时,内核只知道是这个引脚有信号了,但不知道具体是哪个设备触发的。
      • action 链表的作用:
        • 此时,irq_desc[B] 中的 action 链表就派上用场了。每一个连接到该引脚的设备,在注册中断时(调用request_irq),都会将自己的“行动方案”(一个irqaction结构体)挂载到这个链表上。
        • 当中断B的handle_irq(比如handle_edge_irq)被调用时,它就会遍历 action 链表,依次执行链表上每一个irqaction中定义的处理函数(handler)。
        • 每个设备自己的处理函数都有责任判断:“这次中断是不是我产生的?”如果是,就进行处理;如果不是,就直接返回。

irqaction

irqaction 是中断处理的最小执行单元,它详细定义了“做什么”(handler/thread_fn)和“是谁”(name/dev_id),并通过挂载到 irq_desc 的 action 链表上,将具体的设备驱动与内核的中断处理流程无缝地连接了起来。

  • 核心执行人:handler 和 thread_fn (中断的上半部与下半部)
    1. handler (中断处理函数 - 上半部)
      • 当中断发生时,CPU会立刻放下手头所有的事情,跳转到这个 handler 函数来执行。它必须在尽可能短的时间内完成工作,因为它执行期间会屏蔽掉同级别的其他中断。
      • 它运行在特殊的“中断上下文”中,不能执行任何可能导致睡眠或阻塞的操作(比如内存分配、获取信号量等)。
      • 通常只做最紧急、最核心的事情,比如:
        • 读取硬件寄存器,判断中断来源。
        • 清除中断标志位。
        • 将获取到的数据拷贝到内存缓冲区。
    2. thread_fn (中断线程处理函数 - 下半部)
      • 执行环境正常:thread_fn 实际上是一个普通的内核线程。它不像 handler 那样有诸多限制,可以执行可能会睡眠的操作(比如进行复杂的计算、与用户空间进行数据交换、写文件等)。
      • 延时执行:它不会立即执行,而是由 handler 执行完毕后唤醒,然后等待内核调度器在合适的时机来运行它。
  • 身份标识:name 和 dev_id
    1. name (中断名称)
    • 作用:这是一个字符串,用来给这个中断起一个有意义的名字,比如 “my-keyboard-irq”。
    • 用途:当您查看 /proc/interrupts 文件时,您会看到这个名字,这对于调试和系统分析非常有用,让您能一眼看出哪个中断属于哪个设备。
    1. dev_id (设备ID)
    • 作用:这是一个 void * 类型的指针,通常用来指向设备自己的私有数据结构(比如 platform_device 结构体指针)。
    • 用途:
      1. 传递上下文:dev_id 会作为参数传递给您的 handler 和 thread_fn 函数。这样,在处理函数内部,您就可以通过这个指针访问到您设备的所有信息。
      2. 区分共享中断:这是最重要的用途!当多个设备共享同一个中断号时,它们的 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 号来索引的。
  1. irq_data - “双语对照卡”
    • 您可以把 irq_data 结构体想象成一张“双语对照卡”或者一个“中转站”。每一张卡都只描述一个具体的中断,上面记录了关于这个中断的所有关键信息,最重要的就是两种“语言”的对照。
    • hwirq (硬件中断号): 记录了这个中断在它所属的硬件控制器内部的本地编号。比如,在 GPIO 控制器里的第 5 号中断引脚,那么 hwirq 就是 5。
    • irq (软件中断号): 记录了这个中断在整个 Linux 内核中全局唯一的编号。比如,内核可能会给 GPIO 的 5 号中断分配一个全局编号 137。这个 137 就是您在 request_irq() 函数中实际使用的中断号。
    • chip 指针: 指向我们稍后会讲的 irq_chip 结构体,也就是这个中断的“硬件遥控器”。
    • domain 指针: 指向管理这张卡的“首席翻译官”,也就是 irq_domain。
  2. irq_domain - “首席翻译官”
    • irq_domain 结构体就是负责创建和管理上述“双语对照卡”的“首席翻译官”。它的存在,完美地解决了硬件中断的“方言”问题。
    1. 解决“方言”冲突 (Namespace)
      • 每个中断控制器(比如一个 GIC、一个 GPIO 控制器、一个 I2C 控制器)都有自己的一套本地中断号,这就是一种“方言”。如果没有 irq_domain,内核就无法区分 GPIO 的“1号中断”和 UART 的“1号中断”。
      • irq_domain 的做法是,为每一个中断控制器都分配一个独立的 irq_domain 实例。这样,每个“翻译官”都只负责自己“域”内的翻译工作,互不干扰。
    2. 核心工作:执行翻译 (Mapping)
      • irq_domain 的主要工作流程,尤其是在现代基于设备树的系统中,是这样的:
      1. 解读“申请材料”(解析设备树)
        • 当内核解析设备树时,会看到类似这样的描述:
          interrupt-parent = <&gpio1>;
          interrupts = <5 IRQ_TYPE_EDGE_RISING>;
          • 这段话的意思是“我需要一个中断,它来自 gpio1 控制器,是这个控制器里的第 5 号中断”。 此时,gpio1 所对应的 irq_domain 就会被激活。它内部有一个 irq_domain_ops 结构体,里面有一个 xlate 函数,这个函数专门负责读懂 interrupts = <5 …> 这句话,从中提取出 hwirq 是 5,触发方式是上升沿等关键信息。
      2. 分配“通用编号”并制作“对照卡”(映射)
        • 当 xlate 函数成功解读出 hwirq = 5 之后,irq_domain 的 map 函数就会被调用。map 函数的核心任务是:
          • 向内核申请一个尚未被使用的、全局唯一的 irq 号(比如内核分配了 138)。
          • 创建一个 irq_data 结构体(制作一张新的“双语对照卡”)。
          • 在这张卡上填写好:hwirq = 5, irq = 138。
          • 同时,将这张卡和对应的 irq_chip(硬件操作方法)等信息关联起来。

irq_chip

irq_chip 是Linux中断系统硬件抽象层(HAL)的完美体现。它通过一套标准的函数指针接口,将千差万别的硬件操作统一起来。

  • 世界上有成百上千种不同的中断控制器芯片(比如 ARM 的 GIC、MIPS 的内建中断控制器、各种GPIO控制器等)。它们实现“屏蔽中断”这个功能的具体方法(即需要读写哪个寄存器的哪一位)是完全不同的。
  • 如果让内核的中断核心代码去适配每一种芯片,那代码将会变得无比臃肿和混乱。
  • 于是,Linux内核的开发者们设计了 irq_chip 这个绝妙的抽象层。它的工作模式是:
    1. 内核核心代码(上层):只下达标准化的命令,比如“屏蔽掉75号中断”。它不关心具体怎么做。
    2. irq_chip(遥控器):接收到“屏蔽”这个标准命令。
    3. 芯片驱动(下层):为这个“遥控器”的“屏蔽”按键 (irq_mask) 接上具体的电路,也就是实现了irq_mask这个函数,函数内部去操作特定芯片的寄存器。
  • irq_chip 是如何工作的?
    1. “装配遥控器”: 当中断控制器的驱动(比如GIC或GPIO驱动)初始化时,它会创建一个irq_chip结构体的实例,并将自己实现的底层硬件操作函数(比如gic_mask_irq, gpio_unmask_irq)的地址,赋值给irq_chip里的相应函数指针(.irq_mask, .irq_unmask)。
    2. “绑定遥控器”: 在 irq_domain 将 hwirq 映射为 irq 的过程中,这个“装配”好的irq_chip遥控器会被和每一个具体的irq_data(双语对照卡)绑定在一起。
    3. “使用遥控器”:
      • 当您调用 request_irq() 时,内核中断核心代码不需要知道您用的是什么芯片,它只会执行类似 irq_startup(irq_data) 这样的标准操作。
      • 这个标准操作会通过 irq_data->chip 指针,找到您芯片专属的那个“遥控器”,然后调用遥控器上的 .irq_startup 按键,最终执行到您芯片驱动里写的具体硬件操作函数。
      • 同样,当您的中断处理函数执行完毕后,内核会自动调用 irq_eoi(irq_data),这也将通过遥控器调用到您芯片的 .irq_eoi 函数,完成中断结束的硬件操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值