0x01 Namespaces:隔离的魔法
Linux Namespaces 提供了一种将系统资源(如进程ID、网络接口、挂载点等)进行抽象和隔离的机制。这意味着在不同的命名空间中,进程会看到不同的系统资源视图,从而实现了隔离。
常见的Namespaces类型及其隔离内容:
PID Namespace(进程ID命名空间): 隔离进程ID。在新的PID Namespace中,进程会从PID 1开始编号,并且只能看到该Namespace内的进程。外部Namespace的进程看不到内部进程的PID,反之亦然。这是容器内进程拥有独立进程树的关键。Mount Namespace(挂载命名空间): 隔离文件系统挂载点。每个Mount Namespace都有一套独立的挂载点列表。这意味着在一个Namespace内挂载或卸载文件系统,不会影响到其他Namespace。这是容器拥有独立根文件系统的基础。Network Namespace(网络命名空间): 隔离网络资源,包括网络接口、IP地址、路由表、防火墙规则等。每个Network Namespace都有自己独立的网络栈。这是容器拥有独立IP地址和端口空间的基石。UTS Namespace(UNIX Timesharing System命名空间): 隔离主机名和域名。允许每个Namespace拥有独立的主机名和域名。IPC Namespace(Interprocess Communication命名空间): 隔离进程间通信(System V IPC和POSIX消息队列)。User Namespace(用户命名空间): 隔离用户和组ID。允许一个Namespace内的用户ID映射到其外部Namespace的不同用户ID,增强了安全性,例如,容器内的root用户在宿主机上可以是普通用户。
Namespaces的工作原理:
当一个进程在新的Namespace中启动时,它会获得该Namespace的资源视图。例如,当使用unshare命令创建一个新的PID Namespace时,新的shell进程及其子进程将拥有独立的PID空间。
# 查看当前shell pid
echo $$
# 创建一个新的PID Namespace并启动一个子shell
unshare --pid --fork --mount-proc bash
# 查看隔离后的pid 和进程树
root@dev-1-35:/home/u1timate# cho $$
1
root@dev-1-35:/home/u1timate# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 7636 4284 pts/1 S 10:39 0:00 bash
root 7 0.0 0.0 10072 1644 pts/1 R+ 10:40 0:00 ps aux
# 退出新的子shell后,宿主机环境的ps aux又会恢复正常
exitunshare 命令解释
--fork:在新的 Namespace 中启动一个子进程。
--mount-proc:挂载一个新的 /proc,否则你看到的还是宿主机的进程表。
0x02 Cgroups:资源的管家
Linux Cgroups (Control Groups) 提供了一种机制来组织进程,并对其资源使用进行限制、审计和优先级管理。简单来说,Namespaces负责“隔离”,而Cgroups则负责“限制”。
Cgroups的主要功能:
资源限制 (Resource Limiting): 限制进程组可以使用的资源量,例如CPU、内存、I/O带宽等。
优先级 (Prioritization): 调整进程组对资源的访问优先级。
审计 (Accounting): 监控进程组的资源使用情况。
控制 (Control): 挂起或恢复进程组。
常见的Cgroup子系统(Subsystems/Controllers):
cpu: 控制CPU资源访问,例如CPU时间片的分配 (cpu.cfs_quota_us, cpu.cfs_period_us) 和CPU亲和性。memory: 限制内存使用,包括物理内存和交换空间 (memory.limit_in_bytes, memory.memsw.limit_in_bytes)。blkio: 限制块设备(如硬盘)的I/O访问。pids: 限制一个Cgroup内可以创建的进程数量。net_cls / net_prio: 标记网络数据包,以便通过流量控制器(Traffic Controller, TC)进行优先级和带宽管理。
Cgroups的工作原理:
cgroup v1 的管理方式
在
/sys/fs/cgroup/下,每个子系统(CPU、内存、blkio 等)都有独立的层级结构。你可以通过在对应子系统目录下创建子目录来建立新的 cgroup。
配置资源限制时,向该目录下的控制文件写入值,例如
memory.limit_in_bytes。将进程加入 cgroup 时,把 PID 写入 tasks 文件。
cgroup v2 的变化
v2 统一了层级结构:所有控制器共享一个单一的层级,而不是每个子系统独立。
进程管理文件也有所不同:使用
cgroup.procs文件而不是 tasks 文件来添加进程。控制文件的命名和语义也发生了变化,例如 CPU 使用 cpu.weight 而不是 v1 的 cpu.shares。
在大多数现代发行版(如 RHEL 9、Ubuntu 22.04),默认启用的是 cgroup v2。
检查cgroups版本
root@dev-1-35:/home/u1timate# cat /proc/filesystems | grep cgroup
nodev cgroup
nodev cgroup2
# 查看实际正在使用的版本
root@dev-1-35:/home/u1timate# mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)
输出包含 cgroup → v1 支持。
输出包含 cgroup2 → v2 支持。
创建一个cgroup(以 v2 版本为例)
# 安装stress工具
sudo apt-get install stress # Debian/Ubuntu
# 创建一个cgroup
mkdir /sys/fs/cgroup/test
# 设置限制
# 设置CPU配额:限制该组进程在100ms内最多使用50ms的CPU时间(即50% CPU)
echo "50000 100000" > /sys/fs/cgroup/test/cpu.maxcpu.max 文件定义了进程在该 cgroup 内可用的 CPU 时间配额。
格式:quota period
quota:允许使用的时间(微秒)。
period:调度周期(微秒)。
test文件夹下结构:

