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 泄漏”问题至关重要。
生命周期总览: 创建 → 填充 → 提交 → 调度 → 传输 → 完成回调 → 回收。
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 问题的第一利器。