docker 容器基本原理——Namespace

Linux Namespace

参考文章 1:DOCKER基础技术:LINUX NAMESPACE(上)
参考文章 2:DOCKER基础技术:LINUX NAMESPACE(下)
官方文档:《Namespaces in operation》

Linux Namespace 是 Linux 提供的一种内核级别环境隔离的方法。Unix 中有一个名为 chroot 的系统调用(通过修改根目录把用户 jail 到一个特定目录下),chroot 提供了一种简单的隔离模式:chroot 内部的文件系统无法访问外部的内容。Linux Namespace 在此基础上,提供了对 UTS、IPC、Mount、PID、Network、User 等六种隔离机制。

基础程序

本文中后续程序都是在基础程序基础上形成的,基础程序如下。

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>

/* 定义一个给 clone 用的栈,栈大小1M */
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];

char* const container_args[] = {
    "/bin/bash",
    NULL
};

int container_main(void* arg)
{
    printf("Container[%d] - inside the container!\n", getpid());

    /* 直接执行一个shell,以便我们观察这个进程空间里的资源是否被隔离了 */
    execv(container_args[0], container_args);

    printf("Something's wrong!\n");
    return 1;
}

int main()
{
    printf("Parent[%d] - start a container!\n", getpid());

    /* 调用clone函数,其中传出一个函数,还有一个栈空间的(为什么传尾指针,因为栈是反着的) */
    int container_pid = clone(container_main, container_stack+STACK_SIZE, SIGCHLD, NULL); 

    /* 等待子进程结束 */
    waitpid(container_pid, NULL, 0);

    printf("Parent - container stopped!\n");
    return 0;
}


UTS Namespace

UTS 实现主机名隔离,在程序中使用如下:


int container_main(void* arg)
{
    ...
    sethostname("container", 10); /* 设置主机名 */
    printf("Something's wrong!\n");
    return 1;
}

int main()
{
    ...
    /* 调用clone函数,其中传出一个函数,还有一个栈空间的(为什么传尾指针,因为栈是反着的) */
    int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWUTS | 
                              SIGCHLD, NULL); /* 加上 UTS 参数 */
    ...
}


IPC Namespace

IPC 全称 Inter-Process Communication,是Unix/Linux下进程间通信的一种方式,IPC有共享内存、信号量、消息队列等方法。IPC 隔离实现如下:

int main()
{
    ...
    /* 调用clone函数,其中传出一个函数,还有一个栈空间的(为什么传尾指针,因为栈是反着的) */
    int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWUTS | 
                              CLONE_NEWIPC | SIGCHLD, NULL); /* 加上 PID 参数 */
    ...
}
  • 检验 IPC 隔离,clone 时先不指定 CLONE_NEWIPC 参数。使用 ipcmk -Q 创建一个 IPC 队列,使用 ipcs -q 查看队列创建是否成功,如下图所示。
    ipc 队列创建与查看

  • 运行未指定 CLONE_NEWIPC 参数的程序,使用 ipcs -q 查看能否看到刚才创建的队列,结果如下,发现能够访问到。
    未使用 CLONE_NEWIPC

  • 指定 CLONE_NEWIPC 参数后重新编译执行,继续使用 ipcs -q 查看,结果如下,已经看不到创建的 IPC 队列了,实现了 IPC 隔离。
    使用了 CLONE_NEWIPC 后


PID Namespace

PID Namespace 将子进程 PID 设置为 1。在传统的UNIX系统中,PID为1的进程是init,地位非常特殊。他作为所有进程的父进程,有很多特权(比如:屏蔽信号等)。另外,其还会为检查所有进程的状态,如果某个子进程脱离了父进程(父进程没有wait它),那么init就会负责回收资源并结束这个子进程。所以,要做到进程空间的隔离,首先要创建出PID为1的进程,最好就像 chroot 那样,把子进程的 PID 在容器内变成 1 。

clone 时将 CLONE_NEWPID传入即可, 可在程序内使用 getpid() 获取进程 PID 并打印,对比使用 CLONE_NEWPID 前后,子进程的 PID 有无变化。


Mount Namespace

加上 CLONE_NEWPID 子进程内使用 pstop 等命令依然可以查看所有进程,因为子进程仍旧和父进程共享 /proc 文件系统,在 clone 时传入 CLONE_NEWNS 标志位,且在子进程内重新挂载 /proc 文件系统可以解决这一问题,修改如下(这里通过实践发现原文中两个小错误,一是如果不在父进程内重新 mount -t proc proc /proc,子进程结束后再在系统中使用 ps 就会报错;二是很奇怪,即使不指定 CLONE_NEWNS 标志,单纯的在子进程内 mount 也能实现同样功能。)

int container_main(void* arg)
{
    ...
    sethostname("container", 10); /* 设置主机名 */
    system("mount -t proc proc /proc"); 
    ...
}

int main()
{
    ...
    /* 调用clone函数,其中传出一个函数,还有一个栈空间的(为什么传尾指针,因为栈是反着的) */
    int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWUTS |  CLONE_NEWPID | 
                              CLONE_NEWIPC | CLONE_NEWNS | SIGCHLD, NULL); /* 加上 NS */
  
    /* 等待子进程结束 */
    waitpid(container_pid, NULL, 0);
    system("mount -t proc proc /proc"); /* 不加这行,退出后使用 ps 报错 */
    ...
}

Network Namespace

  • 待完成

User Namespace

  • 待完成

版权声明:本文为weixin_41195426原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。