10. 【Linux】cgroups:容器化GPU隔离的基石
在 Docker 容器里运行 nvidia-smi,报错 Failed to initialize NVML: Unknown Error
为什么宿主机能看到 8 块 GPU,容器里却"找不到设备"?
答案藏在 Linux 的 cgroups 机制里。
1. cgroups 是什么?
核心问题:Linux 如何让进程 A 只能用 GPU 0,进程 B 只能用 GPU 1?
答案是控制组(Control Groups,简称 cgroups) —— Linux 内核提供的资源隔离机制。
它不仅能限制 CPU 和内存,更重要的是能控制进程能访问哪些设备。
1.1 三个核心概念
cgroups 由三个关键组件构成:
控制组(cgroup):进程的"资源配额组"。每个 cgroup 就像一个容器,规定了组内进程能使用多少资源、能访问哪些设备。
子系统(subsystem):不同类型的资源控制器。每种资源有独立的控制器:
cpu:控制 CPU 时间分配memory:限制内存使用量devices:控制设备访问权限(本文重点)blkio:限制磁盘 IO 带宽
层次结构(hierarchy):cgroup 以树形方式组织。父组的限制会自动应用到所有子组。
1.2 直白版:公司门禁系统
想象一个公司的门禁系统:
公司(根cgroup)
├─ 研发部(子cgroup)
│ ├─ 门禁权限:能进实验室、机房
│ └─ 设备权限:能用GPU服务器
├─ 财务部(子cgroup)
│ ├─ 门禁权限:能进财务室
│ └─ 设备权限:只能用办公电脑
- 控制组 = 部门分组
- 子系统 = 不同类型的权限(门禁、设备使用)
- 层次 = 组织架构(公司 → 部门 → 小组)
1.3 cgroups 在文件系统中的样子
Linux 把 cgroups 实现为一个特殊的文件系统,挂载在 /sys/fs/cgroup/ 下:
$ ls /sys/fs/cgroup/
blkio/ # 磁盘IO控制
cpu/ # CPU时间控制
devices/ # 设备访问控制 ← GPU隔离用这个
memory/ # 内存限制
...
每个子系统是一个独立目录,目录下可以创建子目录来组织 cgroup 层次:
$ tree /sys/fs/cgroup/devices/
/sys/fs/cgroup/devices/
├── devices.allow # 根cgroup的规则文件
├── devices.deny
├── cgroup.procs # 属于这个cgroup的进程列表
├── docker/ # Docker创建的子cgroup
│ └── <容器ID>/
│ ├── devices.allow
│ └── cgroup.procs
└── slurm/ # Slurm创建的子cgroup
└── uid_1000/
└── job_12345/
关键点:每个 cgroup 目录下都有配置文件,通过写入这些文件来设置规则。
2. devices 子系统——设备访问的守门员
核心问题:如何让进程"看不见"某个 GPU?
答案是devices 子系统。它使用白名单机制:默认拒绝所有设备访问,只有明确允许的设备才能打开。
2.1 Linux 如何表示设备
在 Linux 中,所有硬件设备都以文件形式出现在 /dev/ 目录下。GPU 也不例外:
$ ls -l /dev/nvidia*
crw-rw-rw- 1 root root 195, 0 Oct 21 10:00 /dev/nvidia0
crw-rw-rw- 1 root root 195, 1 Oct 21 10:00 /dev/nvidia1
crw-rw-rw- 1 root root 195, 2 Oct 21 10:00 /dev/nvidia2
crw-rw-rw- 1 root root 195, 255 Oct 21 10:00 /dev/nvidiactl
crw-rw-rw- 1 root root 510, 0 Oct 21 10:00 /dev/nvidia-uvm
关键信息解读:
| 字段 | 含义 | GPU 示例 |
|---|---|---|
| 第 1 列首字符 | 设备类型:c=字符设备,b=块设备 | c(GPU 是字符设备) |
| 第 5 列 | 主设备号(标识驱动程序) | 195(NVIDIA GPU 驱动) |
| 第 6 列 | 次设备号(标识具体设备) | 0(第一块 GPU) |
| 文件名 | 设备节点路径 | /dev/nvidia0 |
主设备号 195 代表 NVIDIA GPU 驱动。所有 NVIDIA GPU 共享这个主设备号,通过次设备号区分:
195:0→/dev/nvidia0(第一块 GPU)195:1→/dev/nvidia1(第二块 GPU)195:255→/dev/nvidiactl(控制设备,所有 GPU 操作必需)
设备号 510 是 NVIDIA UVM(Unified Virtual Memory)驱动:
510:0→/dev/nvidia-uvm(CUDA 统一内存管理)
2.2 devices 子系统的工作原理
devices 子系统通过两个文件控制设备访问:
devices.allow:添加设备到白名单
# 格式:<type> <major>:<minor> <permissions>
echo "c 195:0 rwm" > devices.allow
devices.deny:从白名单移除设备
echo "c 195:1 rwm" > devices.deny
规则格式详解:
| 部分 | 可选值 | 说明 | GPU 示例 |
|---|---|---|---|
| type | c(字符)或 b(块) | 设备类型 | c |
| major | 数字或 *(所有) | 主设备号 | 195 |
| minor | 数字或 *(所有) | 次设备号 | 0, 1, 255 |
| permissions | r/w/m的组合 | r=读,w=写,m=mknod 创建节点 | rwm |
通配符示例:
c 195:* rwm→ 允许所有 NVIDIA GPU(次设备号任意)a *:* rwm→ 允许所有设备(a 表示所有类型)
2.3 直白版:门禁卡系统
把 devices 子系统想象成公司的门禁系统:
默认状态:
├─ 所有房间的门都锁上(默认拒绝一切设备)
└─ 员工没有任何钥匙
devices.allow 操作:
├─ "发一张195:0号房间的钥匙给这个进程"
└─ 现在进程能打开 /dev/nvidia0 这扇门
devices.deny 操作:
├─ "收回195:1号房间的钥匙"
└─ 进程不能再访问 /dev/nvidia1
关键点:当进程尝试 open("/dev/nvidia0") 时,内核会检查:
- 进程属于哪个 cgroup?
- 该 cgroup 的
devices.allow里有c 195:0吗? - 有 → 允许打开;没有 → 返回
Permission denied
2.4 为什么需要 nvidiactl 和 nvidia-uvm?
使用 GPU 不是打开单个设备文件那么简单。CUDA 程序需要访问三类设备:
| 设备 | 设备号 | 作用 | 必需性 |
|---|---|---|---|
/dev/nvidia0 | 195:0 | GPU 0 的计算核心 | ✅ 必需(如果用 GPU 0) |
/dev/nvidiactl | 195:255 | GPU 驱动控制接口 | ✅ 必需(任何 GPU 操作) |
/dev/nvidia-uvm | 510:0 | 统一内存管理 | ✅ 必需(CUDA 6.0+) |
实例:如果只允许 195:0 而不允许 195:255,CUDA 程序会在初始化时失败:
import torch
torch.cuda.is_available() # False
# 内部尝试打开 /dev/nvidiactl 失败
3. 实战:手动隔离 GPU 访问
场景:让进程 A 只能用 GPU 0,进程 B 只能用 GPU 1,进程 C 完全看不到 GPU。
3.1 准备工作:确认设备号
首先确认你的 GPU 设备号(不同驱动版本可能不同):
$ ls -l /dev/nvidia* /dev/nvidia-uvm
crw-rw-rw- 1 root root 195, 0 /dev/nvidia0
crw-rw-rw- 1 root root 195, 1 /dev/nvidia1
crw-rw-rw- 1 root root 195, 255 /dev/nvidiactl
crw-rw-rw- 1 root root 510, 0 /dev/nvidia-uvm
# 确认cgroups v1已挂载
$ mount | grep cgroup
cgroup on /sys/fs/cgroup/devices type cgroup (rw,devices)
如果没有挂载,执行:
sudo mount -t cgroup -o devices none /sys/fs/cgroup/devices
3.2 创建隔离 cgroup:只能用 GPU 0
完整流程(需要 root 权限):
# 步骤1:创建cgroup目录
sudo mkdir -p /sys/fs/cgroup/devices/gpu0_only
# 步骤2:默认拒绝所有设备(可选,大多数系统已配置)
# 如果 devices.list 是空的,需要先继承父组规则
sudo bash -c 'cat /sys/fs/cgroup/devices/devices.list > /sys/fs/cgroup/devices/gpu0_only/devices.list'
# 步骤3:只允许必需的设备
# 3.1 允许GPU 0
sudo bash -c 'echo "c 195:0 rwm" > /sys/fs/cgroup/devices/gpu0_only/devices.allow'
# 3.2 允许nvidiactl(必需)
sudo bash -c 'echo "c 195:255 rwm" > /sys/fs/cgroup/devices/gpu0_only/devices.allow'
# 3.3 允许nvidia-uvm(必需)
sudo bash -c 'echo "c 510:0 rwm" > /sys/fs/cgroup/devices/gpu0_only/devices.allow'
# 步骤4:把当前shell进程加入cgroup
echo $$ | sudo tee /sys/fs/cgroup/devices/gpu0_only/cgroup.procs
为什么用 bash -c?
因为重定向 > 操作由 shell 执行,sudo echo 只提升了 echo 的权限,重定向仍是普通用户。正确做法是 sudo bash -c 'echo ... > ...'。
3.3 验证隔离效果
现在这个 shell 和它启动的子进程都受 cgroup 限制:
# 测试1:能看到几块GPU?
$ python3 -c "import torch; print(f'可见GPU数量: {torch.cuda.device_count()}')"
可见GPU数量: 1
# 测试2:是GPU 0吗?
$ python3 -c "import torch; print(torch.cuda.get_device_name(0))"
NVIDIA A100-SXM4-40GB
# 测试3:尝试强制使用GPU 1
$ CUDA_VISIBLE_DEVICES=1 python3 -c "import torch; print(torch.cuda.device_count())"
可见GPU数量: 0 # ✅ 看不到GPU 1
# 测试4:底层设备访问
$ python3 -c "open('/dev/nvidia0', 'r')" # 成功
$ python3 -c "open('/dev/nvidia1', 'r')" # 失败
PermissionError: [Errno 13] Permission denied: '/dev/nvidia1'
关键观察:
torch.cuda.device_count()返回 1,因为 CUDA 驱动尝试打开/dev/nvidia1失败- 直接打开设备文件会得到
Permission denied错误 CUDA_VISIBLE_DEVICES=1不起作用,因为 cgroup 在更底层拦截
3.4 查看当前 cgroup 的规则
# 查看这个cgroup允许哪些设备
$ cat /sys/fs/cgroup/devices/gpu0_only/devices.list
c 195:0 rwm # GPU 0
c 195:255 rwm # nvidiactl
c 510:0 rwm # nvidia-uvm
# (实际输出可能还包含其他系统设备,如 /dev/null)
# 查看当前进程属于哪个cgroup
$ cat /proc/self/cgroup | grep devices
8:devices:/gpu0_only
3.5 创建只能用 GPU 1 的 cgroup
同样的方法创建另一个隔离组:
sudo mkdir -p /sys/fs/cgroup/devices/gpu1_only
sudo bash -c 'echo "c 195:1 rwm" > /sys/fs/cgroup/devices/gpu1_only/devices.allow'
sudo bash -c 'echo "c 195:255 rwm" > /sys/fs/cgroup/devices/gpu1_only/devices.allow'
sudo bash -c 'echo "c 510:0 rwm" > /sys/fs/cgroup/devices/gpu1_only/devices.allow'
# 在新的shell中运行
sudo bash # 启动新shell
echo $$ | sudo tee /sys/fs/cgroup/devices/gpu1_only/cgroup.procs
# 验证
python3 -c "import torch; print(torch.cuda.get_device_name(0))"
# 现在看到的"第0块GPU"实际是物理GPU 1
3.6 创建完全无 GPU 的 cgroup
sudo mkdir -p /sys/fs/cgroup/devices/no_gpu
# 不添加任何 195:* 设备到白名单
echo $$ | sudo tee /sys/fs/cgroup/devices/no_gpu/cgroup.procs
$ python3 -c "import torch; print(torch.cuda.is_available())"
False # ✅ 完全看不到GPU
3.7 退出 cgroup
将进程移回根 cgroup 即可恢复完整权限:
echo $$ | sudo tee /sys/fs/cgroup/devices/cgroup.procs
3.8 完整自动化脚本
#!/bin/bash
# create_gpu_cgroup.sh - 创建GPU隔离cgroup
if [ "$#" -ne 1 ]; then
echo "用法: $0 <GPU编号>"
echo "示例: $0 0 # 创建只能用GPU 0的cgroup"
exit 1
fi
GPU_ID=$1
CGROUP_NAME="gpu${GPU_ID}_only"
CGROUP_PATH="/sys/fs/cgroup/devices/${CGROUP_NAME}"
# 创建cgroup
sudo mkdir -p "$CGROUP_PATH"
# 允许必需的设备
sudo bash -c "echo 'c 195:${GPU_ID} rwm' > ${CGROUP_PATH}/devices.allow"
sudo bash -c "echo 'c 195:255 rwm' > ${CGROUP_PATH}/devices.allow"
sudo bash -c "echo 'c 510:0 rwm' > ${CGROUP_PATH}/devices.allow"
echo "✅ cgroup创建成功: $CGROUP_NAME"
echo "运行以下命令进入隔离环境:"
echo " echo \$\$ | sudo tee ${CGROUP_PATH}/cgroup.procs"
使用示例:
$ ./create_gpu_cgroup.sh 0
✅ cgroup创建成功: gpu0_only
$ echo $$ | sudo tee /sys/fs/cgroup/devices/gpu0_only/cgroup.procs
$ python3 my_model.py # 只会用GPU 0
4. cgroups v1 vs v2:两个版本的差异
核心问题:为什么有两套 cgroups 系统?
Linux 社区在 cgroups v1 使用多年后发现了设计缺陷,于 2016 年推出了重构的 v2 版本。两者并存,系统默认使用其中之一。
4.1 关键差异对比
| 维度 | cgroups v1 | cgroups v2 | 影响 |
|---|---|---|---|
| 挂载点 | /sys/fs/cgroup/<subsystem>/ | /sys/fs/cgroup/ 统一 | v2 更简洁 |
| 层次结构 | 每个子系统独立树 | 所有子系统统一树 | v1 可能冲突 |
| devices 控制 | devices.allow/deny 文件 | eBPF 程序 | v2 更灵活但复杂 |
| 进程分配 | 可在叶子和内部节点 | 只能在叶子节点 | v2 更严格 |
| 内核版本 | 2.6.24+(2008 年) | 4.5+(2016 年) | v2 需新内核 |
| 系统默认 | RHEL 7, Ubuntu 18.04 | RHEL 8+, Ubuntu 20.04+ | 检查你的系统 |
4.2 如何判断系统使用哪个版本?
方法 1:检查挂载点
$ mount | grep cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec)
# 有输出 → 使用v2(unified hierarchy)
$ mount | grep cgroup | grep devices
cgroup on /sys/fs/cgroup/devices type cgroup (rw,devices)
# 有独立devices挂载 → 使用v1
方法 2:检查目录结构
# v1:每个子系统独立目录
$ ls /sys/fs/cgroup/
blkio/ cpu/ devices/ memory/ ...
# v2:统一目录,无子系统子目录
$ ls /sys/fs/cgroup/
cgroup.controllers cgroup.procs memory.max ...
方法 3:读取 cgroup 版本文件
$ cat /proc/cgroups | grep devices
devices 1 234 1 # 第3列>0 → v1启用
4.3 对 GPU 隔离的实际影响
好消息:容器工具(Docker、Podman、Kubernetes)会自动适配版本。你不需要修改配置。
如果你需要手动操作:
cgroups v1(本文前面的示例)
# 直接操作文件
echo "c 195:0 rwm" > /sys/fs/cgroup/devices/my_cgroup/devices.allow
cgroups v2(使用 eBPF)
# 需要通过systemd或编写eBPF程序
systemd-run --unit=my-gpu-app \
--property=DeviceAllow="/dev/nvidia0 rwm" \
python3 my_model.py
v2 的 devices 控制改用 eBPF(extended Berkeley Packet Filter)实现,提供了更灵活的过滤逻辑,但直接操作更复杂。实际使用中推荐:
- 生产环境:通过容器引擎或资源管理器(Docker、K8s、Slurm)
- 调试开发:用 v1 的简单文件接口(如果系统支持)
4.4 混合模式(Hybrid)
部分发行版支持 v1 和 v2 同时挂载:
$ mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 # v2统一层次
cgroup on /sys/fs/cgroup/devices type cgroup # v1 devices子系统
这种配置下:
- 新应用优先用 v2
- 需要 devices 控制的传统应用仍可用 v1 接口
5. 容器引擎如何使用 cgroups
核心问题:Docker 的 --device 和 --gpus 参数背后做了什么?
5.1 Docker 的设备隔离
当你运行:
docker run --gpus '"device=0"' nvidia/cuda:11.8.0-base nvidia-smi
Docker 在背后执行了这些操作(cgroups v1 为例):
1. 创建cgroup
mkdir /sys/fs/cgroup/devices/docker/<容器ID>
2. 配置devices规则
echo "c 195:0 rwm" > .../devices.allow # GPU 0
echo "c 195:255 rwm" > .../devices.allow # nvidiactl
echo "c 510:0 rwm" > .../devices.allow # nvidia-uvm
3. 启动容器进程
PID=$(docker-containerd-shim ...)
4. 加入cgroup
echo $PID > /sys/fs/cgroup/devices/docker/<容器ID>/cgroup.procs
验证:查看运行中的容器
$ docker ps
CONTAINER ID IMAGE
abc123def456 nvidia/cuda
$ cat /sys/fs/cgroup/devices/docker/abc123def456*/devices.list
c 195:0 rwm
c 195:255 rwm
c 510:0 rwm
...
5.2 --device vs --gpus 的区别
Docker 提供两种方式指定 GPU:
方式 1:--device(手动指定设备文件)
docker run \
--device=/dev/nvidia0 \
--device=/dev/nvidiactl \
--device=/dev/nvidia-uvm \
nvidia/cuda:11.8.0-base
- 直接操作 devices cgroup
- 需要手动列出所有必需设备
- 适合精确控制
方式 2:--gpus(高级 GPU 支持,需要 nvidia-docker2)
docker run --gpus '"device=0,1"' nvidia/cuda:11.8.0-base
- 通过 nvidia-container-toolkit 自动处理
- 自动添加所有必需设备(nvidiactl、uvm、libraries)
- 自动设置环境变量(
NVIDIA_VISIBLE_DEVICES)
对比表格:
| 特性 | --device | --gpus |
|---|---|---|
| 需要 nvidia-docker2 | ❌ 否 | ✅ 是 |
| 自动处理依赖设备 | ❌ 需手动指定 | ✅ 自动 |
| 支持 MIG 分区 | ❌ 否 | ✅ 是 |
| 挂载 CUDA 库 | ❌ 需手动-v | ✅ 自动 |
| 设置环境变量 | ❌ 需手动-e | ✅ 自动 |
| 适用场景 | 简单测试 | 生产环境 |
5.3 Kubernetes 的 Device Plugin 机制
Kubernetes 通过 Device Plugin 框架管理 GPU。当你创建 Pod:
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
spec:
containers:
- name: cuda-container
image: nvidia/cuda:11.8.0-base
resources:
limits:
nvidia.com/gpu: 1 # 请求1块GPU
背后的流程:
1. kubelet调用nvidia-device-plugin
"有哪些可用GPU?"
→ 插件返回:nvidia0, nvidia1, nvidia2...
2. Scheduler分配
"给这个Pod分配nvidia0"
3. kubelet配置容器
├─ 设置cgroup规则(允许访问 195:0)
├─ 挂载设备文件(/dev/nvidia0)
└─ 设置环境变量(CUDA_VISIBLE_DEVICES=0)
4. 容器启动
只能看到被分配的GPU
验证:查看 Pod 的 cgroup
# 找到Pod的容器ID
$ kubectl describe pod gpu-pod | grep "Container ID"
Container ID: containerd://xyz789
# 查看cgroup配置
$ cat /sys/fs/cgroup/devices/kubepods/pod<uid>/xyz789/devices.list
c 195:0 rwm
c 195:255 rwm
c 510:0 rwm
5.4 Slurm 的 task/cgroup 插件
Slurm 使用 task/cgroup 插件自动管理作业的资源隔离。配置示例:
slurm.conf:
# 启用cgroup任务管理
TaskPlugin=task/cgroup
# 约束设备访问
ConstrainDevices=yes
# cgroup配置文件
TaskPluginParam=autobind=yes
cgroup.conf:
CgroupAutomount=yes
ConstrainDevices=yes
# 设置cgroup挂载点
CgroupMountpoint=/sys/fs/cgroup
提交作业:
$ sbatch --gres=gpu:1 my_job.sh
Submitted batch job 12345
Slurm 创建的 cgroup 结构:
/sys/fs/cgroup/devices/slurm/
└── uid_1000/ # 用户ID
└── job_12345/ # 作业ID
└── step_0/ # 作业步骤
├── devices.allow
└── cgroup.procs
查看作业的 GPU 权限:
$ cat /sys/fs/cgroup/devices/slurm/uid_1000/job_12345/step_0/devices.list
c 195:0 rwm # Slurm自动根据--gres=gpu:1分配了GPU 0
c 195:255 rwm
c 510:0 rwm
自动隔离的效果:
- 作业 A 的进程只能访问分配给它的 GPU
- 作业 B 的进程无法"看到"作业 A 的 GPU
- 作业结束后自动清理 cgroup
5.5 对比:三种工具的 cgroup 使用
| 工具 | cgroup 创建方式 | GPU 隔离粒度 | 配置方式 |
|---|---|---|---|
| Docker | 每个容器一个 cgroup | 按容器 | --device或--gpus |
| Kubernetes | Pod/Container 层次 | 按容器(Device Plugin) | Pod YAML 的 resources.limits |
| Slurm | 按用户/作业/步骤 | 按作业 | --gres=gpu:N |
| 手动 | mkdir 创建 | 自定义 | 直接写 devices.allow |
6. 实战案例:多用户 GPU 共享服务器
场景:一台 8 卡 GPU 服务器,3 个用户(Alice、Bob、Charlie),如何保证隔离?
6.1 需求分析
| 用户 | 分配 GPU | 要求 |
|---|---|---|
| Alice | GPU 0, 1 | 不能看到其他 GPU |
| Bob | GPU 2, 3, 4 | 不能看到其他 GPU |
| Charlie | GPU 5, 6, 7 | 不能看到其他 GPU |
6.2 方案设计
使用 cgroups 创建用户级隔离:
#!/bin/bash
# setup_gpu_isolation.sh
# 为Alice创建cgroup(GPU 0,1)
sudo mkdir -p /sys/fs/cgroup/devices/user_alice
for gpu in 0 1; do
echo "c 195:${gpu} rwm" | sudo tee -a /sys/fs/cgroup/devices/user_alice/devices.allow
done
echo "c 195:255 rwm" | sudo tee -a /sys/fs/cgroup/devices/user_alice/devices.allow
echo "c 510:0 rwm" | sudo tee -a /sys/fs/cgroup/devices/user_alice/devices.allow
# 为Bob创建cgroup(GPU 2,3,4)
sudo mkdir -p /sys/fs/cgroup/devices/user_bob
for gpu in 2 3 4; do
echo "c 195:${gpu} rwm" | sudo tee -a /sys/fs/cgroup/devices/user_bob/devices.allow
done
echo "c 195:255 rwm" | sudo tee -a /sys/fs/cgroup/devices/user_bob/devices.allow
echo "c 510:0 rwm" | sudo tee -a /sys/fs/cgroup/devices/user_bob/devices.allow
# 为Charlie创建cgroup(GPU 5,6,7)
sudo mkdir -p /sys/fs/cgroup/devices/user_charlie
for gpu in 5 6 7; do
echo "c 195:${gpu} rwm" | sudo tee -a /sys/fs/cgroup/devices/user_charlie/devices.allow
done
echo "c 195:255 rwm" | sudo tee -a /sys/fs/cgroup/devices/user_charlie/devices.allow
echo "c 510:0 rwm" | sudo tee -a /sys/fs/cgroup/devices/user_charlie/devices.allow
echo "✅ GPU隔离cgroup创建完成"
6.3 用户登录时自动加入 cgroup
编辑 /etc/security/limits.conf 或使用 PAM 模块,在用户登录时自动加入对应 cgroup:
# /usr/local/bin/cgroup_login.sh
#!/bin/bash
case "$PAM_USER" in
alice)
echo $$ | sudo tee /sys/fs/cgroup/devices/user_alice/cgroup.procs
;;
bob)
echo $$ | sudo tee /sys/fs/cgroup/devices/user_bob/cgroup.procs
;;
charlie)
echo $$ | sudo tee /sys/fs/cgroup/devices/user_charlie/cgroup.procs
;;
esac
在 /etc/pam.d/sshd 添加:
session required pam_exec.so /usr/local/bin/cgroup_login.sh
6.4 验证隔离效果
Alice 登录:
alice@server:~$ python3 -c "import torch; print(f'可见GPU: {torch.cuda.device_count()}')"
可见GPU: 2
alice@server:~$ nvidia-smi --query-gpu=index,name --format=csv
index, name
0, NVIDIA A100-SXM4-40GB
1, NVIDIA A100-SXM4-40GB
# ✅ 只能看到GPU 0和1
Bob 登录:
bob@server:~$ python3 -c "import torch; print(f'可见GPU: {torch.cuda.device_count()}')"
可见GPU: 3
bob@server:~$ nvidia-smi --query-gpu=index --format=csv
index
0
1
2
# ✅ 看到的是GPU 2,3,4(但CUDA内部编号为0,1,2)
关键点:CUDA 会重新编号它能访问的 GPU。Bob 看到的"GPU 0"实际是物理 GPU 2。
7. 调试与故障排查
7.1 常见问题诊断表
| 症状 | 可能原因 | 排查命令 |
|---|---|---|
| 容器内 nvidia-smi 失败 | devices 规则缺失 | cat /sys/fs/cgroup/devices/.../devices.list |
| Permission denied 访问/dev/nvidia0 | 未授权该设备 | ls -l /dev/nvidia* 确认设备号 |
| cgroup 文件不存在 | cgroups 未挂载 | mount | grep cgroup |
| 隔离不生效 | 进程不在 cgroup 内 | cat /proc/<PID>/cgroup |
| CUDA 初始化失败 | 缺少 nvidiactl 或 uvm | 检查是否允许 195:255 和 510:0 |
| 能看到所有 GPU | 进程在根 cgroup | 检查 /proc/self/cgroup |
7.2 调试技巧
技巧 1:查看进程的 cgroup 归属
# 方法1:查看当前shell
cat /proc/self/cgroup
# 方法2:查看特定进程
cat /proc/<PID>/cgroup | grep devices
# 输出示例:8:devices:/docker/abc123
# 方法3:查看Python进程
python3 -c "import os; print(open(f'/proc/{os.getpid()}/cgroup').read())"
技巧 2:查看 cgroup 的完整规则
# 找到进程的devices cgroup路径
CGROUP_PATH=$(cat /proc/self/cgroup | grep devices | cut -d: -f3)
# 查看该cgroup允许的所有设备
cat /sys/fs/cgroup/devices${CGROUP_PATH}/devices.list
技巧 3:测试设备访问权限
# 直接测试能否打开设备文件
python3 -c "
import os
for i in range(8):
try:
fd = os.open(f'/dev/nvidia{i}', os.O_RDONLY)
os.close(fd)
print(f'✅ GPU {i}: 可访问')
except PermissionError:
print(f'❌ GPU {i}: 权限拒绝')
except FileNotFoundError:
print(f'⚠️ GPU {i}: 设备不存在')
"
技巧 4:临时提权测试
# 如果怀疑是cgroup限制,临时移到根cgroup测试
original_cgroup=$(cat /proc/self/cgroup | grep devices | cut -d: -f3)
echo $$ | sudo tee /sys/fs/cgroup/devices/cgroup.procs
# 测试程序
python3 test_gpu.py
# 恢复
echo $$ | sudo tee /sys/fs/cgroup/devices${original_cgroup}/cgroup.procs
7.3 性能影响
cgroups 的 devices 检查非常轻量,几乎无性能影响:
| 操作 | 开销 | 说明 |
|---|---|---|
| 检查 devices 规则 | ~10 ns | 内核内存查找 |
| 首次打开设备 | +50 ns | 一次性开销 |
| 后续 GPU 操作 | 0 | 设备已打开,无额外检查 |
| CUDA kernel 执行 | 0 | cgroup 不影响 GPU 计算 |
关键点:cgroup 只在打开设备文件时检查,之后 GPU 操作完全不受影响。
7.4 清理 cgroup
如果测试创建了很多 cgroup,清理方法:
# 删除cgroup前,必须先移出所有进程
# 1. 查看cgroup内的进程
cat /sys/fs/cgroup/devices/gpu0_only/cgroup.procs
# 2. 移动进程到根cgroup
for pid in $(cat /sys/fs/cgroup/devices/gpu0_only/cgroup.procs); do
echo $pid | sudo tee /sys/fs/cgroup/devices/cgroup.procs
done
# 3. 删除空cgroup
sudo rmdir /sys/fs/cgroup/devices/gpu0_only
批量清理脚本:
#!/bin/bash
for cgroup in /sys/fs/cgroup/devices/gpu*_only; do
if [ -d "$cgroup" ]; then
# 移出所有进程
while read pid; do
echo $pid | sudo tee /sys/fs/cgroup/devices/cgroup.procs
done < "$cgroup/cgroup.procs"
# 删除cgroup
sudo rmdir "$cgroup"
echo "已删除: $cgroup"
fi
done
8. 总结
8.1 核心要点
-
cgroups 是进程资源的访问控制系统
- 通过文件系统接口操作
- 影响所有子进程
- 内核级强制执行
-
devices 子系统使用白名单机制
- 默认拒绝所有设备访问
- 通过
devices.allow明确授权 - 规则格式:
<type> <major>:<minor> <permissions>
-
GPU 隔离需要三个设备
/dev/nvidia0(195:0):计算核心/dev/nvidiactl(195:255):控制接口/dev/nvidia-uvm(510:0):统一内存管理
-
cgroups v1 vs v2
- v1:简单文件接口,直接操作
- v2:统一层次,eBPF 实现,容器工具自动适配
- 生产环境:通过容器引擎管理,无需关心版本
-
容器工具自动使用 cgroups
- Docker:
--device或--gpus - Kubernetes:Device Plugin 框架
- Slurm:task/cgroup 插件
- Docker:
8.2 适用场景对比
| 场景 | 推荐方案 | 管理复杂度 | 隔离强度 |
|---|---|---|---|
| 单机开发测试 | 手动 cgroup | 低 | 中 |
| 多用户共享服务器 | Slurm + cgroup | 中 | 高 |
| 容器化部署 | Docker/K8s | 低(自动化) | 高 |
| 云原生 GPU 应用 | K8s Device Plugin | 低(自动化) | 高 |
| HPC 集群 | Slurm + cgroup | 中 | 高 |
8.3 进一步学习
内核文档:
# cgroups v1 devices子系统
/usr/src/linux/Documentation/cgroup-v1/devices.txt
# cgroups v2
/usr/src/linux/Documentation/cgroup-v2.txt
实践项目:
- 用 cgroups 实现简易"容器":隔离进程的 CPU、内存、设备
- 编写 Device Plugin:为 K8s 添加自定义资源类型
- 分析 Docker 源码:看容器引擎如何操作 cgroups
相关技术:
- Namespace:隔离进程的视图(网络、PID、挂载点)
- eBPF:cgroups v2 使用的底层技术
- NVIDIA MIG:GPU 物理分区(与 cgroups 互补)
附录:快速参考卡片
# === 查看系统信息 ===
# GPU设备号
ls -l /dev/nvidia*
# cgroups版本
mount | grep cgroup
# 进程的cgroup
cat /proc/self/cgroup
# === 创建GPU隔离cgroup(v1) ===
# 创建cgroup
sudo mkdir -p /sys/fs/cgroup/devices/my_cgroup
# 允许GPU 0
echo "c 195:0 rwm" | sudo tee /sys/fs/cgroup/devices/my_cgroup/devices.allow
echo "c 195:255 rwm" | sudo tee /sys/fs/cgroup/devices/my_cgroup/devices.allow
echo "c 510:0 rwm" | sudo tee /sys/fs/cgroup/devices/my_cgroup/devices.allow
# 加入cgroup
echo $$ | sudo tee /sys/fs/cgroup/devices/my_cgroup/cgroup.procs
# === 验证隔离 ===
# Python测试
python3 -c "import torch; print(f'GPU数量: {torch.cuda.device_count()}')"
# 设备文件测试
python3 -c "open('/dev/nvidia0', 'r')" # 应该成功
python3 -c "open('/dev/nvidia1', 'r')" # 应该失败
# === 调试 ===
# 查看允许的设备
cat /sys/fs/cgroup/devices/my_cgroup/devices.list
# 查看cgroup内的进程
cat /sys/fs/cgroup/devices/my_cgroup/cgroup.procs
# === 清理 ===
# 退出cgroup
echo $$ | sudo tee /sys/fs/cgroup/devices/cgroup.procs
# 删除cgroup
sudo rmdir /sys/fs/cgroup/devices/my_cgroup
关键命令速查:
| 需求 | 命令 |
|---|---|
| 查看 GPU 设备号 | ls -l /dev/nvidia* |
| 创建 cgroup | mkdir /sys/fs/cgroup/devices/<name> |
| 允许设备 | echo "c 195:0 rwm" > .../devices.allow |
| 加入 cgroup | echo $$ > .../cgroup.procs |
| 查看进程 cgroup | cat /proc/self/cgroup |
| 查看允许的设备 | cat .../devices.list |
| 退出 cgroup | echo $$ > /sys/fs/cgroup/devices/cgroup.procs |
| 删除 cgroup | rmdir /sys/fs/cgroup/devices/<name> |