4. USB请求块(URB)

USB请求块(USB Request Block,URB)是Linux USB子系统中最核心的数据结构之一,它充当了USB设备驱动与主机控制器硬件之间的通信载体。每个URB描述一次完整的USB数据传输事务,包括传输类型、目标端点、数据缓冲区、传输长度以及完成后的回调函数。理解URB的结构、生命周期以及提交/回调机制,是编写正确、高效的USB设备驱动的关键前提。

核心认知:URB的设计借鉴了网络协议栈中sk_buff的思想,将一次数据传输的所有元数据封装在一个独立的对象中,支持异步提交和完成通知,从而最大化USB总线利用率。

4.1 URB结构体详解

URB结构体定义在<linux/usb.h>头文件中,是驱动与USB核心层交互的主要数据结构。下面列出其关键字段及其作用:

字段名 类型 描述
urb_list struct list_head 用于将URB链接到提交队列或完成队列的内核链表节点
pipe unsigned int USB管道标识,编码了传输方向(IN/OUT)、端点号、传输类型和设备地址
dev struct usb_device * 指向目标USB设备的指针,URB最终由该设备的主机控制器处理
transfer_buffer void * 指向数据缓冲区的内核虚拟地址,用于存放待发送或接收的数据
transfer_buffer_length int 数据缓冲区的长度(字节数),驱动必须正确设置此值
actual_length int URB完成时实际传输的字节数,由USB核心层在回调前填充
status int URB传输状态,0表示成功,负值表示错误码(如-EPIPE、-ENOENT)
transfer_flags unsigned int 传输标志位,如URB_NO_TRANSFER_DMA_MAP、URB_ISO_ASAP等
complete usb_complete_t URB完成时的回调函数指针,驱动在此函数中处理传输结果
context void * 回调函数的私有上下文指针,驱动可传入任意自定义数据
start_frame int 等时传输中指定起始帧号,用于周期性传输的时序控制
number_of_packets int 等时传输的包描述符数量,每个包对应一个iso_frame_desc结构
interval int 中断传输的轮询间隔(以帧或微帧为单位),与端点描述符中的bInterval对应
关键字段解读:
  • pipe:通过usb_rcvbulkpipe()、usb_sndctrlpipe()等宏构造,驱动不应直接操作位域。
  • transfer_flags:常用标志包括URB_NO_TRANSFER_DMA_MAP(驱动自行管理DMA映射)和URB_FREE_BUFFER(完成时自动释放缓冲区)。
  • actual_length:对于批量传输,此值可能小于请求长度,驱动必须检查并处理短包。
URB结构体核心字段关系图 URB结构体核心字段关系 struct urb pipe | dev | transfer_flags transfer_buffer | transfer_buffer_length complete (回调函数) | context status | actual_length start_frame | interval (等时/中断专用) 传输描述 pipe + dev 确定目标端点 transfer_flags 控制行为 完成通知 complete 回调 + context status + actual_length 结果

4.2 URB生命周期

一个URB从创建到销毁经历完整的生命周期,驱动必须遵循以下阶段操作URB:

  1. 创建(Allocation):使用usb_alloc_urb()分配URB实例,传入等时包数量(非等时传输传0)。
  2. 初始化(Initialization):填充URB的关键字段,包括pipe、dev、transfer_buffer、complete回调等。对于批量/中断/控制传输,可使用辅助函数如usb_fill_bulk_urb()简化初始化。
  3. 提交(Submission):调用usb_submit_urb()将URB提交给USB核心层。提交后URB进入待处理状态,驱动不能再修改其关键字段。
  4. 处理(Processing):USB核心层将URB传递给主机控制器驱动(HCD),HCD执行实际的USB总线事务。此阶段URB处于不可用状态。
  5. 完成(Completion):传输完成后,USB核心层调用驱动注册的complete回调函数,并更新status和actual_length字段。驱动在回调中处理数据或错误。
  6. 回收(Reclamation):驱动在回调中或之后调用usb_free_urb()释放URB内存。如果URB需要复用,可调用usb_unlink_urb()取消未完成的URB后再重新初始化。
常见陷阱:
  • 不要在提交URB后立即释放它——必须等待回调完成或调用usb_kill_urb()确保URB不再被使用。
  • 回调函数在中断上下文中执行,不能调用可能睡眠的函数(如kmalloc(GFP_KERNEL)、mutex_lock())。
  • 复用URB时,必须重新设置transfer_buffer、transfer_buffer_length和complete字段,但pipe和dev通常保持不变。
URB生命周期状态转换图 URB生命周期状态转换 创建 (Allocated) 已提交 (Submitted) 处理中 (Processing) 完成 (Completed) usb_submit_urb() HCD 执行事务 传输结束 → 回调触发 usb_unlink_urb() 重新初始化后复用

4.3 URB提交与回调机制

URB的提交和回调是驱动与USB核心层交互的核心接口。驱动通过usb_submit_urb()将URB提交给核心层,核心层在传输完成后调用complete回调通知驱动。理解这两个接口的语义和约束,是编写可靠USB驱动的必要条件。

4.3.1 URB提交:usb_submit_urb()

usb_submit_urb()是异步非阻塞函数,调用后立即返回。其原型如下:

int usb_submit_urb(struct urb *urb, gfp_t mem_flags);

参数说明:

  • urb:已初始化完成的URB指针。
  • mem_flags:内存分配标志,在中断上下文中必须使用GFP_ATOMIC,在进程上下文中可使用GFP_KERNEL。

返回值:0表示成功提交;负值表示错误,常见错误码包括:

