3. URB 生命周期:创建、填充、提交、完成、取消与回收的完整流程

URB(USB Request Block)是 USB 设备驱动与主机控制器之间的核心通信载体,其生命周期贯穿整个数据传输过程。理解 URB 的完整生命周期——从创建到回收——是编写稳定、高效 USB 驱动的基石。本章将逐一拆解每个阶段的操作细节、数据结构变化以及常见陷阱。

核心要点:URB 的生命周期是一个严格的状态机,任何非法的状态转换都会导致内核警告或设备通信异常。驱动开发者必须确保每个 URB 在正确的时机被创建、提交、取消和回收。

3.1 URB 生命周期全景图

下图展示了 URB 从创建到回收的完整状态流转路径,以及每个阶段对应的关键操作函数。

URB 生命周期状态流转图 1. 创建 usb_alloc_urb() 2. 填充 usb_fill_*_urb() 3. 提交 usb_submit_urb() 4. 完成 完成回调函数 5. 取消 usb_unlink_urb() 6. 回收 usb_free_urb() 分配 urb 结构体 和传输缓冲区 设置传输类型、端点、 回调、超时等参数 将 URB 加入 HCD 调度队列 HCD 完成传输, 调用回调函数 主动终止正在进行的 URB 传输 释放 urb 和缓冲区内存
开发提示:URB 的状态转换是单向的,不允许从“完成”或“取消”状态重新回到“提交”状态。如果需要重新使用 URB,必须从“回收”阶段重新开始“创建-填充-提交”流程。

3.2 创建阶段:分配 URB 结构体

URB 的创建通过 usb_alloc_urb() 函数完成,该函数在内核堆中分配 struct urb 结构体及其内部数据缓冲区。

#include <linux/usb.h>

struct urb *usb_alloc_urb(int iso_packets, gfp_t mem_flags);
参数 说明 典型值
iso_packets 等时传输的包数量,非等时传输设为 0 0(批量/中断/控制传输)
mem_flags 内存分配标志 GFP_KERNEL(进程上下文)
GFP_ATOMIC(中断上下文)
常见错误:在中断上下文(如定时器回调、tasklet)中使用 GFP_KERNEL 会导致内核休眠,引发 BUG: scheduling while atomic 错误。必须使用 GFP_ATOMIC。

创建成功后,返回的 struct urb * 指针指向一块已清零的内存区域。驱动开发者需要检查返回值是否为 NULL:

struct urb *urb = usb_alloc_urb(0, GFP_KERNEL);
if (!urb) {
    dev_err(&interface->dev, "Failed to allocate URB\n");
    return -ENOMEM;
}

3.3 填充阶段:配置传输参数

填充阶段是 URB 生命周期中最关键的步骤,需要设置传输类型、目标端点、数据缓冲区、回调函数和超时参数。Linux 内核提供了针对不同传输类型的专用填充函数:

传输类型 填充函数 关键参数
批量传输 usb_fill_bulk_urb() pipe, buffer, len, complete_fn, context
中断传输 usb_fill_int_urb() pipe, buffer, len, complete_fn, context, interval
控制传输 usb_fill_control_urb() pipe, setup_packet, buffer, len, complete_fn, context
等时传输 手动填充 urb->iso_frame_desc[] packet_count, offset, length, actual_length

以批量传输为例,填充操作的典型代码模式如下:

// 分配传输缓冲区
char *buffer = kmalloc(MAX_PACKET_SIZE, GFP_KERNEL);
if (!buffer) {
    usb_free_urb(urb);
    return -ENOMEM;
}

// 填充 URB
usb_fill_bulk_urb(urb, 
                  udev, 
                  usb_sndbulkpipe(udev, BULK_OUT_EP), 
                  buffer, 
                  MAX_PACKET_SIZE, 
                  bulk_write_complete, 
                  context);

