容器核心技术--Namespace
上文提到,容器技术的核心有两个:Namespace 和 Cgroup。本节先来介绍 Namespace 是什么。
简单来说,Namespace 可以为容器提供系统资源隔离能力。
当然,这样讲过于笼统,我们来举个例子:假如一个容器中的进程需要使用 root 权限,出于安全考虑,我们不可能把宿主机的 root 权限给他。但是通过 Namespace 机制,我们可以隔离宿主机与容器的真实用户资源,谎称一个普通用户就是 root,骗过这个程序。从这个角度看,Namespace 就是内核对进程说谎的机制,目前(Linux最新的稳定版本为5.6),内核可以说的谎话有 8 种:
Namespace | 系统调用 |
---|---|
Mount | CLONE_NEWNS |
UTS | CLONE_NEWUTS |
IPC | CLONE_NEWIPC |
PID | CLONE_NEWPID |
Network | CLONE_NEWNET |
User | CLONE_NEWUSER |
Cgroup | CLONE_NEWCGROUP |
Time | CLONE_NEWTIME |
1. Namespace 详解
1.1 Mount Namespace
Mount Namespace 用来隔离文件系统的挂载点,不同的 Mount namespace 拥有各自独立的挂载点信息。在 Docker 这样的容器引擎中,Mount namespace 的作用就是保证容器中看到的文件系统的视图。
1.2 UTS Namespace
UTS Namespace 用来隔离系统的主机名、hostname 和 NIS 域名。
1.3 IPC Namespace
IPC 就是在不同进程间传递和交换信息。IPC Namespace 使得容器内的所有进程,进行的数据传输、共享数据、通知、资源共享等范围控制在所属容器内部,对宿主机和其他容器没有干扰。
1.4 PID Namespace
PID namespaces用来隔离进程的 ID 空间,使得不同容器里的进程 ID 可以重复,相互不影响。
1.5 Network Namespace
Network namespace 用来隔离网络,每个 namespace 可以有自己独立的网络栈,路由表,防火墙规则等。
1.6 user namespace
user namespace 是例子中讲到的,控制用户 UID 和 GID 在容器内部和宿主机上的一个映射,主要用来管理权限。
1.7 Time namespace
这个 Namespace 允许操作系统为进程设定不同的系统时间。
1.8 Cgroup Namespace
这个 Namespace 用来限制 CGroup 根目录下不同层级目录的权限,使得 CGROUP 根目录下的子目录的进程无法影响到父目录。
2. 实践出真知
2.1 感受 Namespace
讲了那么多理论,Namespace 能不能让我们直接观察到呢?
让我们进入 Linux 环境,执行如下操作:
# 进入/proc/目录
cd /proc/
# 查看当前目录下有哪些文件或目录
ls
# 随便进入一个以数字(进程号)命名的目录,比如1
cd 1
# 查看ns(Namespace)目录下的内容
ls -al ns
当前目录下红色的链接,就是这个进程对应的 Namespace。
有兴趣的读者可以看看其他不同进程的 Namespace,比对下是否有差异。如果你找到某个进程的Namespace 与其他的不一致,就说明这个进程指定了 Namespace 隔离。
2.2 使用 Namespace 自制简易容器
将以下代码保存到/root/test/container.c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <sys/capability.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];
char* const container_args[] = {
"/bin/bash",
NULL
};
int pipefd[2];
void set_map(char* file, int inside_id, int outside_id, int len) {
FILE* mapfd = fopen(file, "w");
if (NULL == mapfd) {
perror("open file error");
return;
}
fprintf(mapfd, "%d %d %d", inside_id, outside_id, len);
fclose(mapfd);
}
void set_uid_map(pid_t pid, int inside_id, int outside_id, int len) {
char file[256];
sprintf(file, "/proc/%d/uid_map", pid);
set_map(file, inside_id, outside_id, len);
}
void set_gid_map(pid_t pid, int inside_id, int outside_id, int len) {
char file[256];
sprintf(file, "/proc/%d/gid_map", pid);
set_map(file, inside_id, outside_id, len);
}
int container_main()
{
char ch;
close(pipefd[1]);
read(pipefd[0], &ch, 1);
sethostname("container",10);
/* Mount Namespace */
mount("proc", "/proc", "proc", 0, NULL);
mount("none", "/tmp", "tmpfs", 0, "");
execv(container_args[0], container_args);
return 1;
}
int main()
{
const int gid=getgid(), uid=getuid();
pipe(pipefd);
int container_pid = clone(container_main, container_stack+STACK_SIZE,
CLONE_NEWCGROUP|CLONE_NEWIPC|CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL);
set_uid_map(container_pid, 0, uid, 1);
set_gid_map(container_pid, 0, gid, 1);
close(pipefd[1]);
waitpid(container_pid, NULL, 0);
return 0;
}
我们不用读懂这个代码,只需要留意下 main 主函数中这部分
int container_pid = clone(container_main, container_stack+STACK_SIZE,
CLONE_NEWCGROUP|CLONE_NEWIPC|CLONE_NEWNET|CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL);
这段代码 调用 clone
实现线程的系统调用,用来创建一个新的进程,并可以通过设计上述参数达到隔离。
执行下面的操作
# 安装可能需要的依赖
sudo dnf install -y libcap-devel
# 编译这个文件
cc container.c -o container
# 运行
./container
执行我们编译好的container程序后,发现我们处于一个新的环境的终端中,你可以在这里验证你的猜测,比如查看当前环境的进程 ps
,当前登录的用户 whoami
,网络状况 ip a
等等,使用exit
可以退出回到原来的环境。
我们确实通过系统调用,创建了一个与宿主机资源隔离的容器环境。
3. 小结
本节我们介绍了 Namespace 机制,和它的 8 种隔离类型,并实现了一具有命名空间隔离功能的“容器”,在这个过程中,希望大家对容器和 Namespace 机制有了更深入的理解。