3. 中断机制基础:Linux中断子系统、中断上半部与下半部、中断上下文与进程上下文

中断是嵌入式系统响应外部事件最核心的机制。在RK3399显示驱动中,VSync(垂直同步)中断、DMA传输完成中断、显示控制器(VOP)状态中断共同构成了驱动与硬件之间的“信号桥梁”。理解Linux中断子系统的分层设计、上半部与下半部的拆分哲学,以及中断上下文与进程上下文的严格界限,是编写高效、无死锁显示驱动的基石。

3.1 Linux中断子系统架构

Linux内核将中断处理抽象为三个层次,从硬件信号到驱动回调,逐层解耦:

层次 组件 职责 RK3399相关
硬件层 GIC-400(通用中断控制器) 接收外设中断信号,进行优先级仲裁,向CPU核触发IRQ/FIQ VOP、MIPI DSI、eDP、HDMI 中断线均连接至GIC
核心层 irq_chip / irq_domain 管理中断控制器注册、中断号映射、中断使能/屏蔽/ACK 设备树中 interrupt-parent = <&gic> 建立映射
驱动层 request_irq() / devm_request_irq() 驱动注册中断处理函数,指定触发方式(边沿/电平) VOP驱动调用 devm_request_irq(dev, irq, vop_isr, 0, "vop", vop)
关键概念: 在RK3399上,GIC-400支持SPI(共享外设中断)和PPI(私有外设中断)。显示相关中断均为SPI,中断号范围在32~223之间,具体编号由设备树中的interrupts = <&GIC_SPI 148 IRQ_TYPE_LEVEL_HIGH>指定。

3.1.1 中断子系统数据流

下图展示了从VOP硬件产生中断信号到驱动处理函数被调用的完整路径:

Linux中断子系统数据流(RK3399 VOP中断为例) VOP 硬件 GIC-400 irq_domain vop_isr() 上半部(硬中断) 下半部(软中断) 说明:VOP触发中断 → GIC仲裁 → irq_domain查找映射 → 调用驱动注册的vop_isr() → 上半部记录状态 → 下半部调度DMA或更新FIFO

3.2 中断上半部(Top Half)与下半部(Bottom Half)

中断处理必须“快进快出”。Linux将中断处理拆分为两个阶段:

  • 上半部(硬中断上下文): 在关闭本地CPU中断(或至少关闭当前中断线)的情况下执行。只做最紧急、最轻量的工作,如:清除中断标志位、拷贝硬件寄存器状态、标记中断发生。必须毫秒级甚至微秒级完成。
  • 下半部(软中断/任务/workqueue上下文): 在中断使能的环境下执行,可以睡眠、可以持有锁、可以执行耗时操作。负责真正的数据处理,如:更新显示缓冲区指针、启动下一次DMA传输、唤醒等待队列。
对比维度 上半部(Top Half) 下半部(Bottom Half)
执行上下文 中断上下文(不可睡眠) 软中断 / tasklet / workqueue(可睡眠)
中断状态 当前中断线通常被屏蔽 所有中断均已使能
允许操作 仅原子操作、寄存器读写、标记位设置 内存分配、互斥锁、DMA API、调度
典型耗时 < 100 μs 可到数毫秒
RK3399显示示例 读取VOP_INTR_STATUS寄存器,清除VSync位 更新vblank计数器,触发framebuffer切换
RK3399实践建议: VOP的VSync中断处理中,上半部仅读取VOP_INTR_STATUS寄存器并写1清除中断位,然后调用tasklet_schedule()或workqueue。下半部才执行drm_crtc_handle_vblank()和DMA描述符重装。实测表明,若在下半部执行超过500μs的操作,会导致帧率抖动。

3.2.1 下半部机制对比

Linux提供三种下半部实现机制,驱动开发者需根据场景选择:

机制 特点 是否可睡眠 并发模型 显示驱动适用场景
tasklet 基于软中断,同一tasklet串行执行 否 单CPU串行 VSync帧计数更新、简单状态机推进
workqueue 基于内核线程,可阻塞、可调度 是 多CPU并行(默认) DMA描述符分配、FB内存映射、I2C配置
软中断(softirq) 最底层,性能最高,需自行处理并发 否 多CPU并行 极少直接使用,网络/块设备场景为主
常见错误: 在RK3399显示驱动的上半部中调用drm_crtc_handle_vblank(),该函数内部会获取spin_lock并可能唤醒等待队列,若此时持有其他锁,极易导致死锁。正确做法:上半部只做寄存器ACK,下半部调用DRM核心API。

