容器环境与可插拔设备(一)---udev

近期在调试一个环境,运行的情况主要是宿主机+lxc容器+linux容器系统。有点类似于在宿主机上运行一个docker,然后docker中运行了一个容器系统。

然后有同事反馈了一个问题:为什么我在容器环境中配置了udev的一些rule,但是这些rule全部都没有生效。最典型的一个rule就是:

$ cat /etc/udev/rules.d/uinput.rules 
KERNEL=="uinput", MODE="0666"

但是系统的uinput权限始终都是0664。
linux内核中,有三个比较关键同时也很特殊的文件系统,其文件系统中的内容,均是内核的数据映射出来的。

  • proc
  • dev
  • sys

proc

proc主要是对于内核中管理的进程的状态信息做了映射,我们通过proc这个文件系统,能够很方便的在用户空间里面获取各个进程的状态信息。最简单的,ps命令,大部分的数据就是从/proc中读取出来的。

$ strace ps -ef
openat(AT_FDCWD, "/proc/548404/status", O_RDONLY) = 6
read(6, "Name:\tkworker/3:3-mm_percpu_wq\nU"..., 2048) = 767
close(6)                                = 0
openat(AT_FDCWD, "/proc/548404/cmdline", O_RDONLY) = 6
read(6, "", 131072)                     = 0
close(6)

另外再举一个例子。比如我想要获取weston这个进程运行时候的环境变量(不包括运行过程中创建的环境变量)。

sed 's:\x0:\n:g' /proc/$(pidof weston)/environ
SHELL=/usr/bin/bash
QT_ACCESSIBILITY=1
XDG_SESSION_PATH=/org/freedesktop/DisplayManager/Session0
GTK_IM_MODULE=fcitx
LANGUAGE=zh_CN.UTF-8
XDG_DATA_HOME=/home/user/.local/share
XDG_CONFIG_HOME=/home/user/.config
XMODIFIERS=@im=fcitx
DESKTOP_SESSION=deepin
XDG_SEAT=seat0
PWD=/home/user
XDG_SESSION_DESKTOP=deepin
LOGNAME=user
XDG_SESSION_TYPE=wayland
GPG_AGENT_INFO=/run/user/1000/gnupg/S.gpg-agent:0:1
XDG_GREETER_DATA_DIR=/var/lib/lightdm/data/user
HOME=/home/user
LANG=zh_CN.UTF-8
XDG_SEAT_PATH=/org/freedesktop/DisplayManager/Seat0
XDG_CACHE_HOME=/home/user/.cache
XDG_SESSION_CLASS=user
USER=user
DISPLAY=:0
SHLVL=0
QT_IM_MODULE=fcitx
XDG_VTNR=1
XDG_SESSION_ID=5
XDG_RUNTIME_DIR=/run/user/1000
GDK_BACKEND=x11
PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:/sbin:/usr/sbin
GDMSESSION=deepin
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
QT_FONT_DPI=144

可以看到我们只需要获取weston这个进程的PID,然后读取 /proc/$PID/environ 这个文件,就可以获取对应进程的环境变量了,但是由于获取出来的环境变量是通过\0来分割的,所以为了方便阅读,需要通过sed进行换行替换。

sys

sys是linux内核用来管理系统设备的一个内存文件系统。系统的大部分设备的信息,均可以从这个文件系统中获取到(读文件操作),同时用户空间还能通过这个文件系统还来修改设备的状态(写文件操作)。

dev

dev是内核中设备的文件映射,用来与实际的设备进行数据交互的。比如鼠标设备(usb),其对应的设备为

/dev/input/event7

那么鼠标移动、左键右键点击、滚轮滑动等数据信息,均是从这个节点读取来的。

但是想这个鼠标对应的usb设备,是什么名称、是什么设备类型,厂商id是什么,设备编号是什么等,这些信息,均由sys文件系统提供

