容器基础入门:Cgroup(容器资源限制)、linux Namespace(容器隔离)
摘要 本文介绍了Linux cgroupv2的基本概念和使用方法,包括创建cgroup子系统、设置资源限制以及测试进程加入控制的过程。通过实验演示了内存限制导致程序被kill的情况。同时探讨了Linux namespace的资源隔离机制,包括UTS隔离(主机名修改)、IPC隔离(进程通信隔离)和Mount隔离(挂载点隔离)的实现方法和验证过程。文章提供了详细的代码示例和操作步骤,并介绍了相关Lin
Cgroupv2学习
概念
cgroup 是 Linux 内核的一个功能,用来限制、控制与分离一个进程组的资源(如CPU、内存、磁盘输入输出等)。
测试进程加入Cgroup控制
-
创建Cgroup子系统(默认cgroup根目录都在 /sys/fs/cgroup中)

-
查看目录内容

- cgroup.procs 这个文件可读写,可以查看或者写入需要被限制cgroup的进程ID/限制ID。
- cgroup.max.depth cgroup子目录的最大深度
- memory.max 限制cgroup最大使用内存
- pids.max 限制cgroup创建的最大pid数
- cpuset.cpus 限制进程使用的cpu核心
- memory.stat cgroup使用的内存信息
- pids.current:显示当前cgroup中的进程个数。包括其子孙cgroup。
注意:
cgroup.controllers:当前cgroup可以限制的资源
cgroup.subtree_control: 控制了子组的cgroup可以限制的资源
-
设置cgroup内存限制 1MB

-
启动测试程序
func main() {
time.Sleep(time.Second \* 10)
fmt.Println("开始了")
var lastTime time.Time
var test = "terminating"
fmt.Println(lastTime.Nanosecond())
err := func() error {
fmt.Println("err: in err")
return nil
}()
switch {
case err != nil:
fmt.Println("not nil")
case err == nil:
fmt.Println("nil")
case test == "terminating":
fmt.Println("terminating")
}
fmt.Println(a.(string))
time.Sleep(time.Minute * 3)
//申请内存
testString := make([]string, 0, 9999999999)
fmt.Println(unsafe.Sizeof(testString))
time.Sleep(time.Minute * 1000)
fmt.Println("结束了")
}
- 程序被kill掉,观察机器日志内容
机器日志内容:
Apr 1 17:11:44 localhost kernel: [1809761.449573] [ pid ] uid tgid total_vm rss pgtables_bytes swapents oom_score_adj name
Apr 1 17:11:44 localhost kernel: [1809761.449576] [ 56726] 0 56726 39913984 638 69632 0 0 test-demo
Apr 1 17:11:44 localhost kernel: [1809761.449579] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=test-demo,mems_allowed=0,oom_memcg=/test-demo,task_memcg=/test-demo,task=test-demo,pid=56726,uid=0
Apr 1 17:11:44 localhost kernel: [1809761.449594] Memory cgroup out of memory: Killed process 56726 (test-demo) total-vm:159655936kB, anon-rss:1460kB, file-rss:1092kB, shmem-rss:0kB, UID:0 pgtables:68kB oom_score_adj:0
Linux namespace 资源隔离
UTS隔离(主机名信息)
环境信息及操作
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/utsname.h>
#define STACK_SIZE (1024 * 1024)
static int child_function(void *hostname) {
// 修改子进程的主机名
sethostname(hostname, sizeof(hostname));
// 输出子进程的主机名
printf("Child process: Hostname is %s\n", (char *)hostname);
return 0;
}
int main() {
char *child_stack;
//定义子进程内的UTS命名空间名称
char hostname1[] = "child-hostname";
// 为子进程分配堆栈空间
child_stack = malloc(STACK_SIZE);
if (child_stack == NULL) {
perror("malloc");
exit(EXIT_FAILURE);
}
// 创建子进程,在子进程中修改主机名
pid_t pid = clone(child_function, child_stack + STACK_SIZE, CLONE_NEWUTS | SIGCHLD, hostname1);
if (pid == -1) {
perror("clone");
exit(EXIT_FAILURE);
}
// 等待子进程退出
waitpid(pid, NULL, 0);
// 获取父进程的主机名并打印
struct utsname uts;
if (uname(&uts) == -1) {
perror("uname");
exit(EXIT_FAILURE);
}
printf("Parent process: Hostname is %s\n", uts.nodename);
return 0;
}
执行
//编译执行
gcc -o uts uts.c -pthread
./uts
结果验证
Child process: Hostname is child-hostname
Parent process: Hostname is vm-3-23-centos
IPC隔离(Linux的进程通信方式(信号量,共享内存,消息队列))
环境信息及操作
-
两个相同机器的shell终端