// 设置超时时间(单位:毫秒)
urb->transfer_flags |= URB_TIMEOUT;
urb->timeout = msecs_to_jiffies(5000);  // 5秒超时
关键字段说明:
urb->transfer_flags:控制 URB 行为,常用标志包括 URB_TIMEOUT(启用超时)、URB_ZERO_PACKET(批量传输末尾发送零长度包)、URB_SHORT_NOT_OK(短包视为错误)。
urb->timeout:以 jiffies 为单位的超时值,仅在设置 URB_TIMEOUT 标志后生效。

3.4 提交阶段:将 URB 送入主机控制器

提交 URB 使用 usb_submit_urb() 函数,该函数将 URB 加入主机控制器驱动(HCD)的调度队列。提交后,URB 的所有权从驱动转移给 USB 核心层和 HCD。

int usb_submit_urb(struct urb *urb, gfp_t mem_flags);

提交操作的返回值及其含义:

返回值 含义 常见原因
0 提交成功 URB 已加入 HCD 队列
-ENOMEM 内存不足 HCD 内部 DMA 缓冲区分配失败
-EINVAL 参数无效 端点号错误、URB 已提交、缓冲区未对齐
-ESHUTDOWN 设备已断开 USB 设备已被拔出或复位
-EPIPE 端点被 stall 端点处于 halt 状态,需先调用 usb_clear_halt()
ret = usb_submit_urb(urb, GFP_KERNEL);
if (ret) {
    dev_err(&interface->dev, "URB submission failed: %d\n", ret);
    // 根据错误码进行恢复处理
    if (ret == -EPIPE) {
        usb_clear_halt(urb->dev, urb->pipe);
    }
    goto error_cleanup;
}
最佳实践:提交 URB 后,驱动不应再修改 URB 的 transfer_buffer、transfer_dma、setup_packet 等字段,直到回调函数被调用。HCD 可能在提交后立即开始 DMA 传输。

3.5 完成阶段:回调函数处理

当 HCD 完成数据传输(或发生错误)时,会调用 URB 的完成回调函数。回调函数在中断上下文执行,必须快速返回,不能执行可能休眠的操作。

static void bulk_write_complete(struct urb *urb)
{
    struct my_context *ctx = urb->context;

    // 检查传输状态
    switch (urb->status) {
    case 0:
        // 传输成功
        dev_info(&ctx->interface->dev, 
                 "Bulk write completed, %d bytes sent\n",
                 urb->actual_length);
        break;
    case -ECONNRESET:
    case -ENOENT:
    case -ESHUTDOWN:
        // URB 被取消或设备断开,正常退出
        return;
    case -ETIMEDOUT:
        // 超时错误
        dev_err(&ctx->interface->dev, "Bulk write timed out\n");
        // 触发超时恢复流程
        schedule_work(&ctx->timeout_recovery_work);
        return;
    default:
        // 其他错误
        dev_err(&ctx->interface->dev, 
                "Bulk write error: %d\n", urb->status);
        return;
    }

    // 传输成功,可以重新提交或释放 URB
    complete(&ctx->transfer_done);
}

回调函数中 urb->status 的常见值:

状态值 含义 处理建议
0 传输成功 检查 actual_length 确认数据完整性
-ETIMEDOUT 超时 执行超时恢复,可能需要复位端点或设备
-EPIPE 端点 stall 调用 usb_clear_halt() 清除 halt 状态
-ECONNRESET URB 被取消 正常退出,不要重新提交
-ENOENT URB 被同步取消 正常退出,不要重新提交
-ESHUTDOWN 设备断开 停止所有 I/O,等待设备重新连接

3.6 取消阶段:终止正在进行的传输

当需要提前终止 URB 传输时(如设备断开、超时恢复、模块卸载),使用 usb_unlink_urb() 或 usb_kill_urb() 函数。

函数 行为 适用场景
usb_unlink_urb() 异步取消,立即返回 中断上下文、不希望阻塞的路径
usb_kill_urb() 同步取消,等待回调完成 进程上下文、模块卸载、设备断开处理
// 异步取消(不等待)
int ret = usb_unlink_urb(urb);
if (ret == 0) {
    dev_info(&interface->dev, "URB unlinked successfully\n");
} else if (ret == -EINPROGRESS) {
    dev_info(&interface->dev, "URB already completed\n");
}