错误码 含义 常见原因
-ENODEV 设备已断开 提交时设备已被拔出或复位
-ENOMEM 内存不足 无法分配DMA缓冲区或内部数据结构
-EINVAL 参数无效 URB字段设置错误,如pipe与端点不匹配
-ESHUTDOWN 主机控制器已关闭 系统正在进入挂起或关机状态
提交规则:
  • 同一个URB不能同时提交多次——必须等待前一次完成或取消后才能重新提交。
  • 提交后,驱动不能再修改URB的transfer_buffer、pipe、dev等字段,直到回调返回。
  • 对于批量端点,USB规范允许主机控制器对短包进行特殊处理,驱动应检查actual_length。

4.3.2 回调函数:complete 回调

回调函数的类型定义为:

typedef void (*usb_complete_t)(struct urb *urb);

回调函数在URB传输完成后被调用,驱动在此函数中:

  • 检查urb->status判断传输是否成功。
  • 读取urb->actual_length获取实际传输字节数。
  • 处理接收到的数据(对于IN传输)或确认发送完成(对于OUT传输)。
  • 决定是否重新提交URB以继续传输(如批量流式传输)。
  • 调用usb_free_urb()释放URB,或将其归还给URB池。

回调函数执行在中断上下文(或软中断上下文)中,因此必须遵守以下约束:

  • 不能调用可能睡眠的函数(如wait_event()、mutex_lock()、kmalloc(GFP_KERNEL))。
  • 不能执行长时间运行的操作——应尽快返回,将耗时工作推迟到工作队列或tasklet中。
  • 可以安全地调用usb_submit_urb()重新提交URB(使用GFP_ATOMIC标志)。
URB提交与回调交互流程 URB提交与回调交互流程 USB设备驱动 USB核心层 + HCD USB设备 1. usb_alloc_urb() + 初始化 2. usb_submit_urb() 3. 解析URB,构造TD,提交给HCD 4. 执行USB事务 5. 更新status/actual_length 6. complete 回调执行 检查status → 处理数据 重新提交或释放URB

4.3.3 典型URB提交代码示例

以下代码展示了一个完整的批量OUT传输URB的创建、初始化和提交流程:

#include <linux/usb.h>

/* 批量传输完成回调 */
static void bulk_write_complete(struct urb *urb)
{
    struct my_device *dev = urb->context;
    
    if (urb->status == 0) {
        /* 传输成功,urb->actual_length 为实际发送字节数 */
        dev_dbg(&dev->interface->dev, "Bulk write OK, %d bytes sent\n",
                urb->actual_length);
    } else if (urb->status == -ENOENT) {
        /* URB被 usb_kill_urb() 取消,正常关闭路径 */
    } else {
        dev_err(&dev->interface->dev, "Bulk write failed: %d\n",
                urb->status);
    }
    
    /* 通知等待的进程(如果有) */
    complete(&dev->write_done);
}

/* 提交批量写URB */
int submit_bulk_write(struct my_device *dev, u8 *data, int len)
{
    struct urb *urb;
    int ret;
    
    /* 1. 分配URB */
    urb = usb_alloc_urb(0, GFP_KERNEL);
    if (!urb)
        return -ENOMEM;
    
    /* 2. 初始化URB */
    usb_fill_bulk_urb(urb, dev->udev,
                      usb_sndbulkpipe(dev->udev, dev->bulk_out_endpoint),
                      data, len,
                      bulk_write_complete, dev);
    
    /* 3. 提交URB */
    ret = usb_submit_urb(urb, GFP_KERNEL);
    if (ret) {
        dev_err(&dev->interface->dev,
                "Failed to submit bulk URB: %d\n", ret);
        usb_free_urb(urb);
        return ret;
    }
    
    /* URB已提交,不要释放——回调中会处理 */
    return 0;
}
最佳实践:
  • 使用usb_fill_bulk_urb()、usb_fill_int_urb()、usb_fill_control_urb()等辅助函数初始化URB,减少手动设置字段的错误。
  • 对于频繁提交的URB,考虑使用URB池(usb_alloc_urb() + 复用)而不是每次分配/释放,以降低内存碎片和性能开销。
  • 在驱动卸载或设备断开时,调用usb_kill_urb()确保所有待处理的URB被安全取消,避免回调在驱动已卸载后执行。

4.4 URB传输类型与回调行为对比

不同的USB传输类型在URB提交和回调行为上存在差异,驱动开发者需要根据传输类型选择合适的处理策略:

传输类型 回调触发时机 典型回调处理 注意事项
控制传输 Setup包+Data阶段(如有)+Status阶段完成后 检查status,读取响应数据 USB核心层自动处理控制传输的各个阶段;驱动只需关注最终结果
批量传输 每个URB对应的批量事务完成后 检查actual_length,处理短包,重新提交以继续流 批量传输可能被拆分为多个事务;短包表示传输结束(流控制)
中断传输 每个轮询间隔内的事务完成后 读取数据,重新提交URB以保持周期性轮询 中断URB通常被设计为持续提交的循环;驱动在回调中立即重新提交
等时传输 整个帧/微帧的所有包传输完成后 遍历iso_frame_desc数组,检查每个包的状态 等时传输没有重传机制;驱动必须处理丢包或错误包
关键区别:中断传输和等时传输具有周期性,驱动通常会在回调中立即重新提交URB以维持连续的传输流;而批量传输和控制传输通常是事务性的,一次提交对应一次请求-响应。
四种传输类型URB回调时序对比 控制 提交 Setup+Data+Status 回调 批量 提交 事务 回调 → 可能重新提交