我们来通过一个完整、易懂的示例来演示 Linux 命名空间相关的四个核心系统调用:clone, unshare, setns 和 ioctl_ns (通过 ioctl)。
data-ad-format="fluid" data-ad-layout-key="-7k+ex-4a-9w+4a">这个示例将模拟一个简单的容器环境创建和管理过程,包含以下步骤:
使用 clone 创建一个带有隔离环境的新进程(容器的“init”进程)。
在新进程中使用 unshare 进一步隔离其网络命名空间。
父进程使用 setns 加入子进程的 Mount 命名空间。
父进程使用 ioctl 查询子进程命名空间的信息。
linux命名空间系统调用及示例
getgroups系统调用及示例
linux命名空间系统调用及示例-CSDN博客
为了简化,我们将重点放在 Mount (mnt) 和 Network (net) 命名空间上。
重要提示:
需要 root 权限:操作命名空间,特别是挂载文件系统,通常需要 root 权限。
环境要求:你的 Linux 内核需要支持这些命名空间(现代 Linux 发行版默认支持)。
安全:此示例涉及系统级操作,请在测试环境中运行。
完整示例代码
1 | #define _GNU_SOURCE // 启用 GNU 扩展 |
如何编译和运行
1 | # 1. 保存代码为 namespace_demo.c |
详细说明
场景 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
open(“/proc/
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 (with NS_GET_NSTYPE) 如何查询命名空间文件描述符的类型。
通过这种方式,Linux 命名空间系统调用共同协作,提供了强大的资源隔离能力,构成了现代容器技术的基础。
2
好的,我们来构建一个高度简化但原理正确的“现代容器环境”示例。这个示例将展示创建一个容器所需的核心步骤,并对每一部分进行详细解释。
我们将模拟一个像 docker run 那样的命令,它会:
创建一个隔离的环境(使用命名空间)。
设置资源限制(使用 cgroups)。
准备一个文件系统根目录(使用 chroot 和 mount)。
在这个隔离的环境中运行一个指定的命令。
重要提示:
需要 root 权限:操作命名空间、cgroups、挂载文件系统都需要 root 权限。
简化:真实的容器运行时(如 runc)极其复杂,涉及大量细节、安全加固、兼容性处理等。此示例仅展示核心原理。
环境要求:你的 Linux 内核需要支持所需的特性(现代发行版默认支持)。
安全性:此示例代码不安全,仅用于学习,请勿在生产环境使用。
完整示例代码:my_simple_container.c
1 | #define _GNU_SOURCE // 启用 GNU 扩展 |
代码详细解释
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 | # 1. 将代码保存为 my_simple_container.c |
总结原理
这个示例通过组合 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)的镜像,从而在隔离环境中运行该命令。
通过以上步骤,一个与宿主机环境隔离、资源受限、拥有独立文件系统和进程/网络视图的“沙盒”环境就被创建出来了,这就是容器的核心工作原理。