PCIe 和 USB 都能枚举设备,但给驱动的控制方式不同。PCI 驱动通常直接操作 BAR 寄存器并建立 DMA 队列;USB 接口驱动把请求提交给 USB 核心,由主机控制器执行总线事务。两者共同的难点是:设备和请求可以异步消失,资源必须在最后一个使用者退出后释放。
1. PCI:配置空间、BAR 与 DMA 是三件事
配置空间提供设备标识、能力和资源窗口信息。系统扫描拓扑并保留或分配可用资源;固件仍可描述根桥、配置空间窗口等平台信息。完整地址例如 0000:03:00.0,跨多个 domain 时只写 bus:device.function 不足以唯一定位。
| 接口 | 用途 | 不应混淆的对象 |
| 配置空间 | 身份、能力、BAR 配置 | 普通设备业务寄存器 |
| BAR 资源 | 寄存器或设备内存的 MMIO / I/O 窗口 | 主存 DMA 缓冲区 |
| DMA 地址 | 设备读取或写入主存时使用的地址 | CPU 虚拟指针 |
普通 endpoint 配置头有六个 BAR 槽位,64 位 BAR 占相邻两个槽,桥的布局又不同。因此不能把内核 pci_dev.resource 伪写成固定六项并据此遍历所有资源。通过 pci_resource_start/len/flags() 等 API 解释资源,并使用适当的映射和 I/O 访问器。
pci_iomap() 一类映射返回的 __iomem 地址不能当普通 RAM 随意解引用或 memcpy。使用 ioread32() / iowrite32() 等与设备协议匹配的访问方式;MMIO 顺序、posted write 的完成要求还要查硬件及平台规则。
2. probe 的资源依赖
典型初始化包括使能设备、申请所需 BAR、设置硬件实际支持的 DMA mask、映射寄存器、分配队列和中断,然后注册对外接口。具体顺序受硬件复位、固件加载及中断屏蔽要求影响,不能把一份通用调用列表直接当可运行驱动。
启用 bus mastering 允许设备发起事务,应在缓冲区和设备状态准备妥当的阶段处理。对外发布接口之前,错误返回路径必须能逆序撤销已取得的资源。新驱动通常用 dev_pm_ops 表达电源管理,而不是照抄旧版本 pci_driver 的 suspend/resume 字段。
PCI 驱动文档解释了初始化与资源接口;编写时应对照目标内核头文件,保持 managed/unmanaged 资源的释放责任一致。
3. MSI/MSI-X:申请上限不是返回数量
向量预算:按实际返回数量建立后续资源
MSI/MSI-X 用设备发出的消息触发中断。MSI-X 表最多支持 2048 项,但设备实现、平台及可用资源会限制实际数量。请求 1 到 16 个向量可能只得到 1、4 或 8 个。
下面是初始化阶段的局部示例,并非完整驱动:
int nvec = pci_alloc_irq_vectors(pdev, 1, 16, PCI_IRQ_MSIX);
if (nvec < 0)
return nvec;
for (int i = 0; i < nvec; ++i) {
int irq = pci_irq_vector(pdev, i);
dev_dbg(&pdev->dev, "vector %d uses IRQ %d\n", i, irq);
}
i 是设备的向量索引,irq 是 Linux IRQ 号;两者不能互换。向量数量也不等于队列或 CPU 数量:多个队列可以共享中断,亲和性决定路由而不是永久绑定某个工作线程。
拆除时先阻止设备继续产生相关中断,正确同步处理函数,再按选用 API 的约定释放处理函数和向量。部分 managed PCI 接口具有自动清理行为,不能同时无条件手工释放。见 MSI 驱动指南。
八个队列只拿到三个向量,怎样分配
假定设备允许多个完成队列路由到一个 MSI-X 向量。我们选用一项明确的驱动策略:有两个以上向量时,把索引 0 留给管理事件,其余向量轮流承接数据队列;只有一个向量时,管理事件与所有数据队列共用索引 0。是否支持这种路由必须由硬件确认,不能由操作系统返回数量推断。
| 实际向量数 | 管理事件 | 数据队列 0~7 对应的向量索引 |
| 1 | 0 | 0、0、0、0、0、0、0、0 |
| 3 | 0 | 1、2、1、2、1、2、1、2 |
| 5 | 0 | 1、2、3、4、1、2、3、4 |
def route_queues(queue_count, linux_irqs):
if queue_count < 0 or not linux_irqs or len(set(linux_irqs)) != len(linux_irqs):
raise ValueError("需要有效队列数与不重复的向量 IRQ 映射")
nvec = len(linux_irqs)
indices = [0 if nvec == 1 else 1 + q % (nvec - 1)
for q in range(queue_count)]
groups = {i: [] for i in range(nvec)}
for q, index in enumerate(indices):
groups[index].append(q)
return indices, groups
indices, groups = route_queues(8, [48, 113, 207])
assert indices == [1, 2, 1, 2, 1, 2, 1, 2]
assert groups == {0: [], 1: [0, 2, 4, 6], 2: [1, 3, 5, 7]}
for nvec in (1, 3, 5):
for count in (0, 1, 8, 17):
indices, groups = route_queues(count, list(range(100, 100 + nvec)))
assert sorted(q for group in groups.values() for q in group) == list(range(count))
assert all(0 <= i < nvec for i in indices)
assert route_queues(8, [48])[0] == [0] * 8
这里 IRQ 113 的处理函数要检查队列 0、2、4、6 的完成状态,不能用“收到一次中断”代替“恰好完成一个请求”。中断合并、多个完成条目以及队列继续产生工作,都要求按设备队列协议推进消费位置。共享一个设备向量也不等于使用传统 INTx 的共享线路模式,两种“共享”应分开理解。
初始化失败也要按实际建立的资源回滚。若已分配三个向量,但第三个 request_irq() 失败,就只释放前两个已安装的处理函数,随后释放向量;队列和 DMA 缓冲仍按自己的取得顺序处理。进入正常运行后,除了同步硬中断,还要排空处理函数已经派生的工作,否则释放 IRQ 并不能证明工作项不再访问队列。
4. USB:驱动常绑定 interface
一个 USB device 可以提供多个 configuration,选中的 configuration 包含若干 interface;interface 还有 alternate settings,每个设置描述一组 endpoint。摄像头的音频与视频功能因此可以由不同接口驱动处理。
常见 usb_driver.probe() 接收的是 usb_interface *。驱动应检查所绑定接口当前设置的 endpoint,而不是假设“第一个 endpoint 永远是 bulk IN”。端点零是设备的默认控制端点,不应套用成每个 interface 独有的端点。
| 传输类型 | 适合的任务 | 服务特征 |
| Control | 标准请求和配置命令 | 包含 setup 等阶段 |
| Bulk | 存储及大块数据 | 可靠传输,无固定带宽/延迟保证 |
| Interrupt | 键盘等小量周期数据 | 主机按服务间隔调度 |
| Isochronous | 音视频流 | 按时服务,出错通常不重传 |
这里的 interrupt 不是外设任意时刻直接打断 CPU。USB 总线传输由主机调度,HCD 的 CPU 中断是另一层机制。端口连接检测随 USB 速率与协议不同,也不能统一解释为 D+ 电平变化。
5. URB 是异步请求,不是数据已经发送的证明
| usb_submit_urb 结果 | 所有权与后续责任 |
| 返回错误 | 请求未成功交给核心,调用方处理错误和资源 |
| 返回 0 | 请求已接受,缓冲区与上下文必须活到完成 |
提交前先准备好端点、缓冲区和回调上下文。提交成功不意味着设备已经结束使用数据。
提交成功的请求可能很快完成,回调甚至可能在 usb_submit_urb() 返回之前发生;因此回调会使用的上下文必须在调用提交接口前准备好。图中“接受请求”发生在提交调用期间,不能把函数返回当成回调开始的屏障。
先取得回调引用,再提交请求
驱动私有请求对象与 URB 本身是两个对象。USB 核心对 URB 的管理不会自动为任意 urb->context 指针取得引用。下面用一个单次请求说明配对方式:调用者先持有一份私有对象引用,提交前再预留一份给完成路径。成功提交后由回调释放这份引用;提交失败、不产生此次完成回调时,由提交方撤销预留。
class Request:
def __init__(self):
self.refs = 1
self.ready = True
self.callback_owned = False
self.completions = 0
self.freed = False
def put(self):
assert self.refs > 0 and not self.freed
self.refs -= 1
self.freed = self.refs == 0
def reserve_callback(self):
assert self.ready and not self.freed and not self.callback_owned
self.refs += 1
self.callback_owned = True
def finish(self):
assert self.ready and self.callback_owned and not self.freed
self.completions += 1
self.callback_owned = False
self.put()
def rollback_failed_submit(self):
assert self.callback_owned and self.completions == 0
self.callback_owned = False
self.put()
def one_request(mode):
r = Request()
r.reserve_callback() if mode == "failure":
result = -1 else:
result = 0
if mode == "early":
r.finish() if result:
r.rollback_failed_submit()
assert not r.freed r.put() if mode == "late":
assert not r.freed r.finish()
assert r.freed and r.refs == 0
assert r.completions == (mode != "failure")
for mode in ("failure", "early", "late"):
one_request(mode)
这段模型只核算单次提交的私有对象寿命;真实并发代码还需要合适的原子引用、状态同步,以及独立的 URB/缓冲区管理。如果回调重新提交,必须为新一次请求建立完整的交接,而不能把旧请求完成误认为整个会话结束。单独保存 submitted = true 并在提交返回后才增加引用,在提前完成路径上已经太晚。
状态成功、字节完整、业务完整要分别判断
下面讨论 bulk IN 等非等时传输。请求读 4096 字节,成功返回 512 字节可以是合法短读;如果上层协议要求一个固定 4096 字节记录,这次 USB 成功仍不代表记录完整。反过来,一次 URB 也可能承载多个业务记录,记录边界需要协议解析器维护。
| 完成结果 | 可以确定的事实 | 上层还要决定什么 |
| status=0,actual_length=4096 | 本次实际收到 4096 字节 | 内容、校验及记录边界是否有效 |
| status=0,actual_length=512 | 未把短读作为错误,收到 512 字节 | 一条短响应,还是需要继续拼接 |
| status 为错误,actual_length=512 | 出错前可能已有部分数据 | 丢弃、保留前缀或重新同步协议 |
| actual_length=0 | 本次没有数据字节 | 协议是否允许空响应,不能直接等同文件 EOF |
非等时读取可用 URB_SHORT_NOT_OK 要求把短读报告为错误,但它不会补齐剩余字节,也不会验证业务记录。等时传输则要逐项查看 iso_frame_desc 的状态和长度,不能只用 URB 总体状态推断所有帧成功。字段含义见 USB Host Side API。
完成时按顺序检查:
- 用
urb->status 判断结果。
- 用
actual_length 判断实际长度,成功不保证一定等于请求长度。
- 需要重提时,先确认没有进入断开、关闭或暂停状态。
回调处于不可睡眠上下文,复杂阻塞工作应交给合适工作机制;否则退出过程中可能重新启动传输。
usb_unlink_urb() 请求异步取消,不能据此立即释放仍可能被回调访问的对象。
usb_kill_urb() 等待请求退出及完成回调结束,调用位置必须允许睡眠,且不能持有回调需要的锁。
- USB anchor 可跟踪一组 URB,便于停止成组在途请求,但仍需管理驱动自己的打开文件、工作项和引用。
接口规则和取消语义详见 URB 文档。
在请求飞行或回调执行期间尝试释放 buffer。取消请求、回调退出与释放资格分别显示;再加入一个回调需要的锁,观察同步等待为何可能卡住。
正在呈现知识画面
请求取消以后,谁还可能使用缓冲区?usb_unlink_urb 是取消请求,不能替代完成回调结束。同步 kill 等待请求和回调退出,等待时不能持有回调所需锁。
{"id":"urb-cancellation-lifetime","title":"请求取消以后,谁还可能使用缓冲区?","summary":"usb_unlink_urb 是取消请求,不能替代完成回调结束。同步 kill 等待请求和回调退出,等待时不能持有回调所需锁。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"submit\" data-i18n=\"submit\"></button><button id=\"cancel\" data-i18n=\"cancel\"></button></div><div class=\"actions\"><button id=\"enter\" data-i18n=\"enter\"></button><button id=\"leave\" data-i18n=\"leave\"></button></div><label class=\"check\"><input type=\"checkbox\" id=\"lock\"><span data-i18n=\"lock\"></span></label><div class=\"actions\"><button id=\"kill\" data-i18n=\"kill\"></button><button id=\"free\" data-i18n=\"free\"></button></div><div class=\"track-row\"><span data-i18n=\"state\"></span><div id=\"state\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"buffer\"></span><div id=\"buffer\" class=\"track\"></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"blocked\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"deadlock\"></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","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 phase=0,cancelled=false,alive=true,message='note';function draw(){cells('state',[{text:t(['idle',cancelled?'cancelled':'flight','callback','done'][phase]),cls:phase===1||phase===2?'gold':'on'}]);cells('buffer',[{text:t(alive?'alive':'freed'),cls:alive?'on':'empty'}]);$('#submit').disabled=phase!==0||!alive;$('#cancel').disabled=phase!==1||cancelled;$('#enter').disabled=phase!==1;$('#leave').disabled=phase!==2||$('#lock').checked;$('#kill').disabled=phase===0||phase===3;$('#free').disabled=!alive;set('explanation',t(message));}$('#submit').onclick=()=>{phase=1;draw();};$('#cancel').onclick=()=>{cancelled=true;draw();};$('#enter').onclick=()=>{phase=2;draw();};$('#leave').onclick=()=>{phase=3;message='note';draw();};$('#kill').onclick=()=>{if($('#lock').checked)message='deadlock';else{cancelled=true;phase=3;message='note';}draw();};$('#free').onclick=()=>{if(phase===1||phase===2)message='blocked';else{alive=false;message='note';}draw();};$('#lock').onchange=draw;$('#reset').onclick=()=>{phase=0;cancelled=false;alive=true;message='note';$('#lock').checked=false;draw();};draw();","audio":false,"strings":{"alive":"仍保留","blocked":"仍有设备或回调使用者,缓冲区保持有效。","buffer":"缓冲区寿命","callback":"回调进行中","cancel":"请求异步取消","cancelled":"已请求取消,仍待完成","deadlock":"循环等待:kill 等回调,回调等 L,调用者持有 L。先解除这种依赖,不能靠同步等待自动修复。","done":"回调已退出","enter":"开始完成回调","flight":"已提交,设备仍可使用","free":"尝试释放缓冲区","freed":"已释放","heading":"请求取消以后,谁还可能使用缓冲区?","idle":"未提交","intro":"提交 URB 后请求异步取消,再分别进入和退出回调。可以随时尝试释放,模型会保留未满足寿命条件的资源。","kill":"同步 kill 并等待","leave":"完成回调退出","lock":"调用者持有回调所需锁 L","note":"异步取消之后,释放仍被拒绝。等回调退出再释放,才跨过最后使用者的边界。","reset":"重置","scope":"一个成功提交的 URB,无重新提交。接受的取消在此模型产生取消完成,不枚举与正常完成竞争的所有返回值。同步 kill 按钮在满足条件时折叠完成等待;持锁时只报告循环等待,不真的阻塞浏览器。图中拆开逻辑事件,不承诺回调晚于 submit 返回。","scopeLabel":"模型范围与约定","state":"请求 / 回调状态","submit":"提交 URB"}}
6. 热拔插与电源管理都要求先停止请求
disconnect/remove 应先标记离线并阻止新请求,再撤销异步请求、等待回调、注销功能接口并释放资源。用户可能仍持有设备文件;后续操作应看到合适的错误,而不是访问已释放私有数据。物理设备已经拔掉,也意味着不能依赖“再写一次寄存器一定成功”完成清理。
提交引用:空闲挂起与系统 suspend 分开处理
runtime PM 处理空闲期间的挂起和恢复,系统 suspend 则协调整机状态。提交 I/O 前取得正确的 PM 使用引用,失败时处理返回值;传输完成后按约定释放引用。USB 的自动挂起需要接口驱动与核心协作,PCI 也可能在恢复后重建设备状态。参见电源管理。
继续阅读
- 设备模型:发现、匹配与对象引用。
- DMA API:PCI 队列背后的地址和缓冲区交接。
- 内核源码入口:
drivers/pci/、drivers/usb/core/、include/linux/usb.h;实际驱动的错误回滚和 remove 路径应与 probe 一起阅读。