一、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. * \returns 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支持,不考虑),仅仅通过读取、解析操作系统暴露给应用的文件就拿到了所需信息。