一、中断是什么
中断是硬件主动通知 CPU 的机制。一个外设产生了数据或者发生了事件,不需要 CPU 轮询去问"你好了没",而是直接发一个信号给 CPU,CPU 暂停当前工作去处理这个信号。
ARM 架构里,这个"接收开关"是 CPSR 寄存器(当前程序状态寄存器,全局硬件中断总闸)里的 I 位,当前位=0 表示开中断,CPU 回应;=1 表示关中断,CPU 不理。
二、中断信号的三段接力(设备 → GIC → CPU)
一个中断从产生到被 CPU 处理,经过三个硬件关卡:
第一段:设备发出中断信号。 网卡收完一帧数据、串口 FIFO 到了阈值、定时器计数溢出------设备内部的硬件电路把一根信号线拉高,告诉中断控制器"我有事"。这一步和 CPU 无关,设备自己完成。
第二段:GIC 仲裁与分发。 GIC(Generic Interrupt Controller)是 ARM 的通用中断控制器。所有设备的中断信号汇集到这里。GIC 做的三件事:给每个中断分配一个编号(IRQ number)、按优先级排序、把优先级最高的那个发给 CPU。GIC 内部还有一个 pending 寄存器------如果一个中断来了但 CPU 没理,GIC 就把对应位设为 1,等 CPU 开门后立刻补送。
注:优先级分配
IRQ 编号分配 设备树 dts 里写interrupts = <中断类型 中断号 触发方式>,内核解析后给外设绑定固定 IRQ 编号;无硬件固定编号的外设,内核动态分配空闲 IRQ 号。
第三段:CPU 硬件判断。 GIC 把中断信号送到 CPU 的 IRQ 引脚上。CPU 硬件检查 CPSR 的 I 位------如果 当前位=0,CPU 保存当前上下文、跳转到中断向量表、进入中断处理流程;如果 当前位=1,CPU 在硬件层面直接忽略这个信号,连进入异常模式的流程都不走。
三个环节各有各的控制开关,这就是下面要说的三道门。
三、中断的三道屏蔽门
一道中断信号要从设备到达内核中断处理函数,要穿过三道门,每道门由不同的人控制:
门一:设备的中断使能寄存器。
每个设备芯片内部有一个"开关面板"(叫中断使能寄存器),是一堆 0 或 1 的比特位。比如:
- 第 0 位设成 1 → 收完一包数据就发中断
- 第 0 位设成 0 → 收完一包数据不发中断
驱动往这个寄存器写 0 或 1 来拨动开关。这是设备内部的事,改了只影响这一个设备。别的设备不知道也不受影响。
门二:GIC 的中断线屏蔽。 GIC 为每个中断号维护一个使能位。Linux 内核函数 disable_irq(n) 就是清掉第 n 号中断的使能位。关掉之后,这个设备的中断信号到了 GIC 就被拦截,不会传给 CPU。但其他中断号不受影响。控制者:内核代码调用 disable_irq / enable_irq。
门三:CPU 的全局中断使能位(CPSR.I 全局硬件中断总闸**)。** 这个位一清零,不管 GIC 送来什么、不管哪个设备的、不管优先级多高------CPU 一概不理。Linux 内核函数 local_irq_save 就是做这件事,它先读取当前 CPSR 值保存到 flags 变量,然后执行汇编指令 cpsid i 把 当前位 置 1,中断全关。local_irq_restore 把之前保存的 CPSR 值写回去,恢复原状态。控制者:任意内核代码都能调。
三道门的关系:门一和门二是"精准控制",想关谁关谁;门三是"粗暴控制",一关全关。 DS18B20 驱动用的是门三。
四、中断通路 vs 数据通路(为什么关总闸不会丢数据)
这是最容易被搞混的地方------中断不负责传数据,中断只负责递通知。硬件里是两条完全独立的通路:
数据通路: 设备硬件自动运行。网卡的 MAC 控制器自己收帧、组帧、写入内部 FIFO 缓冲区。串口的 UART 控制器自己采样 RX 线、组装字节、推入硬件 FIFO。整个过程不需要 CPU 参与,不需要中断参与。通电就能跑。中断关掉时,数据通路照常运转,数据持续存入硬件缓冲区。(中断恢复后,CPU 在中断处理函数里通过读硬件 FIFO 寄存器把数据取出来)
中断通路: 设备填了一定量的数据到 FIFO 后,额外拉一根信号线通知 GIC,GIC 再通知 CPU。这条通路的唯一作用就是"喊 CPU 来取数据"。DS18B20 驱动关的是第三条通路的 CPU 入口,数据通路完全不受影响。
缓冲区就是兜底机制。 不是"关中断时临时调用的机制",而是设备硬件本身的标配。网卡 FIFO 通常几 KB,串口 FIFO 至少 16 字节。几百微秒的时间,对于硬件数据速率来说远不够填满缓冲区。中断恢复后 CPU 正常取数,一个字节都不会丢。
五、注册中断 vs 关中断 ------ 两个完全独立的概念
request_irq(n, handler, ...) 是软件层面的注册行为------你告诉内核"如果第 n 号中断来了,请调用我的 handler 函数"。它登记的是一个"响应者"。没有注册的话,来了中断也没人处理,但不影响中断信号本身能不能到达 CPU。
local_irq_save(flags) 是硬件层面的开关操作------它直接修改 CPU 的 CPSR 寄存器,把全局中断响应能力关掉。它不关心你注册了什么中断、有没有注册中断。它就是一条 CPU 指令。
DS18B20 驱动的真实情况: 驱动根本没有调用过 request_irq,它不注册任何中断。它靠轮询方式主动读写 GPIO。调用 local_irq_save 不是为了关"自己的中断"(本来就没有),而是为了防止别人的中断(时钟、网卡、其他外设)打断它的微秒级 GPIO 时序。
六、优先级机制及其边界
GIC 支持中断优先级------每个中断号有一个优先级值,数值越小优先级越高。GIC 同时收到多个中断时,把优先级最高的发给 CPU。CPU 正在处理低优先级中断时,高优先级中断来了可以抢占(中断嵌套)。
但优先级在门二(GIC 层)起作用,门三(CPU 层)一关全部失效。 local_irq_save 关的是门三的 CPSR.I 位。这个时候 GIC 按优先级把中断信号送到 CPU 门口,CPU 硬件直接不接------管你优先级多高,一概拒之门外。
DS18B20 场景不需要考虑优先级。 因为关中断时间太短(一次 read_byte 全部 8 个 bit 总共不超过 600μs),就算时钟中断被延迟了 500μs,调度器完全感知不到。不需要高优先级中断来"抢先处理"。
七、DS18B20 为什么必须关总闸
根本原因:i.MX6ULL 没有硬件 1-Wire 控制器。 1-Wire 协议完全靠 CPU 手动操作 GPIO 来模拟,这种方式叫 bit-banging (软件位模拟 / 手动比特脉冲模拟)。
bit-banging 对时间的精度要求是微秒级------DS18B20 的写 0 时序要求 GPIO 保持低电平至少 60μs(范围 60~120μs)。如果在这 60μs 内 CPU 被中断打断去处理了 30μs 的网卡中断函数,DS18B20 看到的就是"5μs 低 + 中断打断间隙 + 25μs 低",而不是连续 60μs 低电平------通信失败。
mutex、spin_lock、preempt_disable 都不够用。 mutex 只让当前线程睡觉,中断照样来。spin_lock 只防止其他 CPU 核访问同一数据,不关中断。preempt_disable 只阻止调度器切走当前线程,但硬件中断一来照样跳到中断处理函数。
唯一的保证微秒级时序的手段就是关中断。 这是 bit-banging 方案的必然代价,绕不过去。
八、关总闸的风险与兜底机制
风险: 关中断期间,所有设备的中断都无法得到 CPU 的即时响应。如果关太久,网络丢包、定时器漂移、系统变卡。
兜底一:GIC pending 寄存。 关中断期间设备产生的中断不会被丢弃。GIC 把对应的 pending 位置 1。总闸一开,GIC 立刻按优先级把排队的中断送给 CPU。中断信号不丢,只是延迟处理。
兜底二:设备硬件缓冲区。 中断虽然没被处理,但数据通路不受影响。网卡 FIFO 继续收帧,串口 FIFO 继续收字节,定时器硬件计数器继续计。几百微秒远不够填满硬件缓冲区。
兜底三:内核代码规范。 内核里任何关中断的代码段都控制在几百微秒以内。这是铁律。如果关中断超过几毫秒,系统表现为明显卡顿。开启 CONFIG_IRQSOFF_TRACER 后内核会自动检测并打印 warning。
兜底四:DS18B20 本身的特性。 一次完整的 1-Wire 操作(reset + write_byte + read_byte + write_byte + ...)总时长约 1~1.5ms。两次操作之间间隔至少 750ms(温度转换时间)。99.9999% 的时间中断是开着的。关中断占比极低,对系统整体调度几乎无影响。
九、内核中关中断的真实分布
实际 Linux 内核中,会用到 local_irq_save 的地方非常少。绝大部分驱动的同步(保护内核空间数据)需求靠 mutex 和 spin_lock 就能满足:
- mutex(可睡眠锁): 用于线程上下文,等锁时会睡眠让出 CPU,中断正常响应。绝大多数驱动用这个。
- spin_lock(忙等锁): 用于中断上下文或临界区极短的场景。不关中断,只防其他 CPU 核。网卡驱动中断处理函数常用。
- spin_lock_irqsave(忙等锁 + 关本核中断): 比 local_irq_save 多一层锁保护。比关总闸安全,因为只关本核。
- local_irq_save(关总闸): 只用于 bit-banging 时序保护等极端场景。一个系统里可能就一个驱动需要。
"每个人都关几百微秒"是对生产环境内核的误解。 真实情况是:一个加载了 50 个驱动的系统,需要关总闸的可能只有 0 到 1 个。而那个驱动的关中断时间占总运行时间不到 0.0001%。
十、i.MX6ULL 上的替代方案
如果不想要关中断这套,有且只有以下替代路径:
硬件方案:外接 DS2482 芯片。 I2C 转 1-Wire 桥接芯片,1-Wire 时序由 DS2482 硬件完成。i.MX6ULL 通过 I2C 和它通信。内核有现成的 ds2482 驱动。不需要写 1-Wire 时序,不需要关中断。代价是硬件成本增加,PCB 多一颗芯片两根线。
十一、全链路总结
设备的中断信号要经过三层门(设备使能 → GIC 屏蔽 → CPU 总闸),DS18B20 驱动因为用 GPIO bit-banging 模拟 1-Wire 时序,必须关 CPU 总闸来保证微秒级精度,但关的时间极短(几百微秒),中断不会丢失(GIC pending 兜底),数据不会丢失(设备 FIFO 兜底),对系统调度影响可忽略 (关中断占比不到 0.0001%)