4. URB(USB Request Block)结构详解:urb结构体成员、标志位与生命周期

URB(USB Request Block)是 Linux USB 子系统中最核心的数据结构,它是主机控制器驱动与 USB 设备驱动之间进行数据交换的载体。每一个 USB 数据传输请求,无论方向是 IN 还是 OUT,都会被封装为一个 URB 实例,通过内核的 USB 核心层提交到主机控制器硬件。理解 URB 的结构、标志位及其生命周期,是进行 USB 驱动故障排查的必备基础。

核心认知: URB 本质上是一个“异步请求描述符”。驱动开发者通过填充 URB 结构体来定义一次传输的所有参数(端点、缓冲区、回调函数等),然后将其提交给 USB 核心。核心层负责将 URB 调度到主机控制器,并在传输完成后调用回调函数通知驱动。

4.1 urb 结构体核心成员

struct urb 定义在 <linux/usb.h> 中,包含数十个成员。对于驱动开发和故障排查,以下成员最为关键:

成员名称 类型 方向 功能描述
dev struct usb_device * 输入 指向目标 USB 设备的指针,URB 提交前必须赋值。
pipe unsigned int 输入 端点管道信息,包含端点号、方向、传输类型。通过 usb_rcvintpipe() 等宏构造。
transfer_buffer void * 输入/输出 指向数据缓冲区的指针。对于 OUT 传输,包含要发送的数据;对于 IN 传输,接收数据。
transfer_buffer_length int 输入 缓冲区长度(字节)。实际传输的数据量可能小于此值。
actual_length int 输出 实际传输的字节数。在 URB 完成回调中读取,用于判断数据完整性。
status int 输出 传输状态码。0 表示成功,负值表示错误(如 -EPROTO、-EILSEQ)。
complete usb_complete_t 输入 完成回调函数指针。URB 传输结束后(成功或失败)由 USB 核心调用。
context void * 输入 回调函数的私有上下文指针,通常指向驱动定义的请求结构体。
interval int 输入 中断传输或等时传输的轮询间隔(单位:帧/微帧)。
number_of_packets int 输入 等时传输的包数量。仅用于等时传输。
iso_frame_desc[] struct usb_iso_packet_descriptor 输入/输出 等时传输的帧描述数组,描述每个包的偏移、长度和状态。
排查要点: 当 URB 完成时,status 和 actual_length 是首要检查的字段。status 为 0 不代表数据正确,仅表示总线传输无错误;actual_length 若小于 transfer_buffer_length,可能表示设备提前终止传输或发生了短包(short packet)。

4.2 URB 标志位详解

URB 结构体中的 transfer_flags 字段控制 URB 的行为模式。这些标志位在 URB 提交前设置,影响内核如何处理缓冲区、DMA 映射以及传输完成后的动作。

标志位 值 含义
URB_SHORT_NOT_OK 0x0001 对于 IN 传输,如果接收到的数据少于预期长度,将 status 设置为 -EREMOTEIO。默认情况下,短包被认为是正常结束。
URB_ISO_ASAP 0x0002 等时传输专用。指示主机控制器尽快调度该 URB,而不是等待特定的帧号。
URB_NO_TRANSFER_DMA_MAP 0x0004 告知 USB 核心不要对 transfer_buffer 进行 DMA 映射。驱动已自行处理 DMA 缓冲区。
URB_NO_INTERRUPT 0x0080 禁止在 URB 完成时触发中断。用于批量传输的特定场景,减少中断开销。
URB_FREE_BUFFER 0x0100 URB 完成回调后,由 USB 核心自动释放 transfer_buffer。驱动无需手动释放。
URB_DIR_IN 0x0200 由内核内部设置,表示数据传输方向为设备到主机(IN)。驱动不应手动设置。
URB_DIR_OUT 0x0400 由内核内部设置,表示数据传输方向为主机到设备(OUT)。驱动不应手动设置。
常见陷阱: 许多驱动在批量 IN 传输中未设置 URB_SHORT_NOT_OK,导致设备发送短包时驱动误认为传输成功。在需要严格数据长度的场景(如固件下载),必须设置此标志位以捕获截断数据。

4.3 URB 生命周期与状态机

一个 URB 从创建到销毁,经历一系列明确的状态转换。理解这一生命周期对于调试“URB 提交失败”、“URB 挂起”或“URB 泄漏”问题至关重要。

生命周期总览: 创建 → 填充 → 提交 → 调度 → 传输 → 完成回调 → 回收。
URB 生命周期状态机 创建 (usb_alloc_urb) 填充 (usb_fill_*) 提交 (usb_submit_urb) 调度 (HCD 队列) 硬件传输 完成回调 (complete) 回收 (usb_free_urb) 提交失败 关键说明: • 提交失败路径:若 usb_submit_urb() 返回负值(如 -ENOMEM、-ESHUTDOWN),URB 直接进入完成回调,status 携带错误码。 • 回收必须在完成回调之后进行,否则会导致 use-after-free 错误。推荐在回调函数中调用 usb_free_urb()。

