1. 简介
Docker 是一个广泛使用的工具和平台,能够更方便地开发和容器化应用程序。通过它,我们可以将程序及其依赖打包在一起,从而在不同硬件或操作系统(OS)环境下更便捷地部署和运行。然而,有时我们也需要将宿主机上的物理设备暴露给容器使用。
在本文中,我们将探讨如何让容器化的应用访问宿主机上的设备。首先,我们会回顾一下特权容器(privileged containers)的相关知识;接着,演示如何通过赋予容器部分权限来共享设备;然后,介绍两种在不启用特权模式的情况下暴露宿主设备的方法;最后,我们还将讲解如何模拟热插拔功能和设备直通(device passthrough)。
需要注意的是,我们假设所有操作环境都已成功执行过如下命令:
$ apt-get update && apt-get install tree usbutils udev
为了明确区分在容器内外执行的命令,我们在容器内执行的命令前使用特定的提示符。示例中以 USB 设备为例进行说明,但这些方法同样适用于串口设备。
本文示例代码在 Debian 12(Bookworm)与 GNU Bash 5.2.15 环境下测试通过,应适用于大多数 POSIX 兼容系统。
2. 特权容器模式
首先,我们来回顾一下容器的“特权模式”。
容器的核心理念之一是与宿主机隔离。通常,这种隔离是通过以下命名空间(namespace)实现的:
- PID Namespace(
pid
):进程 - Network Namespace(
net
):网络 - Mount Namespace(
mnt
):挂载与文件系统 - UTS Namespace(
uts
):主机名与域名 - IPC Namespace(
ipc
):进程间通信 - User Namespace(
user
):用户与组标识 - Cgroup Namespace(
cgroup
):控制组
这些隔离机制共同构成了一个完整的运行环境,避免容器对宿主机产生影响。
使用 --privileged
参数可以让容器访问宿主机所有设备,可能导致容器完全接管宿主机。例如,容器可以修改 cgroup 配置,从而控制其他容器;或者通过 mnt
权限访问宿主机文件系统。
因此,一般不建议运行特权容器。如果确实需要使用该模式,应通过 SELinux 等机制进行额外隔离。
3. 使用特权容器共享设备
当我们需要访问原始设备时,可以直接使用 --privileged
参数启动容器:
$ docker run --privileged --tty --interactive debian /bin/bash
该命令执行以下操作:
✅ 创建新容器
✅ 以特权模式运行
✅ 基于 debian
镜像
✅ 启动交互式 bash
shell
进入容器后,我们可以通过 tree
和 lsusb
查看 USB 设备:
root@6660deadbeef:/# tree /dev/bus/usb
/dev/bus/usb
`-- 001
|-- 001
`-- 002
root@6660deadbeef:/# lsusb
Bus 001 Device 002: ID 0666:4301 X Corp. Mass Storage Device
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
接着使用 usb-devices
获取更详细信息:
root@6660deadbeef:/# usb-devices
...
若设备为存储设备,可通过 udevadm
查看其设备路径:
$ udevadm info --path=/sys/block/sdb --query=env
...
最后,尝试挂载设备:
root@6660deadbeef:/# mkdir /mnt/usb
root@6660deadbeef:/# mount /dev/sdb /mnt/usb
root@6660deadbeef:/# ls /mnt/usb/
file1 file2
4. 非特权容器设备共享
如果我们不想使用特权容器,也有两种方式可以暴露宿主机设备:
4.1 使用 --volume
--volume
参数可以将宿主机的目录或文件挂载到容器中:
$ docker run --tty --interactive --volume=/dev/bus/usb:/dev/bus/usb --volume=/dev/sdb:/dev/sdb debian /bin/bash
容器内可查看设备信息:
root@6670beddeaf0:/# lsusb
Bus 001 Device 002: ID 0666:4301 X Corp. Mass Storage Device
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
但无法挂载设备:
root@6670beddeaf0:/# mount /dev/sdb /mnt/usb
mount: /mnt/usb: permission denied.
这是因为 --volume
只是挂载了文件路径,而不是设备节点。若宿主机已挂载设备,可直接挂载挂载点目录:
$ docker run --tty --interactive --volume=/mnt/usb:/mnt/usb debian /bin/bash
4.2 使用 --device
--device
参数允许将宿主机上的设备节点直接暴露给容器:
$ docker run --tty --interactive --device=/dev/bus/usb --device=/dev/sdb debian /bin/bash
查看设备信息:
root@6680beaddeed:/# lsusb
Bus 001 Device 002: ID 0666:4301 X Corp. Mass Storage Device
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
尝试挂载设备失败:
root@6680beaddeed:/# mount /dev/sdb /mnt/usb
mount: /mnt: permission denied.
此时需要添加权限:
$ docker run --security-opt apparmor=unconfined --cap-add SYS_ADMIN --tty --interactive --device=/dev/bus/usb --device=/dev/sdb debian /bin/bash
成功挂载:
root@6690bed0deb:/# mkdir /mnt/usb
root@6690bed0deb:/# mount /dev/sdb /mnt/usb
root@6690bed0deb:/# ls /mnt/usb/
file1 file2
5. 非特权容器下的 cgroup 规则配置
前面的方法虽然能共享设备,但无法处理设备热插拔的情况。例如,设备插拔后总线 ID 变化,导致容器无法继续访问。
一种解决方案是使用 --device-cgroup-rule
动态授权:
$ MAJOR_TTY_GROUP=4
$ docker run --device-cgroup-rule='c '$MAJOR_TTY_GROUP':* rwm' --tty --interactive debian /bin/bash
上述规则允许容器对 tty
类设备进行读写和创建节点(mknod)。
为支持热插拔,还需添加 udev 规则:
$ cat /etc/udev/rules.d/90-docker-tty.rules
ACTION=="add", SUBSYSTEM=="tty", RUN+="/usr/shr/bin/docker-tty.sh 'plugged' '%E{DEVNAME}' '%M' '%m'"
ACTION=="remove", SUBSYSTEM=="tty", RUN+="/usr/shr/bin/docker-tty.sh 'unplugged' '%E{DEVNAME}' '%M' '%m'"
并创建脚本:
$ cat /usr/shr/bin/docker-tty.sh
#!/usr/bin/env bash
CONTAINERNAME=<CONTAINER_NAME>
if [ -n "$(docker ps --quiet --filter name=$CONTAINERNAME)" ]; then
if [ "$1" == "plugged" ]; then
docker exec --user root env_dev mknod $2 c $3 $4
docker exec --user root env_dev chmod --recursive 777 $2
else
docker exec --user root env_dev rm $2
fi
fi
$ chmod +x /usr/shr/bin/docker-tty.sh
这样,当设备插拔时,容器会自动更新设备节点。
6. 小结
本文介绍了如何在不启用特权容器的前提下,将宿主机上的原始设备(如 USB 或串口设备)暴露给容器。
✅ 使用 --privileged
虽然简单,但风险较大
✅ 使用 --volume
可以共享挂载点但不能访问原始设备
✅ 使用 --device
更安全,但需配合权限配置
✅ 使用 --device-cgroup-rule
和 udev 支持热插拔
通过这些方法,可以在保持容器隔离性的同时,实现设备共享功能。