zoukankan      html  css  js  c++  java
  • libusb(2)listdevs 实现分析

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

  • 相关阅读:
    不错的电影(先收藏着)
    getchar() getch() getche() gets() puts() scanf()的用法及区别
    java反射获取字段的属性值,以及为字段赋值等方法
    oracle将查询结果横转纵
    关于MySQL 的LEFT JOIN ON的问题
    MySQL表名和数据库关键字相同解决办法
    ajax 后台正常执行 错误类型却是404
    BIT 树状数组 详解 及 例题
    HDU 2689 Sort it (树状数组)
    HDU Cow Sorting (树状数组)
  • 原文地址:https://www.cnblogs.com/rockyching2009/p/14174818.html
Copyright © 2011-2022 走看看