3.3 中断上下文与进程上下文

理解这两种上下文的区别,是避免内核崩溃和调度延迟的前提:

  • 进程上下文: 当前运行在某个进程的上下文中,可以通过current宏访问进程描述符,可以睡眠、可以被抢占、可以持有mutex。系统调用、内核线程、workqueue处理函数均运行在此上下文中。
  • 中断上下文: 当前运行在中断服务路径中,没有关联的进程描述符,current宏指向被中断的进程(不可信赖)。不可睡眠、不可持有mutex、不可访问用户空间。上半部和tasklet均运行在此上下文中。

3.3.1 上下文检测与断言

内核提供宏用于运行时检测当前上下文,驱动调试时非常有用:

#include <linux/preempt.h>
#include <linux/hardirq.h>

/* 检测是否在中断上下文中 */
if (in_interrupt()) {
    pr_debug("当前在中断上下文中,不可睡眠\n");
}

/* 检测是否在硬中断上半部 */
if (in_irq()) {
    pr_debug("当前在硬中断上半部\n");
}

/* 检测是否在软中断(含tasklet) */
if (in_softirq()) {
    pr_debug("当前在软中断或tasklet中\n");
}

/* 断言:确保不在中断上下文中(用于workqueue回调) */
might_sleep();  // 若在中断上下文,触发内核警告

3.3.2 上下文切换示意图

下图展示了一次VSync中断触发后,上下文从进程→中断→下半部→进程的完整切换路径:

中断上下文与进程上下文切换流程(RK3399 VSync中断) 时间→ 进程上下文 显示更新线程 VSync中断 上半部(硬中断) 清除中断标志 tasklet_schedule 下半部(tasklet) 更新vblank计数 返回 恢复 上下文类型:进程上下文(可睡眠) → 中断上下文(不可睡眠) → 软中断上下文(不可睡眠) → 进程上下文 ⚠ 禁止操作 上半部中:禁止调用mutex_lock()、kmalloc(GFP_KERNEL)、copy_from_user()、msleep() 性能数据 RK3399上,一次完整中断上下文切换(进程→中断→下半部→进程)约需 2~5 μs,其中保存/恢复寄存器占主要开销

3.4 RK3399显示驱动中断设计要点

基于以上理论基础,在RK3399 VOP驱动中,中断处理的设计遵循以下原则:

  1. 中断号获取: 通过设备树解析或platform_get_irq()获取VOP中断号,通常为148(VOPB)和149(VOPL)。
  2. 注册时机: 在vop_bind()或probe()函数中,完成时钟和电源初始化后,调用devm_request_irq()注册中断。
  3. 触发方式: 使用IRQF_TRIGGER_HIGH(电平触发)或IRQF_TRIGGER_RISING(边沿触发),VOP通常使用电平触发,避免中断丢失。
  4. 上半部最小化: 仅读取VOP_INTR_STATUS寄存器,判断中断类型(VSync/LineFlag/BusError),清除中断位,然后调用tasklet_schedule()或queue_work()。
  5. 下半部数据处理: 在tasklet或workqueue中调用drm_crtc_handle_vblank()、更新vop->vblank_count、检查DMA传输完成状态。
RK3399实测数据: 在4K@60Hz显示模式下,VSync中断频率为60Hz,每次中断处理总耗时需控制在16.67ms以内。上半部实测耗时约3~5μs,下半部(含DRM核心调用)约50~100μs,完全满足实时性要求。若下半部执行超过1ms,则会出现帧率下降至30fps的现象。

3.5 本章小结

  • Linux中断子系统通过GIC → irq_domain → 驱动回调三层架构,实现了硬件中断到软件处理的解耦。
  • 上半部执行在中断上下文中,必须快速、不可睡眠;下半部执行在软中断或进程上下文中,可处理耗时逻辑。
  • 中断上下文与进程上下文有严格界限:中断上下文中禁止睡眠、禁止持有互斥锁、禁止访问用户空间。
  • RK3399显示驱动中,VSync中断处理遵循“上半部清标志、下半部做更新”的原则,确保帧率稳定。