一次 Kingston U 盘重定向中获取 Device Descriptor 超时问题排查

背景

最近在实现一个基于 USB/IP 的 U 盘重定向功能。整体架构大致如下:

  • 本地 Windows 客户端通过 libusb + UsbDk 访问真实 USB 设备;
  • 客户端接收远端 USB/IP 请求;
  • 本地通过 libusb_control_transfer、libusb_bulk_transfer 等接口完成真实 USB 通信;
  • 服务端 Linux 通过 vhci_hcd 创建虚拟 USB 设备。

测试过程中,某款 Kingston DataTraveler 3.0 U 盘在多次重定向、断网重连后,偶现服务端无法正常枚举设备,最终远端无法出现可用盘符或块设备。

现象

服务端 dmesg 中可以看到类似日志:

复制代码
unable to get BOS descriptor or descriptor too short
unable to read config index 0 descriptor/all
can't read configurations, error -110
unable to enumerate USB device

其中 error -110 对应 Linux 的 ETIMEDOUT,说明服务端 USB 栈在枚举阶段等待描述符响应超时。

客户端日志中也能看到 libusb_control_transfer 返回超时:

复制代码
libusb_control_transfer err: -7
descType=2

-7 对应:

复制代码
LIBUSB_ERROR_TIMEOUT

最初从日志看,失败点主要集中在:

  • BOS descriptor,descType = 15
  • Configuration descriptor,descType = 2

因此一开始怀疑是 BOS 或 config descriptor 请求本身存在问题。

排查过程

为了避免只依赖完整产品流程,我们单独写了一个测试程序,对同一个 U 盘反复执行不同类型的 descriptor 请求。

测试项包括:

复制代码
GET_DESCRIPTOR DEVICE, wLength = 8
GET_DESCRIPTOR DEVICE, wLength = 18
GET_DESCRIPTOR BOS
GET_DESCRIPTOR CONFIG
libusb_get_device_descriptor 缓存路径
libusb_get_config_descriptor 缓存路径

经过多轮测试,发现一个关键现象:

  • 单独反复请求 BOS descriptor,基本稳定;
  • 单独反复请求 config descriptor,基本稳定;
  • 使用 libusb_get_device_descriptor() 缓存路径,基本稳定;
  • 但反复真实下发 GET_DESCRIPTOR DEVICE 请求时,较容易出现 timeout;
  • wLength = 8 和 wLength = 18 都可能触发。

也就是说,表面上服务端失败点可能落在 BOS/config descriptor,但真正更可疑的是前面反复执行的 device descriptor control 请求。

为什么会有 8 字节 Device Descriptor 请求

USB 枚举过程中,主机经常会先读取 device descriptor 的前 8 字节,用于获取:

复制代码
bMaxPacketSize0

也就是端点 0 的最大包大小。

完整 device descriptor 长度是 18 字节,但请求中的 wLength 不一定只会是 18。常见请求包括:

复制代码
wValue = 0x0100, wLength = 8
wValue = 0x0100, wLength = 18

甚至也可能出现更大的 wLength,设备通过 short packet 返回实际的 18 字节。

代码中的问题

原来的代码已经对完整的 18 字节 device descriptor 做了特殊处理:

复制代码
if (GET_DESCRIPTOR DEVICE && wLength == LIBUSB_DT_DEVICE_SIZE)
{
    libusb_get_device_descriptor(...);
}

也就是说,当远端请求 18 字节 device descriptor 时,本地不会真实下发 control transfer,而是直接使用 libusb 缓存的 device descriptor。

但是对于 8 字节请求,旧逻辑没有覆盖,仍然会走:

复制代码
libusb_control_transfer(...)

实际日志中也验证了这一点:

复制代码
route=control
wValue=0x100
wLength=8
descType=1

这意味着每次重定向或断网重连时,都会真实向设备发送一次 GET_DESCRIPTOR DEVICE, wLength=8。