// 同步取消(等待回调执行完毕)
usb_kill_urb(urb);
// 此时 URB 的回调函数已经执行完毕,可以安全释放资源
死锁风险:不要在 URB 的回调函数中调用 usb_kill_urb() 取消同一个 URB,这会导致死锁。回调函数中只能使用 usb_unlink_urb() 进行异步取消。

3.7 回收阶段:释放 URB 资源

当 URB 不再需要时,必须调用 usb_free_urb() 释放其占用的内存。回收前必须确保 URB 已经处于“完成”或“取消”状态,且回调函数已经执行完毕。

// 安全回收 URB 的标准流程
usb_kill_urb(urb);           // 确保 URB 已停止
usb_free_urb(urb);           // 释放 URB 结构体
kfree(transfer_buffer);      // 释放传输缓冲区(如果是驱动分配的)

URB 回收的完整状态检查流程:

URB 安全回收决策流程 URB 是否已提交? 是 usb_kill_urb() usb_free_urb() 否 直接 usb_free_urb()
内存泄漏防范:每个 usb_alloc_urb() 必须对应一个 usb_free_urb()。在驱动卸载路径中,建议使用 usb_poison_urb() 替代 usb_kill_urb(),前者会阻止 URB 被重新提交,确保在驱动卸载后不会有悬空指针回调。

3.8 完整生命周期示例代码

以下代码展示了批量传输 URB 从创建到回收的完整流程,包含超时处理和错误恢复:

static int my_usb_bulk_transfer(struct usb_device *udev, 
                                struct usb_interface *interface,
                                void *data, size_t len)
{
    struct urb *urb;
    char *buffer;
    int ret;
    DECLARE_COMPLETION_ONSTACK(done);

    // 1. 创建 URB
    urb = usb_alloc_urb(0, GFP_KERNEL);
    if (!urb)
        return -ENOMEM;

    // 2. 分配缓冲区
    buffer = kmalloc(len, GFP_KERNEL);
    if (!buffer) {
        usb_free_urb(urb);
        return -ENOMEM;
    }
    memcpy(buffer, data, len);

    // 3. 填充 URB
    usb_fill_bulk_urb(urb, udev, 
                      usb_sndbulkpipe(udev, BULK_OUT_EP),
                      buffer, len,
                      bulk_transfer_complete, &done);
    urb->transfer_flags |= URB_TIMEOUT;
    urb->timeout = msecs_to_jiffies(3000);  // 3秒超时

    // 4. 提交 URB
    ret = usb_submit_urb(urb, GFP_KERNEL);
    if (ret) {
        dev_err(&interface->dev, "submit urb failed: %d\n", ret);
        kfree(buffer);
        usb_free_urb(urb);
        return ret;
    }

    // 5. 等待完成或超时
    if (!wait_for_completion_timeout(&done, 
                                     msecs_to_jiffies(5000))) {
        // 超时,取消 URB
        usb_unlink_urb(urb);
        wait_for_completion(&done);
        dev_err(&interface->dev, "transfer timed out\n");
        ret = -ETIMEDOUT;
    } else {
        // 检查传输结果
        if (urb->status == 0) {
            ret = urb->actual_length;
        } else {
            dev_err(&interface->dev, "transfer error: %d\n", 
                    urb->status);
            ret = urb->status;
        }
    }

    // 6. 回收资源
    usb_kill_urb(urb);   // 确保 URB 完全停止
    usb_free_urb(urb);
    kfree(buffer);

    return ret;
}

static void bulk_transfer_complete(struct urb *urb)
{
    struct completion *done = urb->context;
    complete(done);
}
工程建议:在实际驱动中,建议将 URB 的创建和回收封装为独立的函数,并在驱动结构体中维护 URB 池。批量传输场景下,使用 URB 池可以避免频繁的内存分配和释放,提升系统吞吐量。