我们来通过一个完整、易懂的示例来演示 Linux 命名空间相关的四个核心系统调用:clone
, unshare
, setns
和 ioctl_ns
(通过 ioctl
)。
这个示例将模拟一个简单的容器环境创建和管理过程,包含以下步骤:
- 使用
clone
创建一个带有隔离环境的新进程(容器的“init”进程)。 - 在新进程中使用
unshare
进一步隔离其网络命名空间。 - 父进程使用
setns
加入子进程的 Mount 命名空间。 - 父进程使用
ioctl
查询子进程命名空间的信息。
linux命名空间系统调用及示例
为了简化,我们将重点放在 Mount (mnt) 和 Network (net) 命名空间上。
重要提示:
- 需要 root 权限:操作命名空间,特别是挂载文件系统,通常需要 root 权限。
- 环境要求:你的 Linux 内核需要支持这些命名空间(现代 Linux 发行版默认支持)。
- 安全:此示例涉及系统级操作,请在测试环境中运行。
完整示例代码
#define _GNU_SOURCE // 启用 GNU 扩展
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sched.h> // 包含 clone 标志和 unshare
#include <sys/syscall.h> // 包含 syscall 和系统调用号
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h> // 包含 waitpid
#include <fcntl.h> // 包含 open, O_RDONLY 等
#include <errno.h>
#include <string.h>
#include <sys/mount.h> // 包含 mount
#include <linux/nsfs.h> // 包含 ioctl_ns 的常量 (NS_GET_nstype 等)
#include <sys/ioctl.h> // 包含 ioctl
// 定义子进程栈大小
#define STACK_SIZE (1024 * 1024) // 1MB
// 子进程1:由 clone 创建,拥有自己的 Mount 和 UTS 命名空间
// 然后它会调用 unshare 来获得独立的 Network 命名空间
int container_init(void *arg) {
printf("\n--- Inside Container Init Process (PID: %d) ---\n", getpid());
// 1. 更改容器内的主机名 (在 UTS 命名空间内)
// 这不会影响宿主机的主机名
if (sethostname("my-container", strlen("my-container")) == -1) {
perror("sethostname (in container)");
} else {
printf("Container: Set hostname to 'my-container'.\n");
}
// 2. 创建一个挂载点并挂载 tmpfs (在 Mount 命名空间内)
// 这个挂载在宿主机上不可见
const char* mount_point = "/tmp/container_root";
if (mkdir(mount_point, 0755) == -1 && errno != EEXIST) {
perror("mkdir (in container)");
return 1;
}
if (mount("tmpfs", mount_point, "tmpfs", 0, NULL) == -1) {
perror("mount tmpfs (in container)");
return 1;
}
printf("Container: Mounted tmpfs on %s\n", mount_point);
// 3. 在挂载点内创建一个文件
char file_path[256];
snprintf(file_path, sizeof(file_path), "%s/container_file.txt", mount_point);
FILE *f = fopen(file_path, "w");
if (f) {
fprintf(f, "This file exists only inside the container's mount namespace.\n");
fclose(f);
printf("Container: Created file %s\n", file_path);
} else {
perror("fopen (in container)");
}
// 4. 使用 unshare 脱离当前的 Network 命名空间,进入一个新的空的 Network 命名空间
// 这使得容器拥有完全隔离的网络视图
printf("Container: Calling unshare(CLONE_NEWNET) to get isolated network...\n");
if (unshare(CLONE_NEWNET) == -1) {
perror("unshare CLONE_NEWNET");
umount(mount_point); // 清理
return 1;
}
printf("Container: Successfully unshared network namespace.\n");
printf("Container: Network is now isolated. 'ip link' should show only loopback.\n");
// 5. 容器主循环:等待信号或执行任务
// 这里我们简单地睡眠,以便我们可以从外部观察
printf("Container: Sleeping for 60 seconds. Explore from host and container.\n");
printf("Container: From host terminal, run:\n");
printf(" - 'ls /tmp/container_root' (should NOT see the file)\n");
printf(" - 'sudo nsenter -t %d -n ip link' (should see only loopback)\n", getpid());
printf("Container: From another terminal (as root), run this program's second part:\n");
printf(" - './namespace_demo join %d'\n", getpid());
sleep(60); // 睡眠 60 秒
// 6. 清理 (退出时内核通常会自动清理命名空间和挂载)
printf("Container: Cleaning up and exiting.\n");
umount(mount_point);
rmdir(mount_point);
return 0;
}
// 辅助函数:打开并返回指定进程的指定类型命名空间的文件描述符
int open_namespace_fd(pid_t pid, const char* ns_type) {
char path[256];
snprintf(path, sizeof(path), "/proc/%d/ns/%s", pid, ns_type);
int fd = open(path, O_RDONLY);
if (fd == -1) {
perror("open_namespace_fd");
fprintf(stderr, "Failed to open %s namespace for PID %d\n", ns_type, pid);
}
return fd;
}
// 辅助函数:使用 ioctl_ns 获取命名空间类型
void query_namespace_type(int ns_fd) {
// NS_GET_NSTYPE 是一个 ioctl 命令,用于获取命名空间类型
int ns_type = ioctl(ns_fd, NS_GET_NSTYPE);
if (ns_type == -1) {
perror("ioctl NS_GET_NSTYPE");
return;
}
const char* type_str;
switch (ns_type) {
case CLONE_NEWNS: type_str = "Mount (CLONE_NEWNS)"; break;
case CLONE_NEWCGROUP: type_str = "Cgroup (CLONE_NEWCGROUP)"; break;
case CLONE_NEWUTS: type_str = "UTS (CLONE_NEWUTS)"; break;
case CLONE_NEWIPC: type_str = "IPC (CLONE_NEWIPC)"; break;
case CLONE_NEWUSER: type_str = "User (CLONE_NEWUSER)"; break;
case CLONE_NEWPID: type_str = "PID (CLONE_NEWPID)"; break;
case CLONE_NEWNET: type_str = "Network (CLONE_NEWNET)"; break;
default: type_str = "Unknown";
}
printf(" Namespace fd %d type is: %s\n", ns_fd, type_str);
}
// 主函数:演示创建和加入命名空间
int main(int argc, char *argv[]) {
// --- 场景 1: 加入已存在的命名空间 ---
if (argc == 3 && strcmp(argv[1], "join") == 0) {
pid_t target_pid = atoi(argv[2]);
if (target_pid <= 0) {
fprintf(stderr, "Invalid PID provided.\n");
exit(EXIT_FAILURE);
}
printf("--- Joining Existing Namespace (as separate process) ---\n");
printf("This process (PID: %d) will join the mount namespace of PID: %d\n", getpid(), target_pid);
// 1. 打开目标进程的 Mount 命名空间文件描述符
int target_mnt_ns_fd = open_namespace_fd(target_pid, "mnt");
if (target_mnt_ns_fd == -1) exit(EXIT_FAILURE);
// 2. 查询并打印命名空间类型 (使用 ioctl)
printf("Querying namespace type using ioctl...\n");
query_namespace_type(target_mnt_ns_fd);
// 3. 使用 setns 加入目标的 Mount 命名空间
printf("Calling setns() to join the mount namespace...\n");
if (syscall(SYS_setns, target_mnt_ns_fd, CLONE_NEWNS) == -1) {
perror("setns");
close(target_mnt_ns_fd);
exit(EXIT_FAILURE);
}
printf("Successfully joined the mount namespace of PID %d.\n", target_pid);
close(target_mnt_ns_fd);
// 4. 现在,当前进程的文件系统视图与目标进程相同
printf("Checking for file that exists in the target namespace...\n");
if (access("/tmp/container_root/container_file.txt", F_OK) == 0) {
printf("SUCCESS: Found '/tmp/container_root/container_file.txt'. We are inside the container's mount namespace!\n");
} else {
printf("FAIL: Could not find the file. setns might have failed or file doesn't exist.\n");
}
printf("Exiting join process.\n");
exit(EXIT_SUCCESS);
}
// --- 场景 2: 创建新的隔离进程 (容器) ---
printf("--- Creating Isolated Process (Container) ---\n");
printf("Main process PID: %d\n", getpid());
// 1. 分配子进程栈
char *child_stack = malloc(STACK_SIZE);
if (!child_stack) {
perror("malloc");
exit(EXIT_FAILURE);
}
// 2. 使用 clone 创建子进程,并让它在新的 Mount 和 UTS 命名空间中启动
// SIGCHLD: 子进程退出时发送 SIGCHLD 信号给父进程
pid_t container_pid = syscall(SYS_clone,
CLONE_NEWNS | CLONE_NEWUTS | SIGCHLD,
child_stack + STACK_SIZE, // 栈是向下增长的
NULL, NULL);
if (container_pid == -1) {
perror("clone");
free(child_stack);
exit(EXIT_FAILURE);
}
if (container_pid == 0) {
// --- 在子进程中 ---
free(child_stack); // 子进程不需要父进程的栈指针
container_init(NULL); // 运行容器初始化函数
exit(EXIT_SUCCESS);
}
// --- 回到父进程 ---
printf("\n--- Back in Parent Process ---\n");
printf("Parent: Created container process with PID: %d\n", container_pid);
// 等待几秒,让子进程完成初始化 (挂载等)
sleep(3);
// 3. 父进程演示:获取子进程的命名空间文件描述符并查询信息
printf("\n--- Parent: Inspecting Container's Namespaces ---\n");
int child_mnt_ns_fd = open_namespace_fd(container_pid, "mnt");
int child_net_ns_fd = open_namespace_fd(container_pid, "net");
if (child_mnt_ns_fd != -1) {
printf("Parent: Opened child's Mount namespace fd: %d\n", child_mnt_ns_fd);
query_namespace_type(child_mnt_ns_fd);
// 注意:此时父进程还未加入,所以访问 /tmp/container_root 应该失败
if (access("/tmp/container_root/container_file.txt", F_OK) != 0) {
printf("Parent: As expected, '/tmp/container_root/container_file.txt' is NOT accessible from parent namespace.\n");
}
// close(child_mnt_ns_fd); // 暂时不关闭,后面 setns 还要用
}
if (child_net_ns_fd != -1) {
printf("Parent: Opened child's Network namespace fd: %d\n", child_net_ns_fd);
query_namespace_type(child_net_ns_fd);
close(child_net_ns_fd);
}
// 4. 父进程演示:使用 setns 加入子进程的 Mount 命名空间
// (注意:实际应用中,你可能不会在父进程中这样做,这里仅为演示)
/*
printf("\n--- Parent: Attempting to join child's Mount Namespace ---\n");
if (child_mnt_ns_fd != -1) {
if (syscall(SYS_setns, child_mnt_ns_fd, CLONE_NEWNS) == 0) {
printf("Parent: Successfully joined child's mount namespace.\n");
// 现在父进程可以访问容器内的文件了
if (access("/tmp/container_root/container_file.txt", F_OK) == 0) {
printf("Parent: Now I can access '/tmp/container_root/container_file.txt'.\n");
}
} else {
perror("Parent: setns failed");
}
close(child_mnt_ns_fd);
}
*/
// 5. 等待子进程结束
printf("\n--- Parent: Waiting for container process (PID: %d) to finish ---\n", container_pid);
printf("Parent: The container will exit automatically after its sleep.\n");
printf("Parent: Or, you can manually stop it with 'kill %d'\n", container_pid);
int status;
waitpid(container_pid, &status, 0);
if (WIFEXITED(status)) {
printf("Parent: Container process exited with status %d.\n", WEXITSTATUS(status));
} else {
printf("Parent: Container process did not exit normally.\n");
}
free(child_stack);
printf("Parent: Main process finished.\n");
return 0;
}
如何编译和运行
# 1. 保存代码为 namespace_demo.c
# 2. 编译 (需要 root 权限来运行,但编译不需要)
gcc -o namespace_demo namespace_demo.c
# 3. 打开两个终端 (Terminal 1 和 Terminal 2)
# --- Terminal 1: 创建容器 ---
# 运行程序的第一部分,创建一个隔离的容器进程
sudo ./namespace_demo
# 你会看到类似输出:
# --- Creating Isolated Process (Container) ---
# Main process PID: 12345
# Parent: Created container process with PID: 12346
#
# --- Inside Container Init Process (PID: 12346) ---
# Container: Set hostname to 'my-container'.
# Container: Mounted tmpfs on /tmp/container_root
# Container: Created file /tmp/container_root/container_file.txt
# Container: Calling unshare(CLONE_NEWNET) to get isolated network...
# Container: Successfully unshared network namespace.
# Container: Network is now isolated. 'ip link' should show only loopback.
# Container: Sleeping for 60 seconds. Explore from host and container.
# ...
# --- Terminal 2: 加入容器 ---
# 在程序输出中找到容器的 PID (例如 12346),然后运行程序的第二部分
# 加入容器的 Mount 命名空间
sudo ./namespace_demo join 12346
# 你会看到类似输出:
# --- Joining Existing Namespace (as separate process) ---
# This process (PID: 12347) will join the mount namespace of PID: 12346
# Querying namespace type using ioctl...
# Namespace fd 3 type is: Mount (CLONE_NEWNS)
# Calling setns() to join the mount namespace...
# Successfully joined the mount namespace of PID 12346.
# Checking for file that exists in the target namespace...
# SUCCESS: Found '/tmp/container_root/container_file.txt'. We are inside the container's mount namespace!
# Exiting join process.
详细说明
场景 1: 使用 clone
创建隔离进程
main
函数:作为父进程启动。malloc(STACK_SIZE)
:为子进程分配独立的栈空间。syscall(SYS_clone, ...)
:调用clone
系统调用。CLONE_NEWNS
:告诉内核为新进程创建一个新的 Mount 命名空间。CLONE_NEWUTS
:告诉内核为新进程创建一个新的 UTS 命名空间(用于隔离主机名)。child_stack + STACK_SIZE
:传递子进程栈的顶部指针(栈向下增长)。
if (container_pid == 0)
:在clone
返回后,执行流分叉。在子进程中,clone
返回 0。container_init
:子进程执行此函数,它现在运行在一个隔离的 Mount 和 UTS 命名空间中。它设置了主机名、挂载了文件系统并创建了文件。然后它调用unshare
。
场景 2: 在进程中使用 unshare
增加隔离
container_init
函数内部:在子进程完成初步设置后。unshare(CLONE_NEWNET)
:调用unshare
系统调用。CLONE_NEWNET
:告诉内核让当前进程脱离当前的 Network 命名空间,并加入一个新创建的、空的 Network 命名空间。
- 效果:现在这个子进程拥有完全独立的网络视图(例如,只有
lo
回环接口)。
场景 3: 使用 setns
加入已存在的命名空间
main
函数 (第二个实例):我们运行./namespace_demo join <PID>
来启动一个新的、独立的进程,专门用于加入命名空间。open("/proc/<PID>/ns/mnt", O_RDONLY)
:打开目标进程(容器)的 Mount 命名空间文件。这返回一个文件描述符。syscall(SYS_setns, fd, CLONE_NEWNS)
:调用setns
系统调用。fd
:之前打开的命名空间文件描述符。CLONE_NEWNS
:指定要加入的命名空间类型(Mount)。
- 效果:调用
setns
的这个新进程,其文件系统视图现在与目标容器进程完全相同。它可以访问容器内挂载的/tmp/container_root
及其内部的文件。
场景 4: 使用 ioctl
查询命名空间信息
open_namespace_fd
:一个辅助函数,用于获取命名空间文件描述符。ioctl(ns_fd, NS_GET_NSTYPE)
:对命名空间文件描述符调用ioctl
系统调用。NS_GET_NSTYPE
:这是一个特定的ioctl
命令,请求内核返回该文件描述符对应的命名空间类型。
- 效果:程序可以动态地确定一个未知的命名空间文件描述符代表的是哪种类型的命名空间(Mount, Network, PID 等)。
总结
这个示例完整地演示了:
clone
如何在创建新进程时就为其分配隔离的环境(Mount, UTS)。unshare
如何让一个已经在运行的进程(容器内)进一步获得新的隔离(Network)。setns
如何让一个外部进程(或另一个独立的工具进程)加入到已存在的隔离环境(Mount)中。ioctl
(withNS_GET_NSTYPE
) 如何查询命名空间文件描述符的类型。
通过这种方式,Linux 命名空间系统调用共同协作,提供了强大的资源隔离能力,构成了现代容器技术的基础。
2
好的,我们来构建一个高度简化但原理正确的“现代容器环境”示例。这个示例将展示创建一个容器所需的核心步骤,并对每一部分进行详细解释。
我们将模拟一个像 docker run
那样的命令,它会:
- 创建一个隔离的环境(使用命名空间)。
- 设置资源限制(使用 cgroups)。
- 准备一个文件系统根目录(使用
chroot
和mount
)。 - 在这个隔离的环境中运行一个指定的命令。
重要提示:
- 需要 root 权限:操作命名空间、cgroups、挂载文件系统都需要 root 权限。
- 简化:真实的容器运行时(如
runc
)极其复杂,涉及大量细节、安全加固、兼容性处理等。此示例仅展示核心原理。 - 环境要求:你的 Linux 内核需要支持所需的特性(现代发行版默认支持)。
- 安全性:此示例代码不安全,仅用于学习,请勿在生产环境使用。
完整示例代码:my_simple_container.c
#define _GNU_SOURCE // 启用 GNU 扩展
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sched.h> // 命名空间相关
#include <sys/syscall.h> // syscall
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h> // waitpid
#include <fcntl.h> // open, O_* flags
#include <errno.h>
#include <string.h>
#include <sys/mount.h> // mount, umount
#include <limits.h> // PATH_MAX
#include <ftw.h> // nftw (用于递归删除目录)
#include <signal.h> // kill
// --- 配置部分 ---
#define STACK_SIZE (1024 * 1024) // 1MB 子进程栈
#define CGROUP_NAME "my_simple_container"
#define MEMORY_LIMIT "50M" // 限制内存为 50MB
// --- 全局变量 ---
char child_stack[STACK_SIZE]; // 子进程栈
char *container_root = "/tmp/my_container_root"; // 容器的根文件系统目录
char *host_fs_path = "/tmp/host_fs_for_container"; // 容器内挂载宿主机目录的位置
// --- 辅助函数 ---
// 递归删除目录的回调函数 (用于 nftw)
int remove_callback(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) {
(void)sb; (void)typeflag; (void)ftwbuf; // 忽略未使用的参数
int res = remove(fpath);
if (res == -1) {
perror(fpath);
}
return res;
}
// 递归删除整个目录树
int remove_directory(const char *path) {
return nftw(path, remove_callback, 64, FTW_DEPTH | FTW_PHYS);
}
// 安全地写入文件内容
int write_file(const char *path, const char *content) {
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open (write_file)");
return -1;
}
if (write(fd, content, strlen(content)) != (ssize_t)strlen(content)) {
perror("write (write_file)");
close(fd);
return -1;
}
close(fd);
return 0;
}
// --- 核心功能函数 ---
// 1. 准备容器的根文件系统
// 这是容器运行的基础,它需要包含运行命令所需的所有文件 (如 /bin, /lib, /etc 等)
int prepare_rootfs() {
printf("[*] Preparing root filesystem at %s\n", container_root);
// 创建容器根目录
if (mkdir(container_root, 0755) == -1 && errno != EEXIST) {
perror("mkdir container_root");
return -1;
}
// 挂载 tmpfs 作为容器的根文件系统
// tmpfs 是内存中的临时文件系统,非常适合做实验性的 rootfs
if (mount("tmpfs", container_root, "tmpfs", 0, NULL) == -1) {
perror("mount tmpfs root");
return -1;
}
printf(" Mounted tmpfs on %s\n", container_root);
// 在容器根目录下创建基本的目录结构
const char *dirs[] = {"/bin", "/lib", "/lib64", "/etc", "/proc", "/sys", "/dev", "/tmp", host_fs_path+strlen(container_root)};
for (int i = 0; i < sizeof(dirs)/sizeof(dirs[0]); i++) {
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s%s", container_root, dirs[i]);
if (mkdir(path, 0755) == -1) {
perror("mkdir (dirs)");
return -1;
}
}
printf(" Created basic directory structure\n");
// 复制或绑定挂载一些必要的二进制文件和库
// 这里我们只复制一个简单的命令: /bin/sh (shell)
// 注意:实际的容器镜像会包含一个完整的文件系统
const char *bins[] = {"/bin/sh"};
for (int i = 0; i < sizeof(bins)/sizeof(bins[0]); i++) {
char dst_path[PATH_MAX];
snprintf(dst_path, sizeof(dst_path), "%s%s", container_root, bins[i]);
// 使用硬链接或复制。这里简单使用系统调用 `cp`
char cmd[PATH_MAX * 2];
snprintf(cmd, sizeof(cmd), "cp -p %s %s", bins[i], dst_path);
printf(" Copying %s...\n", bins[i]);
if (system(cmd) != 0) {
fprintf(stderr, "Failed to copy %s\n", bins[i]);
return -1;
}
}
// 创建一个简单的 /etc/passwd 和 /etc/group 文件,这样 `whoami` 等命令能工作
char passwd_path[PATH_MAX], group_path[PATH_MAX];
snprintf(passwd_path, sizeof(passwd_path), "%s/etc/passwd", container_root);
snprintf(group_path, sizeof(group_path), "%s/etc/group", container_root);
write_file(passwd_path, "root:x:0:0:root:/root:/bin/sh\nnobody:x:65534:65534:nobody:/:/bin/false\n");
write_file(group_path, "root:x:0:\nnobody:x:65534:\n");
printf(" Created minimal /etc/passwd and /etc/group\n");
// 绑定挂载宿主机的 /lib, /lib64 目录到容器内,这样 /bin/sh 能找到它需要的共享库
// 注意:这会暴露宿主机的库,实际容器会自带库或使用更精细的复制
char lib_dst[PATH_MAX], lib64_dst[PATH_MAX];
snprintf(lib_dst, sizeof(lib_dst), "%s/lib", container_root);
snprintf(lib64_dst, sizeof(lib64_dst), "%s/lib64", container_root);
if (mount("/lib", lib_dst, NULL, MS_BIND | MS_REC, NULL) == -1 ||
mount("/lib64", lib64_dst, NULL, MS_BIND | MS_REC, NULL) == -1) {
perror("mount /lib or /lib64");
return -1;
}
printf(" Bind-mounted /lib and /lib64\n");
// 在容器内创建一个挂载点,用于挂载宿主机的一个目录,演示数据共享
// 这类似于 `docker run -v /host/path:/container/path`
if (mkdir(host_fs_path, 0755) == -1 && errno != EEXIST) {
perror("mkdir host_fs_path (host)");
return -1;
}
char container_host_fs_path[PATH_MAX];
snprintf(container_host_fs_path, sizeof(container_host_fs_path), "%s%s", container_root, host_fs_path + strlen(container_root));
if (mount(host_fs_path, container_host_fs_path, NULL, MS_BIND, NULL) == -1) {
perror("mount host_fs_path");
return -1;
}
printf(" Bind-mounted host directory %s to container directory %s\n", host_fs_path, container_host_fs_path);
printf("[+] Root filesystem prepared successfully.\n");
return 0;
}
// 2. 设置 cgroups 以限制资源
// 这里我们只演示内存限制
int setup_cgroups(pid_t pid) {
printf("[*] Setting up cgroups (memory limit: %s)\n", MEMORY_LIMIT);
char cgroup_path[256];
char tasks_path[256];
char limit_path[256];
// 创建我们自己的 cgroup 子目录
snprintf(cgroup_path, sizeof(cgroup_path), "/sys/fs/cgroup/memory/%s", CGROUP_NAME);
if (mkdir(cgroup_path, 0755) == -1 && errno != EEXIST) {
perror("mkdir cgroup");
// 如果 cgroup v2 或其他问题,可能需要更复杂的处理,这里简化
fprintf(stderr, "Warning: Failed to create cgroup, skipping resource limits.\n");
return 0; // 不算致命错误
}
// 设置内存限制
snprintf(limit_path, sizeof(limit_path), "%s/memory.limit_in_bytes", cgroup_path);
if (write_file(limit_path, MEMORY_LIMIT) == -1) {
fprintf(stderr, "Warning: Failed to set memory limit, skipping.\n");
return 0;
}
// 将子进程 PID 写入 tasks 文件,使其受此 cgroup 控制
snprintf(tasks_path, sizeof(tasks_path), "%s/tasks", cgroup_path);
char pid_str[32];
snprintf(pid_str, sizeof(pid_str), "%d", pid);
if (write_file(tasks_path, pid_str) == -1) {
fprintf(stderr, "Warning: Failed to add process to cgroup, skipping.\n");
return 0;
}
printf("[+] Cgroups configured.\n");
return 0;
}
// 3. 容器初始化函数 (在子进程中运行)
// 这是容器内第一个执行的用户态进程 (PID 1)
int container_main(void *arg) {
char **args = (char **)arg;
printf("\n[*** INSIDE CONTAINER ***]\n");
printf("Container PID 1: %d\n", getpid());
// --- 关键步骤 1: 切换根文件系统 (chroot) ---
// 将容器的根目录设置为我们准备好的目录
if (chdir(container_root) == -1) {
perror("chdir to container root");
return 1;
}
// chroot 系统调用将当前进程看到的文件系统根目录 '/' 改变
if (chroot(".") == -1) {
perror("chroot");
return 1;
}
printf(" Changed root to %s using chroot\n", container_root);
// --- 关键步骤 2: 挂载必要的虚拟文件系统 ---
// 容器内进程需要 /proc, /sys, /dev 等来获取系统信息和设备访问
if (mount("proc", "/proc", "proc", 0, NULL) == -1 ||
mount("sysfs", "/sys", "sysfs", 0, NULL) == -1 ||
mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "size=65536k,mode=755") == -1) {
perror("mount essential filesystems");
return 1;
}
// 创建基本的设备节点 (简化)
if (mknod("/dev/null", S_IFCHR | 0666, makedev(1, 3)) == -1 && errno != EEXIST) {
perror("mknod /dev/null");
}
printf(" Mounted /proc, /sys, /dev\n");
// --- 关键步骤 3: 运行用户指定的命令 ---
printf(" Executing command: ");
for (int i = 0; args[i] != NULL; i++) {
printf("%s ", args[i]);
}
printf("\n");
printf("[*** END OF CONTAINER SETUP ***]\n\n");
// execvp 会用新程序替换当前进程的镜像
execvp(args[0], args);
// 如果 execvp 返回,说明执行失败
perror("execvp");
return 1;
}
// --- 主函数 ---
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <command> [args...]\n", argv[0]);
fprintf(stderr, "Example: sudo %s /bin/sh\n", argv[0]);
exit(EXIT_FAILURE);
}
printf("=== My Simple Container Runtime ===\n");
// 1. 准备 rootfs
if (prepare_rootfs() == -1) {
fprintf(stderr, "Failed to prepare root filesystem\n");
exit(EXIT_FAILURE);
}
// 2. 使用 clone 创建子进程,并设置命名空间隔离
// CLONE_NEWPID: 新的 PID 命名空间 (容器内 PID 从 1 开始)
// CLONE_NEWNS: 新的 Mount 命名空间 (隔离文件系统挂载点)
// CLONE_NEWUTS: 新的 UTS 命名空间 (隔离主机名)
// CLONE_NEWIPC: 新的 IPC 命名空间 (隔离 IPC 资源)
// CLONE_NEWNET: 新的 Network 命名空间 (隔离网络)
// SIGCHLD: 子进程结束时通知父进程
int flags = CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWNET | SIGCHLD;
pid_t container_pid = syscall(SYS_clone, flags, child_stack + STACK_SIZE, NULL, NULL);
if (container_pid == -1) {
perror("clone");
// 清理
umount(container_root);
remove_directory(container_root);
exit(EXIT_FAILURE);
}
if (container_pid == 0) {
// --- 在子进程中 ---
// 设置主机名
sethostname("my-container", strlen("my-container"));
// 调用容器主函数
container_main(argv + 1); // 传递命令行参数 (跳过 argv[0])
exit(EXIT_FAILURE); // 如果 container_main 返回,说明 exec 失败
}
// --- 回到父进程 ---
printf("[*] Started container process with PID %d\n", container_pid);
// 3. 设置 cgroups (在子进程启动后)
if (setup_cgroups(container_pid) == -1) {
fprintf(stderr, "Warning: Cgroup setup failed\n");
}
// 4. 等待容器进程结束
printf("[*] Waiting for container (PID %d) to finish...\n", container_pid);
int status;
if (waitpid(container_pid, &status, 0) == -1) {
perror("waitpid");
}
if (WIFEXITED(status)) {
printf("[*] Container exited with status %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("[*] Container killed by signal %d\n", WTERMSIG(status));
}
// 5. 清理资源
printf("[*] Cleaning up...\n");
// 卸载挂载点
char container_host_fs_path[PATH_MAX];
snprintf(container_host_fs_path, sizeof(container_host_fs_path), "%s%s", container_root, host_fs_path + strlen(container_root));
umount(container_host_fs_path);
umount("/lib64");
umount("/lib");
// 注意:chroot 后的卸载需要在 chroot 环境内或特殊处理,这里简化,依赖系统重启或手动清理
// 通常容器运行时会更仔细地管理这些挂载
umount(container_root);
// 删除临时目录
remove_directory(container_root);
remove_directory(host_fs_path);
printf("[+] Cleanup finished.\n");
printf("=== Container Runtime Finished ===\n");
return 0;
}
代码详细解释
1. 配置和辅助部分
#define
和全局变量:定义了栈大小、cgroup 名称、容器根目录路径等常量和全局变量,方便修改和使用。remove_directory
:使用nftw
递归遍历并删除整个目录树,用于清理工作。write_file
:一个安全的小函数,用于向文件写入内容,避免重复代码。
2. prepare_rootfs
– 准备文件系统
这是容器技术中最复杂的部分之一,因为容器需要一个完整的、自包含的文件系统。
mkdir
和mount("tmpfs", ...)
:创建容器根目录,并挂载一个tmpfs
。tmpfs
是基于内存的文件系统,非常适合做实验,因为它启动快且隔离性好。- 创建基本目录:
/bin
,/lib
,/etc
,/proc
,/sys
,/dev
是 Linux 系统运行程序所必需的目录。 - 复制/绑定挂载二进制文件:这里简化地只复制了
/bin/sh
。真实的容器镜像(如 Docker 镜像)会包含一个完整的根文件系统(/bin
,/usr
,/lib
等所有内容)。 - 绑定挂载库文件:为了让
/bin/sh
能运行,它需要依赖宿主机的共享库(.so
文件)。我们通过mount --bind
将宿主机的/lib
和/lib64
挂载到容器内对应位置。注意:这在实际生产中是不安全的,因为容器会使用宿主机的库,可能导致版本不兼容或安全问题。真正的容器会自带所需的库。 - 创建
/etc/passwd
等:为了让一些基础命令(如whoami
)能正常工作,需要创建这些用户/组信息文件。 - 绑定挂载宿主机目录:模拟
docker run -v
功能,展示容器与宿主机之间的数据共享。
3. setup_cgroups
– 设置资源限制
cgroups (Control Groups) 用于限制、记录和隔离进程组的资源使用(CPU、内存、磁盘 I/O 等)。
mkdir
:在/sys/fs/cgroup/memory/
下创建一个子目录,作为我们容器专用的 cgroup。write_file(limit_path, MEMORY_LIMIT)
:向memory.limit_in_bytes
文件写入限制值(如 “50M”),内核会自动应用这个限制。write_file(tasks_path, pid_str)
:将容器进程的 PID 写入 cgroup 的tasks
文件。这一步是关键,它告诉内核:“请把这个 PID 的进程放进这个 cgroup 里,让它受到资源限制。”
4. container_main
– 容器内的初始化
这是容器内第一个运行的用户态进程(通常 PID 为 1)。
chdir
和chroot
:chdir(container_root)
:先切换当前工作目录到我们准备好的容器根目录。chroot(".")
:这是核心操作。chroot
系统调用会将调用进程及其子进程的根目录(/
)永久性地更改为当前工作目录(.
,即container_root
)。执行此操作后,进程将无法访问原宿主机根目录之外的任何文件。对它来说,container_root
就是世界的尽头,里面的/
就是真正的/
。
- 挂载虚拟文件系统:
mount("proc", "/proc", "proc", ...)
:挂载proc
文件系统。进程需要通过/proc
来读取自身信息(如/proc/self/status
)、查看子进程、获取 CPU 信息等。mount("sysfs", "/sys", "sysfs", ...)
:挂载sysfs
,用于访问内核和硬件设备信息。mount("tmpfs", "/dev", "tmpfs", ...)
:挂载一个tmpfs
到/dev
,容器内的程序可能需要在这里创建设备节点或临时文件。mknod
:创建基本的设备文件,如/dev/null
。
execvp(args[0], args)
:这是最后一步,也是至关重要的一步。execvp
系列函数会用磁盘上的一个新程序(由args[0]
指定)完全替换当前进程的内存镜像(代码、数据、堆栈等)。执行成功后,容器内运行的就是用户指定的命令了,而不再是我们的container_main
函数。这个新程序的 PID 仍然是 1(因为在新的 PID 命名空间中)。
5. main
– 主流程控制
- 参数检查:确保用户提供了要运行的命令。
- 调用
prepare_rootfs
:准备隔离环境。 - 调用
clone
:- 这是启动隔离进程的核心。我们传递了多个
CLONE_NEW*
标志:CLONE_NEWPID
:创建新的 PID 命名空间。这使得容器内的第一个进程 PID 为 1,并且它无法看到或操作宿主机上 PID 命名空间中的进程。CLONE_NEWNS
:创建新的 Mount 命名空间。这使得容器内的mount
和umount
操作不会影响宿主机的文件系统视图。CLONE_NEWUTS
:创建新的 UTS 命名空间。这使得容器可以拥有独立的主机名 (hostname
) 和 NIS 域名。CLONE_NEWIPC
:创建新的 IPC 命名空间。这隔离了 System V IPC 和 POSIX 消息队列。CLONE_NEWNET
:创建新的 Network 命名空间。这使得容器拥有独立的网络设备、IP 地址、路由表、端口等。示例中未配置网络,所以容器内网络功能受限。
child_stack + STACK_SIZE
:传递子进程栈的顶部指针。
- 这是启动隔离进程的核心。我们传递了多个
- 子进程分支 (
if (container_pid == 0)
):- 在子进程中,设置主机名,然后调用
container_main
进行初始化。
- 在子进程中,设置主机名,然后调用
- 父进程分支:
- 调用
setup_cgroups
来限制子进程的资源。 - 使用
waitpid
等待容器进程结束。 - 容器结束后,执行清理工作:卸载文件系统、删除临时目录。
- 调用
如何编译和运行
# 1. 将代码保存为 my_simple_container.c
# 2. 编译 (需要 root 权限来运行,但编译不需要)
gcc -o my_simple_container my_simple_container.c
# 3. 运行容器,执行 /bin/sh
# 必须使用 sudo,因为涉及命名空间、cgroups、挂载等特权操作
sudo ./my_simple_container /bin/sh
# 你会看到类似输出:
# === My Simple Container Runtime ===
# [*] Preparing root filesystem at /tmp/my_container_root
# Mounted tmpfs on /tmp/my_container_root
# Created basic directory structure
# Copying /bin/sh...
# Created minimal /etc/passwd and /etc/group
# Bind-mounted /lib and /lib64
# Bind-mounted host directory /tmp/host_fs_for_container to container directory /tmp/host_fs_for_container
# [+] Root filesystem prepared successfully.
# [*] Started container process with PID 12345
# [*] Setting up cgroups (memory limit: 50M)
# [+] Cgroups configured.
# [*] Waiting for container (PID 12345) to finish...
#
# [*** INSIDE CONTAINER ***]
# Container PID 1: 1
# Changed root to /tmp/my_container_root using chroot
# Mounted /proc, /sys, /dev
# Executing command: /bin/sh
# [*** END OF CONTAINER SETUP ***]
#
# # <--- 你现在在容器的 shell 里面了,PID 为 1 ---
# # ps aux
# USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
# root 1 0.0 0.0 2284 1536 ? Ss 10:30 0:00 /bin/sh
# root 12 0.0 0.0 3864 3168 ? R+ 10:31 0:00 ps aux
# #
# # hostname
# my-container
# #
# # df -h
# Filesystem Size Used Avail Use% Mounted on
# tmpfs 1.9G 0 1.9G 0% /
# proc 1.9G 0 1.9G 0% /proc
# sysfs 1.9G 0 1.9G 0% /sys
# tmpfs 64M 0 64M 0% /dev
# tmpfs 1.9G 0 1.9G 0% /tmp
# /dev/sda1 20G 5.0G 15G 26% /tmp/host_fs_for_container
# #
# # cd /tmp/host_fs_for_container
# # touch file_created_in_container
# # exit
# # <--- 退出容器 ---
#
# [*] Container exited with status 0
# [*] Cleaning up...
# [+] Cleanup finished.
# === Container Runtime Finished ===
#
# # <--- 回到宿主机 ---
# # ls /tmp/host_fs_for_container/
# file_created_in_container # 你在容器里创建的文件在宿主机上也能看到
总结原理
这个示例通过组合 Linux 内核的几个关键特性,模拟了现代容器的运行原理:
- 隔离 (Isolation) – 命名空间 (Namespaces):
clone
系统调用配合CLONE_NEW*
标志,在创建新进程时就为其分配了独立的视图,包括进程 ID (CLONE_NEWPID
)、文件系统挂载点 (CLONE_NEWNS
)、主机名 (CLONE_NEWUTS
)、IPC 资源 (CLONE_NEWIPC
) 和网络 (CLONE_NEWNET
)。chroot
系统调用进一步将进程的文件系统根目录 (/
) 切换到一个预先准备好的、与宿主机隔离的目录,实现了文件系统的彻底隔离。
- 资源限制 (Resource Limiting) – Control Groups (Cgroups):
- 通过在
/sys/fs/cgroup
下创建子目录并配置参数(如memory.limit_in_bytes
),然后将容器进程的 PID 添加到该 cgroup 的tasks
列表中,实现了对该进程资源使用的限制(此处为内存)。
- 通过在
- 文件系统 (Filesystem) – Rootfs:
- 准备一个包含运行所需程序和库的目录 (
/tmp/my_container_root
)。 - 使用
tmpfs
、bind mount
等技术构建这个目录。 - 通过
chroot
使其成为容器进程的根目录。
- 准备一个包含运行所需程序和库的目录 (
- 执行 (Execution):
- 在完成所有隔离和设置后,使用
execvp
系统调用,用用户指定的命令(如/bin/sh
)替换容器初始化进程(PID 1)的镜像,从而在隔离环境中运行该命令。
- 在完成所有隔离和设置后,使用
通过以上步骤,一个与宿主机环境隔离、资源受限、拥有独立文件系统和进程/网络视图的“沙盒”环境就被创建出来了,这就是容器的核心工作原理。