Docker原理之隔离篇 --- namespace隔离简介

概念

Linux内核提供的一种隔离机制,Docker就是使用内核namespace的隔离机制来实现对应的资源隔离。共有六种隔离:

namespace 系统调用参数 隔离内容
UTS CLONE_NEWUTS 主机名和域名
IPC CLONE_NEWIPC 信号量、消息队列、共享内存
PID CLONE_NEWPID 进程号
Mount CLONE_NEWNS 文件系统(挂载点)
Network CLONE_NEWNET 网络设备、网络栈、端口
User CLONE_NEWUSER 用户和用户组

UTS

UTS 全称为 UNIX Time-sharing System 该种隔离提供对于主机名和域名的隔离,Docker利用该种隔离机制为每个Docker提供独立的机器名和域名,在网络中就可以被当做单独的一个服务节点使用。

IPC

IPC的全称是 Inter-Process Communication 是Linux系统提供的进程间通信机制,常见的包括 信号量、消息队列、共享内存等。申请IPC资源其实就是向内核申请了一个全局唯一的32位ID,在同一个IPC namespace下的进程彼此可见,不同IPC namespace下的进程互不可见。Docker运用该机制实现了容器之间IPC的隔离。

PID

PID 是Linux系统的进程号,每个pid namespace 中都有一个计数器用于标识当前最大的PID,pid namespace隔离可以对每个namespace中pid计数器重新标号,不同pid namespace中的进程可以拥有相同的pid号。内核中对PID namespace 的组织是个树状结构,在树形的父节点中可以看到子节点的所有进程,反之则不行。即父级 pid namespace可以看到子级pid namespace中所有的进程,并可以对其进行管理,而子级pid namespace中进程无法看到且管理兄弟或父级pid namespace。

Mount

Mount namespace 通过隔离文件系统的挂载点提供对隔离文件系统的支持。在创建新的mount ns时,系统会复制当前的文件结构给新的namespace。在新的nsmespace中进行的相关操作则不会再影响以前的ns。

Network

Network ns主要提供关于网络资源的隔离,包括网络设备、协议栈、路由表、防火墙、/proc/net目录、/sys/class/net目录、socket等的隔离。

User

User ns主要提供用户安全相关的隔离,例如用户组ID、用户ID、root目录、秘钥文件等隔离。通过该隔离手段可以达到更加灵活的权限控制。例如,某个用户在宿主机上拥有普通用户权限,但是其进入容器后却可以用于容器的root权限,也就是说,容器里的root用户并不是真正的root用户,映射到宿主机上只是宿主机上一个普通用户而已。

重要系统调用

clone

  1. 概述:Linux系统创建进程的函数,fork和vfork底层都调用clone实现
  2. 签名:int clone(int (*child_func)(void *), void *child_stack, int flags, void *args);
  3. 参数说明:
    a. child_func:子进程的main函数
    b. child_stack:子进程使用的栈空间
    c. flags:使用哪些CLONE_*标志位,与namespace相关的见上图
    d. args:子进程main函数的参数
  4. 返回值:新进程的pid
  5. 其他:可通过 /proc/{pid}/ns 文件查看对应namespace编号,下边的所有文件都是对应的文件描述符,打开文件的方括号中的数值即为文件的inode,如果两个进程所指向的inode号一样,则说明这俩进程在同一个namespace下。例如:

Docker原理之隔离篇 --- namespace隔离简介_第1张图片

sentns

  1. 概述:将进程加入到已存在的namespace中
  2. 签名:int setns(int fd, int nstype);
  3. 参数说明:
    a. fd:要加入namespace的文件描述符(指打开/proc/{pid}/ns下文件的描述符)
    b. nstype:限定目标ns的类型
    ⅰ. 0:不限定
    ⅱ. 上表中所有的参数类型,表示传入fd对应的ns类型必须是nstype类型的ns
  4. 返回值:
    a. 0:成功
    b. -1:失败

