Linux内核分析之设备驱动-03
This language version is unavailable; showing the other language.
42.1 硬件中断处理 —— 上半部
硬中断处理程序(上半部)运行在内核最严苛的上下文:抢占全关、不可睡眠、随时打断任何代码。它的职责因此被压缩为一件事——尽快告知内核"事件来了",把工作移交延迟层。本节解剖注册接口与上下文约束。
42.1.1 注册与返回值协议
// include/linux/interrupt.h:104 与 :123(节选)
typedef irqreturn_t (*irq_handler_t)(int irq, void *dev_id);
struct irqaction {
irq_handler_t handler;
void *dev_id; /* 传回驱动的实例指针 */
...
unsigned int irq;
unsigned int flags; /* IRQF_SHARED/ONESHOT/TRIGGER... */
struct irqaction *next; /* 共享中断链 */
irq_handler_t thread_fn; /* 42.4 节线程侧 */
struct task_struct *thread;
...
};
request_irq 的封装与返回值协议:
request_irq(irq, handler, flags, name, dev_id)
→ request_threaded_irq(irq, handler, NULL, ...) (manage.c:2115,
thread_fn=NULL 即纯硬中断模式)
handler 的三种回答:
IRQ_HANDLED 事件已处理
IRQ_NONE 不是我的事件 (共享中断链的礼貌:
对不上号的 handler 必须如实说 NONE,
spurious 检测 kernel/irq/spurious.c
统计连续 NONE → 禁用该中断线)
IRQ_WAKE_THREAD 唤醒 thread_fn (42.4 节)
flags 的关键位:
IRQF_SHARED 多设备共享一条线 (dev_id 区分)
IRQF_ONESHOT 硬中断完成后保持屏蔽直到线程跑完
IRQF_TRIGGER_* 触发方式 (边沿/电平)
dev_id 是共享中断的身份证:同一条线的每个注册者带自己的实例指针,触发时链上逐个调用——IRQ_NONE 诚实在共享线上是义务。
42.1.2 上下文约束清单
硬中断上下文禁止做的事 (全部源于"不调度"本质):
✗ 睡眠 (任何 mutex/semaphore/kmalloc(GFP_KERNEL))
✗ 长时间占用 (目标 <10μs; 超时会被 watchdog 记账,
/proc/interrupts 与 irq timing 工具可见)
✗ 访问用户内存 (可能缺页)
✓ 自旋锁 (spin_lock_irqsave, 16.1 节)
✓ 原子变量 / 标记位 + 唤醒延迟层
✓ GFP_ATOMIC 分配 (池预留, 慎用)
典型的最小上半部 (网卡收包):
handler:
ack 硬件中断 (防风暴)
while (环里有新包 && 配额内) {
取包描述符 → napi_gro_receive() (软中断侧)
}
if (还有) 标记 NAPI poll 待跑 → __napi_schedule
return IRQ_HANDLED;
(实际收包工作在 softirq, 42.2 节 NAPI)
为什么"快"如此硬性:中断期间的代码打断一切——包括"正在持锁的回写线程、正在跑的调度器"。上半部每多 1μs,全系统的最坏延迟就多 1μs(11 章调度延迟、RT 场景尤其敏感)。
42.1.3 屏蔽与嵌套
进入硬中断时 CPU 自动屏蔽本线 (level 触发的语义保证);
现代内核进一步: 整个硬中断上下文 local_irq_disable
语义 (CONFIG_IRQ_OFFLOAD 级别, 不允许再被同级打断),
嵌套中断已基本绝迹 — 硬中断是"原子区中的原子区"。
per-CPU 中断嵌套的历史包袱由
generic_handle_irq_desc (kernel/irq/handle.c) 的
级联控制器(级联=一条线的 handler 是子控制器分发器,
42.5 节 irqchip 树) 承担
小结
上半部的注册(request_irq→irqaction,dev_id 身份、flags 触发与共享语义)、返回值协议(HANDLED/NONE/WAKE_THREAD,NONE 的诚实性受 spurious 统计监督)、上下文约束(不睡、不慢、不碰用户内存)构成硬中断的全部骨架;它的存在只为"取数+标记+移交",真正的工作按需下沉到 42.2-42.4 的延迟层。下一节看第一级延迟机制——softirq 与 tasklet。
42.2 软中断 (softirq) 与 tasklet
softirq 是"静态编译的延迟处理通道":一组固定的类型(网络/定时器/RCU…),每 CPU 各自执行,可被硬中断打断但绝不睡眠。tasklet 是软中断之上的动态实例化封装(同一通道按队列执行回调),正在被淘汰(新代码应选 workqueue 或 threaded irq)。本节解剖执行时机与风暴防护。
42.2.1 softirq 的类型与触发
// include/linux/interrupt.h:552-563
HI_SOFTIRQ=0, /* 高优先级 tasklet */
TIMER_SOFTIRQ, /* 定时器到期处理 */
NET_TX_SOFTIRQ, /* 网络发送 */
NET_RX_SOFTIRQ, /* 网络接收 */
...
TASKLET_SOFTIRQ, /* 普通 tasklet */
...
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last */
NR_SOFTIRQS
类型是编译期固定的——驱动不能新增 softirq 类型,只能用 TASKLET/HI_SOFTIRQ 两类或转向 workqueue。触发是位图置位:__raise_softirq_irqoff(nr) 置 per-CPU 的 pending 位图,真正的执行发生在"中断返回边界"(42.1 节说了硬中断快退,退到哪?退到这个检查点)或 ksoftirqd 线程。
42.2.2 执行时机与风暴防护
// kernel/softirq.c:654
asmlinkage __visible void __softirq_entry __do_softirq(void)
__do_softirq 的执行纪律:
[1] 按位图依次执行各类型 handler (硬中断返回路径)
[2] 执行中新软中断可被 raise → 循环重扫
[3] 两道闸门 (防"软中断饿死一切"):
time limit: 总时长超 2ms (jiffies 检查)
need_resched: 有更高优先级工作要调度
→ 剩余 pending 交给 ksoftirqd/<cpu> 内核线程
(每个 CPU 一条, nice 高但仍可调度)
[4] 重入禁止: 同 CPU 同类型不并发
(这要求 handler 的数据天然 per-CPU 或自旋锁)
ksoftirqd 的两面性:
正常时: 几乎不跑 (中断返回顺带就做完了)
风暴时: 跑满 CPU — 它是"系统过载"的可见症状
(top 里 ksoftirqd 高 = 网络包风暴/timer 风暴)
NAPI 是 softirq 风暴的业界答案(44.3 节展开):收包硬中断只做"通知",真正的收包循环在 NET_RX_SOFTIRQ 里轮询批量处理,处理完才重新使能硬中断——把"每包一个中断"变成"每批一个中断"。
42.2.3 tasklet:软中断的动态封装
// include/linux/interrupt.h:691
struct tasklet_struct
{
struct tasklet_struct *next; /* 链到 softirq 的队列 */
unsigned long state; /* TASKLET_STATE_SCHED/RUN */
atomic_t count; /* 使能计数 (0=允许执行) */
bool use_callback;
union {
void (*func)(struct tasklet_struct *t);
void (*callback)(struct tasklet_struct *t);
};
};
tasklet 协议:
tasklet_schedule(&t): 挂链 + raise TASKLET/HI_SOFTIRQ
同一 tasklet 永不在两个 CPU 同时跑
(state 的 SCHED/RUN 位保证) — 天然序列化,
这是它比 workqueue 快的原因, 也是并发受限的原因
tasklet_enable/disable: count 门控
现状 (7.0): tasklet 处于淘汰轨道 —
RT 内核下 tasklet 的"原子但不抢占"语义与
实时性冲突 (改为软派发), 新代码三选一:
workqueue (要睡眠) / threaded irq (要线程化)
/ NAPI 式软中断 (网络类)
小结
softirq 以"固定类型 + per-CPU 位图 + 中断返回边界执行"提供最快的延迟处理,__do_softirq 的 2ms/need_resched 双闸门把过载卸给 ksoftirqd 线程(风暴的可见症状);tasklet 是其动态封装——同一实例天然序列化,但正因原子语义与 RT 冲突而被淘汰轨道收编,新驱动应在 workqueue/threaded irq/NAPI 式软中断中选择。下一节看真正可睡眠的延迟层——workqueue。
42.3 工作队列 (workqueue)
workqueue 是延迟处理的"进程上下文"层:work 项跑在内核线程里,可以睡眠(拿 mutex、kmalloc(GFP_KERNEL)、做 IO),是驱动延迟逻辑的万能兜底。现代实现(CMWQ,并发管理工作队列)按"池 × 优先级 × CPU 亲和"组织 worker 线程,按需伸缩。本节拆解其结构与并发模型。
42.3.1 work 与 workqueue 的两级抽象
两级模型:
work (work_struct): 一个待执行回调
INIT_WORK(&w, my_fn) / schedule_work(&w) (系统队列)
queue_work(my_wq, &w) (自建队列)
workqueue (struct workqueue_struct): work 的投递通道
alloc_workqueue("my_wq", flags, max_active) (workqueue.h:513)
flags 的语义组合:
WQ_UNBOUND 不绑 CPU (可在任意 worker 跑; 慢但有伸缩性)
WQ_HIGHPRI 高优先级池 (与 RT 线程竞争)
WQ_CPU_INTENSIVE 声明 CPU 密集 (不占并发额度)
WQ_MEM_RECLAIM 内存回收专用 worker (关键: 回收路径
本身不能再等内存 — 18 章回收、36.3 回写都靠它防死锁)
max_active: 同一 workqueue 同 CPU 上并发执行上限
"为什么不能只 schedule_work":系统全局队列(events)所有驱动共享,慢 work 会互相排队;WQ_MEM_RECLAIM 是必答项——任何在内存回收路径可能执行的工作若挂在没有 reclaim worker 的队列上,OOM 时构成死锁。这是驱动 review 的标准检查点。
42.3.2 CMWQ:worker 池与并发
CMWQ (concurrency managed workqueue) 的执行模型:
worker_pool: 每 CPU 普通池 + 高优池 + unbound 池
worker 线程动态起落 (空闲超时销毁)
并发管理器:
work 排队 → 唤醒池内空闲 worker
worker 发现"当前所有 worker 都在睡等某 work" →
动态唤起新 worker 保证推进 (deadlock 避免)
非 CPU_INTENSIVE 的 worker 不占"并发额度"时让出
flush/cancel 协议:
flush_workqueue(wq): 等全部排空 (卸载前)
cancel_delayed_work_sync: 取消+等待 (probe 失败清理)
delayed_work: 定时投递 (42.2 节 timer 的进程上下文版)
驱动视角的纪律:work 回调是普通进程上下文——可睡眠锁可用,但不得无限阻塞(占并发额度);同一 work 自身不得排队等自己(自死锁,CMWQ 检测后 WARN)。卸载序列的标准形:cancel_delayed_work_sync → destroy_workqueue → free。
42.3.3 与其他延迟机制的对照
| 机制 | 上下文 | 睡眠 | 并发 | 典型用户 |
|---|---|---|---|---|
| hardirq | 原子 | 否 | 多 CPU 同线共享 | 取数+ack(42.1) |
| softirq/tasklet | 原子 | 否 | 同类型不并发 | 网络/定时器(42.2) |
| workqueue | 进程 | 可 | 按池伸缩 | 驱动延迟逻辑 |
| threaded irq | 进程 | 可 | 每中断一线程 | 42.4 |
| kworker 观测 | ps -eLf \| grep kworker — worker 名 kworker/u16:3+my_wq 标注队列与亲和 |
workqueue 的统计与延迟在 /sys/kernel/debug/workqueue/(37.3 节 debugfs)可查——pool_workqueue 的排队时长是"驱动把活排太重"的直接证据。
小结
workqueue 以"work 项 + 通道(alloc_workqueue 的 flags/max_active)"两级抽象提供可睡眠的延迟处理,CMWQ 的 worker 池按需伸缩并以并发管理避免假死;WQ_MEM_RECLAIM 是驱动必检的生命周期红旗,flush/cancel 系列是卸载清理的标准动作。它是延迟阶梯的万能兜底——除非有硬实时或极致低延迟诉求(42.4 的线程化中断与 42.2 的软中断各占其位)。下一节看把"中断本身线程化"的方案。
42.4 线程化中断 (threaded IRQ)
线程化中断把"中断处理"本身变成一个内核线程:硬侧只做最少的屏蔽与应答,真正处理在 thread_fn 的进程上下文完成,跑完才重新使能中断。它是 RT 系统的基石(PREEMPT_RT 全量线程化),也是 I2C/SPI 这类"handler 本身就要睡眠"的慢总线的标准姿势。本节解剖其两段式协议。
42.4.1 两段式注册
// kernel/irq/manage.c:2115(签名)
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)
// :997(缺省硬侧)
static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
{ return IRQ_WAKE_THREAD; }
// kernel/irq/manage.c:1141 与 :1158(线程侧两种执行器)
static irqreturn_t irq_thread_fn(struct irq_desc *desc, struct irqaction *action)
{ irqreturn_t ret = action->thread_fn(action->irq, action->dev_id); ... }
static irqreturn_t irq_forced_thread_fn(struct irq_desc *desc, struct irqaction *action)
{ /* 硬中断上下文强制线程化时: 原子区包装 */ }
两段协议:
[1] 硬侧 handler (可以只是缺省的
irq_default_primary_handler: 永远回答
IRQ_WAKE_THREAD):
应答硬件 → (IRQF_ONESHOT 时)屏蔽本线
→ 唤醒 irq/<n>-name 线程
[2] 线程侧 thread_fn:
进程上下文跑真正处理 (可睡眠!)
返回 IRQ_HANDLED → 内核重新使能中断线
ONESHOT 的意义: 电平触发 + 处理期间必须屏蔽 —
否则"处理完之前线还平"会立即再中断 (风暴)
IRQF_ONESHOT 保证"处理完成 → unmask"的顺序
风暴防护: 线程版的重入抑制 — thread_fn 还没跑完
新中断只会置位待定标志, 不会堆叠线程
42.4.2 与 RT、与驱动的选型
为什么 RT 内核全量线程化:
硬中断不可抢占 → 它的执行时间直接计入系统最坏
延迟; 全部 handler 线程化后, 中断延迟只剩
"屏蔽+唤醒线程", 处理时长受调度器管理 —
PREEMPT_RT 的确定性来源之一
(forced threading: irq_forced_thread_fn :1158 把
拒绝线程化的老驱动也强制按线程跑, 原子区包裹)
驱动选型 (42 章总表的落点):
I2C/SPI 设备 (寄存器读取本身要睡眠):
必须 threaded: handler 只回 WAKE_THREAD,
thread_fn 里 i2c_read...
→ i2c_smbus 系列在原子区调用 = BUG
高频低延迟 (NVMe 完成):
硬中断 + 简单处理 (41.2 节完成路径)
复杂但原子可完成的 (网卡收包):
硬中断 + softirq NAPI (42.2/44.3)
接口细节:irq_default_primary_handler(:997)的存在使"I2C 设备驱动只传 thread_fn 不传 handler"合法;request_irq 宏(interrupt.h:213 区)就是 handler + NULL 的 threaded 变体。线程的调度属性可用 irq_set_irq_type/thread-> 相关接口调(RT 上常配 SCHED_FIFO)。
小结
线程化中断以"硬侧应答+屏蔽(缺省 handler 永答 WAKE_THREAD)、线程侧处理、ONESHOT 保证处理完再使能"的两段协议,把中断处理搬进进程上下文——慢总线(I2C/SPI)的正确姿势、RT 系统确定性(forced threading 兜底老驱动)的基石。它与 workqueue 的分界是"每中断一线程、与屏蔽/unmask 强耦合"——处理粒度更细、延迟路径更短。下一节离开软件面,看三架构的中断控制器硬件层。
42.5 GIC 与 RISC-V PLIC/APLIC 中断控制器
中断从设备到 CPU 要经过中断控制器的路由与优先级裁决:ARM64 的 GICv3 (+ITS)、RISC-V 的 PLIC/APLIC、x86 的 APIC。内核以 irqchip 框架(drivers/irqchip/)把控制器抽象为域树(root domain → 级联 domain),设备中断经 DT/ACPI 的 phandle 绑定到具体域。本节对照三架构的控制器与它们的 MSI 变体。
42.5.1 irqchip 框架与域树
irqchip 的三层抽象 (kernel/irq/ + drivers/irqchip/):
irq_domain: 一个控制器的"中断号命名空间"
(设备的 hwirq → Linux 虚拟 irq 的映射表)
级联: 子控制器的一条输出线挂在父域上,
父线的 handler 做分发 (generic_handle_irq_desc,
42.1.3 节) → 逐级下钻到具体设备中断
DT 绑定: 设备节点 interrupts = <&gic SPI 5 ...>
phandle 指明挂在哪个域 (39.2 节 DT 属性的
中断面)
驱动可见的统一入口:
gpiod_to_irq()/platform_get_irq() → 虚拟 irq
request_irq (42.1 节) — 控制器差异被域树吸收
42.5.2 ARM64:GICv3 与 ITS
GICv3 的层级:
SPI (共享外设中断): 传统线式, GICD 分发器裁决
PPI (每 CPU 中断): 本地定时器/IPI
LPI (基于消息): ITS (Interrupt Translation Service)
设备写 MSI 地址 → 内存中的转换表 → LPI 路由
(MSI: 中断即内存写 — 无需每设备一条线,
大规模设备/虚拟化的基础, 49 章 KVM 复用)
drivers/irqchip/irq-gic-v3.c + irq-gic-v3-its.c:
ITS 的设备表/集合表/.pending 表三级翻译,
设备首次分配 MSI 时建立表项
CPU 侧: gic_handle_irq 读 IAR 寄存器认领中断号
→ generic_handle_domain_irq → 域树下钻
42.5.3 RISC-V:PLIC 与 APLIC;x86:APIC
RISC-V 双代控制器:
PLIC (旧): 平台级中断控制器
所有设备线 → PLIC 优先级矩阵 → 每 hart 一个上下文
处理: 认领(claim)=读 claim 寄存器拿最高优先源,
处理完写 complete
drivers/irqchip/ 的 sifive-plic 系列
APLIC/IMSIC (新, AIA 规范): 消息化
IMSIC ≈ RISC-V 版 ITS: 设备写内存地址投递 MSI
与 42.5.2 的 LPI 同构 — "中断消息化"是共同的
演进方向
x86 对照: Local APIC (每核, IPI/定时器) + IOAPIC
(线式路由) + MSI/MSI-X (消息化, 41 章 NVMe 的
中断就是 MSI-X) — 1.4 节 IDT 的实设备层
IPI(处理器间中断)是 SMP 的神经:调度器踢远端 CPU(13.7 节迁移)、TLB 广播(19.5 节 flush_tlb_multi)、RCU 催促(17 章 expedited)全走 IPI;GIC 的 SGI/PLIC 的软件中断/APIC 的 IPI 寄存器是三架构各自的机制名。虚拟化的交集:49 章 KVM 的虚拟中断注入直接复用 ITS/IMSIC 的消息模型——中断控制器因此也是虚拟化硬件的底座。
小结
三架构控制器同构而名异:线式(GIC SPI/PLIC/IOAPIC)以优先级矩阵与认领-完成协议路由,消息式(ITS-LPI/IMSIC/MSI-X)以内存写投递并天然支持大规模与虚拟化;内核 irqchip 框架以域树(hwirq→虚拟 irq 映射+级联分发)吸收全部差异,驱动只见 platform_get_irq + request_irq 的统一面。IPI 子系统承载调度/TLB/RCU 的跨核协作。设备驱动章至此完成;下一章进入 DMA 与 IOMMU——设备访问内存的正规通道。