C++ `cgroups` / `namespaces`:资源隔离与容器技术底层原理

哈喽,各位好!

今天咱们来聊聊C++ cgroupsnamespaces,这两个听起来有点高大上的家伙,其实是资源隔离和容器技术的底层基石。说白了,它们就是让你的程序在一个“小房子”里安全、独立地玩耍,互不干扰。

一、为啥要资源隔离?

想象一下,你和你的室友合租一套房子。如果你室友疯狂下载电影,把带宽占满了,你还怎么愉快地刷抖音? 如果你的室友写了个死循环程序,把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-devsudo yum install libcgroup-devel 安装。
  • 这个代码需要在 root 权限下运行,因为创建和管理 cgroups 需要 root 权限。
  • 错误处理非常重要。 在实际应用中,你需要更完善的错误处理机制。
  • 上述例子中涉及到的 cpu.shares ,cpu.cfs_quota_uscpu.cfs_period_us 是控制CPU使用的不同方式。 cpu.shares 是相对权重,而 cpu.cfs_quota_uscpu.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;
}

编译和运行:

  1. 保存为 namespaces_example.cpp
  2. 编译: g++ namespaces_example.cpp -o namespaces_example
  3. 运行: 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;
}

使用方法:

  1. 找到一个 namespace 的路径。 例如,一个 Docker 容器的网络 namespace 的路径通常是 /proc/<pid>/ns/net,其中 <pid> 是容器中某个进程的 PID。
  2. 编译: g++ setns_example.cpp -o setns_example
  3. 运行: sudo ./setns_example /proc/<pid>/ns/net (需要 root 权限)

这个程序会将当前进程加入到指定的网络 namespace 中。 加入之后,你就可以访问该 namespace 中的网络资源了。

重要提示:

  • 这个代码需要在 root 权限下运行。
  • 你需要知道 namespace 的路径才能使用 setns
  • 加入 namespace 之后,你的进程会受到该 namespace 的限制。 例如,如果加入了一个网络 namespace,你只能访问该 namespace 中的网络资源。

四、cgroupsnamespaces 的关系

cgroups 负责资源限制,namespaces 负责资源隔离。它们通常一起使用,为进程创建一个隔离的、资源受限的环境。

打个比方:

  • namespaces 就像一个个独立的房间,每个房间里的东西(进程、网络、文件系统)都是独立的。
  • cgroups 就像每个房间的“装修合同”,限制房间的大小、家具的数量、用电量等等。

五、容器技术:cgroupsnamespaces 的应用

Docker 等容器技术就是基于 cgroupsnamespaces 实现的。容器技术利用 namespaces 来隔离进程、网络、文件系统等资源,利用 cgroups 来限制 CPU、内存、IO 等资源的使用。 这样,每个容器就像一个独立的虚拟机,可以运行不同的应用程序,而不会互相干扰。

六、总结

cgroupsnamespaces 是 Linux 内核提供的强大的资源隔离和管理机制。它们是容器技术的基础,也是构建安全、可靠的应用程序的重要工具。

最后,我们用一个表格来总结一下今天的内容:

技术 功能 优点 缺点
cgroups 资源限制和管理。 可以限制进程使用的 CPU 时间、内存大小、IO 带宽等等。 资源利用率高,可以灵活地控制资源的分配。 配置复杂,需要对 Linux 内核有一定的了解。
namespaces 资源隔离。 可以隔离进程的 PID、网络、文件系统挂载点等等。 安全性高,可以防止进程互相干扰。 隔离性强,可能会导致一些应用程序无法正常运行。
容器技术 基于 cgroupsnamespaces 实现的轻量级虚拟化技术。 可以将应用程序及其依赖项打包到一个容器中,然后在不同的环境中运行。 部署方便,易于管理,资源利用率高。 隔离性不如虚拟机,安全性方面需要注意。镜像体积可能较大。

希望今天的分享对大家有所帮助。 下次有机会再和大家聊聊容器技术的其他方面。 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注