$ ls -al /sys/devices/pci0000:00/0000:00:14.0/usb3/3-5/3-5:1.0/0003:093A:2510.0002/input/input7
总用量 0
drwxr-xr-x 7 root root    0 5月  29 13:43 .
drwxr-xr-x 3 root root    0 5月  29 13:43 ..
drwxr-xr-x 2 root root    0 5月  29 13:43 capabilities
lrwxrwxrwx 1 root root    0 5月  29 13:43 device -> ../../../0003:093A:2510.0002
drwxr-xr-x 3 root root    0 5月  29 13:43 event6
drwxr-xr-x 2 root root    0 5月  29 13:43 id
-rw-r--r-- 1 root root 4096 5月  29 13:43 inhibited
-r--r--r-- 1 root root 4096 5月  29 13:43 modalias
drwxr-xr-x 3 root root    0 5月  29 13:43 mouse2
-r--r--r-- 1 root root 4096 5月  29 13:43 name
-r--r--r-- 1 root root 4096 5月  29 13:43 phys
drwxr-xr-x 2 root root    0 5月  29 13:43 power
-r--r--r-- 1 root root 4096 5月  29 13:43 properties
lrwxrwxrwx 1 root root    0 5月  29 13:43 subsystem -> ../../../../../../../../../class/input
-rw-r--r-- 1 root root 4096 5月  29 13:43 uevent
-r--r--r-- 1 root root 4096 5月  29 13:43 uniq

其中一个比较关键的文件是uevent,我们通过往这个文件中写入不同的action值,可以控制这个设备的状态。
比如往这个设备写入change,内核就会重新加载这个设备,并使用netlink上报change event上来。

echo change > uevent

我们在linux系统中,经常会顺手udevadm trigger这个命令,这个命令的作用是重新触发设备加载流程,其原理就是往uevent里面写入一个action(默认就是写change)
如下是udevadm中trigger的逻辑片段:

