2. URB 数据结构深度解析:struct urb 成员详解

URB(USB Request Block,USB 请求块)是 Linux USB 子系统中最核心的数据结构,它充当了 USB 设备驱动与主机控制器驱动之间的通信载体。每一个 USB 数据传输操作——无论是控制传输、批量传输、中断传输还是等时传输——都必须通过构造并提交一个 URB 来完成。本章将深入剖析 struct urb 的每一个关键成员,帮助你在驱动开发中精准地配置和使用 URB。

核心认知:URB 本质上是一个“异步请求描述符”。驱动通过填充 URB 成员来描述“要做什么数据传输”,然后将其提交给 USB 核心层,由后者调度给主机控制器硬件执行。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 使用场景。
struct urb 成员分组架构 struct urb 管道与端点 pipe dev ep 数据缓冲区 transfer_buffer transfer_dma transfer_buffer_length 传输控制 status actual_length error_count 回调与上下文 complete context URB 管理 urb_list anchor setup_packet

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);    /* 是否为控制传输 */
pipe 成员位域编码结构 (32-bit) 传输类型 [17:16] 设备地址 [30:16] 方向 [15] 端点号 [14:0] 保留 [31] 位 16-17 位 16-30 位 15 位 0-14 位 31 传输类型编码 00: 控制 01: 等时 10: 批量 11: 中断

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 时非常有用。
URB 生命周期与回调流程 1. 分配 URB usb_alloc_urb() 2. 初始化 URB 填充 pipe/buffer/complete 3. 提交 URB usb_submit_urb() 4. 硬件执行 HCD 调度传输 5. 回调执行 complete(urb) 被调用 6. 回收 URB usb_free_urb() 重新提交

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 的生命周期(分配