-
在第二个终端使用 unshare -i ,加入到新的IPC命名空间

-
在第二个终端中,创建IPC信号量,分别查看两个终端的ipc信息,可以看到此时两个终端的IPC信息是互相隔离的

结果验证
把第一个终端的ipc加入到第二个终端中,然后验证结果,以及对其它uts等信息的隔离

使用到的linux工具和知识
- nsenter(可以进入不同的linux 命名空间): https://man7.org/linux/man-pages/man1/nsenter.1.html
- ipcmk(用于创建进程通信的信号量): https://wker.com/linux-command/ipcmk.html
- ipcs(用于查看进程通信方式的列表信息): https://wker.com/linux-command/ipcs.html
- unshare(让进程进入到新的命名空间): https://man7.org/linux/man-pages/man1/unshare.1.html
- linux进程间通信:https://www.cnblogs.com/huansky/p/13170125.html
- 参考文档: https://www.testerfans.com/archives/linux-namespace-ipc
Mount隔离(挂载点信息的隔离)
-
创建挂载目录 /mnt/shell-1和 /mnt/shell-2,并把目录制作镜像文件

-
第一个终端中,确认挂载的mount namespace,把1.iso挂载到/mnt/shell-1上面

-
第二个终端加入到新的mnt namespace,查看挂载信息,可以看到实现了挂载点的拷贝

-
在第二个终端,卸载1.iso,挂载2.iso查看效果,看下与第一个终端的差异(在默认情况下 mnt namespace 会拷贝挂载信息)

-
第一个终端的挂载信息

结果验证
通过上面的操作可以看到,两个终端的的挂载信息是互不影响的情况
使用到的linux知识和工具
- mkisofs(把目录生成镜像文件工具):https://www.runoob.com/linux/linux-comm-mkisofs.html
- 参考文章:https://www.testerfans.com/archives/linux-namespace-mount
Pid隔离(进程ID信息的隔离)
环境准备及操作
两个终端,其中一个终端加入到新的pid命名空间,可以看到子进程的PID号是 1
mount-proc是因为默认不同的mnt namespace挂载信息是共享的,/proc目录也是挂载的一部分,所以mount-proc可以解决共享挂载问题,让子进程的proc直接就是新的信息
Pid的嵌套
-
两个终端,一个终端使用三次
unshare --uts --mount-proc --pid --fork /bin/bash不断创建子进程和pidnamespace等信息 -
另一个终端通过
pstree -p pid查看第二个终端的pid,查看pid关系
-
通过nsenter进入到子PIDnamespace中,查看进程关系,nsenter --pid --mount -t 854889 /bin/bash

-
继续往深处进入, nsenter --pid --mount -t 854957 /bin/bash

-
查看最后一个,nsenter --pid --mount -t 854957 /bin/bash

为什么在pstree中没有看到 nsenter进入的PID呢?

可以看到,我们当前进程59和1号进程的PPID(父进程)都为0。但PID为59进程并不属于当前 PID namespace 中 init 进程的子进程,所以不会在pstree中显示。这也是我们嵌套PID namespace和最外层PID namespace不同的地方:子PID namespace可以有多个PPID为0的进程。
使用到的linux知识和工具
- pstree 查看进程树状结构
- nsenter 进入指定进程的namespace
- unshare 使进程进入新的namespace
- 参考文章:https://www.testerfans.com/archives/linux-namespace-pid
Network隔离(IP地址,协议,网卡等信息的隔离)
环境信息及操作
- 创建network namespace
ip netns add liuchong-net,每创建一个netns,/var/run/netns/目录下面,会出现netns名称的文件,进入创建的netns,查看基本信息,可以看到 netns成功隔离了 - 使用
ip link set lo up可以启动lo网卡,分配出一个127.0.0.1,可以ping通

多个netns进行通信
-
创建网桥


-
创建veth网卡对,并加入到
liuchong-testnetns中
-
分配ip,启动网卡

-
veth网卡对另外一段绑定到网桥上

-
配置另外一个netns

结果验证

需要提醒的是 当只有两个veth互联,可以不创建网桥,直接创建veth网卡对分别放入到netns中,设置基本信息启动,可以直接互联。
使用到的linux知识和工具
- ip-netns(创建删除networkNamespace):https://man7.org/linux/man-pages/man8/ip-netns.8.html
- ip-link(对网卡的基本操作):https://man7.org/linux/man-pages/man8/ip-link.8.html
- iptables(linux防火墙):https://man7.org/linux/man-pages/man8/iptables.8.html
结语
通过这篇文章有没有解决大家的疑惑呢,下次再见,拜拜
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)