修复思路

修复方向是:所有标准 device descriptor 请求都走缓存路径,而不是只处理 18 字节请求。

判断条件从:

复制代码
wLength == LIBUSB_DT_DEVICE_SIZE

改为:

复制代码
wLength > 0

并返回:

复制代码
min(wLength, LIBUSB_DT_DEVICE_SIZE)

示例逻辑:

复制代码
if (requestType == LIBUSB_ENDPOINT_IN &&
    bRequest == LIBUSB_REQUEST_GET_DESCRIPTOR &&
    wValue == (LIBUSB_DT_DEVICE << 8) &&
    wIndex == 0 &&
    wLength > 0)
{
    struct libusb_device_descriptor desc = {0};

    status = libusb_get_device_descriptor(dev, &desc);
    if (status == LIBUSB_SUCCESS)
    {
        desc.bcdUSB = libusb_cpu_to_le16(desc.bcdUSB);
        desc.idVendor = libusb_cpu_to_le16(desc.idVendor);
        desc.idProduct = libusb_cpu_to_le16(desc.idProduct);
        desc.bcdDevice = libusb_cpu_to_le16(desc.bcdDevice);

        uint16_t len = std::min<uint16_t>(wLength, LIBUSB_DT_DEVICE_SIZE);
        memcpy(data, &desc, len);
        status = len;
    }
}

这里需要注意,libusb_get_device_descriptor() 返回的是 host-endian 结构体,而 USB descriptor 在线路上是 little-endian。因此多字节字段最好通过 libusb_cpu_to_le16() 转换后再返回。

结论

这次问题的核心不是简单的 BOS/config descriptor 请求失败,而是:

在 libusb + UsbDk + Kingston DataTraveler 3.0 的组合场景下,反复真实下发 GET_DESCRIPTOR DEVICE 请求,尤其是 8 字节请求,可能导致设备或 EP0 control 通道进入不稳定状态,后续 BOS/config 请求表现为 timeout。

修复后,将所有 device descriptor 请求统一改为使用 libusb_get_device_descriptor() 的缓存结果返回,避免重复真实访问设备 EP0,问题得到规避。

经验总结

  1. descriptor timeout 的失败点不一定是真正根因。
  2. USB 枚举中的 8 字节 device descriptor 请求非常常见,不能只处理 18 字节版本。
  3. libusb_get_device_descriptor() 是缓存路径,不会再次向设备发送 control 请求。
  4. 在 USB/IP 重定向场景中,尽量减少对 EP0 的重复真实请求。
  5. 对特殊 U 盘兼容性问题,最好用独立测试程序拆分验证,而不是只看完整产品日志。
相关推荐
钱栈up37 分钟前
电脑卡死7分钟,日志断在13:56:03:一次Windows硬挂死的根因复盘
windows
宛 禾1 小时前
Qt 控件入门:QLabel 从拖拽到对象树,一篇讲透
c++·qt·学习
hetao17338371 小时前
联盟场-提高组-R7 & ZYZSC-S-Round 11
c++·算法
程序喵大人1 小时前
【C++入门】编译链接模型 - 07 链接器怎样把符号对上号
开发语言·c++·编译器·编译链接·链接器
局部有解2 小时前
Hot100 - 三数之和
数据结构·c++·算法
longlongzihan2 小时前
C++ 常用数据结构速查:vector、stack、queue、list、树、图、哈希表等声明与核心操作
数据结构·c++·list
我是海飞2 小时前
使用杰理 AC7925B 实现 Windows电脑 USB 副屏
windows·单片机·电脑·嵌入式·杰理
别动我齐刘海2 小时前
实时控制学习2
linux·c++·人工智能·学习·算法
hetao17338373 小时前
校内场-提高组 ZYZSC-S-Round 2
c++·算法
律宏阔4 小时前
Windows 双网卡配置:通过跃点数实现网线访问局域网、Wi-Fi 访问公网
windows