结构解释:
通用 cgroup 文件
cgroup.controllers:当前层级可用的控制器列表(如 cpu、memory、io 等)。
cgroup.subtree_control:启用/禁用子 cgroup 的控制器。
cgroup.events:记录 cgroup 的事件(如是否被冻结)。
cgroup.freeze:冻结/解冻 cgroup 内的所有进程。
cgroup.kill:终止 cgroup 内的所有进程。
cgroup.max.depth:子 cgroup 最大嵌套深度。
cgroup.max.descendants:子 cgroup 最大数量。
cgroup.procs:该 cgroup 内所有进程的 PID。
cgroup.threads:该 cgroup 内所有线程的 TID。
cgroup.type:cgroup 类型(如 domain、thread)。
cgroup.stat:统计信息(如 nr_descendants、nr_dying_descendants)。
CPU 控制器
cpu.max:CPU 使用配额(quota/period),用于限制 CPU 百分比。
cpu.max.burst:允许的额外突发 CPU 时间。
cpu.weight:CPU 调度权重(相对值,默认 100)。
cpu.weight.nice:与 nice 值对应的权重。
cpu.idle:控制 CPU 空闲时的行为。
cpu.pressure:CPU 压力信息(延迟统计)。
cpu.stat:CPU 使用统计(如 user/system 时间)。
cpu.uclamp.min / cpu.uclamp.max:统一 CPU clamp,限制最小/最大 CPU 性能水平。
CPUSET 控制器
cpuset.cpus:允许使用的 CPU 集合。
cpuset.cpus.effective:实际可用的 CPU 集合(考虑父 cgroup 限制)。
cpuset.cpus.partition:CPU 分区设置。
cpuset.mems:允许使用的内存节点集合。
cpuset.mems.effective:实际可用的内存节点集合。
内存控制器
memory.current:当前内存使用量。
memory.max:最大内存限制。
memory.high:高水位限制,超过时触发回收。
memory.low:低水位保证,尽量保留。
memory.min:最小保证内存。
memory.swap.current:当前 swap 使用量。
memory.swap.max:最大 swap 限制。
memory.swap.high:swap 高水位限制。
memory.swap.events:swap 相关事件统计。
memory.events / memory.events.local:内存事件(如 OOM、high/low 触发)。
memory.pressure:内存压力信息。
memory.oom.group:是否作为 OOM 杀进程的整体。
memory.stat:详细内存统计(anon、file、slab 等)。
memory.numa_stat:按 NUMA 节点的内存使用统计。
IO 控制器
io.max:IO 限制(带宽/IOPS)。
io.weight:IO 权重(相对值)。
io.prio.class:IO 优先级类别。
io.stat:IO 使用统计。
io.pressure:IO 压力信息。
PIDs 控制器
pids.current:当前进程数。
pids.max:最大允许进程数。
pids.events:进程数相关事件(如达到上限)