unshare

  1. 概述:分离对应的namespace并加入新的namespace
  2. 签名:int unshare(int flags);
  3. 参数说明:
    a. flags:同上表,表示要分离的namespace类型
  4. 返回值:
    a. 0:成功
    b. -1:失败
  5. 区别
    a. 与clone:不会创建新进程,unshare是在本进程操作
    b. 与setns:sentns只能加入已存在的namespace

实操

注:因 PID namespace 较为复杂且坑比较多,所以这里仅以pid namespace为例,其他可自行探索。

clone

clone.c

#define _GNU_SOURCE
#include 
#include 
#include 
#include 
#include 

#define STACK_SIZE 65535

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

int childBash() {
    printf("child bash  my PID: %ld\n", (long)getpid());
    execv(bash_args[0], bash_args);

    return -1;
}


int childProcess() {
    printf("child process  my PID: %ld\n", (long)getpid());

    char *stack = malloc(STACK_SIZE);

    if (!stack) {
        perror("Failed to allocate memory\n");
        return EXIT_FAILURE;
    }
    pid_t child_pid = clone(childBash, stack + STACK_SIZE, SIGCHLD, NULL);
    printf("bash() = %ld\n", (long)child_pid);

    waitpid(child_pid, NULL, 0);

    return 0;
}

int main(int agrc, char *argv[]) {

    char *stack = malloc(STACK_SIZE);

    if (!stack) {
        perror("Failed to allocate memory\n");
        return EXIT_FAILURE;
    }

    pid_t child_pid = clone(childProcess, stack + STACK_SIZE, CLONE_NEWPID | SIGCHLD, NULL);
    printf("clone() = %ld\n", (long)child_pid);

    waitpid(child_pid, NULL, 0);
    printf("child terminated!\n");


    return EXIT_SUCCESS;
}

执行 gcc clone.c 编译后执行结果如下:
Docker原理之隔离篇 --- namespace隔离简介_第2张图片

从上图中我们可以看到,子进程自己无论是调用getpid()函数还是bash中执行 echo $$ 中看到的pid都是新ns的pid,说明clone时创建新ns已生效。但是通过ps看到bash的pid却是和父进程是同一个视角,这个是因为ps 依赖于proc文件系统,我们没有单独挂载文件系统导致的。要想解决这个问题,我们只需要步即可:

  1. 调用clone时传入CLONE_NEWNS flag
  2. 重新挂载子进程的proc即可

完整源代码如下:
clone_v2.c

#define _GNU_SOURCE
#include 
#include 
#include 
#include 
#include 

#define STACK_SIZE 65535

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

int childBash() {
    printf("child bash  my PID: %ld\n", (long)getpid());
    execv(bash_args[0], bash_args);

    return -1;
}


int childProcess() {
    printf("child process  my PID: %ld\n", (long)getpid());

    // 重新挂载proc
    system("mount -t proc proc /proc");

    char *stack = malloc(STACK_SIZE);

    if (!stack) {
        perror("Failed to allocate memory\n");
        return EXIT_FAILURE;
    }
    pid_t child_pid = clone(childBash, stack + STACK_SIZE, SIGCHLD, NULL);
    printf("bash() = %ld\n", (long)child_pid);

    waitpid(child_pid, NULL, 0);

    return 0;
}

int main(int agrc, char *argv[]) {

    char *stack = malloc(STACK_SIZE);

    if (!stack) {
        perror("Failed to allocate memory\n");
        return EXIT_FAILURE;
    }

    // 增加CLONE_NEWNS flag
    pid_t child_pid = clone(childProcess, stack + STACK_SIZE, CLONE_NEWPID |CLONE_NEWNS | SIGCHLD, NULL);
    printf("clone() = %ld\n", (long)child_pid);

    waitpid(child_pid, NULL, 0);
    printf("child terminated!\n");


    return EXIT_SUCCESS;
}

效果如下:

Docker原理之隔离篇 --- namespace隔离简介_第3张图片

