一、listdevs 简介
listdevs 用于获取并显示系统当前的 USB 设备信息,包含:VID、PID、bus 编号、设备地址、端口号。
$ ./listdevs 1d6b:0002 (bus 1, device 1) 0e0f:0002 (bus 2, device 3) path: 2 0e0f:0003 (bus 2, device 2) path: 1 1d6b:0001 (bus 2, device 1)
二、listdevs 入口
int main(void)
{
libusb_device **devs;
int r;
ssize_t cnt;
r = libusb_init(NULL);
if (r < 0)
return r;
cnt = libusb_get_device_list(NULL, &devs);
if (cnt < 0){
libusb_exit(NULL);
return (int) cnt;
}
print_devs(devs);
libusb_free_device_list(devs, 1);
libusb_exit(NULL);
return 0;
}
main 函数很清晰简洁,这也说明 libusb 库的封装非常好。
骨架函数就三个:libusb_init() -> libusb_get_device_list() -> print_devs(),分别对应 libusb 初始化,获取 USB 设备列表,打印 USB 设备列表(信息)。
libusb 规定,在调用任何 libusb 功能函数之前都需要 libusb_init(),在分析 libusb_init() 之前先来看下 libusb_device{} 结构体:
struct libusb_device {
/* lock protects refcnt, everything else is finalized at initialization
* time */
usbi_mutex_t lock;
int refcnt;
struct libusb_context *ctx;
struct libusb_device *parent_dev;
uint8_t bus_number;
uint8_t port_number;
uint8_t device_address;
enum libusb_speed speed;
struct list_head list;
unsigned long session_data;
struct libusb_device_descriptor device_descriptor;
int attached;
}
libusb_device{} 描述一个 USB 设备的信息:
lock/refcnt:引用计数,一旦有其他地方引用到该设备,refcnt 值加一;避免设备在其他模块内部使用的情况下被销毁。自动指针、动态内存管理里面广泛使用引用计数。
libusb_context:该设备所属的上下文,官方术语叫“libusb session”。通过在 libusb_init() 时指定互相独立的“libusb session”,进而允许你的程序独立的使用 libusb,而不会因为其他地方调用 libusb_exit() 而销毁自己正在使用的资源。“libusb session”使用 session_id 唯一标识。
parent_dev:当前设备所属的父设备(通常指控制器/Hub)。
bus_number/port_number/device_address/speed:这些是 USB 规范及内核驱动里的概念,speed 标识设备速率(低速,全速,高速,超高速),device_address 标识内核驱动枚举时为设备分配的地址,port_number 标识设备占用的 Hub 端口号,bus_number 标识设备所属的总线编号。
list:每一个“libusb session”有个 usb_devs 链表,usb_devs 链表上保存属于该 session 的 USB 设备。在构件完成一个设备之后,就把它加入到自己所属的 usb_devs 链表上。
session_data:保存 session_id。
device_descriptor:顾名思义,该设备的设备描述符。
attached:标识设备状态是 attached(已连接上主机)还是 detached(未连接主机)。
接下来就看下 libusb_init() 做了哪些事情。
三、libusb_init() 分析
3.1 libusb_init() 注释
/** ingroup libusb_lib
* Initialize libusb. This function must be called before calling any other
* libusb function.
*
* If you do not provide an output location for a context pointer, a default
* context will be created. If there was already a default context, it will
* be reused (and nothing will be initialized/reinitialized).
*
* param context Optional output location for context pointer.
* Only valid on return code 0.
*
eturns 0 on success, or a LIBUSB_ERROR code on failure
* see libusb_contexts
*/
int libusb_init(libusb_context **context) {}
libusb_init()初始化 libusb 内部资源,所以在使用 libusb 其他函数之前,必需优先调用该函数。
context:listdevs 这个demo中入参为 NULL,即使用默认的 libusb_context,即 usbi_default_context,在 3.5 小节对其进行创建。
3.2 struct timespec timestamp_origin
int libusb_init(libusb_context **context)
{
if (!timestamp_origin.tv_sec)
usbi_get_monotonic_time(×tamp_origin);
}
调用 clock_gettime() 初始化 timestamp_origin 全局变量,timestamp_origin 用于 log 打印的时间戳显示。
3.3 又是引用计数
int libusb_init(libusb_context **context)
{
if (!context && usbi_default_context) {
usbi_dbg("reusing default context");
default_context_refcnt++;
usbi_mutex_static_unlock(&default_context_lock);
return 0;
}
}
3.4 log 设置
int libusb_init(libusb_context **context)
{
ctx->debug = get_env_debug_level();
if (ctx->debug != LIBUSB_LOG_LEVEL_NONE)
ctx->debug_fixed = 1;
}
既然是分析源码,我们自然要把 log 输出都打印出来,所以修改 get_env_debug_level() 函数,使其返回 LIBUSB_LOG_LEVEL_DEBUG。
3.5 创建 usbi_default_context
int libusb_init(libusb_context **context)
{
/* default context should be initialized before calling usbi_dbg */
if (!usbi_default_context) {
usbi_default_context = ctx;
default_context_refcnt++;
usbi_dbg("created default context");
}
usbi_dbg("libusb v%u.%u.%u.%u%s", libusb_version_internal.major, libusb_version_internal.minor,
libusb_version_internal.micro, libusb_version_internal.nano, libusb_version_internal.rc);
}
struct libusb_version:libusb 的版本号定义很规范!这里值得学习~
3.6 锁/链表初始化
int libusb_init(libusb_context **context)
{
usbi_mutex_init(&ctx->usb_devs_lock);
usbi_mutex_init(&ctx->open_devs_lock);
usbi_mutex_init(&ctx->hotplug_cbs_lock);
list_init(&ctx->usb_devs);
list_init(&ctx->open_devs);
list_init(&ctx->hotplug_cbs);
ctx->next_hotplug_cb_handle = 1;
}
这里提一句 libusb 的链表:struct list_head { struct list_head *prev, *next; }; 自然,这是从 Linux 内核移植过来的,其精妙无需赘言,至今见过好多开源项目使用该链表。
3.7 libusb_context 链表
int libusb_init(libusb_context **context)
{
if (first_init) {
first_init = 0;
list_init(&active_contexts_list);
}
list_add (&ctx->list, &active_contexts_list);
}
active_contexts_list 是用来管理 libusb_context 链表,源于先前提到的“libusb session”思想。
3.8 重头戏来也:op_init()
int libusb_init(libusb_context **context)
{
if (usbi_backend.init) {
r = usbi_backend.init(ctx);
if (r)
goto err_free_ctx;
}
}
usbi_backend.init 指向 op_init() 函数:
int op_init(struct libusb_context *ctx)
{
/* 通过 uname() 系统调用获取内核版本号 */
if (get_kernel_version(ctx, &kversion) < 0) {}
/* 检查系统版本是否满足 libusb 的要求 */
if (!kernel_version_ge(&kversion, 2, 6, 32)) {}
/* 查找 usbfs_path,最终结果为 "/dev/bus/usb" */
usbfs_path = find_usbfs_path();
/* 同步传输包限制大小,同步传输时用到 */
if (!max_iso_packet_len) {
if (kernel_version_ge(&kversion, 5, 2, 0))
max_iso_packet_len = 98304;
else if (kernel_version_ge(&kversion, 3, 10, 0))
max_iso_packet_len = 49152;
else
max_iso_packet_len = 8192;
}
/* 检查 sysfs 是否正常 */
if (sysfs_available == -1) {
struct statfs statfsbuf;
r = statfs(SYSFS_MOUNT_PATH, &statfsbuf);
if (r == 0 && statfsbuf.f_type == SYSFS_MAGIC) {
usbi_dbg("sysfs is available");
sysfs_available = 1;
} else {
usbi_warn(ctx, "sysfs not mounted");
sysfs_available = 0;
}
}
if (init_count == 0) {
/* 启动热插拔监听线程,后续讲到 hotplugtest 这个 demo 的时候再谈 */
r = linux_start_event_monitor();
}
if (r == LIBUSB_SUCCESS) {
/* 扫描系统中的 USB 设备,这里是 listdevs 功能的核心实现 */
r = linux_scan_devices(ctx);
if (r == LIBUSB_SUCCESS)
init_count++;
else if (init_count == 0)
linux_stop_event_monitor();
} else {
usbi_err(ctx, "error starting hotplug event monitor");
}
return r;
}
3.9 系统中 USB 设备的扫描
没有使用 libudev,所以 linux_scan_devices() 调用流程为:linux_scan_devices() -> linux_default_scan_devices() -> sysfs_get_device_list()
3.9.1 sysfs_get_device_list()
int sysfs_get_device_list(struct libusb_context *ctx)
{
DIR *devices = opendir("/sys/bus/usb/devices");
while ((entry = readdir(devices))) {
if ((!isdigit(entry->d_name[0]) && strncmp(entry->d_name, "usb", 3))
|| strchr(entry->d_name, ':'))
continue;
num_devices++;
if (sysfs_scan_device(ctx, entry->d_name)) {
usbi_dbg("failed to enumerate dir entry %s", entry->d_name);
continue;
}
num_enumerated++;
}
closedir(devices);
/* successful if at least one device was enumerated or no devices were found */
if (num_enumerated || !num_devices)
return LIBUSB_SUCCESS;
else
return LIBUSB_ERROR_IO;
}
来看下 /sys/bus/usb/devices 目录里面都有什么:

