时间:2017-2-21来源:本站原创作者:佚名
写在前面的话

整个分享过程中有以下几个关键词:Linux、Docker、fork、exec、task_struct、namespace以及dockerinit。分享内容可能比较底层,但对大家理解Docker的本质会很有帮助,大家要有耐心哦。

分享的内容包含三部分:

Linux进程的基本知识;

Docker与Linux进程的关系;

Docker中dockerinit,entrypoint和cmd三者的关系。

Part1Linux进程的基本知识

这部分我们会比较基础地介绍Linux进程管理中的fork、exec、进程描述符task_struct以及namespace。

1Linux中创建进程的基本模型

Linux操作系统中,由父进程创建并执行子进程,创建通过fork完成,执行通过exec完成。简单示意图如下:

在上图中,我们看到进程A创建了一个新的进程B,最终两个进程各自运行。创建时,进程A通过fork系统调用来完成。fork之后,两个进程最大的区别就是:进程A依然拥有原来的PID,新创建的进程B会占用一个全新的PID,两者的PID不同。

并且Linux内核会在fork系统调用时,会拷贝进程A的task_struct,拷贝的副本是为进程B准备的。完成fork操作之后,拥有全新PID的进程B会执行exec操作,保证执行新的程序,真正开始进程B的运行逻辑。

进程B运行过程中,假若B正常或者异常退出,那么内核就会给进程B的父进程A,发送一个SIGHOLD信号,父进程A则对退出的进程B执行wait操作,实现对B进程资源的回收,如进程描述符task_struct等。

如此一来,Linux系统中进程最基本的生命周期就完整了。大家不要觉得这和Docker没有什么关系,其实我也卖个关子,Docker容器的底层实现原理,就是基于Linux进程的fork和exec。

2Linuxfork的具体实现细节do_forkLinux操作系统在实现fork时,其实更为底层的是:实现一个名为do_fork的函数。Do_fork函数是在内核态,完成了新进程的创建流程。do_fork函数的运行流程可以如下图:

上图中,我们最关心的Linux内核为子进程,拷贝父进程内容的环节(copy_process)。这个环节中,Linux会:首先,查看传入的flag参数,为后续是否创建新namespace做准备;随后,拷贝父进程的进程描述符task_struct;接着查看系统的资源限制;而后,拷贝父进程的具体细节,这个环节也是本次分享的一个重点,因为其中涉及了命令空间的拷贝(copy_namespaces);最后,内核仍会完成一些其他操作。

在这里,我们已经多次强调了task_struct和namespace,那不妨对两者多做一些介绍。简单理解,task_struct唯一地定义了一个进程的多种属性,内核调度进程时,绝大多数的信息都来源于task_struct。

而namespace为进程定义了一整套的命名空间,实现不同命名空间内进程之间的完全隔离。说到隔离,Docker就有能力实现容器间的隔离,其实最终的原理正是通过namespace来实现的。

这部分内容比较底层,大家可以多多揣摩一下哈。

3task_struct和namespace的关系

通俗一点来讲,Linux内核在调度进程的时候,所有信息都来源于进程描述符task_struct,并从task_struct中找到相应的命令空间信息。也就是说通过task_struct,内核有能力找到这个进程的namespace。

比如说:一个进程A要使用网络设备(网卡)实现网络通信。那在宿主机上有多个网络设备的时候,如何定位要具体的网络设备呢。很简单,Linux内核首先找到进程A的进程描述符task_struct,然后在task_struct中找到进程具体所在的网络命名空间(netnamespace),netnamespace中会有一个对象真正对应于具体的网络设备,那么关于改进程具体的网络通信,Linux内核就会交给这个网络设备来完成。

看下图可以理解的更清楚:

在进程描述符task_struct中,有一个nsproxy会为进程代理所有的namespace,具体的nsproxy会有多个指针指向具体的namespace。具体由nsproxy代理的namespace,有:uts_namespace,mnt_namespace,pid_namespace,ipc_namespace和netnamespace。

讲了这么多的namespace,大家肯定会有疑惑,namespace中到底包含了哪些内容:

具体可以看下图哦:

比如说,pidnamespace可以认为是为Linux容器完成内部进程PID的管理。在pidnamespace中,会有很多的数据结构。其中有一个属性特别重要,那就是一个名为child_reaper的指针,执行一个task_struct。顾名思义,这个属性可以实现对很多子进程进程清理。这个进程一般会被认为是容器的init进程(pid=1),一旦这个进程退出,那么内核会给容器内其他的所有进程发送一个终止信号,确保容器的退出。

总结而言,就是容器init进程,决定容器的存活~

另外,像mnt_namespace,它为容器实现了根目录的隔离视角,也就是传统意义上chroot的作用。该mnt_namespace中最重要的自然是root属性,定义了容器的根目录。

还有uts_namespace,定义了容器的命名信息等。

以上的所有内容,就是为Docker原理准备的基本内容。总结一下,有Linux进程的fork、exec、task_struct以及namespace。

下面,我们会来介绍Linux进程与Docker的关系。

Part2Docker与Linux进程的关系

第一部分讲了很多Linux进程的概念,我们今天的主题是Docker,Docker在哪里呢?

回到Docker最经典的三层架构:dockerclient、dockerdaemon以及dockercontainer。DockerDaemon作为守护进程管理着所有的容器,其实在创建容器的时候,DockerDaemon仅仅是实现了一个fork,然后在fork的过程中实现了copy_namespaces。