运行进程测试
限制前:

# 使用cgexec将stress命令运行在test中
sudo cgexec -g cpu:test stress -c 8 -t 10s限制后:

0x03 容器如何利用Cgroups和Namespaces
容器运行时(例如Docker、Containerd)在创建容器时,会执行以下核心步骤:
创建Namespaces: 为新容器进程创建一组新的Namespaces(通常是PID、Mount、Network、UTS、IPC等),使其与宿主机和其他容器隔离。
配置Cgroups: 为容器进程创建一个或多个Cgroups,并根据容器的资源配置(如CPU限制、内存限制)向这些Cgroup的控制文件写入相应的值。
启动进程: 在新创建的Namespaces和Cgroups环境中启动容器的第一个进程。
这样,容器内的进程就拥有了独立的运行环境,并且其资源使用也受到了严格的控制,从而实现了轻量级的、高性能的隔离。

0x04 cgroup v1
文件夹结构
/sys/fs/cgroup/
├── cpu/
│ ├── tasks
│ ├── cgroup.procs
│ ├── cpu.shares
│ ├── cpu.cfs_quota_us
│ └── cpu.cfs_period_us
├── memory/
│ ├── tasks
│ ├── memory.limit_in_bytes
│ ├── memory.usage_in_bytes
│ ├── memory.stat
│ └── memory.failcnt
├── blkio/
│ ├── tasks
│ ├── blkio.weight
│ ├── blkio.throttle.read_bps_device
│ └── blkio.throttle.write_bps_device
├── cpuset/
│ ├── tasks
│ ├── cpuset.cpus
│ ├── cpuset.mems
│ └── cpuset.memory_migrate
├── freezer/
│ ├── tasks
│ └── freezer.state
└── net_cls/
├── tasks
└── net_cls.classid每个控制器(如 cpu, memory, blkio)是一个单独的目录。
每个目录下可以创建子 cgroup(子目录),用于隔离不同进程组。
每个子目录中包含:
tasks:用于写入进程 PID,把它加入该 cgroup。
控制器特有的配置文件(如 cpu.shares, memory.limit_in_bytes)。
你可以通过 mkdir 创建子 cgroup,然后写入配置和 PID 来控制资源。
# 以CPU子系统为例,创建并配置一个Cgroup
# 1. 创建一个名为my_limited_cpu_group的Cgroup
sudo mkdir /sys/fs/cgroup/cpu/my_limited_cpu_group
# 2. 设置CPU配额:限制该组进程在100ms内最多使用50ms的CPU时间(即50% CPU)
sudo sh -c "echo 50000 > /sys/fs/cgroup/cpu/my_limited_cpu_group/cpu.cfs_quota_us"
sudo sh -c "echo 100000 > /sys/fs/cgroup/cpu/my_limited_cpu_group/cpu.cfs_period_us"
# 3. 将当前shell的PID添加到该Cgroup中 (或者使用cgexec)
# 获取当前shell的PID
# echo $$
# 将PID添加到Cgroup的tasks文件中
sudo sh -c "echo $(echo $$) > /sys/fs/cgroup/cpu/my_limited_cpu_group/tasks"
# 4. 运行一个CPU密集型任务(如stress工具),观察其CPU使用率
# 注意:直接运行stress可能需要将其PID移入Cgroup,或者使用cgexec
# 安装stress工具
sudo apt-get install stress # Debian/Ubuntu
# 使用cgexec将stress命令运行在my_limited_cpu_group中
sudo cgexec -g cpu:my_limited_cpu_group stress -c 1 -t 10s
# 观察CPU使用率,会发现stress进程的CPU使用被限制在50%左右
# top # 在另一个终端观察
# 5. 清理Cgroup
sudo rm -rf /sys/fs/cgroup/cpu/my_limited_cpu_group