2、中断注册与注销:InterruptAttach()、InterruptDetach()、中断处理函数原型

好,咱们接着聊中断。上一节我们把中断控制器和中断向量表理了一遍,算是把硬件底子打好了。这一节,咱们要真正上手写代码了——怎么把一个中断处理函数挂到系统里,又怎么把它摘下来。

说白了,就是两个核心API:InterruptAttach()InterruptDetach()。再加上中断处理函数到底长什么样,参数怎么传,返回值怎么处理。这些搞清楚了,你写中断驱动才算入了门。

2.1 中断处理函数原型——先搞清楚你要写什么

在QNX里,中断处理函数的签名是固定的。你别想着自己随便定义参数,系统不认。原型长这样:

const struct sigevent *handler(void *area, int id);

两个参数:

  • void *area —— 你注册中断时传进来的那个上下文指针。我习惯叫它“cookie”,你想传什么就传什么,比如设备结构体指针、寄存器基地址。说白了,这就是你的私有数据通道。
  • int id —— 系统分配的中断ID。这个ID在你注册成功后由内核返回,处理函数里一般用不到,但偶尔需要用它来注销中断时会有用。

返回值是一个 const struct sigevent *。这个有意思了——它告诉内核:中断处理完了,你要不要唤醒某个线程?

  • 返回 NULL:表示不需要唤醒任何线程,中断处理完全在ISR里搞定。
  • 返回一个 struct sigevent 指针:内核会把这个事件发给对应的线程,通常是发一个脉冲信号,让线程知道“数据准备好了,你来处理吧”。

嗯,这里要注意:中断处理函数是在中断上下文里执行的,不能阻塞,不能调用任何可能引起阻塞的API。你想想看,中断上下文里如果调了 sleep(),整个系统就挂了。我刚开始做QNX开发时,就犯过这个错——在ISR里打印日志,结果日志驱动自己也要用锁,直接死锁。教训深刻啊。

2.2 InterruptAttach()——把中断挂上去

注册中断的API有两个版本,我重点讲 InterruptAttach(),因为它更常用,也更灵活。

int InterruptAttach(
    int intr,
    const struct sigevent *(*handler)(void *, int),
    void *area,
    int size,
    unsigned flags
);

参数说明:

参数 含义 我的建议
intr 中断向量号,就是上一节说的IRQ号 从硬件手册或BSP里查,别猜
handler 中断处理函数指针 注意函数签名必须完全匹配
area 传给handler的上下文指针 我习惯传设备结构体指针
size area指向的数据大小,通常传0 除非你要用内核帮你拷贝数据
flags 标志位,常用 INTR_ATTACH_STANDARD 共享中断时用 INTR_ATTACH_FLAG_SHARED

返回值:成功返回一个非负的ID(就是前面说的 id),失败返回 -1,并设置errno。

我个人的习惯是,注册中断时一定检查返回值。别偷懒。有一次我在项目里没检查返回值,结果中断根本没挂上,设备轮询跑了一整天,我还纳闷为什么CPU占用率那么高。后来一查,是中断号写错了。

核心要点:注册中断时,area 参数是你的“救命稻草”。把设备相关的所有信息打包成一个结构体传进去,ISR里就能拿到一切你需要的东西。我通常这样定义:

typedef struct {
    uintptr_t base_addr;   // 寄存器基地址
    int       irq;         // 中断号
    int       dev_id;      // 设备ID
    // ... 其他设备相关字段
} my_device_t;

// 注册时
my_device_t *dev = malloc(sizeof(my_device_t));
dev->base_addr = 0xFE200000;
dev->irq = 67;
int id = InterruptAttach(dev->irq, my_isr, dev, 0, INTR_ATTACH_STANDARD);

2.3 InterruptDetach()——用完记得摘下来

有注册就有注销。函数原型很简单:

int InterruptDetach(int id);

参数就是 InterruptAttach() 返回的那个ID。成功返回0,失败返回 -1。

什么时候需要注销?

  • 驱动卸载时,必须注销中断。否则中断来了,处理函数已经被卸载了,系统直接panic。
  • 设备热插拔时,拔掉设备前要注销。
  • 重新配置中断时,先注销旧的,再注册新的。

警告:千万不要在中断处理函数里调用 InterruptDetach() 来注销自己!这会导致死锁。我曾经见过一个同事这么干,结果系统直接挂死,连panic信息都打印不出来。正确的做法是:在ISR里设置一个标志位,然后在线程上下文中调用 InterruptDetach()

2.4 一个完整的中断注册示例

光说不练假把式。我给你写一个完整的例子,从注册到处理到注销,一条龙:

#include <sys/neutrino.h>
#include <hw/inout.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    uintptr_t base;
    int       irq;
    volatile int  irq_count;
} dev_ctx_t;

const struct sigevent *my_isr(void *area, int id) {
    dev_ctx_t *ctx = (dev_ctx_t *)area;
    uint32_t status;

    // 读中断状态寄存器
    status = in32(ctx->base + 0x10);
    if (status & 0x01) {
        ctx->irq_count++;
        // 清除中断标志
        out32(ctx->base + 0x10, 0x01);
    }

    // 返回NULL,表示不需要唤醒线程
    return NULL;
}

int main(int argc, char *argv[]) {
    dev_ctx_t *ctx = malloc(sizeof(dev_ctx_t));
    ctx->base = 0xFE200000;
    ctx->irq  = 67;
    ctx->irq_count = 0;

    int id = InterruptAttach(ctx->irq, my_isr, ctx, 0, INTR_ATTACH_STANDARD);
    if (id == -1) {
        perror("InterruptAttach failed");
        free(ctx);
        return EXIT_FAILURE;
    }

    printf("Interrupt attached, ID = %d\n", id);

    // 模拟运行一段时间
    sleep(10);

    // 注销中断
    if (InterruptDetach(id) == -1) {
        perror("InterruptDetach failed");
    }

    printf("Total IRQ count: %d\n", ctx->irq_count);
    free(ctx);
    return EXIT_SUCCESS;
}

这个例子虽然简单,但骨架是完整的。你想想看,实际项目里无非就是在这个基础上加更多逻辑——比如用 sigevent 唤醒线程、处理多个中断源、支持共享中断等等。

2.5 避坑指南

最后,我把自己踩过的坑总结一下,你记好了:

  • 中断处理函数要快——别在里面做复杂计算,别调用printf,别申请内存。我见过有人在ISR里malloc,直接导致系统响应变慢,最后整个系统卡死。
  • 共享中断要小心——如果你的设备支持共享中断,注册时加上 INTR_ATTACH_FLAG_SHARED 标志。同时,ISR里一定要检查中断状态寄存器,确认是不是自己的设备触发的中断。不是的话,赶紧返回NULL,让下一个handler处理。
  • 中断ID要保存好——注销时要用的。我习惯把它放在设备结构体里,和irq号放一起,这样不会丢。
  • 多线程环境下注意同步——ISR和线程共享的数据,要用原子操作或者关中断的方式来保护。别用互斥锁,ISR里不能用。

好了,中断注册和注销就讲到这里。下一节我们聊聊中断处理函数里到底能做什么、不能做什么,以及怎么和用户态线程高效通信。到时候我会分享一个我实际项目中用过的中断分发框架,挺实用的。