1、GPU中断基础:中断概念、中断向量表、GPU中断控制器、中断请求(IRQ)处理流程

各位同学,咱们今天聊聊GPU中断。说实话,中断这东西,很多做上层开发的兄弟觉得跟自己没关系,觉得那是驱动和硬件的事。但我要说,如果你不懂中断,你写的多任务调度代码,迟早会出大问题。

我当年刚接触GPU时,就吃过这个亏。一个并行任务跑着跑着突然卡死,查了三天,最后发现是中断处理没配好,GPU一直在等一个永远不会来的信号。嗯,从那以后,我再也不敢小看中断了。

1.1 中断到底是什么?

中断,说白了就是硬件在喊你:「嘿,CPU,我这边有事,你赶紧来处理一下!」

你想想看,CPU平时在流水线上吭哧吭哧干活,GPU也在那边疯狂算矩阵。但总有些突发情况——比如渲染完成了、显存访问出错了、DMA传输结束了——这些事不能等CPU轮询,必须立刻通知它。

中断的核心价值就一个字:

我个人习惯把中断比作「硬件级别的回调函数」。你写代码时注册一个回调,事件发生了就自动调用。中断也是这个道理,只不过它是在硬件层面实现的,响应速度是纳秒级的。

中断 vs 轮询

轮询:CPU每隔几微秒问一次「GPU你好了没?」——浪费大量算力。

中断:GPU好了主动喊CPU——CPU该干嘛干嘛,效率高得多。

1.2 中断向量表——硬件的「电话本」

中断来了,CPU怎么知道该执行哪段代码?这就得靠中断向量表了。

中断向量表,你可以把它想象成一个电话本。每个中断源都有一个编号,叫中断向量号。CPU收到中断后,拿着这个号去查表,找到对应的处理函数地址,然后跳过去执行。

在x86架构下,中断向量表放在内存的固定位置,从0x0000开始。但在现代GPU系统里,情况要复杂得多。GPU有自己的中断控制器,向量表也往往是可配置的。

// 伪代码:中断向量表的结构
struct interrupt_vector {
    uint64_t handler_address;  // 处理函数地址
    uint32_t flags;            // 中断标志位
    uint16_t priority;         // 优先级
} __attribute__((packed));

// 典型的GPU中断向量表配置
interrupt_vector ivt[256] = {
    [0]  = { .handler = &gpu_reset_handler,    .priority = 0 },  // 最高优先级
    [1]  = { .handler = &gpu_fault_handler,    .priority = 1 },  // 硬件错误
    [2]  = { .handler = &gpu_dma_done_handler, .priority = 2 },  // DMA完成
    [3]  = { .handler = &gpu_render_done,      .priority = 3 },  // 渲染完成
    // ... 其他中断向量
};

这里有个坑,我必须要提醒你:中断向量表不能随便改。我曾经在调试一个GPU驱动时,手贱改了中断向量表的基地址,结果系统直接崩溃。因为CPU找不到中断处理函数了。

注意:中断向量表的配置必须在系统初始化阶段完成,且一旦配置好,运行期间尽量不要动。如果非要动态修改,一定要关中断、改表、开中断,三步缺一不可。

1.3 GPU中断控制器——中断的「交通警察」

GPU不像CPU那样只有一个中断源。一个现代GPU可能有几十个甚至上百个中断源:计算单元完成、显存控制器报错、PCIe链路状态变化、温度传感器告警……这么多中断源,如果一股脑全扔给CPU,CPU会疯掉的。

所以,GPU内部有一个专门的中断控制器,负责管理所有中断源。它的工作包括:

  • 中断屏蔽:暂时忽略某些中断源
  • 中断优先级:高优先级中断可以打断低优先级
  • 中断聚合:把多个小中断合并成一个
  • 中断路由:决定把中断发给哪个CPU核心

我参与过一个项目,GPU有64个SM(流式多处理器),每个SM都能产生中断。如果不做聚合,CPU每秒要处理几百万次中断,光上下文切换就把性能吃光了。

解决方案是什么?用中断控制器做聚合。把同一批次的中断合并成一个,告诉CPU「这批活干完了」,而不是「干完一个就喊一声」。

中断源类型 典型中断号 优先级 说明
GPU复位 0 最高 硬件复位完成
内存错误 1 ECC错误或越界访问
DMA完成 2 数据传输结束
渲染完成 3 一帧渲染结束
温度告警 4 温度超过阈值

1.4 IRQ处理流程——从硬件到软件的全链路

好了,前面铺垫了这么多,咱们来看看一个完整的中断请求(IRQ)是怎么处理的。我把它拆成6步:

  1. 中断产生:GPU内部某个单元完成了任务,拉高中断信号线。
  2. 中断仲裁:GPU中断控制器检查优先级,决定是否立即上报。
  3. 中断上报:通过PCIe MSI-X或传统INTx线,把中断发给CPU。
  4. 中断响应:CPU保存当前上下文,查中断向量表,找到处理函数。
  5. 中断处理:执行驱动里的中断服务例程(ISR)。
  6. 中断返回:恢复上下文,CPU继续干原来的活。

这里面最关键的,其实是第5步——中断处理。ISR要干的事很多:读取GPU的状态寄存器、清除中断标志、把数据从GPU拷贝到系统内存、唤醒等待的任务……

但ISR有个硬性要求:必须快。因为ISR执行期间,CPU是关中断的,其他中断进不来。如果你在ISR里做太多事,系统响应就会变慢。

我的经验:ISR里只做最紧急的事——比如读取状态、清除中断。剩下的耗时操作(比如数据拷贝、任务调度),放到下半部(bottom half)或者工作队列里去做。这样既能快速响应中断,又不会阻塞其他中断。

我曾经见过一个同事,在ISR里做了个显存分配,结果分配函数里有个锁,锁被另一个线程占着,直接死锁。嗯,ISR里千万别干这种事。

// 一个典型的GPU中断处理流程(简化版)
void gpu_irq_handler(int irq, void *dev_id) {
    struct gpu_device *gpu = (struct gpu_device *)dev_id;
    
    // 1. 读取中断状态寄存器
    uint32_t status = readl(gpu->mmio + GPU_IRQ_STATUS);
    
    // 2. 清除中断标志(必须第一时间做)
    writel(status, gpu->mmio + GPU_IRQ_CLEAR);
    
    // 3. 判断中断类型
    if (status & GPU_IRQ_DMA_DONE) {
        // DMA完成——唤醒等待的进程
        wake_up_interruptible(&gpu->dma_wait_queue);
    }
    
    if (status & GPU_IRQ_RENDER_DONE) {
        // 渲染完成——提交到工作队列处理
        schedule_work(&gpu->render_work);
    }
    
    if (status & GPU_IRQ_ERROR) {
        // 硬件错误——紧急处理
        gpu_handle_error(gpu);
    }
}

看到没?ISR里只做了三件事:读状态、清标志、唤醒或调度。真正的数据处理,交给工作队列去慢慢搞。这就是中断处理的黄金法则——快进快出

1.5 小结

好了,这一章的内容就这些。咱们把中断的基础概念、中断向量表、GPU中断控制器、IRQ处理流程都过了一遍。说白了,中断就是硬件和软件之间的「紧急联络机制」。搞懂了它,你才能理解GPU是怎么跟CPU配合工作的。

下一章,咱们会深入GPU的多任务调度,看看中断是怎么影响任务切换的。到时候你会发现,中断处理得好不好,直接决定了你的GPU利用率高不高。

嗯,今天就到这儿。有问题随时问我。