USB 设备在 sysfs 中的表示:(host/roothub)-port : configuration.(interface/endpoint)
例如,1-3:1.0 表示:Hub 编号为 1,使用 3 号端口,配置为 1,端点为 0。
所以遍历 devices 时就过滤掉了":"式的目录。
int sysfs_scan_device(struct libusb_context *ctx, const char *devname)
{
uint8_t busnum, devaddr;
int ret;
ret = linux_get_device_address(ctx, 0, &busnum, &devaddr, NULL, devname, -1);
if (ret != LIBUSB_SUCCESS)
return ret;
return linux_enumerate_device(ctx, busnum, devaddr, devname);
}
3.9.2 linux_get_device_address()
int linux_get_device_address(struct libusb_context *ctx, int detached,
uint8_t *busnum, uint8_t *devaddr, const char *dev_node,
const char *sys_name, int fd)
{
int sysfs_val;
int r;
r = read_sysfs_attr(ctx, sys_name, "busnum", UINT8_MAX, &sysfs_val);
if (r < 0)
return r;
*busnum = (uint8_t)sysfs_val;
r = read_sysfs_attr(ctx, sys_name, "devnum", UINT8_MAX, &sysfs_val);
if (r < 0)
return r;
*devaddr = (uint8_t)sysfs_val;
return LIBUSB_SUCCESS;
}
得益于 sysfs 的便捷,需要获取什么信息直接读取对应的文件就可以了。
比如,如果当前遍历 usb1 目录,那么读 usb1/busnum 文件就得到可 bus 编号。
3.9.3 linux_enumerate_device()
int linux_enumerate_device(struct libusb_context *ctx,
uint8_t busnum, uint8_t devaddr, const char *sysfs_dir)
{
unsigned long session_id;
struct libusb_device *dev;
int r;
session_id = busnum << 8 | devaddr;
dev = usbi_get_device_by_session_id(ctx, session_id);
if (dev) {
/* 设备已经存在,又是引用计数 */
usbi_dbg("session_id %lu already exists", session_id);
libusb_unref_device(dev);
return LIBUSB_SUCCESS;
}
/* 为该设备分配内存 */
dev = usbi_alloc_device(ctx, session_id);
/* 设备信息初始化,这里重点关注描述符的信息获取 */
r = initialize_device(dev, busnum, devaddr, sysfs_dir, -1);
/* 根据 initialize_device() 读取的描述符信息做进一步的合法性检查 */
r = usbi_sanitize_device(dev);
r = linux_get_parent_info(dev, sysfs_dir);
usbi_connect_device(dev);
return r;
}
linux_get_parent_info() 父设备信息获取流程:
- 如果当前目录是 usbX,说明位于 roothub 下,没有父设备,跳过后续流程;
- 如果当前目录是 x-y,说明有父设备,且父设备为 roothub,名称为 "usbx";
- 如果当前目录是 x-y:a.b,说明有父设备,名称为 "x-y:a";
- 然后在 libusb_context 上遍历,如果有对应父设备名称的设备,说明父设备已经枚举过;否则就需要调用 sysfs_scan_device() 进行一次设备扫描。
3.9.4 usbi_connect_device()
void usbi_connect_device(struct libusb_device *dev)
{
list_add(&dev->list, &dev->ctx->usb_devs);
/* Signal that an event has occurred for this device if we support hotplug AND
* the hotplug message list is ready. This prevents an event from getting raised
* during initial enumeration. */
if (libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG) && dev->ctx->hotplug_msgs.next) {
usbi_hotplug_notification(ctx, dev, LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED);
}
}
对于 listdevs 来说,关键信息就这个 list_add(),把设备加入到 libusb_context 的设备链表上。从这里也可以看出,libusb_context 是一个贯穿始终的东东。
四、USB 描述符的获取
USB 描述符的知识参阅:果壳中的USB(5)USB描述符。
描述符信息在 initialize_device() 读取,下述代码段只表现描述符相关:
int initialize_device(struct libusb_device *dev, uint8_t busnum,
uint8_t devaddr, const char *sysfs_dir, int wrapped_fd)
{
struct linux_device_priv *priv = usbi_get_device_priv(dev);
struct libusb_context *ctx = DEVICE_CTX(dev);
size_t alloc_len;
int fd, r;
ssize_t nb;
/* 1. 打开文件:/sys/bus/usb/devices/usb1/descriptors */
fd = open_sysfs_attr(ctx, sysfs_dir, "descriptors");
alloc_len = 0;
/* 2. 读取文件内容 */
do {
const size_t desc_read_length = 256;
uint8_t *read_ptr;
alloc_len += desc_read_length;
priv->descriptors = usbi_reallocf(priv->descriptors, alloc_len);
read_ptr = (uint8_t *)priv->descriptors + priv->descriptors_len;
nb = read(fd, read_ptr, desc_read_length);
priv->descriptors_len += (size_t)nb;
} while (priv->descriptors_len == alloc_len);
close(fd);
/* 每一个描述符都有大小规定,这些信息可以参阅内核的 include/uapi/linux/usb/ch9.h,
* 此文件相当于 USB 规范第九章的代码化
*/
if (priv->descriptors_len < LIBUSB_DT_DEVICE_SIZE) {
usbi_err(ctx, "short descriptor read (%zu)", priv->descriptors_len);
return LIBUSB_ERROR_IO;
}
/* 3. 配置描述符解析 */
r = parse_config_descriptors(dev);
/* 4. 得到设备描述符 */
memcpy(&dev->device_descriptor, priv->descriptors, LIBUSB_DT_DEVICE_SIZE);
if (sysfs_dir) {
/* sysfs descriptors are in bus-endian format */
usbi_localize_device_descriptor(&dev->device_descriptor);
return LIBUSB_SUCCESS;
}
/* 5. 获取当前设备使用的配置描述符 */
if (wrapped_fd < 0)
/* 5.1 打开文件节点:/dev/bus/usb/001/001 */
fd = get_usbfs_fd(dev, O_RDWR, 1);
else
fd = wrapped_fd;
if (fd < 0) {
/* cannot send a control message to determine the active
* config. just assume the first one is active. */
usbi_warn(ctx, "Missing rw usbfs access; cannot determine "
"active configuration descriptor");
if (priv->config_descriptors)
priv->active_config = priv->config_descriptors[0].desc->bConfigurationValue;
else
priv->active_config = 0; /* No config dt */
return LIBUSB_SUCCESS;
}
/* 5.2 通过控制传输,向设备发送 GET_CONFIGURATION 命令获取配置描述符 */
r = usbfs_get_active_config(dev, fd);
if (fd != wrapped_fd)
close(fd);
return r;
}
4.1 配置描述符解析:parse_config_descriptors()
在分析 parse_config_descriptors() 之前,我们人肉解析一边:
# hexdump -C /sys/bus/usb/devices/usb1/descriptors 00000000 12 01 00 02 09 00 01 40 6b 1d 02 00 15 04 03 02 |.......@k.......| 00000010 01 01 09 02 19 00 01 01 00 e0 00 09 04 00 00 01 |................| 00000020 09 00 00 00 07 05 81 03 04 00 0c |...........|
第一字节:0x12,bLength 指示描述符长度。
第二字节:0x01,bDescriptionType 指示描述符类型。
于是就得到了一个设备描述符:
12 01 00 02 09 00 01 40 6b 1d 02 00 15 04 03 02 01 01
同样的分析方法,解析出一个配置描述符:
09 02 19 00 01 01 00 e0 00
一个接口描述符:
09 04 00 00 01 09 00 00 00
一个端点描述符:
07 05 81 03 04 00 0c
经过以上步骤,parse_config_descriptors() 也就无需分析了。
4.2 获取当前设备使用的配置描述符
这里实际就一个 ioctl 调用,不表。
可以看出,这部分代码需要内核支持 usbfs:
usbfs:This lets devices provide ways to expose information to user space regardless of where they do (or don't) show up otherwise in the filesystem.
五、打印 USB 设备列表
print_devs(),无需赘述。
六、listdevs 分析总结
经过整体功能的源码分析,发现功能实现并没有实际操作系统中的 USB 设备(4.2 需要usbfs支持,不考虑),仅仅通过读取、解析操作系统暴露给应用的文件就拿到了所需信息。