4. URB 生命周期管理:分配、填充、提交、取消与回收

URB(USB Request Block)是 Linux USB 子系统中最核心的数据结构,它承载了主机与 USB 设备之间每一次数据传输的完整上下文。理解 URB 从创建到销毁的完整生命周期,是编写稳定、高性能 USB 驱动的基础。本章将深入剖析 URB 的五个关键阶段:分配、填充、提交、取消与回收。

核心原则:URB 的生命周期遵循“谁分配,谁回收”的原则。驱动开发者必须确保 URB 在提交后不会被意外释放,且在完成回调中正确处理 URB 的后续状态。
URB 生命周期全景流程图 URB 生命周期全景 1. 分配 2. 填充 3. 提交 4. 完成/取消 5. 回收 说明: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 映射,导致数据损坏或内核崩溃。
URB 填充结构图 URB 填充关键字段 struct urb dev: struct usb_device * pipe: unsigned int transfer_buffer / transfer_buffer_length 填充阶段决定了 URB 的传输方向、数据内容和回调行为,是驱动逻辑的核心。

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 占用的内存。

回收流程的标准模式:

  1. 在完成回调中检查 URB 状态(urb->status)。
  2. 处理传输数据或错误。
  3. 如果 URB 不再需要,调用 usb_free_urb 释放。
  4. 如果 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 个,以流水线方式处理批量数据。
URB 回收与复用决策图 URB 回收与复用决策 完成回调触发 需要复用? 重新填充并提交 调用 usb_free_urb 复用模式可减少内存分配开销,是批量传输高性能的关键设计模式。

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 不再被任何上下文引用