4. URB 生命周期管理:分配、填充、提交、取消与回收
URB(USB Request Block)是 Linux USB 子系统中最核心的数据结构,它承载了主机与 USB 设备之间每一次数据传输的完整上下文。理解 URB 从创建到销毁的完整生命周期,是编写稳定、高性能 USB 驱动的基础。本章将深入剖析 URB 的五个关键阶段:分配、填充、提交、取消与回收。
核心原则:URB 的生命周期遵循“谁分配,谁回收”的原则。驱动开发者必须确保 URB 在提交后不会被意外释放,且在完成回调中正确处理 URB 的后续状态。
4.1 URB 的分配
URB 的分配是生命周期的起点。驱动必须使用内核提供的专用 API 来创建 URB 实例,而不能直接使用 kmalloc。这是因为 URB 结构体内部包含引用计数、链表节点等需要特殊初始化的字段。
常用的分配函数如下:
| 函数 | 说明 | 适用场景 |
|---|---|---|
usb_alloc_urb(int iso_packets, gfp_t mem_flags) |
分配一个 URB 结构体,iso_packets 用于等时传输,批量/中断传输设为 0 |
通用分配,推荐使用 |
usb_get_urb(struct urb *urb) |
增加 URB 的引用计数,防止 URB 在回调中被意外释放 | 需要延长 URB 生命周期时 |
usb_free_urb(struct urb *urb) |
减少引用计数,当计数为 0 时释放 URB 内存 | 回收阶段使用 |
性能提示:在批量传输的高频场景中,建议在驱动初始化时预分配 URB 池(URB Pool),避免在热路径上反复调用
usb_alloc_urb 和 usb_free_urb,从而减少内存分配带来的延迟抖动。
/* URB 分配示例 */
struct urb *urb;
urb = usb_alloc_urb(0, GFP_KERNEL);
if (!urb) {
dev_err(&interface->dev, "Failed to allocate URB\n");
return -ENOMEM;
}
4.2 URB 的填充
分配完成后,驱动需要填充 URB 的各个字段,以描述本次传输的详细信息。填充过程是驱动与 USB 核心之间约定传输协议的步骤。
对于批量传输,通常使用 usb_fill_bulk_urb 辅助函数:
/* 批量传输 URB 填充示例 */
usb_fill_bulk_urb(urb, // URB 指针
dev, // USB 设备结构体
usb_sndbulkpipe(dev, endpoint), // 批量输出端点
buffer, // 数据缓冲区
buffer_length, // 数据长度
complete_callback, // 完成回调函数
context); // 回调上下文参数
URB 填充阶段需要关注的关键字段:
- transfer_buffer:指向 DMA 安全的数据缓冲区,建议使用
usb_alloc_coherent分配。 - transfer_buffer_length:传输数据的字节数,批量传输最大为端点 wMaxPacketSize 的倍数。
- complete:完成回调函数指针,URB 处理结束后内核会调用此函数。
- context:传递给回调函数的私有数据,通常指向驱动定义的请求结构体。
- interval:批量传输不使用此字段,中断传输时指定轮询间隔。
常见错误:填充 URB 时未正确设置
transfer_flags。例如,如果缓冲区是使用 kmalloc 分配的普通内存,必须设置 URB_NO_TRANSFER_DMA_MAP 标志,否则 USB 核心会尝试对缓冲区进行 DMA 映射,导致数据损坏或内核崩溃。
4.3 URB 的提交
URB 填充完毕后,通过 usb_submit_urb 函数提交给 USB 核心层。提交操作将 URB 放入主机控制器驱动(HCD)的调度队列中,等待硬件处理。
/* URB 提交示例 */
ret = usb_submit_urb(urb, GFP_KERNEL);
if (ret) {
dev_err(&interface->dev, "URB submission failed, error %d\n", ret);
usb_free_urb(urb);
return ret;
}
提交阶段的关键行为:
- 原子性:
usb_submit_urb在成功后将 URB 标记为“已提交”状态,此时 URB 归 USB 核心管理。 - 内存标志:提交时的
gfp_t标志决定了内核在提交过程中是否允许睡眠。在中断上下文中必须使用GFP_ATOMIC。 - 错误处理:提交失败时,URB 仍归驱动所有,驱动需要负责释放或重新填充。
- 批量传输特性:批量传输的 URB 提交后,会进入 HCD 的批量调度列表,由硬件按带宽可用性进行传输。
性能优化:批量传输场景下,可以通过
usb_submit_urb 的 GFP_ATOMIC 标志在软中断上下文中提交 URB,减少进程上下文切换带来的延迟。但需注意,频繁使用 GFP_ATOMIC 可能导致内存碎片。
4.4 URB 的取消
在某些情况下,驱动需要在 URB 完成之前终止传输,例如设备断开、超时处理或系统暂停。URB 的取消通过 usb_unlink_urb 或 usb_kill_urb 实现。
| 函数 | 行为 | 适用场景 |
|---|---|---|
usb_unlink_urb(struct urb *urb) |
异步取消,立即返回,URB 完成回调仍会被调用(状态为 -ECONNRESET) | 非关键路径,允许延迟处理 |
usb_kill_urb(struct urb *urb) |
同步取消,阻塞直到 URB 被完全终止,回调执行完毕 | 设备断开、驱动卸载等需要确保 URB 完全停止的场景 |
/* 同步取消 URB 示例 */
usb_kill_urb(urb); // 阻塞等待 URB 完全终止
/* 异步取消 URB 示例 */
usb_unlink_urb(urb); // 立即返回,后续在回调中处理 -ECONNRESET
死锁风险:在 URB 完成回调函数中调用
usb_kill_urb 会导致死锁,因为 usb_kill_urb 会等待回调完成,而回调正在等待自身完成。正确的做法是在回调中设置标志,在进程上下文中执行取消操作。
4.5 URB 的回收
URB 的回收是生命周期的终点。当 URB 完成传输(无论成功、失败还是被取消)后,驱动必须在完成回调中或之后调用 usb_free_urb 释放 URB 占用的内存。
回收流程的标准模式:
- 在完成回调中检查 URB 状态(
urb->status)。 - 处理传输数据或错误。
- 如果 URB 不再需要,调用
usb_free_urb释放。 - 如果 URB 需要复用(例如批量传输的循环提交),则重新填充并再次提交,不释放。
/* URB 完成回调示例 */
static void bulk_complete(struct urb *urb)
{
struct my_context *ctx = urb->context;
if (urb->status == 0) {
/* 传输成功,处理数据 */
process_received_data(urb->transfer_buffer, urb->actual_length);
} else if (urb->status == -ECONNRESET) {
/* URB 被取消,正常情况 */
} else {
/* 其他错误 */
dev_err(ctx->dev, "URB error: %d\n", urb->status);
}
/* 如果不再需要,释放 URB */
usb_free_urb(urb);
}
性能最佳实践:在批量传输的高吞吐场景中,建议采用 URB 复用模式:在完成回调中不清除 URB,而是直接重新填充数据并再次提交。这样可以避免反复分配/释放 URB 的开销,显著提升传输性能。URB 池的大小通常设置为 2~4 个,以流水线方式处理批量数据。
4.6 生命周期总结与对比
下表总结了 URB 生命周期各阶段的关键操作、状态变化和注意事项:
| 阶段 | 关键 API | URB 状态 | 注意事项 |
|---|---|---|---|
| 分配 | usb_alloc_urb |
空闲 | 使用 GFP_KERNEL 或 GFP_ATOMIC |
| 填充 | usb_fill_bulk_urb |
就绪 | 正确设置 transfer_flags |
| 提交 | usb_submit_urb |
已提交 | 检查返回值,失败时释放 URB |
| 取消 | usb_kill_urb / usb_unlink_urb |
已取消 | 避免在回调中调用 usb_kill_urb |
| 回收 | usb_free_urb |
已释放 | 确保 URB 不再被任何上下文引用 |