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——设备访问内存的正规通道。