4.4 各阶段详细说明

4.4.1 创建与填充

URB 通过 usb_alloc_urb() 分配,该函数接受等时包数量参数(非等时传输传 0)。填充阶段使用辅助函数简化操作:

/* 中断传输 URB 填充示例 */
struct urb *urb;
urb = usb_alloc_urb(0, GFP_KERNEL);
if (!urb)
    return -ENOMEM;

usb_fill_int_urb(urb, dev, usb_rcvintpipe(dev, 0x81),
                 buffer, size, complete_callback, context, interval);

/* 批量传输 URB 填充示例 */
usb_fill_bulk_urb(urb, dev, usb_sndbulkpipe(dev, 0x02),
                  buffer, size, complete_callback, context);

4.4.2 提交与调度

usb_submit_urb() 是 URB 生命周期的关键转折点。提交后,URB 进入 USB 核心的内部队列,等待主机控制器驱动程序(HCD)调度。提交可能失败的原因包括:

  • -ENODEV: 设备已断开连接。
  • -ENOMEM: 内存分配失败(如 DMA 映射失败)。
  • -ESHUTDOWN: 主机控制器已关闭(通常发生在系统挂起或复位时)。
  • -EPIPE: 端点被 STALL 握手信号停止(端点错误)。
  • -EINVAL: 参数无效(如缓冲区未对齐、端点号错误)。
排查技巧: 当 usb_submit_urb() 返回 -EPIPE 时,应调用 usb_clear_halt() 清除端点的停止状态,然后重新提交。这是 USB 驱动处理端点错误的标准化流程。

4.4.3 完成回调与回收

URB 完成回调函数在中断上下文或软中断上下文中执行,必须遵守以下规则:

  • 不可睡眠: 不能调用 msleep()、wait_event() 等可能导致阻塞的函数。
  • 不可提交新 URB: 虽然技术上可行,但容易导致递归或死锁。推荐使用工作队列(workqueue)延迟提交。
  • 检查 status: 回调第一件事应检查 urb->status,根据错误码决定处理策略。
  • 及时回收: 回调结束后应调用 usb_free_urb() 释放 URB 内存,除非 URB 被设计为重用(通过 usb_submit_urb() 重新提交)。
/* 典型的 URB 完成回调实现 */
static void my_urb_complete(struct urb *urb)
{
    struct my_context *ctx = urb->context;

    if (urb->status == 0) {
        /* 传输成功,处理 urb->transfer_buffer 中的数据 */
        pr_info("URB completed, actual length: %d\n", urb->actual_length);
        process_received_data(urb->transfer_buffer, urb->actual_length);
    } else if (urb->status == -ENOENT) {
        /* URB 被 usb_kill_urb() 取消,正常退出 */
    } else if (urb->status == -ECONNRESET) {
        /* URB 被 usb_unlink_urb() 取消 */
    } else {
        /* 其他错误,记录日志 */
        dev_err(&urb->dev->dev, "URB error: %d\n", urb->status);
    }

    /* 释放 URB 及其缓冲区 */
    usb_free_urb(urb);
}

4.5 URB 重用与取消

在高速数据流场景(如视频采集、批量传输),频繁分配和释放 URB 会导致性能下降。内核支持 URB 重用机制:

  • 重用流程: 在完成回调中不调用 usb_free_urb(),而是重新填充 transfer_buffer 和 context,然后再次调用 usb_submit_urb()。
  • 取消 URB: usb_kill_urb() 同步取消 URB,等待回调执行完毕;usb_unlink_urb() 异步取消,立即返回。
  • 注意事项: 重用 URB 时,必须确保前一次传输已完成(回调已执行),否则会导致 URB 状态混乱。
内存泄漏风险: 如果 URB 设置了 URB_FREE_BUFFER 标志,内核会在回调后自动释放 transfer_buffer。若此时驱动仍持有该缓冲区指针并尝试重用,将导致 use-after-free。重用场景下不应设置此标志。

4.6 故障排查清单

当遇到 URB 相关问题时,按以下清单逐项检查:

检查项 检查内容 常见问题
URB 分配 usb_alloc_urb() 返回值是否为 NULL 内存不足,等时包数量过大
端点管道 pipe 是否与端点描述符匹配 方向错误(IN/OUT 混淆),端点号错误
缓冲区 transfer_buffer 是否有效且对齐 未分配缓冲区,DMA 对齐要求不满足
回调函数 complete 是否被正确调用 回调中睡眠导致死锁,回调未检查 status
状态码 urb->status 的值 -EPROTO(总线错误)、-EILSEQ(CRC 错误)
实际长度 urb->actual_length 与预期是否一致 短包未处理,数据截断
URB 泄漏 是否每次提交后都调用了 usb_free_urb() 重用场景未正确管理生命周期
调试工具: 开启内核 USB 调试日志(echo 'file drivers/usb/core/urb.c +p' > /sys/kernel/debug/dynamic_debug/control)可以打印每个 URB 的提交、完成和错误信息,是排查 URB 问题的第一利器。