哈喽,各位好!
今天咱们来聊聊C++ cgroups
和 namespaces
,这两个听起来有点高大上的家伙,其实是资源隔离和容器技术的底层基石。说白了,它们就是让你的程序在一个“小房子”里安全、独立地玩耍,互不干扰。
一、为啥要资源隔离?
想象一下,你和你的室友合租一套房子。如果你室友疯狂下载电影,把带宽占满了,你还怎么愉快地刷抖音? 如果你的室友写了个死循环程序,把CPU占满了,你还怎么快乐地敲代码?
资源隔离就是为了解决这个问题。它把CPU、内存、网络、IO等资源划分成一个个“小块”,分配给不同的进程或者进程组。这样,即使某个进程“作妖”,也不会影响到其他进程。
二、cgroups
:资源的“包工头”
cgroups
(Control Groups) 就像一个资源“包工头”,负责管理和限制资源的分配。它可以限制进程使用的CPU时间、内存大小、IO带宽等等。
1. cgroups
的组织结构:
cgroups
采用的是树状结构。根节点是root cgroup
,所有其他的cgroups
都是它的子节点。每个cgroup
可以包含多个进程,并且可以继承父cgroup
的资源限制。
2. cgroups
的使用方法:
cgroups
的使用主要涉及到文件系统的操作。在Linux系统中,cgroups
通常挂载在 /sys/fs/cgroup
目录下。
-
创建
cgroup
:mkdir /sys/fs/cgroup/cpu/my_cgroup
-
设置资源限制:
# 限制CPU使用率为50% (假设系统HZ为100) echo 50000 > /sys/fs/cgroup/cpu/my_cgroup/cpu.cfs_quota_us echo 100000 > /sys/fs/cgroup/cpu/my_cgroup/cpu.cfs_period_us # 限制内存使用为100MB echo 104857600 > /sys/fs/cgroup/memory/my_cgroup/memory.limit_in_bytes
-
将进程添加到
cgroup
:echo <pid> > /sys/fs/cgroup/cpu/my_cgroup/tasks
其中
<pid>
是进程的ID。
3. C++ 代码操作 cgroups
(通过系统调用):
虽然直接操作文件系统可以管理cgroups
,但我们也可以使用C++封装的系统调用,例如cgroupfs-manager
库或者直接使用libcgroup
库提供的API。以下是一个简化的例子,展示了如何使用libcgroup
库(需要安装libcgroup-dev
包):
#include <iostream>
#include <libcgroup.h>
#include <unistd.h>
#include <cstdlib>
int main() {
// 初始化 libcgroup
if (cgroup_init()) {
std::cerr << "Failed to initialize libcgroup: " << cgroup_strerror(errno) << std::endl;
return 1;
}
// 创建 cgroup
const char* group_name = "my_cgroup";
struct cgroup* cg = cgroup_new_cgroup(group_name);
if (!cg) {
std::cerr << "Failed to create cgroup: " << cgroup_strerror(errno) << std::endl;
return 1;
}
// 设置 CPU 限制 (例如,限制 CPU 份额为 512)
cgroup_add_value_int64(cg, "cpu", "cpu.shares", 512);
// 设置内存限制 (例如,限制内存为 100MB)
cgroup_add_value_int64(cg, "memory", "memory.limit_in_bytes", 104857600); // 100MB
// 创建 cgroup (如果不存在)
if (cgroup_create_cgroup(cg, 0)) {
std::cerr << "Failed to create cgroup: " << cgroup_strerror(errno) << std::endl;
cgroup_delete_cgroup(cg, 1); // 如果创建失败,清理已分配的资源
return 1;
}
// 将当前进程添加到 cgroup
pid_t pid = getpid();
if (cgroup_attach_task(cg, pid)) {
std::cerr << "Failed to attach task to cgroup: " << cgroup_strerror(errno) << std::endl;
cgroup_delete_cgroup(cg, 1);
return 1;
}
std::cout << "Process " << pid << " added to cgroup " << group_name << std::endl;
// 在这里执行你的程序逻辑...
// 删除 cgroup (可选,如果不再需要)
// cgroup_delete_cgroup(cg, 1); // 1 表示递归删除
cgroup_free_cgroup(cg); // 释放 cgroup 结构
return 0;
}
重要提示:
- 你需要安装
libcgroup-dev
库才能编译这个代码。 使用sudo apt-get install libcgroup-dev
或sudo yum install libcgroup-devel
安装。 - 这个代码需要在 root 权限下运行,因为创建和管理
cgroups
需要 root 权限。 - 错误处理非常重要。 在实际应用中,你需要更完善的错误处理机制。
- 上述例子中涉及到的
cpu.shares
,cpu.cfs_quota_us
和cpu.cfs_period_us
是控制CPU使用的不同方式。cpu.shares
是相对权重,而cpu.cfs_quota_us
和cpu.cfs_period_us
是绝对时间限制。
4. cgroups
的子系统:
cgroups
通过不同的子系统来实现对不同类型资源的控制。常见的子系统包括:
子系统 | 功能 |
---|---|
cpu |
限制 CPU 使用率。 |
memory |
限制内存使用量。 |
blkio |
限制块设备(磁盘)的 IO 带宽。 |
net_cls |
对网络流量进行分类,可以结合 tc 命令进行流量控制。 |
devices |
控制进程对设备的访问权限。 |
freezer |
暂停和恢复 cgroup 中的进程。 |
cpuset |
将 cgroup 中的进程绑定到特定的 CPU 核心和内存节点上。 |
pids |
限制 cgroup 中进程的数量。 |
hugetlb |
限制HugeTLB的使用。 |
perf_event |
允许将 perf_event 计数器附加到 cgroup 中的所有进程。 |
rdma |
限制RDMA资源的使用。 |
三、namespaces
:隔离的“平行宇宙”
namespaces
就像一个个“平行宇宙”,让进程感觉自己运行在一个独立的系统中。它可以隔离进程的 PID、网络、文件系统挂载点等等。
1. namespaces
的类型:
Linux 支持多种类型的 namespaces
:
namespace 类型 |
功能 |
---|---|
PID |
隔离进程 ID 空间。 每个 PID namespace 都有自己的 PID 树,root PID namespace 中的进程可以看到所有其他的 PID namespace 中的进程,但是子 PID namespace 中的进程只能看到自己以及其祖先 PID namespace 中的进程。 |
Network |
隔离网络接口、路由表、防火墙规则等等。 每个 Network namespace 都有自己的网络栈,可以拥有自己的 IP 地址、路由表等。 |
Mount |
隔离文件系统挂载点。 每个 Mount namespace 都有自己的挂载点视图,在一个 Mount namespace 中挂载或卸载文件系统不会影响到其他的 Mount namespace 。 |
UTS |
隔离主机名和域名。 每个 UTS namespace 都有自己的主机名和域名,在一个 UTS namespace 中修改主机名不会影响到其他的 UTS namespace 。 |
IPC |
隔离进程间通信(IPC)资源,例如 System V IPC 对象和 POSIX 消息队列。 每个 IPC namespace 都有自己的一组 IPC 资源,在一个 IPC namespace 中创建的 IPC 资源只能被同一个 IPC namespace 中的进程访问。 |
User |
隔离用户和组 ID。 每个 User namespace 都有自己的用户和组 ID 映射,在一个 User namespace 中可以拥有不同的用户和组 ID,这使得可以在一个 User namespace 中以非 root 用户身份运行进程,而在另一个 User namespace 中以 root 用户身份运行进程。 |
Cgroup |
隔离 cgroup 根目录。这允许在容器内创建和管理 cgroups ,而不会影响宿主机上的 cgroups 。 |
2. C++ 代码创建 namespaces
(使用 clone
系统调用):
创建 namespaces
主要使用 clone
系统调用。 clone
系统调用可以创建一个新的进程,并且可以选择性地共享父进程的某些资源,例如文件描述符、内存空间等等。 通过指定不同的 clone
标志,可以创建不同类型的 namespaces
。
#define _GNU_SOURCE
#include <iostream>
#include <sched.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
// 定义栈大小
#define STACK_SIZE (1024 * 1024)
// 声明一个静态变量作为子进程的栈空间
static char child_stack[STACK_SIZE];
// 子进程执行的函数
static int child_func(void *arg) {
// 设置新的主机名
if (sethostname("container", 9) == -1) {
perror("sethostname");
return 1;
}
// 在子进程中执行命令 (例如,/bin/bash)
const char* cmd = (const char*)arg;
printf("Executing %s in the containern", cmd);
execl(cmd, cmd, NULL);
// 如果 execl 失败
perror("execl");
return 1;
}
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <command>n", argv[0]);
exit(EXIT_FAILURE);
}
// clone flags: 创建新的 UTS, PID, Mount, IPC 和 Network namespaces
int clone_flags = CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWNET;
// 使用 clone 创建子进程
pid_t child_pid = clone(child_func, child_stack + STACK_SIZE, clone_flags | SIGCHLD, argv[1]);
if (child_pid == -1) {
perror("clone");
exit(EXIT_FAILURE);
}
printf("Created child process with PID: %ldn", (long)child_pid);
// 等待子进程结束
if (waitpid(child_pid, NULL, 0) == -1) {
perror("waitpid");
exit(EXIT_FAILURE);
}
printf("Child process finishedn");
return 0;
}
编译和运行:
- 保存为
namespaces_example.cpp
。 - 编译:
g++ namespaces_example.cpp -o namespaces_example
- 运行:
sudo ./namespaces_example /bin/bash
(需要 root 权限)
这个程序会创建一个新的进程,并且将它放到一个新的 UTS、PID、Mount、IPC 和 Network namespace
中。 在新的 namespace
中,子进程会执行 /bin/bash
命令。 你可以在新的 bash
终端中执行 hostname
命令,你会发现主机名已经变成了 "container"。你也可以通过 ps
命令查看进程列表,你会发现只能看到当前 namespace
中的进程。
重要提示:
- 这个代码需要在 root 权限下运行,因为创建
namespaces
需要 root 权限。 - 错误处理非常重要。 在实际应用中,你需要更完善的错误处理机制。
clone
系统调用比较底层,使用起来比较复杂。 在实际应用中,可以使用一些更高级的库,例如libcontainer
或者docker-init
,来简化namespaces
的创建和管理。- 上面的例子创建了多个
namespace
, 如果只想创建单个的namespace
, 比如只是UTS namespace
, 那么clone_flags
只需要设置为CLONE_NEWUTS
即可。
3. setns
系统调用:
除了使用 clone
系统调用创建 namespaces
之外,还可以使用 setns
系统调用将进程加入到已存在的 namespace
中。
4. C++ 代码使用 setns
:
#define _GNU_SOURCE
#include <iostream>
#include <fcntl.h>
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <namespace_path>n", argv[0]);
exit(EXIT_FAILURE);
}
const char* namespace_path = argv[1];
// 打开 namespace 文件
int fd = open(namespace_path, O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 加入 namespace
if (setns(fd, 0) == -1) {
perror("setns");
close(fd);
exit(EXIT_FAILURE);
}
close(fd);
printf("Joined namespace: %sn", namespace_path);
// 在这里执行你的程序逻辑 (例如,执行 /bin/bash)
execl("/bin/bash", "/bin/bash", NULL);
perror("execl");
exit(EXIT_FAILURE);
return 0;
}
使用方法:
- 找到一个
namespace
的路径。 例如,一个 Docker 容器的网络namespace
的路径通常是/proc/<pid>/ns/net
,其中<pid>
是容器中某个进程的 PID。 - 编译:
g++ setns_example.cpp -o setns_example
- 运行:
sudo ./setns_example /proc/<pid>/ns/net
(需要 root 权限)
这个程序会将当前进程加入到指定的网络 namespace
中。 加入之后,你就可以访问该 namespace
中的网络资源了。
重要提示:
- 这个代码需要在 root 权限下运行。
- 你需要知道
namespace
的路径才能使用setns
。 - 加入
namespace
之后,你的进程会受到该namespace
的限制。 例如,如果加入了一个网络namespace
,你只能访问该namespace
中的网络资源。
四、cgroups
和 namespaces
的关系
cgroups
负责资源限制,namespaces
负责资源隔离。它们通常一起使用,为进程创建一个隔离的、资源受限的环境。
打个比方:
namespaces
就像一个个独立的房间,每个房间里的东西(进程、网络、文件系统)都是独立的。cgroups
就像每个房间的“装修合同”,限制房间的大小、家具的数量、用电量等等。
五、容器技术:cgroups
和 namespaces
的应用
Docker 等容器技术就是基于 cgroups
和 namespaces
实现的。容器技术利用 namespaces
来隔离进程、网络、文件系统等资源,利用 cgroups
来限制 CPU、内存、IO 等资源的使用。 这样,每个容器就像一个独立的虚拟机,可以运行不同的应用程序,而不会互相干扰。
六、总结
cgroups
和 namespaces
是 Linux 内核提供的强大的资源隔离和管理机制。它们是容器技术的基础,也是构建安全、可靠的应用程序的重要工具。
最后,我们用一个表格来总结一下今天的内容:
技术 | 功能 | 优点 | 缺点 |
---|---|---|---|
cgroups |
资源限制和管理。 可以限制进程使用的 CPU 时间、内存大小、IO 带宽等等。 | 资源利用率高,可以灵活地控制资源的分配。 | 配置复杂,需要对 Linux 内核有一定的了解。 |
namespaces |
资源隔离。 可以隔离进程的 PID、网络、文件系统挂载点等等。 | 安全性高,可以防止进程互相干扰。 | 隔离性强,可能会导致一些应用程序无法正常运行。 |
容器技术 | 基于 cgroups 和 namespaces 实现的轻量级虚拟化技术。 可以将应用程序及其依赖项打包到一个容器中,然后在不同的环境中运行。 |
部署方便,易于管理,资源利用率高。 | 隔离性不如虚拟机,安全性方面需要注意。镜像体积可能较大。 |
希望今天的分享对大家有所帮助。 下次有机会再和大家聊聊容器技术的其他方面。 谢谢大家!