___2017

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理
  328 随笔 :: 18 文章 :: 15 评论 :: 18万 阅读

 


一、listdevs 简介

listdevs 用于获取并显示系统当前的 USB 设备信息,包含:VID、PID、bus 编号、设备地址、端口号。

1
2
3
4
5
$ ./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 入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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{} 结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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() 注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** \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

1
2
3
4
5
int libusb_init(libusb_context **context)
{
    if (!timestamp_origin.tv_sec)
        usbi_get_monotonic_time(&timestamp_origin);
}

调用 clock_gettime() 初始化 timestamp_origin 全局变量,timestamp_origin 用于 log 打印的时间戳显示。

3.3 又是引用计数

1
2
3
4
5
6
7
8
9
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 设置

1
2
3
4
5
6
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

1
2
3
4
5
6
7
8
9
10
11
12
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 锁/链表初始化

1
2
3
4
5
6
7
8
9
10
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 链表

1
2
3
4
5
6
7
8
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()

1
2
3
4
5
6
7
8
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() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 时就过滤掉了":"式的目录。

1
2
3
4
5
6
7
8
9
10
11
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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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()

1
2
3
4
5
6
7
8
9
10
11
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() 读取,下述代码段只表现描述符相关:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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() 之前,我们人肉解析一边:

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

posted on   yin'xiang  阅读(1752)  评论(1编辑  收藏  举报
编辑推荐:
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示