2. URB数据结构深度解析:struct urb关键成员及其生命周期
URB(USB Request Block)是USB设备驱动与主机控制器之间的核心通信单元。理解 struct urb 的每个关键成员及其生命周期,是排查URB泄漏、优化传输性能的基础。本章将从数据结构定义出发,逐一剖析核心字段的作用、使用约束与生命周期管理要点。
核心观点:URB 的生命周期从分配(usb_alloc_urb)开始,经过初始化、提交(usb_submit_urb)、回调(complete callback)或取消(usb_unlink_urb),最终释放(usb_free_urb)。任何环节的遗漏都会导致泄漏。
2.1 struct urb 核心成员一览
以下表格列出了 struct urb 中最关键的成员,以及它们在驱动开发中的典型用途和注意事项。
| 成员名称 | 类型 | 方向 | 核心用途 | 生命周期关键点 |
|---|---|---|---|---|
pipe |
unsigned int | 输入/输出 | 编码端点类型、方向、设备地址、端点号 | 提交前必须设置,提交后不应修改 |
transfer_buffer |
void * | 输入/输出 | 指向数据缓冲区的指针 | 必须与 transfer_buffer_length 配合,DMA 安全要求 |
transfer_dma |
dma_addr_t | 输入/输出 | DMA 模式下的缓冲区物理地址 | 使用 URB_NO_TRANSFER_DMA_MAP 标志时有效 |
transfer_buffer_length |
int | 输入 | 缓冲区字节长度 | 提交前设置,回调中读取实际传输长度 |
actual_length |
int | 输出 | 实际传输的字节数(回调时有效) | 仅在 complete 回调中读取 |
status |
int | 输出 | 传输完成状态(0 成功,负值错误码) | 回调中必须检查,决定是否重试或释放 |
complete |
usb_complete_t | 输入 | 传输完成后的回调函数指针 | 提交前赋值,回调中执行清理或数据解析 |
context |
void * | 输入 | 传递给回调函数的私有上下文指针 | 通常指向请求对应的设备结构或完成量 |
interval |
int | 输入 | 中断/isochronous 传输的轮询间隔(ms) | 仅对中断/等时端点有效 |
number_of_packets |
int | 输入 | 等时传输的包数量 | 仅用于 isochronous 传输 |
iso_frame_desc[] |
struct usb_iso_packet_descriptor | 输入/输出 | 等时传输的帧描述数组 | 每个帧的 offset/length/actual_length/status |
开发提示:在调试URB泄漏时,优先检查
transfer_buffer 和 context 指向的内存是否在URB释放前被正确释放。这两个指针最容易导致“二次泄漏”。
2.2 pipe 成员:端点通信的编码器
pipe 是一个位编码的整数,它包含了USB通信所需的全部路由信息。内核提供了一组宏来构建和解析 pipe 值。
// 构建 pipe 的典型宏
unsigned int pipe = usb_sndctrlpipe(dev, 0); // 控制输出端点0
unsigned int pipe = usb_rcvbulkpipe(dev, 1); // 批量输入端点1
unsigned int pipe = usb_sndintpipe(dev, 2); // 中断输出端点2
unsigned int pipe = usb_rcvisocpipe(dev, 3); // 等时输入端点3
// 从 pipe 中提取信息(调试时常用)
unsigned int epnum = usb_pipeendpoint(pipe);
unsigned int dir = usb_pipeout(pipe) ? USB_DIR_OUT : USB_DIR_IN;
unsigned int type = usb_pipetype(pipe); // PIPE_ISOCHRONOUS / PIPE_INTERRUPT / PIPE_CONTROL / PIPE_BULK
常见错误:在URB重用(reinit)时忘记重新设置 pipe,导致数据发往错误的端点。每次
usb_submit_urb 前都应确保 pipe 与当前传输意图一致。
2.3 transfer_buffer 与 DMA 安全
transfer_buffer 指向的数据缓冲区必须满足DMA一致性要求。对于批量/中断传输,推荐使用 usb_alloc_coherent 或 kmalloc 配合 GFP_KERNEL 标志。栈上分配的缓冲区(局部数组)绝对不能用于URB传输。
// 正确分配方式示例
struct urb *urb;
void *buf;
urb = usb_alloc_urb(0, GFP_KERNEL);
buf = kmalloc(size, GFP_KERNEL); // 或 usb_alloc_coherent(dev, size, GFP_KERNEL, &urb->transfer_dma);
urb->transfer_buffer = buf;
urb->transfer_buffer_length = size;
urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; // 如果使用DMA地址
2.4 complete 与 context:回调机制的核心
complete 回调函数在URB传输完成(成功、失败或被取消)时被调用。回调运行在中断上下文或软中断上下文,因此不能执行阻塞操作。context 指针允许驱动将私有数据传递给回调函数,通常指向一个完成量(completion)或请求描述符。
// 典型的 complete 回调实现
static void my_urb_complete(struct urb *urb)
{
struct my_device *mydev = (struct my_device *)urb->context;
if (urb->status == 0) {
// 传输成功,处理 urb->actual_length 字节数据
process_received_data(mydev, urb->transfer_buffer, urb->actual_length);
} else if (urb->status == -ECONNRESET) {
// 被 usb_unlink_urb 取消,正常清理
} else {
// 其他错误,可能需要重试
dev_err(&mydev->interface->dev, "URB failed with status %d\n", urb->status);
}
// 在回调中重新提交(循环URB)或释放
if (mydev->running) {
usb_submit_urb(urb, GFP_ATOMIC);
} else {
usb_free_urb(urb);
}
}
2.5 URB 生命周期状态机
下图展示了URB从创建到销毁的完整状态转换。理解这个状态机是定位泄漏的关键:任何停留在“已提交”或“已取消但未释放”状态的URB都是潜在泄漏点。
2.6 生命周期管理最佳实践
- 分配与初始化配对:每个
usb_alloc_urb必须对应一个usb_free_urb。推荐在驱动探测时分配,在断开时释放。 - 提交前检查:调用
usb_submit_urb前,确保pipe、transfer_buffer、complete、context均已正确赋值。 - 回调中释放:对于一次性URB,在 complete 回调中调用
usb_free_urb。对于循环URB,在驱动停止时先usb_unlink_urb再释放。 - 取消后的清理:
usb_unlink_urb返回后,URB 的 complete 回调仍可能被调用。必须在回调中处理-ECONNRESET状态,并最终释放URB。 - 缓冲区生命周期:
transfer_buffer指向的内存必须在URB完全释放(所有引用计数归零)后才能释放。使用usb_free_coherent或kfree时注意时序。
泄漏排查口诀:分配计数要匹配,提交之前查四件(pipe/buf/complete/context);回调之中清状态,取消之后不忘放。
2.7 关键成员生命周期对比表
下表总结了不同成员在URB生命周期各阶段的有效性和责任方。
| 成员 | 分配后 | 初始化后 | 提交后 | 回调中 | 释放前 |
|---|---|---|---|---|---|
pipe |
未定义 | 必须设置 | 只读 | 只读 | 无需操作 |
transfer_buffer |
未定义 | 必须设置 | 只读(内核可能修改内容) | 读取数据 | 驱动释放 |
transfer_buffer_length |
未定义 | 必须设置 | 只读 | 只读 | 无需操作 |
actual_length |
未定义 | 未定义 | 内核写入 | 读取有效值 | 无需操作 |
status |
未定义 | 未定义 | 内核写入 | 必须检查 | 无需操作 |
complete |
未定义 | 必须设置 | 只读 | 可在此函数内 | 无需操作 |
context |
未定义 | 建议设置 | 只读 | 读取使用 | 驱动释放(如指向动态内存) |
调试技巧:在 complete 回调入口处打印
urb->status 和 urb->actual_length,可以快速定位传输失败或数据截断问题。使用 usb_urb_dir_in(urb) 宏判断传输方向。