static int exec_list(sd_device_enumerator *e, const char *action, Set *settle_set) {
        sd_device *d;
        int r;
        FOREACH_DEVICE_AND_SUBSYSTEM(e, d) {
                _cleanup_free_ char *filename = NULL;
                filename = path_join(syspath, "uevent");
                r = write_string_file(filename, action, WRITE_STRING_FILE_DISABLE_BUFFER);
        return 0;

= lxc配置
正是因为考虑到/sys /dev /proc这三个文件系统是很特殊的,所以lxc需要对这三个文件系统进行特殊处理,特别是/sys和/proc。

lxc.mount.auto = cgroup:mixed proc:mixed sys:mixed

格式就是

文件系统:权限

比如sys:mixed就是说对sys文件系统挂载成mixed模式(大部分目录是只读的,只要net部分是可读可写的)。
其他的可选模式有:

  • sys:ro sys只读
  • sys:rw sys可读可写

我们在上面sys文件系统介绍部分有说明,如果要对设备进行重新加载,则需要往对应的uevent节点写数据,则不可避免的需要/sys文件系统可读可写。

= 宿主机和容器的冲突
既然/sys文件系统是映射的内核资源,而这部分资源在内核中是只有一份的,那么如果容器中,只有一方对该文件系统拥有写权限,则不很产生很大的冲突问题。
如果有两个环境同时对该文件系统拥有写权限,那么可能会对该文件系统的状态产生很大的应用,导致资源访问异常,进而出现黑屏、卡死、重启等问题。

主流容器策略

docker中的sys

docker对于sys的处理策略,默认情况下也是设置成ro模式,避免了容器环境和宿主机对于/sys同时写入导致的资源冲突问题。

lxc 中sys默认配置。

lxc中对sys中默认配置就是mixed状态,即/sys只读,只有内部的net部分可写。并且在ubuntu镜像模板中也是集成lxc的这个默认配置。

= 现状分析
linux系统中通常有一个udev服务,这个服务用来监听kernel的netlink event事件,并根据事件属性来匹配对应的rule规则,
如果规则匹配成功,则对规则进行应用。

目前linux系统下的udevd已经被systemd接管,进程为systemd-udevd。

systemd-udevd未启动

systemd-udevd.service是由systemd服务进程管理的。容器中systemd-udevd未启动的原因也很简单,因为/sys被lxc挂载成了只读的状态,而systemd-udevd作为udev服务,需要处理的不只是监听uevent事件,
中途还会有trigger逻辑存在,即需要往uevent节点中写入数据。因此systemd-udevd需要/sys可读可写这个条件是合理的。

但是安装正常逻辑来推论,如果/sys是只读的话,大部分功能应该是能够正常使用的,估计只有trigger相关的逻辑会失效。

因此第一步需要处理的就是修改systemd-udevd的启动条件,屏蔽对/sys的可读可写检查。

cat systemd-udevd.service 
[Unit]
Description=udev Kernel Device Manager
Documentation=man:systemd-udevd.service(8) man:udev(7)
DefaultDependencies=no
After=systemd-sysusers.service systemd-hwdb-update.service
Before=sysinit.target
#ConditionPathIsReadWrite=/sys
[Service]
ExecStart=/lib/systemd/systemd-udevd

这时候systemd-udevd确实如预料的情况一样,能够正常运行起来。但是rule并没有生效。
为了分析udev rule没有生效的原因,我们需要对systemd-udevd进行简单的调试,先打开日志:
systemd的日志,均可以通过两个环境变量来控制其日志行为:

  • SYSTEMD_LOG_LEVEL 用来设置日志等级,我们设置为debug即可输出尽可能多的日志。
  • SYSTEMD_LOG_TARGET 日志输出的目的地。考虑到systemd-udevd是systemd管理的,会自动把标准输出的日志用journalctl管理起来,所以我们简单设置成console即可用journalctl捕捉到日志。

通过查看日志,发现udevd已经加载了所有rule。既然rule能够正常加载,但是没有起作用,那么就需要理解一下rule生效的流程了。

为了测试方便,编写了一个简单的监听netlink事件的程序,发现容器在启动过程中,并没有可以匹配rule的事件发送出来。

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <ctype.h>  
#include <sys/un.h>  
#include <sys/ioctl.h>  
#include <sys/socket.h>  
#include <linux/types.h>  
#include <linux/netlink.h>  
#include <errno.h>  
#include <unistd.h>  
#include <arpa/inet.h>  
#include <netinet/in.h>  
#ifndef NEWLINE
#define NEWLINE "\r\n"
#endif
#define UEVENT_BUFFER_SIZE 2048  
static int init_hotplug_sock()  
  const int buffersize = 1024;  
  int ret;  
  struct sockaddr_nl snl;  
  bzero(&snl, sizeof(struct sockaddr_nl));  
  snl.nl_family = AF_NETLINK;  
  snl.nl_pid = getpid();  
  snl.nl_groups = 1;  
  int s = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);  
  if (s == -1)   
	  perror("socket");  
	  return -1;  
  setsockopt(s, SOL_SOCKET, SO_RCVBUF, &buffersize, sizeof(buffersize));  
  ret = bind(s, (struct sockaddr *)&snl, sizeof(struct sockaddr_nl));  
  if (ret < 0)   
	  perror("bind");  
	  close(s);  
	  return -1;  
  return s;  
char *truncate_nl(char *s) {
        s[strcspn(s, NEWLINE)] = 0;
        return s;
int device_new_from_nulstr(uint8_t *nulstr, size_t len) {
		int i = 0;
        int r;
        while (i < len) {
                char *key;
                const char *end;
                key = (char*)&nulstr[i];
                end = memchr(key, '\0', len - i);
                if (!end)
					return 0;
                i += end - key + 1;
                truncate_nl(key);
				printf("%s\n", key);	
int main(int argc, char* argv[])  
	int hotplug_sock = init_hotplug_sock();  
	int bufpos;
  	while(1)  
		int len;
  		/* Netlink message buffer */  
  		char buf[UEVENT_BUFFER_SIZE * 2];
		memset(&buf, 0x00, sizeof(buf));	
  		len = recv(hotplug_sock, &buf, sizeof(buf), 0);
		if (len <= 0)
			continue;
		printf("\nnew message:\n");
		bufpos = strlen(buf) + 1;
		printf("%s\n", buf);
		device_new_from_nulstr((uint8_t*)&buf[bufpos], len - bufpos);
  	return 0;  

所以原因就很清楚了,因为容器是后启动的,在容器启动的时候,所有的设备其实已经加载完成了,并且uevent事件也已经发送过了,所以后面启动的systemd-udevd并不会捕捉到对应的事件。

重新触发uevent

udevadm trigger 可以让内核重新触发uevent。但是执行该命令后,发现只有 /sys/devices/virtual/net 相关的下面的设备触发了uevent,其他的均没有任何反应。

这是才想起来我们在配置lxc sys权限的时候,默认使用的mixed,这个权限会将sys配置成只读,但是net会额外处理成可读可写。而udevadm trigger需要对uevent进行写入,所以此时只会有
/sys/devices/virtual/net 能够触发出来。

经过初步评估,rule中的配置,大部分是usb类型,可以优先考虑对uinput设备和usb设备进行处理。

$ cat 99_camera.rules 
SUBSYSTEM=="usb", ATTR{idVendor}=="1234", ATTR{idProduct}=="0101", MODE="0666"
SUBSYSTEM=="usb", ATTR{idVendor}=="1234", ATTR{idProduct}=="0201", MODE="0666"
SUBSYSTEM=="usb", ATTR{idVendor}=="2285", ATTR{idProduct}=="2f11", MODE="0666"
SUBSYSTEM=="usb", ATTR{idVendor}=="0ac8", ATTR{idProduct}=="010c", MODE="0666"
SUBSYSTEM=="usb", ATTR{idVendor}=="0c45", ATTR{idProduct}=="1a11", MODE="0666"
$ cat uinput.rules 
KERNEL=="uinput", MODE="0666"

重新触发uinput设备的change事件

为了能够让uinput这个设备在使用udevadm trigger命令的时候能够正常触发,需要保证uinput设备所在的目录有写权限。
先看一下uinput设备所在的路径

/sys/devices/virtual/misc/uinput/uevent

对于usb设备,可以查看如下目录的内容来确定路径:

$ ls -al /sys/bus/usb/devices/
总用量 0
drwxr-xr-x. 2 root root 0 5月  29 10:46 .
drwxr-xr-x. 4 root root 0 5月  29 10:46 ..
lrwxrwxrwx. 1 root root 0 5月  29 16:00 1-0:1.0 -> ../../../devices/platform/soc/soc:aon/5fff0000.usb/musb-hdrc.0.auto/usb1/1-0:1.0
lrwxrwxrwx. 1 root root 0 5月  29 16:00 usb1 -> ../../../devices/platform/soc/soc:aon/5fff0000.usb/musb-hdrc.0.auto/usb1

可以观察到系统带有一个usb,即usb1,路径为

/sys/devices/platform/soc/soc:aon/5fff0000.usb/musb-hdrc.0.auto/usb1

既然 /sys/devices/virtual/net 能够实现可读写的配置,那么参考这个配置,理论上也可以做到保持usb和uinput同样保持可读可写的权限。

lxc sys:mixed作用

virtual/net 可读可写是通过mixed配置实现的,为了让uinput也实现可读写,我们就需要了解这个配置的具体原理。

先看一下net的mount信息

$ mount|grep virtual/net
sysfs on /sys/devices/virtual/net type sysfs (rw,relatime,seclabel)
sysfs on /sys/devices/virtual/net type sysfs (rw,nosuid,nodev,noexec,relatime,seclabel)

此处暂时还看不出来具体产生这个挂载的原因。

在lxc中搜索一下mixed配置处理的逻辑,可以找到如下代码:

static int lxc_mount_auto_mounts(struct lxc_conf *conf, int flags, struct lxc_handler *handler)
	int i, r;
	static struct {
		int match_mask;
		int match_flag;
		const char *source;
		const char *destination;
		const char *fstype;
		unsigned long flags;
		const char *options;
	} default_mounts[] = {
		{ LXC_AUTO_SYS_MASK,  LXC_AUTO_SYS_MIXED,  "sysfs",                                          "%r/sys",                     "sysfs", MS_NODEV|MS_NOEXEC|MS_NOSUID,                    NULL },
		{ LXC_AUTO_SYS_MASK,  LXC_AUTO_SYS_MIXED,  NULL,                                             "%r/sys",                     NULL,    MS_REMOUNT|MS_BIND|MS_RDONLY,                    NULL },
		{ LXC_AUTO_SYS_MASK,  LXC_AUTO_SYS_MIXED,  "sysfs",                                          "%r/sys/devices/virtual/net", "sysfs", 0,                                               NULL },
		{ LXC_AUTO_SYS_MASK,  LXC_AUTO_SYS_MIXED,  "%r/sys/devices/virtual/net/devices/virtual/net", "%r/sys/devices/virtual/net", NULL,    MS_BIND,                                         NULL },
		{ LXC_AUTO_SYS_MASK,  LXC_AUTO_SYS_MIXED,  NULL,                                             "%r/sys/devices/virtual/net", NULL,    MS_REMOUNT|MS_BIND|MS_NOSUID|MS_NODEV|MS_NOEXEC, NULL },
		{ 0,                  0,                   NULL,                                             NULL,                         NULL,    0,                                               NULL }
	for (i = 0; default_mounts[i].match_mask; i++) {
	return 0;

观察三条virtual/net的记录

"sysfs",                                          "%r/sys/devices/virtual/net", "sysfs", 0,                                             
"%r/sys/devices/virtual/net/devices/virtual/net", "%r/sys/devices/virtual/net", NULL,    MS_BIND,                                       
NULL,                                             "%r/sys/devices/virtual/net", NULL,    MS_REMOUNT|MS_BIND|MS_NOSUID|MS_NODEV|MS_NOEXEC
  • 现在/sys/devices/virtual/net目录挂载sysfs文件系统。
  • 将/sys/devices/virtual/net/devices/virtual/net目录bind到/sys/devices/virtual/net。因为/sys本身就是一个sysfs,然后net下面又挂载了一个可读写的sysfs,那么原来的net目录下的内容就被掩藏了,然后通过bind操作将net下面内容重新映射过来。
  • 设置/sys/devices/virtual/net挂载权限。

这时候基本就能够分析出来有两条挂载记录的原因。

  • 首先将目录挂载成sysfs,但是挂载的是整个sysfs,所以原本目录的内容会被掩藏,并体现在嵌套之后的目录中。
  • 重新通过mout bind将掩藏的内容重定向回来。

uinput设备目录重新挂载可读写。

理解了上面mixed的原理,我们在lxc的hook中模拟配置uinput的目录,为了方便,我们直接操作

/sys/devices/virtual/misc

这个目录

export PATH=/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:/bin:/sbin
mount -t sysfs -o rw,nosuid,nodev,noexec,relatime,seclabel sysfs /sys/devices/virtual/misc
mount --bind /sys/devices/virtual/misc/devices/virtual/misc /sys/devices/virtual/misc

但是怎么让这个命令自动执行呢?而且这个命令需要在容器内部执行。

查看lxc的配置选项可以知道,有一个配置

lxc.hook.start

这个配置可以在执行容器的 init 之前立即在容器的命名空间中运行的一个钩子。这要求该程序在容器中可用。这个钩子可以设置成一个可执行程序或者脚本。

为了考虑方便集成,我们此处采用脚本的方式来处理:

lxc.hook.start = /vendor/bin/lxc-hook-start.sh

配置好了后,重新启动容器,我们就可以看到我们想要的效果了:

$ mount|grep misc
sysfs on /sys/devices/virtual/misc type sysfs (rw,nosuid,nodev,noexec,relatime,seclabel)
sysfs on /sys/devices/virtual/misc type sysfs (rw,nosuid,nodev,noexec,relatime,seclabel)
$ ls /sys/devices/virtual/misc/uinput/
dev  power  subsystem  uevent

可以看到挂载的文件系统已经是可读可写的了,并且目录内容也是符合我们的预期的。

usb设备重新挂载可读写

上我我们处理了uinput所在的misc类型设备的挂载,由于misc的设备路径是固定的 /sys/devices/virtual/misc
所以我们只需要固定写死即可。但是对于usb设备,则不那么实用了。以我手上的这台设备为例,其usb3的路径为:

ls -al /sys/devices/pci0000:00/0000:00:14.0/usb3

前面的 pci0000:00/0000:00:14.0 这一块路径,在不同的设备上,会体现的不一样,而且还可能会存在多个usb设备,具体会使用那个设备也是不可知的。
那么如何能够稳定的找到设备上所有的usb的实际路径,就是一个问题。

我们都知道,linux驱动也固有的标准:driver、device和bus。而usb就是一类常用的bus,我们可以尝试着直接从bus上面入手:

ls -l /sys/bus/usb/devices/
总用量 0
lrwxrwxrwx 1 root root 0 5月  30 11:26 1-0:1.0 -> ../../../devices/pci0000:00/0000:00:0d.0/usb1/1-0:1.0
lrwxrwxrwx 1 root root 0 5月  30 11:26 2-0:1.0 -> ../../../devices/pci0000:00/0000:00:0d.0/usb2/2-0:1.0
lrwxrwxrwx 1 root root 0 5月  24 15:14 3-0:1.0 -> ../../../devices/pci0000:00/0000:00:14.0/usb3/3-0:1.0
lrwxrwxrwx 1 root root 0 5月  30 11:26 3-1 -> ../../../devices/pci0000:00/0000:00:14.0/usb3/3-1
lrwxrwxrwx 1 root root 0 5月  30 11:26 3-10 -> ../../../devices/pci0000:00/0000:00:14.0/usb3/3-10
lrwxrwxrwx 1 root root 0 5月  24 15:14 3-10:1.0 -> ../../../devices/pci0000:00/0000:00:14.0/usb3/3-10/3-10:1.0
lrwxrwxrwx 1 root root 0 5月  30 11:26 3-10:1.1 -> ../../../devices/pci0000:00/0000:00:14.0/usb3/3-10/3-10:1.1
lrwxrwxrwx 1 root root 0 5月  30 11:26 3-1:1.0 -> ../../../devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1:1.0
lrwxrwxrwx 1 root root 0 5月  30 10:15 3-1.2 -> ../../../devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1.2
lrwxrwxrwx 1 root root 0 5月  30 10:15 3-1.2:1.0 -> ../../../devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1.2/3-1.2:1.0
lrwxrwxrwx 1 root root 0 5月  30 10:15 3-1.2:1.1 -> ../../../devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1.2/3-1.2:1.1
lrwxrwxrwx 1 root root 0 5月  30 10:15 3-1.2:1.2 -> ../../../devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1.2/3-1.2:1.2
lrwxrwxrwx 1 root root 0 5月  30 10:15 3-1.2:1.3 -> ../../../devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1.2/3-1.2:1.3
lrwxrwxrwx 1 root root 0 5月  30 11:26 3-1.4 -> ../../../devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1.4
lrwxrwxrwx 1 root root 0 5月  30 11:26 3-1.4:1.0 -> ../../../devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1.4/3-1.4:1.0
lrwxrwxrwx 1 root root 0 5月  30 11:26 3-1.4:1.1 -> ../../../devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1.4/3-1.4:1.1
lrwxrwxrwx 1 root root 0 5月  30 11:26 3-5 -> ../../../devices/pci0000:00/0000:00:14.0/usb3/3-5
lrwxrwxrwx 1 root root 0 5月  30 11:26 3-5:1.0 -> ../../../devices/pci0000:00/0000:00:14.0/usb3/3-5/3-5:1.0
lrwxrwxrwx 1 root root 0 5月  30 11:26 3-7 -> ../../../devices/pci0000:00/0000:00:14.0/usb3/3-7
lrwxrwxrwx 1 root root 0 5月  30 11:26 3-7:1.0 -> ../../../devices/pci0000:00/0000:00:14.0/usb3/3-7/3-7:1.0
lrwxrwxrwx 1 root root 0 5月  30 11:26 3-7:1.1 -> ../../../devices/pci0000:00/0000:00:14.0/usb3/3-7/3-7:1.1
lrwxrwxrwx 1 root root 0 5月  30 11:26 4-0:1.0 -> ../../../devices/pci0000:00/0000:00:14.0/usb4/4-0:1.0
lrwxrwxrwx 1 root root 0 5月  30 11:26 4-1 -> ../../../devices/pci0000:00/0000:00:14.0/usb4/4-1
lrwxrwxrwx 1 root root 0 5月  30 11:26 4-1.1 -> ../../../devices/pci0000:00/0000:00:14.0/usb4/4-1/4-1.1
lrwxrwxrwx 1 root root 0 5月  29 11:15 4-1:1.0 -> ../../../devices/pci0000:00/0000:00:14.0/usb4/4-1/4-1:1.0
lrwxrwxrwx 1 root root 0 5月  30 11:17 4-1.1:1.0 -> ../../../devices/pci0000:00/0000:00:14.0/usb4/4-1/4-1.1/4-1.1:1.0
lrwxrwxrwx 1 root root 0 5月  30 11:26 usb1 -> ../../../devices/pci0000:00/0000:00:0d.0/usb1
lrwxrwxrwx 1 root root 0 5月  30 11:26 usb2 -> ../../../devices/pci0000:00/0000:00:0d.0/usb2
lrwxrwxrwx 1 root root 0 5月  30 11:26 usb3 -> ../../../devices/pci0000:00/0000:00:14.0/usb3
lrwxrwxrwx 1 root root 0 5月  30 11:26 usb4 -> ../../../devices/pci0000:00/0000:00:14.0/usb4

可以看到 /sys/bus/usb/devices/ 这个目录会列表所有的usb,而这些路径其实是一个软连接,那么只需要过滤出实际的usb设备,并找到对应的device路径即可。直接上脚本:

过滤设备:

$ ls  /sys/bus/usb/devices/|grep "usb[0-9]"

获取软链接实际的路径

$ readlink -f /sys/bus/usb/devices/usb1
/sys/devices/pci0000:00/0000:00:0d.0/usb1

目录找出来了,剩下的就是挂载了,既然我们需要对多个目录进行挂载,就需要重复调用之前挂载misc情况下的两次mount流程。为了方便复用,直接封装一个函数:

mount_device() {
    mount -t sysfs -o rw,nosuid,nodev,noexec,relatime,seclabel sysfs /sys/devices/$1
    mount --bind /sys/devices/$1/devices/$1 /sys/devices/$1

该函数接受一个路径参数,但是该路径是去除了前面的 /sys/devices 因此整个逻辑就很明确了:

#!/usr/bin/bash
export PATH=/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:/bin:/sbin
mount_device() {
    mount -t sysfs -o rw,nosuid,nodev,noexec,relatime,seclabel sysfs /sys/devices/$1
    mount --bind /sys/devices/$1/devices/$1 /sys/devices/$1
mount_misc() {
    mount_device "virtual/misc"
mount_all_usb() {
    for usb in $(ls /sys/bus/usb/devices/ | grep "usb[0-9]")
        device=$(readlink -f "/sys/bus/usb/devices/$usb")
        mount_device ${device:13}