2. URB 数据结构深度解析:struct urb 成员详解
URB(USB Request Block,USB 请求块)是 Linux USB 子系统中最核心的数据结构,它充当了 USB 设备驱动与主机控制器驱动之间的通信载体。每一个 USB 数据传输操作——无论是控制传输、批量传输、中断传输还是等时传输——都必须通过构造并提交一个 URB 来完成。本章将深入剖析 struct urb 的每一个关键成员,帮助你在驱动开发中精准地配置和使用 URB。
2.1 struct urb 整体结构概览
struct urb 定义在 include/linux/usb.h 中,其成员数量超过 30 个。为了便于理解,我们可以将其分为以下 5 个功能组:
| 功能组 | 关键成员 | 作用简述 |
|---|---|---|
| 管道与端点 | pipe, dev, ep |
指定目标设备、端点和传输方向 |
| 数据缓冲区 | transfer_buffer, transfer_dma, transfer_buffer_length |
描述数据在内存中的位置和大小 |
| 传输控制 | status, actual_length, error_count |
记录传输结果和状态 |
| 回调与上下文 | complete, context |
传输完成后的通知机制 |
| URB 管理 | urb_list, anchor, setup_packet |
链表管理、控制传输设置包等 |
pipe、transfer_buffer、complete 和 context 这四个成员,它们覆盖了 90% 的 URB 使用场景。
2.2 管道成员:pipe —— 传输的“路由信息”
pipe 是 URB 中最重要的成员之一,它是一个 32 位的无符号整数,编码了 USB 传输所需的全部路由信息:目标设备地址、端点号、传输方向和传输类型。驱动通常不直接操作 pipe 的位域,而是通过内核提供的宏来构造和解析它。
2.2.1 pipe 的编码结构
| 位域 | 位范围 | 描述 |
|---|---|---|
| 设备地址 | [30:16] | USB 设备在总线上的唯一地址(7 位有效,最大 127) |
| 端点号 | [14:0] | 端点编号(4 位有效,0~15) |
| 传输方向 | bit 15 | 0 = OUT(主机到设备),1 = IN(设备到主机) |
| 传输类型 | [17:16] | 00 = 控制,01 = 等时,10 = 批量,11 = 中断 |
2.2.2 常用 pipe 构造宏
内核在 <linux/usb.h> 中提供了以下宏来安全地构造 pipe:
/* 构造各种传输类型的 pipe */
unsigned int usb_rcvctrlpipe(struct usb_device *dev, int endpoint);
unsigned int usb_sndctrlpipe(struct usb_device *dev, int endpoint);
unsigned int usb_rcvbulkpipe(struct usb_device *dev, int endpoint);
unsigned int usb_sndbulkpipe(struct usb_device *dev, int endpoint);
unsigned int usb_rcvintpipe(struct usb_device *dev, int endpoint);
unsigned int usb_sndintpipe(struct usb_device *dev, int endpoint);
unsigned int usb_rcvisocpipe(struct usb_device *dev, int endpoint);
unsigned int usb_sndisocpipe(struct usb_device *dev, int endpoint);
rcv 表示 IN 方向(设备→主机),snd 表示 OUT 方向(主机→设备)。例如,usb_rcvintpipe(dev, 0x81) 构造一个从端点 1 接收中断数据的 pipe。
2.2.3 pipe 解析宏
当需要从已有的 pipe 中提取信息时,可以使用以下宏:
/* 从 pipe 中提取信息 */
unsigned int usb_pipeendpoint(unsigned int pipe); /* 获取端点号 */
unsigned int usb_pipedevice(unsigned int pipe); /* 获取设备地址 */
unsigned int usb_pipein(unsigned int pipe); /* 判断是否为 IN 方向 */
unsigned int usb_pipeout(unsigned int pipe); /* 判断是否为 OUT 方向 */
unsigned int usb_pipetype(unsigned int pipe); /* 获取传输类型 */
unsigned int usb_pipeisoc(unsigned int pipe); /* 是否为等时传输 */
unsigned int usb_pipeint(unsigned int pipe); /* 是否为中断传输 */
unsigned int usb_pipebulk(unsigned int pipe); /* 是否为批量传输 */
unsigned int usb_pipecontrol(unsigned int pipe); /* 是否为控制传输 */
2.3 数据缓冲区成员:transfer_buffer 与 transfer_dma
数据缓冲区是 URB 中承载实际传输数据的部分。根据驱动使用的 DMA 模式不同,需要设置不同的缓冲区成员。
2.3.1 transfer_buffer —— 虚拟地址缓冲区
transfer_buffer 是一个 void * 指针,指向内核虚拟地址空间中的数据缓冲区。这是最常用的缓冲区设置方式,适用于大多数驱动场景。
/* 典型用法:分配内核缓冲区并赋值 */
struct urb *urb;
char *buf;
buf = kmalloc(64, GFP_KERNEL);
urb = usb_alloc_urb(0, GFP_KERNEL);
/* 填充数据 */
memcpy(buf, "Hello USB", 10);
/* 设置 URB 缓冲区 */
urb->transfer_buffer = buf;
urb->transfer_buffer_length = 64;
2.3.2 transfer_dma —— DMA 缓冲区
当使用 DMA 一致性映射时,驱动需要提供 DMA 总线地址。此时应设置 transfer_dma 成员,并设置 URB_NO_TRANSFER_DMA_MAP 标志位,告知 USB 核心不要再次进行 DMA 映射。
/* DMA 缓冲区用法 */
dma_addr_t dma_handle;
char *buf;
/* 分配 DMA 一致性缓冲区 */
buf = usb_alloc_coherent(dev, 64, GFP_KERNEL, &dma_handle);
urb->transfer_dma = dma_handle;
urb->transfer_buffer_length = 64;
urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; /* 关键标志! */
transfer_dma 但忘记设置 URB_NO_TRANSFER_DMA_MAP 标志,USB 核心会尝试对已经映射的 DMA 地址再次进行映射,导致双重映射错误,可能引发数据损坏或系统崩溃。
2.3.3 transfer_buffer_length 与 actual_length
| 成员 | 方向 | 含义 |
|---|---|---|
transfer_buffer_length |
输入 | 驱动期望传输的字节数(URB 提交前设置) |
actual_length |
输出 | 实际传输的字节数(URB 完成后由核心填充) |
对于 IN 方向的 URB,actual_length 可能小于 transfer_buffer_length,表示设备实际返回的数据量。驱动应在回调函数中检查 actual_length 以确定有效数据长度。
2.4 回调函数成员:complete —— 异步通知机制
complete 是 URB 异步模型的核心。它是一个函数指针,当 URB 传输完成(无论成功或失败)时,USB 核心会在中断上下文或软中断上下文中调用该函数。
2.4.1 回调函数原型
typedef void (*usb_complete_t)(struct urb *urb);
回调函数接收一个参数:指向已完成 URB 的指针。驱动通过检查 urb->status 来判断传输结果。
2.4.2 回调函数典型实现
static void my_urb_complete(struct urb *urb)
{
/* 检查传输状态 */
if (urb->status == 0) {
/* 传输成功,处理数据 */
pr_info("URB completed successfully, received %d bytes\n",
urb->actual_length);
process_received_data(urb->transfer_buffer,
urb->actual_length);
} else if (urb->status == -ENOENT) {
/* URB 被 usb_kill_urb() 取消 */
pr_debug("URB was killed\n");
} else if (urb->status == -ECONNRESET) {
/* URB 被 usb_unlink_urb() 取消 */
pr_debug("URB was unlinked\n");
} else {
/* 其他错误 */
pr_err("URB failed with status %d\n", urb->status);
}
/* 如果需要重新提交 URB(如中断端点轮询),在此处处理 */
if (need_resubmit) {
int ret = usb_submit_urb(urb, GFP_ATOMIC);
if (ret)
pr_err("resubmit failed: %d\n", ret);
}
}
2.4.3 回调函数执行上下文
✅ 可以使用
GFP_ATOMIC 分配内存✅ 可以调用
usb_submit_urb() 重新提交 URB❌ 不能睡眠(不能使用
GFP_KERNEL、mutex_lock()、wait_event() 等)❌ 不能访问用户空间内存
2.5 上下文成员:context —— 驱动私有数据传递
context 是一个 void * 指针,用于在提交 URB 时传递驱动自定义数据。回调函数可以通过 urb->context 获取该数据,这是实现“每个 URB 携带独立状态”的关键机制。
/* 定义每个 URB 的私有上下文结构 */
struct my_urb_context {
struct usb_device *udev;
struct completion done;
int result;
};
/* 提交 URB 时传递上下文 */
struct my_urb_context *ctx;
ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
init_completion(&ctx->done);
ctx->udev = udev;
urb->context = ctx;
urb->complete = my_complete_handler;
/* 在回调函数中获取上下文 */
static void my_complete_handler(struct urb *urb)
{
struct my_urb_context *ctx = urb->context;
ctx->result = urb->status;
complete(&ctx->done); /* 唤醒等待的进程 */
}
context + complete 回调 + completion 机制可以轻松实现“异步提交,同步等待”的语义,这在需要将 URB 封装为同步 API 时非常有用。
2.6 其他重要成员速查
| 成员 | 类型 | 说明 |
|---|---|---|
dev |
struct usb_device * |
目标 USB 设备指针,必须在提交前设置 |
ep |
struct usb_host_endpoint * |
目标端点指针,通常由 USB 核心自动设置 |
status |
int |
传输结果状态码,0 表示成功,负值表示错误 |
transfer_flags |
unsigned int |
传输标志位,控制 DMA 映射、短包处理等行为 |
number_of_packets |
int |
等时传输的包数量,仅用于等时 URB |
interval |
int |
中断端点的轮询间隔(以帧/微帧为单位) |
setup_packet |
unsigned char * |
控制传输的设置包(8 字节),仅用于控制 URB |
anchor |
struct usb_anchor * |
URB 锚点,用于批量取消一组 URB |
2.7 常见错误与调试建议
- 未设置
dev指针:提交 URB 前必须将urb->dev指向有效的struct usb_device,否则内核会崩溃。 - 缓冲区大小不匹配:
transfer_buffer_length必须与端点描述符的wMaxPacketSize匹配,尤其是中断端点。 - 回调函数中睡眠:这是最常见的错误之一。回调函数运行在中断上下文,任何睡眠操作都会导致内核调度异常。
- 忘记设置
transfer_flags:使用 DMA 缓冲区时必须设置URB_NO_TRANSFER_DMA_MAP,否则会导致双重映射。 - URB 泄漏:每次
usb_alloc_urb()必须对应一次usb_free_urb(),否则会造成内存泄漏。
/sys/kernel/debug/usb/ 目录下可以查看 URB 的统计信息。启用 CONFIG_USB_DEBUG 和 CONFIG_USB_ANNOUNCE_NEW_DEVICES 可以获得更详细的 URB 调度日志。使用 usbmon 工具可以捕获所有 URB 传输的原始数据包,是调试 USB 驱动问题的利器。
2.8 本章小结
struct urb 是 USB 驱动开发中最核心的数据结构,掌握其关键成员是编写正确 USB 驱动的基础:
- pipe 编码了设备地址、端点号、方向和传输类型,使用宏来构造和解析。
- transfer_buffer / transfer_dma 指定数据缓冲区,注意 DMA 标志位的正确使用。
- complete 回调函数是异步通知的核心,必须遵守中断上下文的约束。
- context 提供了驱动私有数据的传递通道,是实现复杂状态机的基础。
- 正确管理 URB 的生命周期(分配