换言之,Docker容器的诞生仅仅是通过fork和exec一个进程而已。

具体解释原因前,我们来看看DockerDaemon的fork和我们程序员普通的fork有什么区别,为什么Docker的fork,fork出的是容器,而我们的却不叫容器呢?且看下图:

原因是DockerDaemon在fork容器进程的时候,传入了一些特殊的flag参数,而这些参数恰恰在内核执行do_fork函数时,被用来创建新的namespace。

大家如果去看Docker的源码,可以发现Docker对于namespace的支持如下图:

其中user_namespace在Docker中仍然没有被支持,那说明Docker容器还不能支持用户uid的映射,容器的root和宿主机的root属于同一个uid,均为0。其实,这样是否会导致安全问题,肯定是大家最关心的。

回到Docker的fork,DockerDaemon一旦完成fork,那么后面肯定还需要完成子进程的exec。

容器进程被fork之后,进程描述符task_struct有了,新的命名空间也有了,但是进程还没有开始运行,还没有被exec。因此,exec的内容就是Docker的精髓,Docker容器的重中之重。

Part3

Docker中dockerinit,entrypoint和cmd三者的关系

1?dockerinit,entrypoint和cmd三者的初步介绍

此时,我们进入Docker的很多细节部分了,围绕Docker容器的exec,我们需要知道exec的内容到底是什么,因为exec的内容代表了Docker容器的运行逻辑到底是怎么样的。

先看下图:

Docker的世界中,不知道大家是否听说过dockerinit、entrypoint和cmd。相信大家使用Dockerfile打包自身应用的时候,肯定涉及过entrypoint和cmd,然而一般会很少涉及到dockerinit。

从功能的角度来介绍三者:

dockerinit:是在新namespace中第一个运行的内容,作用是设置新namespace中的挂载资源,初始化容器内的网络栈等。完成的属于容器层系统环境的初始化工作。

以网络namespace为例:当DockerDaemon创建容器时,仅仅是fork了一个进程,那么如何让这个进程的netnamespace中包含一个可用的网络栈(虚拟网络设备veth)呢?为容器内部初始化网络栈的角色就是dockerinit,dockerinit会获取DockerDaemon传递来的网络信息,并用来初始化这个容器的netnamespace。保证后续的进程拥有足够的网络能力。

entrypoint:由用户指定,完成容器内用户态配置的工作。

cmd:由用户指定,为容器的运行提供默认的参数,比如默认的运行启动入口等。

既然都属于在容器内部运行的内容,而且最终容器的角色要转移到用户指定的应用指定cmd,而容器内的进程只有一个init进程(pid=1),那么dockerinit、entrypoint和cmd三者与容器init进程之间的关系,会非常重要,毕竟init进程决定的容器的生命状态。

更为清晰的Docker容器进程图如下,其中包含dockerinit、entrypoint以及cmd:

大家可以看到容器进程被fork出来之后,实现exec的时候,第一部分直接exec的是dockerinit,而后是entrypoint,最后是cmd。

图中有一个非常重要的点是,三部分的内容会占用同一个PID,整个流程不会有新的pid诞生。原因很简单,那就是dockerinit执行entrypoint的时候,使用的也是exec操作,确保进程PID不变,进程执行程序切换到entrypoint。Entrypoint到cmd的过程,原理也是如此。

流程的角度来讲,最终容器的init进程(pid=1)的角色能满足最终嫁接到用户指定应用的cmd处。当然图中也显示了,整个容器都处于全新的namespace中,也就是说容器与容器,容器与宿主机之间都是可以实现隔离的。

2DockerDaemon与dockerinit

DockerDaemon作为fork容器进程的发起者,自然是父进程,而dockerinit作为容器内第一个执行的内容,自然是子进程。其实在父子进程之间,还存在一些细枝末节,值得大家去深思与玩味。

简言之,DockerDaemon与dockerinit还存在一些数据的交互。大家还记得dockerinit的作用吗?初始化新建的namespace内的一些重要资源。但是这些资源呢,又是DockerDaemon在宿主机上申请的,比如说虚拟网络设备对(vethpair)。DockerDaemon在原始的命名空间中创建了这些内容之后,需要一种方式,把网络设备对的一个veth交给容器,那就是交给dockerinit。

DockerDaemon与dockerinit具体的通信形式如下:

看上图,我们可以发现最重要的字眼有两个:syncPipe与blocked。

前者可以称为同步管道,作用是协调DockerDaemon和dockerinit的运行顺序。

后者指的是:dockerinit的运行会被阻塞住,直dockerinit从同步管道syncPipe中读到内容为止。

接下来,我们就详细介绍阻塞的原因与两者的协作运行的情况。

上图中,左边代表DockerDaemon的运行流程,右边代表dockerinit的运行流程。现在,我们一步一步详细介绍两者的运行流程,以便深入了解Docker容器的原理。

DockerDaemon的流程如下:

CreateCommand创建容器的执行程序,也就是为Docker容器exec的内容,当然这部分内容自然是dockerinit,毕竟dockerinit是Docker容器内第一个执行的内容。

创建一个同步管道syncPipe,自然是为了和马上要创建的dockerinit建立通信。

将同步管道syncPipe的一端以文件描述符的形式,为即将创建的子进程准备。

执行







































北京治疗白癜风医院哪里比较好
白颠疯

转载请注明原文网址:http://www.gzdatangtv.com/bcyyys/5869.html

------分隔线----------------------------