Linux URB(USB Request Block)是Linux内核USB子系统中最核心的数据结构,代表一次向USB设备发送或接收数据的请求,掌握URB的运作机制是开发Linux USB驱动的基础,也是优化USB传输性能的关键。
什么是Linux URB?核心概念与数据结构
URB本质上是一个数据包裹,封装了USB传输所需的所有要素:目标端点、传输数据缓冲区、传输长度、完成回调函数等,内核通过URB对象管理和调度所有USB通信。
linux urb结构体成员详解
struct urb包含以下关键字段,理解这些字段是正确初始化和使用URB的前提:
- transfer_buffer:指向数据缓冲区的指针
- transfer_buffer_length:缓冲区长度
- actual_length:实际传输的字节数,由内核在完成时填充
- status:传输状态,0表示成功,负值为错误码(-ENOENT表示URB被杀死,-ECONNRESET表示设备断开)
- complete:URB完成时调用的回调函数指针,通过它驱动获得传输结果
- endpoint:目标USB端点地址(低4位为端点号,高1位为方向)
- dev:指向struct usb_device的指针,指示URB属于哪个设备
- transfer_flags:控制URB行为的标志位,例如URB_NO_TRANSFER_DMA_MAP用于绕过DMA映射
URB的生命周期:从分配到释放
- 分配:调用
usb_alloc_urb(int iso_packets, gfp_t mem_flags)创建URB对象,iso_packets参数仅适用于等时传输,用于指定等时数据包个数。 - 填充:根据传输类型使用专用函数初始化URB,例如
usb_fill_bulk_urb、usb_fill_int_urb、usb_fill_control_urb,这些函数会设置端点、管道、缓冲区、长度和回调。 - 提交:调用
usb_submit_urb(struct urb urb, gfp_t mem_flags)将URB交给USB核心,提交后URB进入内核调度队列,驱动应当避免修改URB关键字段。 - 完成回调:当数据传输结束后,内核在中断上下文中调用complete回调函数,驱动在回调中检查status和actual_length,根据结果决定是否重新提交或释放。
- 释放:使用
usb_free_urb(struct urb urb)回收URB内存,在此之前必须确保URB已经杀死(usb_kill_urb)或已经完成。
URB传输类型及其应用场景
| 传输类型 | 端点类型 | 典型应用 | 特点 |
|---|---|---|---|
| 控制传输 | 控制端点 | 设备枚举、配置命令 | 双向、可靠性高 |
| 批量传输 | 批量端点 | U盘、打印机、串口适配器 | 高速批量数据,无实时保证 |
| 中断传输 | 中断端点 | 键盘、鼠标、游戏手柄 | 固定轮询间隔,低延迟 |
| 等时传输 | 等时端点 | 音频、视频流 | 固定带宽,无错误重传 |
Linux URB使用场景:嵌入式驱动开发中的常见操作
在实际嵌入式Linux开发中,URB几乎出现在所有USB驱动中,不同的传输类型对应不同场景。
批量传输URB在USB存储设备中的应用
USB存储(U盘、移动硬盘)使用批量传输实现大数据块交换,典型驱动会维护一个URB链表,维持多个并发的批量URB以保持吞吐,填充流程:
- 使用
usb_fill_bulk_urb指定管道方向和数据缓冲区。 - 提交后,在回调中检查status,如果成功则提取actual_length数据,并提交下一个URB填补队列空缺。
- 当设备断开时,批量URB会收到-ESHUTDOWN错误,此时驱动需停止所有URB并清理。
中断传输URB在HID设备中的应用
USB键盘、鼠标等HID设备依赖中断传输,中断URB的核心在于周期轮询:驱动提交中断URB后,USB核心会在每个帧/微帧发起传输,当数据到达时调用回调。
- 使用
usb_fill_int_urb设置轮询间隔(bInterval字段)。 - 回调中解析HID报告并上报输入子系统。
- 注意回调运行在中断上下文,不能调用可能导致睡眠的函数。
等时传输URB在音频/视频设备中的应用
对于USB摄像头、音频设备,等时传输能保证固定速率和数据完整性,等时URB的填充略有不同,需要指定iso_packets个数和每个包的偏移与长度(通过iso_frame_desc数组),开发者在回调中需处理帧边界和可能的数据丢失情况,因为等时传输不保证重传。
Linux URB编程实战:从urb_alloc到urb_submit
linux urb编程步骤详解
- 分配URB:
struct urb urb = usb_alloc_urb(0, GFP_KERNEL); if (!urb) return -ENOMEM; - 填充URB:
- 批量传输:
usb_fill_bulk_urb(urb, dev, usb_rcvbulkpipe(dev, ep), buf, len, complete_fn, context); - 控制传输:
usb_fill_control_urb(urb, dev, usb_rcvctrlpipe(dev, 0), (u8)setup_pkt, buf, len, complete_fn, context); - 中断传输:
usb_fill_int_urb(urb, dev, usb_rcvintpipe(dev, ep), buf, len, complete_fn, context, interval);
- 批量传输:
- 设置回调上下文:可通过
urb->context传递一个私有数据结构,回调中通过urb->context获取。 - 提交URB:
ret = usb_submit_urb(urb, GFP_KERNEL); if (ret) { usb_free_urb(urb); return ret; } - 在回调函数中处理:
static void urb_complete(struct urb urb) { struct my_dev dev = urb->context; if (urb->status == 0) { // 使用 urb->actual_length 处理数据 // 如需持续传输,可在此再次提交 } else { // 处理错误或通知上层 } } - 结束时确保清理:在驱动卸载或设备断开时调用
usb_kill_urb(urb)或usb_poison_urb(urb)杀死所有活跃URB,然后调用usb_free_urb释放。
多URB队列管理策略
为了提高吞吐量,许多驱动会同时维护多个URB形成流水线,常用做法是预分配一个URB池(数组或链表),在驱动打开时提交所有URB,在回调中重新填入缓冲区并再次提交,需要注意的是,所有URB必须在提交前正确初始化,并在驱动关闭时全部杀死并释放,保持队列深度可以达到硬件全速性能。
URB提交失败的处理策略
常见失败原因包括:
- -ENOMEM:内存不足时,可缩小缓冲区大小或推迟提交。
- -ESHUTDOWN:设备已断开,应立即停止所有URB并通知上层。
- -EPIPE:端点处于停止状态,需发送
usb_clear_halt清除停止条件后重新提交。 - -EAGAIN:资源临时不可用,可稍后重试(通常不建议频繁重试)。
Linux URB调试技巧与常见问题
linux urb调试常用方法
- usbmon工具:通过
cat /sys/kernel/debug/usb/usbmon/0t | grep 设备地址实时查看URB提交和完成事件,是调试协议级问题的最佳方法。 - 内核动态调试:启用USB核心的URB日志:
echo 'file drivers/usb/core/urb.c +p' > /sys/kernel/debug/dynamic_debug/control随后在dmesg中可看到每个URB的分配、提交、完成信息。
-
sysfs设备信息
:查看/sys/kernel/debug/usb/devices可获取当前所有USB设备及端点配置。 - kprobe工具:在usb_submit_urb和complete函数上设置探测点,适用于难以复现的时序问题。
linux urb与usb-skeleton驱动对比
| 维度 | USB Skeleton驱动(drivers/usb/usb-skeleton.c) | 标准URB直接编程 |
|---|---|---|
| 设计目标 | 教学示例,简化URB管理 | 灵活适配各类设备 |
| URB管理方式 | 通过urb结构体数组预分配,内部自动完成队列 | 手动分配、提交、回收 |
| 错误处理 | 简单打印错误并重启 | 需自行处理各种错误码和设备重枚举 |
| 适用场景 | 学习基本框架 | 实际产品级驱动开发 |
行业共识认为,初学者可从Skeleton驱动入门,但理解原生URB接口是应对复杂协议和性能优化的先决条件。
常见问题解答(Q&A)
问题1:Linux URB提交失败后如何排查?
首先检查提交函数返回值对应的错误码:-ENOMEM考虑内存压力;-ESHUTDOWN表示设备已断开;-EPIPE表示端点处于停止状态,需发送usb_clear_halt清除停止,然后启用usbmon捕获USB总线上的数据包,确认URB是否正确送达,最后开启内核动态调试,观察URB提交和完成事件,锁定失败阶段。
问题2:在Linux URB中如何高效使用DMA缓冲区?
将urb->transfer_flags设置URB_NO_TRANSFER_DMA_MAP标志,并使用usb_alloc_coherent或dma_alloc_coherent分配DMA一致性缓冲区,将物理地址赋给urb->transfer_dma,同时将urb->transfer_buffer置为NULL,这样内核会跳过DMA映射步骤,减少CPU消耗,注意缓冲区需对齐到cacheline大小,并在释放URB后调用usb_free_coherent回收。
问题3:Linux URB回调函数在什么上下文中执行?
URB的complete回调通常在中断上下文(hardirq)中执行,部分内核版本可能使用tasklet或工作队列,但驱动应默认按中断上下文处理,在回调中不能调用任何可能导致睡眠的函数(如kmalloc(GFP_KERNEL)),只能使用GFP_ATOMIC分配内存,如果回调中需要执行复杂操作(如复制数据到用户空间),应通过工作队列将任务推迟到进程上下文执行,这是URB编程中常见的陷阱,务必遵守。
首发原创文章,作者:世雄 - 原生数据库架构专家,如若转载,请注明出处:https://idctop.com/article/495399.html



