Docker 是如何实现隔离的

在学习Docker 是时候, 了解到Docker 容器隔离,觉得很神奇,但是Docker 到底是如何实现隔离的 自己并不了解.

从运行一个容器开始

我们开始运行一个简单的容器,这里以busybox镜像为例,它是一个常用的Linux工具箱,可以用来执行很多Linux命令,我们以它为镜像启动容器方便来查看容器内部环境。 执行命令:

docker run -it --name demo_docker busybox /bin/sh

这条命令的意思是:启动一个busybox镜像的 Docker 容器,-it参数表示给容器提供一个输出/输出的交互环境,也就是TTY。/bin/sh表示容器交互运行的命令或者程序。

进程的隔离

执行成功后我们就会进入到了 Docker 容器内部,我们执行ps -ef 查看进程

/ # ps -ef
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    6 root      0:00 ps -ef

使用top命令查看进程资源

Mem: 1784592K used, 97536K free, 968K shrd, 109436K buff, 855756K cached
CPU:  2.4% usr  2.8% sys  0.0% nic 94.5% idle  0.2% io  0.0% irq  0.0% sirq
Load average: 0.26 0.34 0.21 3/252 7
  PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND
    1     0 root     S     1316  0.0   0  0.0 /bin/sh
    7     1 root     R     1308  0.0   0  0.0 top

而我们在宿主机查看下当前执行容器的进程ps -ef|grep busybox

[root@VM-0-15-centos ~]# ps -ef|grep busybox
root      2261  1194  0 14:45 pts/3    00:00:00 grep --color=auto busybox
root     31292 30445  0 14:42 pts/0    00:00:00 /usr/bin/docker-current run -it --name demo_docker busybox /bin/sh

这里我们可以知道,对于宿主机 docker run执行命令启动的只是一个进程,它的pid是2261。而对于容器程序本身来说,它被隔离了,在容器内部都只能看到自己内部的进程,那 Docker 是如何做到的呢?它其实是借助了Linux内核的Namespace技术来实现的,这里我结合一段C程序来模拟一下进程的隔离。

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#include <sys/mount.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("容器进程[%5d] ----进入容器!\n",getpid());
    mount("proc", "/proc", "proc", 0, NULL);
    /**执行/bin/bash */
    execv(container_args[0], container_args);
    printf("出错啦!\n");
    return 1;
}

int main()
{
    printf("宿主机进程[%5d] - 开始一个容器!\n",getpid());
    /* 调用clone函数 */
    int container_pid = clone(container_main, container_stack+STACK_SIZE,  CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);
    /* 等待子进程结束 */
    waitpid(container_pid, NULL, 0);
    printf("宿主机 - 容器结束!\n");
    return 0;
}

考虑到很多同学对C语言不是很熟悉,我这里简单解释下这段程序,这段程序主要就是执行clone()函数,去克隆一个进程,而克隆执行的程序就是我们的container_main函数,接着下一个参数就是栈空间,然后CLONE_NEWPIDCLONE_NEWNS 表示Linux NameSpace的调用类别,分别表示创建新的进程命名空间和 挂载命名空间。

  • CLONE_NEWPID会让执行的程序内部重新编号PID,也就是从1号进程开始
  • CLONE_NEWNS 会克隆新的挂载环境出来,通过在子进程内部重新挂载proc文件夹,可以屏蔽父进程的进程信息

我们执行一下这段程序来看看效果。

  • 编译
    [root@VM-0-15-centos docker]# gcc container.c -o container
    
  • 执行
    [root@VM-0-15-centos docker]# ls
    container  container.c
    [root@VM-0-15-centos docker]# ./container 
    宿主机进程[14599] - 开始一个容器!
    容器进程[    1] ----进入容器!
    

    这里我们看到输出在宿主机看来,这个程序的PID14599,在克隆的子进程来看,它的PID1,我们执行ps -ef 查看一下进程列表

    [root@VM-0-15-centos docker]# ps -ef
    UID        PID  PPID  C STIME TTY          TIME CMD
    root         1     0  0 14:51 pts/6    00:00:00 /bin/bash
    root        28     1  0 14:52 pts/6    00:00:00 ps -ef
    

    我们发现确实只有容器内部的进程在运行了,再执行top命令

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND  
    1 root      20   0  116320   2792   1660 S  0.0  0.1   0:00.02 bash                     
    29 root      20   0  159876   2056   1516 R  0.0  0.1   0:00.00 top  
    

    结果也只有2个进程的信息。

    这就是容器隔离进程的基本原理了,Docker主要就是借助 Linux 内核技术Namespace来做到隔离的,其实包括我后面要说到文件的隔离,资源的隔离都是在新的命名空间下通过mount挂载的方式来隔离的。

文件的隔离

转自Docker是如何实现隔离的 - 木木匠的个人空间 - OSCHINA - 中文开源技术交流社区

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