Docker:容器虚拟化技术基础-namespace,cgroups,资源管理与LXC
- 一.容器虚拟化实现原理
- 二.NameSpace
-
- [2.1什么是 Namespace(命名空间)](#2.1什么是 Namespace(命名空间))
- 2.2namespace隔离实践
-
- 2.2.1基础知识
-
- dd命令
- mkfs命令
- [df 命令](#df 命令)
- mount命令
- unshared命令
- 2.2.2进程隔离实践
- 2.2.3mount隔离实践
- 三.cgroups
- 四.LXC
一.容器虚拟化实现原理
容器虚拟化,有别于主机虚拟化,是操作系统层的虚拟化。通过 namespace 进行各程序的隔离,加上 cgroups 进行资源的控制,以此来进行虚拟化。
二.NameSpace
2.1什么是 Namespace(命名空间)
namespace 是 Linux 内核用来隔离内核资源的方式。通过 namespace 可以让一些进程只能看到与自己相关的一部分资源,而另外一些进程也只能看到与它们自己相关的资源,这两拨进程根本就感觉不到对方的存在。具体的实现方式是把一个或多个进程的相关资源指定在同一个 namespace 中。Linux namespaces 是对全局系统资源的一种封装隔离,使得处于不同 namespace 的进程拥有独立的全局系统资源,改变一个 namespace 中的系统资源只会影响当前namespace 里的进程,对其他 namespace 中的进程没有影响。
Linux 提供了多个 API 用来操作 namespace,它们是 clone()、 setns() 和 unshare() 函数,为了确定隔离的到底是哪项 namespace,在使用这些 API 时,通常需要指定一些调用参数:CLONE_NEWIPC、 CLONE_NEWNET、 CLONE_NEWNS、CLONE_NEWPID、CLONE_NEWUSER、 CLONE_NEWUTS 和CLONE_NEWCGROUP。如果要同时隔离多个 namespace,可以使用 | (按位或)组合这些参数。
举个简单的例子,比如c++中的namespace,当一个函数名称,参数类型与个数完全相同的时候,但是其内部实现的功能又不一样,这时候就需要namespace命名控件对其进行隔离。
Linux中常见的几个命名空间:
| namespace | 系统调用参数 | 被隔离的全局系统资源 | 引入内核版本 |
|---|---|---|---|
| UTS | CLONE_NEWUTS | 主机名和域名 | 2.6.19 |
| IPC | CLONE_NEWIPC | 信号量、消息队列和共享内存 -进程间通信 | 2.6.19 |
| PID | CLONE_NEWPID | 进程编号 | 2.6.24 |
| Network | CLONE_NEWNET | 网络设备、网络栈、端口等 | 2.6.29 |
| Mount | CLONE_NEWNS | 文件系统挂载点 | 2.4.19 |
| User | CLONE_NEWUSER | 用户和用户组 | 3.8 |
以上命名空间在容器环境下的隔离效果:
UTS:每个容器能看到自己的 hostname,拥有独立的主机名和域名。
IPC:同一个 IPC namespace 的进程之间能互相通讯,不同的 IPC namespace 之间不能通信。
PID:每个 PID namespace 中的进程可以有其独立的 PID,每个容器可以有其 PID 为1 的 root 进程。
Network:每个容器用有其独立的网络设备, IP 地址, IP 路由表, /proc/net 目录,端口号。
Mount:每个容器能看到不同的文件系统层次结构。
User:每个 container 可以有不同的 user 和 group id。
2.2namespace隔离实践
我们需要知道隔离能力并不是 docker 提供的,而是操作系统内核提供基本能力。
2.2.1基础知识
dd命令
Linux dd 命令用于读取、转换并输出数据。
dd 可从标准输入或文件中读取数据,根据指定的格式来转换数据,再输出到文件、设备或标准输出。
语法:
bash
dd OPTION
选项参数:
- if=文件名:输入文件名,默认为标准输入。即指定源文件。
- of=文件名:输出文件名,默认为标准输出。即指定目的文件。
- ibs=bytes:一次读入 bytes 个字节,即指定一个块大小为 bytes 个字节。
obs=bytes:一次输出 bytes 个字节,即指定一个块大小为 bytes 个字节。
bs=bytes:同时设置读入/输出的块大小为 bytes 个字节。 - cbs=bytes:一次转换 bytes 个字节,即指定转换缓冲区大小。
- skip=blocks:从输入文件开头跳过 blocks 个块后再开始复制。
- seek=blocks:从输出文件开头跳过 blocks 个块后再开始复制。
- count=blocks:仅拷贝 blocks 个块,块大小等于 ibs 指定的字节数。
- conv=<关键字>,关键字可以有以下 11 种:
▪ conversion:用指定的参数转换文件。
▪ ascii:转换 ebcdic 为 ascii
▪ ebcdic:转换 ascii 为 ebcdic
▪ ibm:转换 ascii 为 alternate ebcdic
▪ block:把每一行转换为长度为 cbs,不足部分用空格填充
▪ unblock:使每一行的长度都为 cbs,不足部分用空格填充▪ lcase:把大写字符转换为小写字符
▪ ucase:把小写字符转换为大写字符
▪ swap:交换输入的每对字节
▪ noerror:出错时不停止
▪ notrunc:不截短输出文件
▪ sync:将每个输入块填充到 ibs 个字节,不足部分用空(NUL)字符补齐。 - --help:显示帮助信息
- --version:显示版本信息
来看两个简单的样例(/dev/zero 是 Linux 系统中的一个特殊设备文件,它是一个提供无限零字节(null bytes,即 0x00)的虚拟设备。)
比如我们要创建8mb的空的镜像文件:
bash
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ dd if=/dev/zero of=test.img bs=8k count=1024
1024+0 records in
1024+0 records out
8388608 bytes (8.4 MB, 8.0 MiB) copied, 0.00484822 s, 1.7 GB/s
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ ll
total 8200
drwxrwxr-x 2 knd knd 4096 Dec 16 12:40 ./
drwxrwxr-x 3 knd knd 4096 Dec 16 12:32 ../
-rw-rw-r-- 1 knd knd 8388608 Dec 16 12:40 test.img
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ ll -h
total 8.1M
drwxrwxr-x 2 knd knd 4.0K Dec 16 12:40 ./
drwxrwxr-x 3 knd knd 4.0K Dec 16 12:32 ../
-rw-rw-r-- 1 knd knd 8.0M Dec 16 12:40 test.img
如果说我们想要将一个文本文件中所有的小写字母转化为大写字母可以这样写:
bash
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ touch 111.txt
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ echo Hello World > 111.txt
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ cat 111.txt
Hello World
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ dd if=111.txt of=test1.txt conv=ucase
0+1 records in
0+1 records out
12 bytes copied, 5.4983e-05 s, 218 kB/s
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ cat test1.txt
HELLO WORLD
mkfs命令
用于在设备上创建 Linux 文件系统,俗称格式化,比如我们使用 U 盘的时候可以格式化。
语法:
bash
mkfs [-V] [-t fstype] [fs-options] filesys [blocks]
选项参数:
bash
-t fstype:指定要建立何种文件系统;如 ext3, ext4
filesys :指定要创建的文件系统对应的设备文件名;
blocks:指定文件系统的磁盘块数。
-V : 详细显示模式
fs-options:传递给具体的文件系统的参数
我们看一个简单样例,比如将我们上面创建的一个镜像文件格式化为ext4格式:
bash
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ mkfs -t ext4 ./test.img
mke2fs 1.47.0 (5-Feb-2023)
Discarding device blocks: done
Creating filesystem with 2048 4k blocks and 2048 inodes
Allocating group tables: done
Writing inode tables: done
Creating journal (1024 blocks): done
Writing superblocks and filesystem accounting information: done
这样就成功的将我们原来的镜像文件格式化为了一个ext4格式的文件。可供挂载使用。
df 命令
Linux df(英文全拼: disk free) 命令用于显示目前在 Linux 系统上的文件系统磁盘使用情况统计。
语法:
bash
df [OPTION]... [FILE]...
选项参数:
bash
-a, --all 包含所有的具有 0 Blocks 的文件系统
-h, --human-readable 使用人类可读的格式(预设值是不加这个选项的...)
-H, --si 很像 -h, 但是用 1000 为单位而不是用 1024
-t, --type=TYPE 限制列出文件系统的 TYPE
-T, --print-type 显示文件系统的形式
简单示例:
bash
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ df
Filesystem 1K-blocks Used Available Use% Mounted on
tmpfs 381268 1084 380184 1% /run
/dev/vda2 41168220 8575100 30794428 22% /
tmpfs 1906340 24 1906316 1% /dev/shm
tmpfs 5120 0 5120 0% /run/lock
tmpfs 381268 12 381256 1% /run/user/1003
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ df -h
Filesystem Size Used Avail Use% Mounted on
tmpfs 373M 1.1M 372M 1% /run
/dev/vda2 40G 8.2G 30G 22% /
tmpfs 1.9G 24K 1.9G 1% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 373M 12K 373M 1% /run/user/1003
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ df -H
Filesystem Size Used Avail Use% Mounted on
tmpfs 391M 1.2M 390M 1% /run
/dev/vda2 43G 8.8G 32G 22% /
tmpfs 2.0G 25k 2.0G 1% /dev/shm
tmpfs 5.3M 0 5.3M 0% /run/lock
tmpfs 391M 13k 391M 1% /run/user/1003
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ df -t ext4
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/vda2 41168220 8575124 30794404 22% /
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ df -T
Filesystem Type 1K-blocks Used Available Use% Mounted on
tmpfs tmpfs 381268 1084 380184 1% /run
/dev/vda2 ext4 41168220 8575140 30794388 22% /
tmpfs tmpfs 1906340 24 1906316 1% /dev/shm
tmpfs tmpfs 5120 0 5120 0% /run/lock
tmpfs tmpfs 381268 12 381256 1% /run/user/1003
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ df -a
Filesystem 1K-blocks Used Available Use% Mounted on
sysfs 0 0 0 - /sys
proc 0 0 0 - /proc
udev 1866472 0 1866472 0% /dev
devpts 0 0 0 - /dev/pts
tmpfs 381268 1084 380184 1% /run
/dev/vda2 41168220 8575148 30794380 22% /
...
df 命令本质上就是 Linux 的"磁盘管理界面",它告诉你:
- 系统中有哪些"盘"(分区/存储设备)
- 每个"盘"有多大容量
- 用了多少,还剩多少
- 这些"盘"挂载在哪个目录下
只是 Linux 用目录树的方式组织所有存储设备,而不是像windows那样用 A:、B:、C: 这样的盘符体系。
mount命令
mount 命令就是 Linux 的"分配盘符"或"连接驱动器"操作,它把存储设备(硬盘分区、U盘、光盘等)连接到目录树的一个位置,让系统可以访问其中的文件。
| Windows 操作 | Linux 对应操作 |
|---|---|
| 插入U盘 → 自动显示为 F: 盘 | 插入U盘 → 需要执行 mount /dev/sdb1 /mnt/usb |
| 打开"此电脑"看到所有驱动器 | df 命令显示已挂载的文件系统 |
| 弹出U盘(安全移除) | umount 命令卸载设备 |
| 盘符(C:, D:) | 挂载点(/, /home, /mnt/data) |
常见用法:
bash
mount [-l]
mount [-t vfstype] [-o options] device dir
常见参数:
bash
-l:显示已加载的文件系统列表;
-t: 加载文件系统类型支持常见系统类型的 ext3,ext4,iso9660,tmpfs,xfs 等,大部分情况
可以不指定, mount 可以自己识别
-o options 主要用来描述设备或档案的挂接方式。
loop:用来把一个文件当成硬盘分区挂接上系统
ro:采用只读方式挂接设备
rw:采用读写方式挂接设备
device: 要挂接(mount)的设备。
dir: 挂载点的目录
比如我现在想要将刚才我们格式化为ext4的镜像挂载到一个指定目录下可以这样操作:
bash
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ ll
total 4228
drwxrwxr-x 3 knd knd 4096 Dec 16 13:23 ./
drwxrwxr-x 3 knd knd 4096 Dec 16 12:32 ../
-rw-rw-r-- 1 knd knd 12 Dec 16 12:42 111.txt
drwxrwxr-x 2 knd knd 4096 Dec 16 13:23 test/
-rw-rw-r-- 1 knd knd 12 Dec 16 12:42 test1.txt
-rw-rw-r-- 1 knd knd 8388608 Dec 16 13:08 test.img
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ sudo mount ./test.img ./test
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ df -t ext4
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/vda2 41168220 8576808 30792720 22% /
/dev/loop4 3568 24 2976 1% /home/knd/dockertest/namespacets/test
因为我们之前的镜像格式化为了ext4格式,所以这里使用df -t ext4能直接看到我们挂载的这个镜像文件。而我们这一系列操作就相当于以格式化后的镜像文件为基础,创建了一块虚拟磁盘分区。这就像是把一个 文件"变成"了一个磁盘。
使用umount可以卸载此虚拟磁盘分区:
bash
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ sudo umount ./test
knd@VM-12-9-ubuntu:~/dockertest/namespacets$ df -t ext4
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/vda2 41168220 8577472 30792056 22% /
unshared命令
unshare 主要能力是使用与父程序不共享的名称空间运行程序。
语法:
bash
unshare [options] program [arguments]
常用参数:
| 参数 | 含义 |
|---|---|
| -i | --ipc 不共享 IPC 空间 |
| -m | --mount 不共享 Mount 空间 |
| -n | --net 不共享 Net 空间 |
| -p | --pid 不共享 PID 空间 |
| -u | --uts 不共享 UTS 空间 |
| -U | --user 不共享用户 |
| -V | --version 版本查看 |
| --fork | 执行 unshare 的进程 fork 一个新的子进程,在子进程里执行 unshare 传入的参数。 |
| --mount-proc | 执行子进程前,将 proc 优先挂载过去 |
比如我想要让两个进程不共享用户:
bash
knd@Nightcode:~$ sudo unshare -u /bin/bash
[sudo] password for knd:
root@Nightcode:/home/knd# hostname test1
root@Nightcode:/home/knd# hostname
test1
root@Nightcode:/home/knd# exit
exit
knd@Nightcode:~$ hostname
Nightcode
我这里执行了一个新的命令行解释器,它与我们当前终端的命令行解释器不共享用户的命名空间。那么在从进程里面修改主机名不会影响主进程的主机名。
2.2.2进程隔离实践
我们可以通过如下命令进行一个新的进程创建,让其与主进程的进程命名空间进行隔离:
bash
knd@Nightcode:~$ sudo unshare -p /bin/bash
bash: fork: Cannot allocate memory
这时我们会看到如上的问题。这是因为 Linux 内核中有一个特殊机制:调用unshare(CLONE_NEWPID) 的进程本身并不进入新的 PID 命名空间,只有它创建的第一个子进程才会进入并成为 PID 1。
当我们不加 --fork 直接运行 bash 时,首先unshare执行execvp()让bash对其进行进程替换,此时bash 进程本身其实还留在"墙外"(老命名空间),但它启动时需要初始化(如作业控制)去 fork 子进程,而这些子进程试图进入"墙内"(新命名空间)成为 PID 1。这种"父进程在墙外,子进程在墙内"的错位关系导致了内核资源分配失败(报 Cannot allocate memory)。Linux 允许父进程和子进程处于不同的PID命名空间,这是容器技术的基础。
但是,当一个进程已创建新PID命名空间但自身未进入时,它随后进行的fork操作会产生跨命名空间的子进程。这种跨命名空间的进程创建在某些场景下(特别是当父进程需要复杂的初始化,如bash的作业控制设置)可能触发内核的安全检查或资源分配限制,导致失败。
unshare --fork 通过先fork再创建命名空间的正确顺序,确保目标进程(bash)在新命名空间内运行,避免了跨命名空间的复杂性和相关限制。
所以正确做法是加上一个--fork选项
bash
knd@Nightcode:~$ sudo unshare -p --fork /bin/bash
root@Nightcode:/home/knd#
--fork 的作用是让 unshare 先创建一个子进程。根据内核规则,这个子进程会真正进入新的命名空间,并成为该空间内的 PID 1。 紧接着,这个子进程执行程序替换(exec)运行 /bin/bash。此时,/bin/bash 就直接继承了 PID 1 的身份,并且身处新命名空间内部。既然 bash 已经是"墙内"的 PID 1,它就可以名正言顺地 fork 它是自己的子进程来处理业务了。
此时按道理来说当我们新启动一个终端执行ps -ef时看到的进程应该与我们上面的看到的不一样,但实际上是一样的:
新的进程内部:
bash
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Nov10 ? 00:09:22 /usr/lib/systemd/systemd --system --deserialize=117 showopts
root 2 0 0 Nov10 ? 00:00:00 [kthreadd]
root 3 2 0 Nov10 ? 00:00:00 [pool_workqueue_release]
root 4 2 0 Nov10 ? 00:00:00 [kworker/R-rcu_g]
root 5 2 0 Nov10 ? 00:00:00 [kworker/R-rcu_p]
root 6 2 0 Nov10 ? 00:00:00 [kworker/R-slub_]
root 7 2 0 Nov10 ? 00:00:00 [kworker/R-netns]
root 12 2 0 Nov10 ? 00:00:00 [kworker/R-mm_pe]
root 13 2 0 Nov10 ? 00:00:00 [rcu_tasks_kthread]
...
老终端执行ps -ef:
bash
knd@Nightcode:~$ ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Nov10 ? 00:09:22 /usr/lib/systemd/systemd --system --deserialize=117 showopts
root 2 0 0 Nov10 ? 00:00:00 [kthreadd]
root 3 2 0 Nov10 ? 00:00:00 [pool_workqueue_release]
root 4 2 0 Nov10 ? 00:00:00 [kworker/R-rcu_g]
root 5 2 0 Nov10 ? 00:00:00 [kworker/R-rcu_p]
root 6 2 0 Nov10 ? 00:00:00 [kworker/R-slub_]
root 7 2 0 Nov10 ? 00:00:00 [kworker/R-netns]
root 12 2 0 Nov10 ? 00:00:00 [kworker/R-mm_pe]
root 13 2 0 Nov10 ? 00:00:00 [rcu_tasks_kthread]
...
这是因为--pid 表示我们的进程隔离的是 pid,而其他命名空间没有隔离而此时就需要加上--mount-proc选项,加上的原因是因为 Linux 下的每个进程都有一个对应的 /proc/PID 目录,该目录包含了大量的有关当前进程的信息。 对一个 PID namespace 而言, /proc 目录只包含当前namespace 和它所有子孙后代 namespace 里的进程的信息。创建一个新的 PIDnamespace 后,如果想让子进程中的 top、 ps 等依赖 /proc 文件系统的命令工作,还需要挂载 /proc 文件系统。而文件系统隔离是 mount namespace 管理的,所以 linux特意提供了一个选项--mount-proc 来解决这个问题。如果不带这个我们看到的进程还是系统的进程信息。
带上此选项时我们便发现新的/bin/bash内执行ps -ef就会少很多进程信息了:
bash
knd@Nightcode:~$ sudo unshare -p --fork --mount-proc /bin/bash
root@Nightcode:/home/knd# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 14:53 pts/2 00:00:00 /bin/bash
root 8 1 0 14:53 pts/2 00:00:00 ps -ef
这时就实现了我们预期的进程隔离。
2.2.3mount隔离实践
打开第一个 shell 窗口 A,执行命令, df -h ,查看主机默认命名空间的磁盘挂载情况
bash
knd@Nightcode:~$ df -h
Filesystem Size Used Avail Use% Mounted on
tmpfs 373M 1.1M 372M 1% /run
/dev/vda2 40G 8.2G 30G 22% /
tmpfs 1.9G 24K 1.9G 1% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 373M 12K 373M 1% /run/user/1003
打开一个新的 shell 窗口 B,执行 Mount 隔离命令
bash
knd@Nightcode:~/dockertest/namespacets$ sudo unshare -m --fork /bin/bash
root@Nightcode:/home/knd/dockertest/namespacets# ls
111.txt test test1.txt test.img
root@Nightcode:/home/knd/dockertest/namespacets# mkfs -t ext4 ./test.img
mke2fs 1.47.0 (5-Feb-2023)
./test.img contains a ext4 file system
last mounted on /home/knd/dockertest/namespacets/test on Tue Dec 16 13:27:08 2025
Proceed anyway? (y,N) y
Discarding device blocks: done
Creating filesystem with 2048 4k blocks and 2048 inodes
Allocating group tables: done
Writing inode tables: done
Creating journal (1024 blocks): done
Writing superblocks and filesystem accounting information: done
root@Nightcode:/home/knd/dockertest/namespacets# mount ./test.img ./test
root@Nightcode:/home/knd/dockertest/namespacets# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/vda2 40G 8.2G 30G 22% /
tmpfs 1.9G 24K 1.9G 1% /dev/shm
tmpfs 373M 1.1M 372M 1% /run
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 373M 12K 373M 1% /run/user/1003
/dev/loop4 3.5M 24K 3.0M 1% /home/knd/dockertest/namespacets/test
再回到Shell窗口A,执行df -h
bash
knd@Nightcode:~$ df -h
Filesystem Size Used Avail Use% Mounted on
tmpfs 373M 1.1M 372M 1% /run
/dev/vda2 40G 8.2G 30G 22% /
tmpfs 1.9G 24K 1.9G 1% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 373M 12K 373M 1% /run/user/1003
发现他是看不到/dev/loop4 这个我们新挂载的镜像文件的。
三.cgroups
3.1什么是cgroups
cgroups(Control Groups) 是 linux 内核提供的一种机制, 这种机制可以根据需求把一系列系统任务及其子任务整合(或分隔)到按资源划分等级的不同组内,从而为系统资源管理提供一个统一的框架。 简单说, cgroups 可以限制、记录任务组所使用的物理资源。本质上来说, cgroups 是内核附加在程序上的一系列钩子(hook),通过程序运行时对资源的调度触发相应的钩子以达到资源追踪和限制的目的。
使用cgroups则是因为 其可以做到对 cpu,内存等资源实现精细化的控制,目前越来越火的轻量级容器Docker 及 k8s 中pod 就使用了 cgroups 提供的资源限制能力来完成 cpu,内存等部分的资源控制。
比如在一个既部署了前端 web 服务,也部署了后端计算模块的八核服务器上,可以使用 cgroups 限制 web server 仅可以使用其中的六个核,把剩下的两个核留给后端计算模块。
cgroups用途:
| Resource limitation: 限制资源使用 | 例:内存使用上限/cpu 的使用限制 |
|---|---|
| Prioritization: 优先级控制 | 例: CPU 利用/磁盘 IO 吞吐 |
| Accounting: 一些审计或一些统计 | |
| Control: 挂起进程/恢复执行进程 |
cgroups 可以控制的子系统:
| blkio | 对块设备的 IO 进行限制。 |
|---|---|
| cpu | 限制 CPU 时间片的分配 |
| cpuacct | 生成 cgroup 中的任务占用 CPU 资源的报告,与 cpu 挂载在同一目录。 |
| cpuset | 给 cgroup 中的任务分配独立的 CPU(多处理器系统) 和内存节点。 |
| devices | 限制设备文件的创建,和对设备文件的读写 |
| freezer | 暂停/恢复 cgroup 中的任务。 |
| memory | 对 cgroup 中的任务的可用内存进行限制,并自动生成资源占用报告。 |
| perf_event | 允许 perf 观测 cgroup 中的 task |
| net_cls | cgroup 中的任务创建的数据报文的类别标识符,这让 Linux 流量控制器(tc 指令)可以识别来自特定 cgroup任务的数据包,并进行网络限制。 |
| hugetlb | 限制使用的内存页数量。 |
| pids | 限制任务的数量。 |
| rdma | 限制 RDMA 资源(Remote Direct Memory Access,远程直接数据存取) |
3.2cgroups资源控制实践
3.2.1基础知识
pidstat命令
pidstat 是 sysstat 的一个命令,用于监控全部或指定进程的 CPU、内存、线程、设备IO 等系统资源的占用情况。 Pidstat 第一次采样显示自系统启动开始的各项统计信息,后续采样将显示自上次运行命令后的统计信息。用户可以通过指定统计的次数和时间来获得所需的统计信息。
安装:
bash
sudo apt install sysstat -y
使用语法:
bash
pidstat [ 选项 ] [ <时间间隔> ] [ <次数> ]
参数选项:
| -u | 默认参数,显示各进程的 CPU 使用统计 |
|---|---|
| -r | 显示各进程的内存使用统计 |
| -d | 显示各进程的 IO 使用情况 |
| -p | 指定进程号,ALL 表示所有进程 |
| -C | 指定命令 |
| -l | 显示命令名和所有参数 |
大家可以自己pidstat带上上面展示的各个选项试试。我们这里就不展示了。注意选项部分是可以将上面的参数选项进行组合使用的。
stress命令
stress 是 Linux 的一个压力测试工具,可以对 CPU、 Memory、 IO、磁盘进行压力测试。
安装:
bash
sudo apt install stress -y
参数选项:
| -c, --cpu N | 产生 N 个进程,每个进程都循环调用 sqrt 函数产生 CPU 压力。 |
|---|---|
| -i, --io N | 产生 N 个进程,每个进程循环调用 sync 将内存缓冲区内容写到磁盘上,产生 IO 压力。通过系统调用 sync 刷新内存缓冲区数据到磁盘中,以确保同步。如果缓冲区内数据较少,写到磁盘中的数据也较少,不会产生 IO 压力。在 SSD 磁盘环境中尤为明显,很可能 iowait 总是 0,却因为大量调用系统调用 sync,导致系统 CPU 使用率 sys 升高。 |
| -m, --vm N | 产生 N 个进程,每个进程循环调用 malloc/free 函数分配和释放内存。 |
| -m,--vm-bytes B | 指定分配内存的大小 |
| -m,--vm-keep | 一直占用内存,区别于不断的释放和重新分配(默认是不断释放并重新分配内存) |
| -d, --hdd N | 产生 N 个不断执行 write 和 unlink 函数的进程(创建文件,写入内容,删除文件) |
| --hdd-bytes B | 指定文件大小 |
| -t, --timeout N | 在 N 秒后结束程序 |
| -q, --quiet | 程序在运行的过程中不输出信息 |
cpu的压力测试
我们结合上面的pidstat来进行cpu的压力测试:
假设我们当前终端为A,这时我们新启一个终端为B并执行如下命令:
bash
sudo pidstat -C stress -p ALL -u 2 10000
它表示我对进程名为stress的所有进程的cpu使用情况进行监控。
然后切回终端A执行如下命令:
bash
sudo stress -c 1
接下来便可以在B上看到如下效果:
bash
05:01:44 PM UID PID %usr %system %guest %wait %CPU CPU Command
05:01:46 PM 0 2093973 0.00 0.00 0.00 0.00 0.00 0 stress
05:01:46 PM 0 2093974 100.00 0.00 0.00 0.00 100.00 0 stress
05:01:46 PM UID PID %usr %system %guest %wait %CPU CPU Command
05:01:48 PM 0 2093973 0.00 0.00 0.00 0.00 0.00 0 stress
05:01:48 PM 0 2093974 100.00 0.00 0.00 0.00 100.00 0 stress
05:01:48 PM UID PID %usr %system %guest %wait %CPU CPU Command
05:01:50 PM 0 2093973 0.00 0.00 0.00 0.00 0.00 0 stress
05:01:50 PM 0 2093974 100.00 0.00 0.00 0.00 100.00 0 stress
...
其中一个stress占用为0的是stress的管理进程。
对系统CPU的压力测试:
接下来我们再进行下对系统CPU的压力测试:
bash
#A
sudo stress -i 1
bash
#B
sudo pidstat -C stress -p ALL -u 2 10000
结果:
bash
#B
05:05:09 PM UID PID %usr %system %guest %wait %CPU CPU Command
05:05:11 PM 0 2094607 0.00 0.00 0.00 0.00 0.00 2 stress
05:05:11 PM 0 2094608 0.50 98.50 0.00 0.00 99.00 2 stress
...
对内存的压力测试
bash
#A
sudo stress -m 4
bash
#B
sudo pidstat -C stress -p ALL -r 2 10000
bash
#B
05:07:50 PM UID PID minflt/s majflt/s VSZ RSS %MEM Command
05:07:52 PM 0 2095447 0.00 0.00 3620 1920 0.05 stress
05:07:52 PM 0 2095448 420908.00 0.00 265768 25344 0.66 stress
05:07:52 PM 0 2095449 420445.00 0.00 265768 57600 1.51 stress
05:07:52 PM 0 2095450 420184.00 0.00 265768 87788 2.30 stress
05:07:52 PM 0 2095451 414317.00 0.00 265768 169088 4.43 stress
^C
Average: UID PID minflt/s majflt/s VSZ RSS %MEM Command
Average: 0 2095447 14.00 0.00 3620 1920 0.05 stress
Average: 0 2095448 279312.00 0.00 265768 66048 1.73 stress
Average: 0 2095449 280321.00 0.00 265768 92032 2.41 stress
Average: 0 2095450 281258.50 0.00 265768 118764 3.11 stress
Average: 0 2095451 275613.12 0.00 265768 138452 3.63 stress
对磁盘的压力测试
bash
#A
sudo stress -d 8
bash
#B
sudo pidstat -C stress -p ALL -d 2 10000
bash
#B
05:11:30 PM UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
05:11:32 PM 0 2096258 0.00 0.00 0.00 0 stress
05:11:32 PM 0 2096259 0.00 45076.00 0.00 0 stress
05:11:32 PM 0 2096260 0.00 44312.00 0.00 0 stress
05:11:32 PM 0 2096261 0.00 44312.00 0.00 0 stress
05:11:32 PM 0 2096262 0.00 45076.00 0.00 0 stress
05:11:32 PM 0 2096263 0.00 44308.00 0.00 0 stress
05:11:32 PM 0 2096264 0.00 44312.00 0.00 0 stress
05:11:32 PM 0 2096265 0.00 45076.00 0.00 0 stress
05:11:32 PM 0 2096266 0.00 44312.00 0.00 0 stress
^C
Average: UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
Average: 0 2096258 0.00 0.00 0.00 0 stress
Average: 0 2096259 0.00 37946.00 0.00 0 stress
Average: 0 2096260 0.00 38207.33 0.00 0 stress
Average: 0 2096261 0.00 38442.00 0.00 0 stress
Average: 0 2096262 0.00 38459.33 0.00 0 stress
Average: 0 2096263 0.00 38457.33 0.00 0 stress
Average: 0 2096264 0.00 38460.00 0.00 0 stress
Average: 0 2096265 0.00 38457.33 0.00 0 stress
Average: 0 2096266 0.00 37688.00 0.00 0 stress
3.2.2cgroups信息查看
cgroups版本信息查看:
bash
knd@Nightcode:~$ cat /proc/filesystems | grep cgroup
nodev cgroup
nodev cgroup2
如果看到 cgroup2,表示支持 cgroup v2,那如何确定系统使用的版本?
bash
knd@Nightcode:~$ stat -fc %T /sys/fs/cgroup/
cgroup2fs
输出 cgroup2fs 表示系统使用的是 cgroup v2;输出 tmpfs 表示使用cgroup v1。
cgroups 子系统查看:
命令为:cat /proc/cgroups
cgroupv1查看:
bash
root@139-159-150-152:/sys/fs/cgroup# cat /proc/cgroups
#subsys_name hierarchy num_cgroups enabled
cpuset 8 2 1
cpu 2 100 1cpuacct 2 100 1
blkio 6 98 1
memory 10 124 1
devices 12 98 1
freezer 7 2 1
net_cls 4 2 1
perf_event 3 2 1
net_prio 4 2 1
hugetlb 9 2 1
pids 5 107 1
rdma 11 2 1
cgroupv2查看:
bash
knd@Nightcode:~$ cat /proc/cgroups
#subsys_name hierarchy num_cgroups enabled
cpuset 0 82 1
cpu 0 82 1
cpuacct 0 82 1
blkio 0 82 1
memory 0 82 1
devices 0 82 1
freezer 0 82 1
net_cls 0 82 1
perf_event 0 82 1
net_prio 0 82 1
hugetlb 0 82 1
pids 0 82 1
rdma 0 82 1
misc 0 82 1
通过子系统信息的查看能确定cgroup支持哪些资源的控制。
cgroups挂载信息查看:
v1:
bash
root@139-159-150-152:/sys/fs/cgroup# mount |grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs
(ro,nosuid,nodev,noexec,mode=755)
cgroup2 on /sys/fs/cgroup/unified type cgroup2
(rw,nosuid,nodev,noexec,relatime,nsdelegate)
cgroup on /sys/fs/cgroup/systemd type cgroup
(rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup
(rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/perf_event type cgroup
(rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup
(rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/pids type cgroup
(rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/blkio type cgroup
(rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/freezer type cgroup(rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/cpuset type cgroup
(rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/hugetlb type cgroup
(rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/memory type cgroup
(rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/rdma type cgroup
(rw,nosuid,nodev,noexec,relatime,rdma)
cgroup on /sys/fs/cgroup/devices type cgroup
(rw,nosuid,nodev,noexec,relatime,device
v2:
bash
knd@Nightcode:~$ mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)
查看一个进程的控制组限制:
比如我们需要看当前进程的控制组限制:
v1:
bash
[root@VM-8-12-centos ~]# cat /proc/$$/cgroup
11:hugetlb:/
10:memory:/user.slice
9:freezer:/
8:cpuset:/
7:perf_event:/
6:net_prio,net_cls:/
5:devices:/user.slice/user-0.slice
4:pids:/user.slice
3:cpuacct,cpu:/user.slice
2:blkio:/user.slice
1:name=systemd:/user.slice/user-0.slice/session-354304.scope
V2:
bash
knd@Nightcode:~$ cat /proc/$$/cgroup
0::/user.slice/user-1003.slice/session-71371.scope
3.2.3使用cgroups进行内存控制
这里我们从V2版本进行介绍:
首先我们使用如下命令查看当前控制是否支持memory:
bash
knd@Nightcode:~$ mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)
knd@Nightcode:~$ cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory hugetlb pids rdma misc
如果看到了memory的字样说明支持。
接下来我们在/sys/fs/cgroup下创建一个目录/test_memory,可以看到系统自动在该目录下创建了一系列文件:
bash
knd@Nightcode:/sys/fs/cgroup$ ll ./test_memory/
total 0
drwxr-xr-x 2 root root 0 Dec 16 17:41 ./
dr-xr-xr-x 14 root root 0 Dec 16 17:41 ../
-r--r--r-- 1 root root 0 Dec 16 17:41 cgroup.controllers
-r--r--r-- 1 root root 0 Dec 16 17:41 cgroup.events
-rw-r--r-- 1 root root 0 Dec 16 17:41 cgroup.freeze
--w------- 1 root root 0 Dec 16 17:41 cgroup.kill
-rw-r--r-- 1 root root 0 Dec 16 17:41 cgroup.max.depth
-rw-r--r-- 1 root root 0 Dec 16 17:41 cgroup.max.descendants
-rw-r--r-- 1 root root 0 Dec 16 17:41 cgroup.pressure
-rw-r--r-- 1 root root 0 Dec 16 17:41 cgroup.procs
-r--r--r-- 1 root root 0 Dec 16 17:41 cgroup.stat
-rw-r--r-- 1 root root 0 Dec 16 17:41 cgroup.subtree_control
-rw-r--r-- 1 root root 0 Dec 16 17:41 cgroup.threads
-rw-r--r-- 1 root root 0 Dec 16 17:41 cgroup.type
-rw-r--r-- 1 root root 0 Dec 16 17:41 cpu.idle
-rw-r--r-- 1 root root 0 Dec 16 17:41 cpu.max
-rw-r--r-- 1 root root 0 Dec 16 17:41 cpu.max.burst
...
配置 cgroup 的策略为最大使用 1M 内存:
可以看到默认情况下是没有限制的:
bash
knd@Nightcode:/sys/fs/cgroup/test_memory$ cat memory.max
max
先切换到root再执行下面的命令:
bash
root@Nightcode:/sys/fs/cgroup/test_memory# echo 1M > memory.max
root@Nightcode:/sys/fs/cgroup/test_memory# cat memory.max
1048576
此时我们使用stress启动一个需要50m内存的程序,另起一个新终端B:
bash
#B
knd@Nightcode:~$ sudo stress -m 1 --vm-bytes 50m
再起一个终端对其进行监控:
bash
#C
knd@Nightcode:~$ sudo pidstat -C stress -p ALL -r 2 10000
05:50:47 PM UID PID minflt/s majflt/s VSZ RSS %MEM Command
05:50:49 PM 0 2105443 0.00 0.00 3620 1920 0.05 stress
05:50:49 PM 0 2105444 748781.50 0.00 54824 3948 0.10 stress
如果此时我们想要对此进程进行内存控制,则需要将其PID加入到cgroup.procs中:
bash
#A
root@Nightcode:/sys/fs/cgroup/test_memory# echo 2105443 >> /sys/fs/cgroup/test_memory/cgroup.procs
root@Nightcode:/sys/fs/cgroup/test_memory# echo 2105443 >> /sys/fs/cgroup/test_memory/cgroup.procs
bash
#B
stress: info: [2110539] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL: [2110539] (425) <-- worker 2110540 got signal 9
stress: WARN: [2110539] (427) now reaping child worker processes
stress: FAIL: [2110539] (461) failed run completed in 35s
bash
#C
06:11:08 PM UID PID minflt/s majflt/s VSZ RSS %MEM Command
06:11:10 PM UID PID minflt/s majflt/s VSZ RSS %MEM Command
^C
Average: UID PID minflt/s majflt/s VSZ RSS %MEM Command
可以看到stress因为无法申请到足够的内存而退出了。
3.2.3使用cgroups对cpu进行控制
创建内存的 cgroup,很简单我们进入到 cgroup 的内存控制目录/sys/fs/cgroup/,我们创建目录 test_cpu,可以看到系统会自动为我们创建 cgroup 的 cpu 策略:
bash
root@Nightcode:/sys/fs/cgroup# mkdir test_cpu
root@Nightcode:/sys/fs/cgroup# cd test_cpu
root@Nightcode:/sys/fs/cgroup/test_cpu# ll
total 0
drwxr-xr-x 2 root root 0 Dec 16 18:18 ./
dr-xr-xr-x 15 root root 0 Dec 16 18:17 ../
-r--r--r-- 1 root root 0 Dec 16 18:18 cgroup.controllers
-r--r--r-- 1 root root 0 Dec 16 18:18 cgroup.events
-rw-r--r-- 1 root root 0 Dec 16 18:18 cgroup.freeze
--w------- 1 root root 0 Dec 16 18:18 cgroup.kill
-rw-r--r-- 1 root root 0 Dec 16 18:18 cgroup.max.depth
-rw-r--r-- 1 root root 0 Dec 16 18:18 cgroup.max.descendants
-rw-r--r-- 1 root root 0 Dec 16 18:18 cgroup.pressure
-rw-r--r-- 1 root root 0 Dec 16 18:18 cgroup.procs
-r--r--r-- 1 root root 0 Dec 16 18:18 cgroup.stat
-rw-r--r-- 1 root root 0 Dec 16 18:18 cgroup.subtree_control
-rw-r--r-- 1 root root 0 Dec 16 18:18 cgroup.threads
-rw-r--r-- 1 root root 0 Dec 16 18:18 cgroup.type
-rw-r--r-- 1 root root 0 Dec 16 18:18 cpu.idle
-rw-r--r-- 1 root root 0 Dec 16 18:18 cpu.max
-rw-r--r-- 1 root root 0 Dec 16 18:18 cpu.max.burst
-rw-r--r-- 1 root root 0 Dec 16 18:18 cpu.pressure
-rw-r--r-- 1 root root 0 Dec 16 18:18 cpuset.cpus
-r--r--r-- 1 root root 0 Dec 16 18:18 cpuset.cpus.effective
-rw-r--r-- 1 root root 0 Dec 16 18:18 cpuset.cpus.exclusive
-r--r--r-- 1 root root 0 Dec 16 18:18 cpuset.cpus.exclusive.effective
-rw-r--r-- 1 root root 0 Dec 16 18:18 cpuset.cpus.partition
-rw-r--r-- 1 root root 0 Dec 16 18:18 cpuset.mems
-r--r--r-- 1 root root 0 Dec 16 18:18 cpuset.mems.effective
-r--r--r-- 1 root root 0 Dec 16 18:18 cpu.stat
-r--r--r-- 1 root root 0 Dec 16 18:18 cpu.stat.local
-rw-r--r-- 1 root root 0 Dec 16 18:18 cpu.uclamp.max
-rw-r--r-- 1 root root 0 Dec 16 18:18 cpu.uclamp.min
-rw-r--r-- 1 root root 0 Dec 16 18:18 cpu.weight
-rw-r--r-- 1 root root 0 Dec 16 18:18 cpu.weight.nice
打开新的 shell 窗口 B 窗口,使用 stress 模拟一个任务, cpu 使用率为 100
bash
#B
sudo stress -c 1
可以看到 cpu 的使用率为 100%
bash
#C
knd@Nightcode:~$ sudo pidstat -C stress -p ALL -u 2 10000
Linux 6.8.0-71-generic (Nightcode) 12/16/2025 _x86_64_ (4 CPU)
06:19:53 PM UID PID %usr %system %guest %wait %CPU CPU Command
06:19:55 PM 0 2112647 0.00 0.00 0.00 0.00 0.00 1 stress
06:19:55 PM 0 2112648 100.00 0.00 0.00 0.00 100.00 1 stress
回到shell 窗口A 窗口,我们设置 cproup 的 cpu 使用率为 30%, cpu 使用率的计算公式 cfs_quota_us/cfs_period_us
1) cfs_period_us: cfs_period_us 表示一个 cpu 带宽,单位为微秒。系统总 CPU 带宽 ,默认值 100000。
2) cfs_quota_us: cfs_quota_us 表示 Cgroup 可以使用的 cpu 的带宽,单位为微秒。cfs_quota_us 为-1,表示使用的 CPU 不受 cgroup 限制。 cfs_quota_us 的最小值为1ms(1000),最大值为 1s。
所以我们将 cfs_quota_us 的值设置为 30000 ,从理论上讲就可以限制 test_cpu 控制的进程的 cpu 利用率最多是 30% 。
bash
#A
root@Nightcode:/sys/fs/cgroup/test_cpu# echo "30000 100000" > cpu.max
root@Nightcode:/sys/fs/cgroup/test_cpu# echo 2112648 > cgroup.procs
bash
#C
06:22:17 PM UID PID %usr %system %guest %wait %CPU CPU Command
06:22:19 PM 0 2112647 0.00 0.00 0.00 0.00 0.00 1 stress
06:22:19 PM 0 2112648 30.00 0.00 0.00 70.00 30.00 1 stress
06:22:19 PM UID PID %usr %system %guest %wait %CPU CPU Command
06:22:21 PM 0 2112647 0.00 0.00 0.00 0.00 0.00 1 stress
06:22:21 PM 0 2112648 30.00 0.00 0.00 70.00 30.00 1 stress
可以看到此进程的cpu使用百分比降低到了30%。
至此我们成功的模拟了对内存和 cpu 的使用控制,而 docker 本质也是调用这些的 API来完成对资源的管理,只不过 docker 的易用性和镜像的设计更加人性化,所以 docker才能风靡全球, docker 课程完后我们会看下 docker 如何对资源控制对比这种控制可以说简单不止一倍。
四.LXC
4.1什么是LXC
LXC(LinuX Containers) Linux 容器,一种操作系统层虚拟化技术,为 Linux 内核容器功能的一个用户空间接口。它将应用软件系统打包成一个软件容器(Container),内含应用软件本身的代码,以及所需要的操作系统核心和库。透过统一的名字空间和共享 API 来分配不同软件容器的可用硬件资源,创造出应用程序的独立沙箱运行环境,使得 Linux 用户可以容易的创建和管理系统或应用容器。
LXC 是最早一批真正把完整的容器技术用一组简易使用的工具和模板来极大的简化了容器技术使用的一个方案
LXC 虽然极大的简化了容器技术的使用,但比起直接通过内核调用来使用容器技术,其复杂程度其实并没有多大降低,因为我们必须要学会 LXC 的一组命令工具,且由于内核的创建都是通过命令来实现的,通过批量命令实现数据迁移并不容易。其隔离性也没有虚拟机那么强大。
后来就出现了 docker,所以从一定程度上来说, docker 就是 LXC 的增强版 。
这部分我们不再进行熟悉学习,有兴趣的读者可自行进行学习。(因这个技术随着系统的升级几乎没人维护,也就没有学习的必要了)