设备不断报中断,屏蔽后 DMA 却仍在写内存。这里要分别追踪通知、设备状态和后台工作。
| 查阅目标 | 阅读位置 |
| 编号与分发 | 第 1~3 节 |
| 执行上下文与延后工作 | 第 4~5 节 |
| 停止与有效亲和性 | 第 6~7 节 |
中断是设备向 CPU 通知事件的一种机制。通知可能对应多个已完成请求,一个请求也可能涉及多个通知,因此驱动最终应检查设备的状态、队列或完成记录,而不是把中断计数直接当成业务完成数。
1. 三种编号与四个对象
| 概念 | 职责 | 容易混淆的对象 |
| hwirq | 某个中断域内的硬件编号 | 不是整个系统唯一编号 |
| Linux IRQ | 内核管理 IRQ 的逻辑标识 | 不等于 x86 CPU vector |
| CPU vector / 架构中断 ID | 架构入口识别和路由 | 具体含义依 APIC、GIC 等机制 |
| irq_domain | 映射并管理硬件域与 Linux IRQ 关系 | 可分层,不是简单加一个固定偏移 |
| irq_chip | 控制器操作,如 mask、ack、EOI、affinity | 不等于驱动处理设备业务的 handler |
| irq_desc | IRQ 状态、控制流、统计及 action 等 | 一个 IRQ 可关联多个共享 action |
| irqaction | 驱动注册的处理函数、参数与线程信息 | 不一定一项就对应一条独占物理线 |
一个 GPIO 控制器可作为另一个中断控制器的下级,中断域因而可能构成层次。PCI MSI 也可经过 MSI domain、重映射与 CPU 路由等多层管理。驱动通常取得子系统提供的 Linux IRQ 后注册处理函数,不应猜测硬件编号。见 irq_domain 文档。
2. 事件分发与确认
架构入口保存必要现场并建立内核处理环境,具体栈切换与保存寄存器规则依架构和入口类型不同。不能把 syscall 入口、x86 外部中断和 ARM 异常处理写成同一个固定函数链。
generic IRQ 层调用为该 IRQ 安装的 flow handler,再调用 irq_chip 与驱动 action。generic_handle_irq_desc() 是通用分发相关接口,不应与 handle_edge_irq()、handle_fasteoi_irq() 并排画成三种触发类型。
mask、ack、EOI 与设备状态
| 操作 | 处理哪一层状态 |
| mask | 是否继续递交通知 |
| ack | 控制器的识别/应答 |
| EOI | 告知控制器相关服务阶段结束 |
| 清设备状态、消费完成队列 | 驱动按设备协议处理真正的事件来源 |
控制器确认与设备业务完成分别发生,不能用其中一个替代另一个。
电平源若仍保持有效,解除屏蔽后可以再次通知;边沿源则要考虑通知是否锁存与处理期间新的边沿。不同 flow handler 为这些约束组织操作,fasteoi 也不能一概写成“先 mask,处理,最后 unmask”——它的核心是相应的 EOI 协议,并依状态、oneshot 等条件处理屏蔽。
向控制器发送 EOI 不自动清除设备的中断原因,屏蔽 IRQ 也不自动停止 DMA。两层弄反可能导致中断风暴或释放后的 DMA 写入。参见 generic IRQ。
分别操作设备源、控制器服务状态和屏蔽位。只发送 EOI 而不清除电平源,再次递送仍可能发生;不需要人为制造一个新设备事件。
正在呈现知识画面
EOI 结束服务,却不清除设备原因设备原因、控制器 in-service 和屏蔽状态是不同对象。EOI 不清设备原因,mask 不停止 DMA 或清除业务队列。
{"id":"irq-source-and-eoi","title":"EOI 结束服务,却不清除设备原因","summary":"设备原因、控制器 in-service 和屏蔽状态是不同对象。EOI 不清设备原因,mask 不停止 DMA 或清除业务队列。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"event\" data-i18n=\"event\"></button><button id=\"clear\" data-i18n=\"clear\"></button></div><label class=\"check\"><input type=\"checkbox\" id=\"masked\"><span data-i18n=\"masked\"></span></label><div class=\"track-row\"><span data-i18n=\"source\"></span><div id=\"source\" class=\"track\"></div></div><div class=\"wire\" id=\"wire\"></div><div class=\"track-row\"><span data-i18n=\"controller\"></span><div id=\"controller\" class=\"track\"></div></div><div class=\"actions\"><button id=\"deliver\" data-i18n=\"deliver\"></button><button id=\"eoi\" data-i18n=\"eoi\"></button></div><div class=\"metrics\"><div><span data-i18n=\"count\"></span><strong id=\"count\"></strong></div><div><span data-i18n=\"eligible\"></span><strong id=\"eligible\"></strong></div></div><div class=\"formula\" id=\"condition\"></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.wire{height:40px;width:2px;background:var(--rule);margin:auto;position:relative}.wire:after{content:\"▼\";position:absolute;bottom:-5px;left:-6px;color:var(--rule);font-size:14px}.wire.on,.wire.on:after{background:var(--accent);color:var(--accent)}.formula{min-height:4.8em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let level=false,service=false,count=0;function draw(){const mask=$('#masked').checked,ok=level&&!mask&&!service;cells('source',[{text:'level = '+Number(level),cls:level?'gold':'empty'}]);cells('controller',[{text:'in_service = '+Number(service),cls:service?'on':'empty'}]);$('#wire').classList.toggle('on',ok);$('#deliver').disabled=!ok;$('#eoi').disabled=!service;$('#clear').disabled=!level;set('count',count);set('eligible',t(ok?'yes':'no'));set('condition',Number(level)+' ∧ ¬'+Number(mask)+' ∧ ¬'+Number(service)+' = '+Number(ok));set('explanation',t('note'));}$('#event').onclick=()=>{level=true;draw();};$('#clear').onclick=()=>{level=false;draw();};$('#deliver').onclick=()=>{if(level&&!service&&!$('#masked').checked){service=true;count++;}draw();};$('#eoi').onclick=()=>{service=false;draw();};$('#masked').onchange=draw;$('#reset').onclick=()=>{level=service=false;count=0;$('#masked').checked=false;draw();};draw();","audio":false,"strings":{"clear":"清除设备原因","controller":"控制器服务状态","count":"累计递送次数","deliver":"递送到 CPU","eligible":"当前可递送","eoi":"发送 EOI","event":"使设备原因置位","heading":"EOI 结束服务,却不清除设备原因","intro":"触发设备电平,尝试递送,再分别清设备状态或发送 EOI。保留设备原因时,观察为何没有新事件也能再次递送。","masked":"屏蔽这个 IRQ","no":"否","note":"条件是 level ∧ ¬masked ∧ ¬in_service。EOI 后若 level 仍为 1,再按递送会增加计数;清设备原因则移除再次通知的来源。","reset":"重置","route":"递送条件","scope":"一个抽象电平触发源,无共享线和并发。递送由按钮显式触发,真实硬件可自动推进。重复事件保持同一原因位,不按事件计数。省略架构 ack/优先级/oneshot 协议,不作为 MSI 或所有 IRQ flow 的通用顺序。","scopeLabel":"模型范围与约定","source":"设备原因 / 电平","yes":"是"}}
3. 共享 IRQ 与返回值
共享线上的 handler 要确认事件是否属于自己。dev_id 标识对应注册项及私有上下文,注销时也要匹配;不能在无法识别来源的情况下无条件宣称处理中断成功。
| 返回值 | 含义 |
| IRQ_NONE | 此 handler 未确认处理属于自己的事件 |
| IRQ_HANDLED | 已处理自己的中断事件 |
| IRQ_WAKE_THREAD | 请求执行注册的 thread_fn |
IRQ_WAKE_THREAD 是返回值,不是名为 IRQF_WAKE_THREAD 的注册标志。持续无人认领的 IRQ 可能触发内核异常统计与处置,但阈值和路径依实现,不能写死“恰好十万次就一定关闭”。
MSI 通常避免传统共享线的来源识别问题,但队列合并、中断聚合与完成处理仍然存在。pci_alloc_irq_vectors() 返回实际分配数量,随后使用 pci_irq_vector() 取得每项 IRQ;它既不固定返回申请上限,也不直接返回第一个 IRQ。详见PCIe 与 USB 驱动模型。
4. Primary handler 与 threaded IRQ
request_threaded_irq() 可注册 primary handler 和 thread_fn。primary handler 在真正硬中断路径中应保持简短且不可睡眠;需要后续线程处理时返回 IRQ_WAKE_THREAD。
IRQF_ONESHOT 使核心按约定在线程处理期间保持 IRQ 屏蔽,避免相关处理尚未完成就再次递交。它不保证一个硬件事件恰好对应一次线程唤醒,线程通常仍要排空设备状态或队列。
在满足约束时,可以使用默认 primary handler 配合 thread_fn;共享线则尤其需要正确识别与协调来源。PREEMPT_RT 的强制线程化和例外会改变实际上下文,不能据进程名或 API 名就推断所有 handler 都可睡眠。
IRQ 线程可以执行适当睡眠操作,但不能在持有 raw spinlock 或依赖被自己阻断的路径时等待。线程化降低硬中断段长度,不自动修复锁顺序和资源寿命。
5. softirq、tasklet 与 workqueue
| 机制 | 执行特征 | 设计边界 |
| softirq | 固定类别、按 CPU 挂起和执行,可能由 ksoftirqd 处理 | 普通 softirq 回调不可随意睡眠,可跨 CPU 并发 |
| tasklet | 基于 softirq 的旧式延后机制 | 同一个 tasklet 实例不会同时执行,不是所有同类 tasklet 互斥 |
| 普通 workqueue | worker pool 提供进程上下文 | 可按规则睡眠,专用 queue 不等于固定独占线程 |
| WQ_BH workqueue | 支持该接口的内核中使用 BH/softirq 上下文 | 不可照搬普通 workqueue 的睡眠假设 |
| IRQ thread | 与 IRQ action 关联的内核线程 | 处理设备协议及 oneshot/共享等约束 |
ksoftirqd 的存在不使 softirq callback 变成可以任意拿 mutex 的普通工作函数。tasklet 仍存在于一些内核路径,新实现通常应选择更合适的线程化、NAPI 或 workqueue 机制,不能简单声称它已从内核删除。
十次通知,为什么不一定运行十次 work
同一 work 已经 pending 时,queue_work() 可能返回 false。它保存的是“这个工作需要执行”的调度状态,不是十条独立业务事件。
若十次 IRQ 都安排同一个 work,事件数量必须由其他队列/计数保存,或由 work 排空设备可查询的完成状态。这样即使调度通知合并,业务事实仍不会只剩一份。
多个 work 的并行、顺序和内存回收上下文由 queue flags、pool 和配置决定;需要严格顺序时使用相应 ordered 接口。见 workqueue 文档。
6. 退出路径:先阻止新活动,再等旧活动
退出时按依赖逐步关闭活动来源:
| 阶段 | 要建立的条件 |
| 停止新提交 | 不再产生新业务 |
| 控制设备源与 DMA | 硬件不再产生危险访问 |
| 注销/同步 IRQ | 相应 handler 不再使用资源 |
| 取消或等待后台工作 | 先前排入的 callback 不再访问上下文 |
| 释放资源 | 前述使用者已退出 |
实际顺序受驱动依赖影响,尤其要防止某个 callback 重新启动已停止资源。
disable_irq_nosync() 不等待正在执行的 handler;disable_irq() 与 synchronize_irq() 等等待接口又不能在持有 handler 所需锁时调用。synchronize_irq() 本身不是永远禁止后续新 IRQ 的措施。
free_irq() 等待并撤销相应处理注册,但不会自动停止设备硬件,也不自动取消此前排入的 work。共享 IRQ 还不能随意通过禁用整条 IRQ 影响其他设备;应优先正确控制自己的设备源。
7. 亲和性与只读观察
/proc/irq/N/smp_affinity_list 表达请求的 CPU 集合,effective_affinity_list 在支持时显示有效路由。managed IRQ、控制器能力、CPU hotplug、irqbalance 和驱动策略都可能影响实际结果。
from pathlib import Path
for irq in sorted(Path('/proc/irq').glob('[0-9]*'), key=lambda p: int(p.name))[:12]:
values = {}
for field in ('smp_affinity_list', 'effective_affinity_list'):
try:
values[field] = (irq / field).read_text().strip()
except OSError:
pass
print(irq.name, values)
/proc/interrupts 还包含某些架构特殊事件行,并非每一行都是普通可重新分配 affinity 的设备 IRQ。计数增长集中在一个 CPU 不一定是错误:需结合队列映射、NUMA、NAPI 与业务负载判断。