4. URB 内存分配 API:usb_alloc_urb 与 usb_free_urb 的底层实现与使用场景

URB(USB Request Block)是 USB 设备驱动与主机控制器之间的核心通信载体。每个 URB 实例的分配与释放,直接决定了数据传输的稳定性与内存泄漏风险。本章深入剖析 usb_alloc_urb 与 usb_free_urb 的底层实现机制、内存布局以及典型使用场景,帮助你在驱动开发中精准管理 URB 生命周期。

4.1 URB 内存结构概览

一个 URB 在内存中并非孤立存在,它由三个连续区域组成:URB 头部、ISO 包描述符数组(仅等时传输使用)以及 Setup 包(仅控制传输使用)。下图展示了 URB 的完整内存布局:

URB 内存布局(连续分配) struct urb 头部 - urb->dev - urb->pipe - urb->transfer_buffer - urb->transfer_buffer_length - urb->complete (回调函数) - urb->context - ... 其他成员 ISO 包描述符 struct usb_iso_packet_descriptor iso_frame_desc[0] iso_frame_desc[1] ... iso_frame_desc[n-1] (仅等时传输) Setup 包 struct usb_ctrlrequest bmRequestType bRequest wValue / wIndex / wLength (仅控制传输) 说明:usb_alloc_urb 一次分配连续内存,ISO 描述符和 Setup 包紧随 urb 头部之后

4.2 usb_alloc_urb 底层实现

函数原型定义在 <linux/usb.h> 中:

struct urb *usb_alloc_urb(int iso_packets, gfp_t mem_flags);

该函数的核心工作流程如下:

  1. 计算总大小:根据 iso_packets 参数计算 ISO 包描述符数组所需字节数,加上 sizeof(struct urb) 得到总分配大小。
  2. 调用 kmalloc:使用 mem_flags(通常为 GFP_KERNEL 或 GFP_ATOMIC)分配连续内核内存。
  3. 初始化 URB 头部:将 urb->dev 置为 NULL,urb->pipe 清零,urb->transfer_flags 设为 URB_NO_TRANSFER_DMA_MAP 等默认值。
  4. 设置 ISO 偏移:将 urb->iso_frame_desc 指针指向紧随 URB 头部之后的 ISO 描述符数组起始地址。
  5. 返回 URB 指针:若分配失败则返回 NULL。
关键提示:iso_packets 参数仅对等时传输有意义。对于批量、中断和控制传输,应传入 0,避免浪费内存。

4.3 usb_free_urb 底层实现

函数原型:

void usb_free_urb(struct urb *urb);

释放过程包含以下安全机制:

步骤 操作 说明
1 空指针检查 若 urb == NULL,立即返回,避免空指针解引用
2 引用计数递减 调用 kref_put(&urb->kref, urb_destroy),仅当引用计数归零时才真正释放
3 调用 kfree 在 urb_destroy 回调中执行 kfree(urb),归还连续内存块
常见错误:直接调用 kfree(urb) 绕过引用计数机制,会导致正在被主机控制器引用的 URB 被释放,引发内核崩溃或 DMA 访问非法内存。

4.4 使用场景对比

不同传输类型对 URB 分配的需求差异显著,下表总结了典型场景:

传输类型 iso_packets 参数 典型 mem_flags 使用场景
批量传输 0 GFP_KERNEL(进程上下文)
GFP_ATOMIC(中断上下文)
U 盘读写、网络适配器数据收发
中断传输 0 GFP_KERNEL / GFP_ATOMIC 键盘、鼠标、游戏手柄输入
控制传输 0 GFP_KERNEL 设备枚举、标准请求(如获取描述符)
等时传输 帧数(如 8/16/32) GFP_KERNEL 音频流、视频流、实时传感器数据

4.5 典型代码模式

以下代码展示了批量传输场景下 URB 的完整生命周期管理:

#include <linux/usb.h>

static struct urb *bulk_urb;
static char *bulk_buffer;

static int setup_bulk_urb(struct usb_device *udev, unsigned int pipe)
{
    int ret;

    /* 1. 分配 URB(批量传输不需要 ISO 包) */
    bulk_urb = usb_alloc_urb(0, GFP_KERNEL);
    if (!bulk_urb) {
        printk(KERN_ERR "Failed to allocate URB\n");
        return -ENOMEM;
    }

    /* 2. 分配传输缓冲区 */
    bulk_buffer = kmalloc(512, GFP_KERNEL);
    if (!bulk_buffer) {
        usb_free_urb(bulk_urb);
        return -ENOMEM;
    }

    /* 3. 填充 URB */
    usb_fill_bulk_urb(bulk_urb, udev, pipe,
                      bulk_buffer, 512,
                      bulk_complete_callback,  /* 完成回调 */
                      NULL);                   /* 上下文 */

    /* 4. 提交 URB */
    ret = usb_submit_urb(bulk_urb, GFP_KERNEL);
    if (ret) {
        printk(KERN_ERR "URB submission failed: %d\n", ret);
        usb_free_urb(bulk_urb);
        kfree(bulk_buffer);
        return ret;
    }

    return 0;
}

static void cleanup_bulk_urb(void)
{
    /* 5. 先取消,再释放 */
    usb_kill_urb(bulk_urb);
    usb_free_urb(bulk_urb);
    kfree(bulk_buffer);
}

4.6 引用计数机制详解

URB 的引用计数由 struct kref 管理,确保在主机控制器处理期间不会被意外释放。下图展示了引用计数的变化流程:

URB 引用计数生命周期 usb_alloc_urb refcnt = 1 usb_submit_urb refcnt = 2 传输完成 refcnt = 1 usb_free_urb refcnt = 0 → 释放 提交后 HCD 持有引用 驱动可安全调用 usb_free_urb

4.7 常见陷阱与最佳实践

  • 重复释放:usb_free_urb 内部通过引用计数防止双重释放,但驱动仍应避免在回调中再次释放已释放的 URB。
  • 中断上下文分配:在中断处理函数或定时器回调中分配 URB 时,必须使用 GFP_ATOMIC,禁止使用 GFP_KERNEL(可能睡眠)。
  • URB 复用:通过 usb_submit_urb 提交后,若需再次使用同一 URB,必须先调用 usb_kill_urb 或 usb_unlink_urb 确保其不再被主机控制器引用。
  • 内存泄漏检测:在驱动卸载路径中,使用 usb_poison_urb 强制终止所有待处理 URB,再逐个释放。
核心原则:usb_alloc_urb 与 usb_free_urb 必须成对出现,且释放前确保 URB 已从主机控制器中安全移除。引用计数机制为并发安全提供了保障,但驱动开发者仍需遵循正确的生命周期管理顺序。