现在我们看到,几个视图下都一样了。

注意:如果你运行完上述程序退出后执行ps 命令发现报错“Error, do this: mount -t proc proc /proc”,说明你的proc默认是shared挂载的,可运行“mount --make-private /proc”将proc显式指定为private挂载方式。具体原因可搜索“Linux 挂载传播”相关内容。

setns

setns.c

#define _GNU_SOURCE
#include 
#include 
#include 
#include 
#include 

#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)


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


int childBash() {
    printf("child bash  my PID: %ld\n", (long)getpid());
    execv(bash_args[0], bash_args);

    return -1;
}


int main(int argc, char *argv[]) {
    int fd;

    if (argc < 2) {
        fprintf(stderr, "%s /proc/PID/ns/FILE \n", argv[0]);
        exit(EXIT_FAILURE);
    }

    fd = open(argv[1], O_RDONLY);   /* Get descriptor for namespace */
    if (fd == -1)
        errExit("open");

    if (setns(fd, 0) == -1)         /* Join that namespace */
        errExit("setns");

    // 这里有坑, Linux man手册中的例子不能改变pid ns
    // 因为pid namespace 比较特殊, 调用后无法改变当前进程的pid ns
    // 所以直接在父进程中exec 是不能改变pid ns 的
    // 必须得 fork 一个子进程, 子进程的pid ns才会被加入新 pid ns
    pid_t pid = fork();
    if (pid == -1) {
        errExit("fork");
    }
    if (pid > 0) {
        waitpid(pid, NULL, 0);
    } else {
        childBash();
    }
}

Docker原理之隔离篇 --- namespace隔离简介_第4张图片Docker原理之隔离篇 --- namespace隔离简介_第5张图片

我们使用上个例子中clone.c先创建一个新的pid ns,再使用setns切换进新创建的pid ns后,可以看到新进程已经被加入了clone.c创建出来的新pid ns。事实上,docker exec命令就是使用此系统调用实现的。

unshare

unshare.c

#define _GNU_SOURCE
#include 
#include 
#include 
#include 
#include 

#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)


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


int childBash() {
    printf("child bash  my PID: %ld\n", (long)getpid());
    execv(bash_args[0], bash_args);

    return -1;
}


int main(int argc, char *argv[]) {
    if (unshare(CLONE_NEWPID) == -1)
        errExit("unshare");

    pid_t pid = fork();
    if (pid == -1) {
        errExit("fork");
    }
    if (pid > 0) {
        printf("child pid %ld\n", (long)pid);
        waitpid(pid, NULL, 0);
    } else {
        childBash();
    }

}

运行后可以看到unshare会分离当前ns,子进程也会加入新ns中。
Docker原理之隔离篇 --- namespace隔离简介_第6张图片

总结

  1. clone调用时会创建新进程,可通过设置flags参数将新进程加入新namespace中
  2. setns可将进程加入一个已存在的namespace中
  3. unshare可分离当前的namespace到新的namespace中
  4. CLONE_NEWPID namespace较为特殊,setns和unshare系统调用无法将调用者的pid ns更改为新的ns

其他

  1. 如果 proc下的ns文件(/proc/xxx/ns/xx)被打开或者挂载,即使改ns所拥有的的进程已经挂掉,那么该ns也不会被删除。
  2. 同一个pid namespace下的1号进程,具有信号屏蔽功能,即同一个pid namespace下的其他进程即使有root权限,发给1号进程的信号会被屏蔽掉。但是其父级namespace中进程可以发给子namespace中1号进程SIGKILL 或 SIGSTOP 信号,此时该ns下所有的进程都会收到SIGKILL信号,从而都被杀死。
  3. 使用setns或者unshare切换 pid ns时,调用者进程并不会进入新的pid ns中。因为进入新的pid ns会导致用户态程序看到的的pid发生变化,从而导致引发程序崩溃。

你可能感兴趣的:(Docker,docker,linux,网络)