Skip to content

Latest commit

 

History

History
215 lines (130 loc) · 9.76 KB

5. Docker内核知识(1)--namespace.md

File metadata and controls

215 lines (130 loc) · 9.76 KB

Docker容器本质上是宿主机上的进程,Docker通过namespace实现资源隔离,通过cgroups实现资源限制,通过写时复制(copy-on-write)实现了高效的文件操作。下面详解linux中namespace和cgroups的技术细节。

namespace资源隔离

Docker需要的6项隔离:

chroot命令使后根目录/的挂载点切换了,实现了文件系统的隔离

为了在分布式的环境下进行通信和定位,容器必须有独立的IP、端口、路由等,此为网络的隔离

进程间的通信需要隔离

对用户和用户组的隔离实现了用户权限的隔离

运行在容器中的应用需要有进程号,自然需要与宿主机中的PID进行隔离

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

1、namespace API介绍

namespace的API包括clone(), setn()和unshare(), 以及/proc下的部分文件。在使用这些API时,namespace通常要指定上述6个参数的一个或几个,通过|(位或)操作实现。

(1)通过clone()在创建新进程的同时创建namespace

使用clone()创建一个独立的namespace的进程时Docker 使用namespace最基本的方法,它的调用方式:

int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);
/*
child_func 传入子进程运行的程序主函数
child_stack 传入子进程使用的栈空间
flags 表示使用哪些CLONE_*标志位
args 则可用于传入用户参数
*/

clone()实际上是linux系统调用fork()的一种更通用的实现方式,可以通过flags来控制使用多少功能。一共有20多种CLONE_*的flag标志位参数。

(2)查看/proc/[pid]/ns文件

用户可以在/proc/[pid]/ns文件下看到指向不同namespace 号的文件,[4026531835]就是namespace号,如下:

$ ls -l /proc/$$/ns          <<-- $$是shell中表示当前运行的进程ID号
total 0
lrwxrwxrwx 1 zjj zjj 0 10月 10 11:12 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 zjj zjj 0 10月 10 11:12 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 zjj zjj 0 10月 10 11:12 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 zjj zjj 0 10月 10 11:12 net -> net:[4026531957]
lrwxrwxrwx 1 zjj zjj 0 10月 10 11:12 pid -> pid:[4026531836]
lrwxrwxrwx 1 zjj zjj 0 10月 10 11:12 user -> user:[4026531837]
lrwxrwxrwx 1 zjj zjj 0 10月 10 11:12 uts -> uts:[4026531838]

如果两个进程指向的namespce号相同,则表示它们在同一个namespace下

上面显示以link形式链接,一旦上述link文件被打开,只要打开的文件描述符(fd)存在,后续进程也可以在加入进来。在Docker中,通过文件描述符定位和加入一个存在的namespace是最基本的方式。

可以使用--bind方式挂载,把namespace保存下来

# touch ~/uts
# mount --bind /proc/27514/ns/uts/ ~/uts

(3)通过setns()加入一个已经存在的namespace

上面提到,在程序结束的情况下,可以通过挂载的形式把namespace保留下来,为以后加入的进程做准备。Docker中,使用docker exec命令在已经运行的容器中执行一个新的命令就用到该方法。

通过sents()系统调用,进程从原先的namespace加入某个已经存在的namespace,通常为了使新加入的pid namespace生效且不影响调用者,会在sents()函数执行后使用clone()创建子进程继续执行命令(由于PID namespace的原因),让原先程序结束。

int setns(int fd, int nstype);
/*
fd 表示要加入namespace的文件描述符,上文提到,它指向/proc/[pid]/ns目录
nstype 让调用者检查fd指向的namespace类型是否符合实际要求,参数为0表示不检查
*/

(4)通过unshare()在原先进程上进行namespace隔离

系统调用unshare()类似于clone(),不同的是:unshare()运行在原先的进程上,不需要启动一个新的进程。

int unshare(int flags);

调用unshare()的主要作用就是不启动一个新进程就可以起到隔离的效果,相当于跳出原先的namespace进行操作。这样,就可以在原进程上进行一些隔离。Linux自带的unshare命令,就是通过unshare()系统调用实现的。

Docker目前没有使用这一系统调用。

(5)fork()系统调用

调用一次,返回两次

...
fpid = fork();
...          # 父进程和子进程都会执行下述代码

根据fpid的返回值,可以判断时父进程还是子进程:

父进程中,fpid为新建子进程的ID

子进程中,fpid为0(若出错,则为负值)

###2、6种namespace详解

(1)UTS(UNIX Time-shating System) namespace提供了主机名和域名的隔离,这样每个Docker容器就可以拥有独立的主机名和域名,在网络上可以被视为一个独立的节点,而非一个进程。Docker中,每个镜像基本以自身所提供的服务名称来命名镜像的hostname,且不会对宿主机产生影响。

(2)IPC namespace

IPC(Inter-Process Communication, 进程间通信)设计的IPC资源包括常见的信号量、消息队列和共享内存。IPC中实际上包含了系统的IPC标识符和实现POSIX消息队列的文件系统。只有在同一个IPC namespace下,进程才彼此可见。 

目前使用这种机智的系统不多,有postgreSQL。Docker当前使用IPC namespace实现了容器与宿主机、容器与容器之间的IPC隔离。

(3)PID namespae

PID namespace隔离会对进程PID进行重新编号,即两个不同的namespace下的程序可以有相同的PID。内核为所有的PID namespace维护了一个树状结构,最顶层的为系统初始时建的root namespace,他创建的PID namespace为child namespace等。父节点可以看到子节点中的进程,并通过信号对子节点产生影响,而子节点却不能看到父节点PID namespace中的任何内容。由此可见:

在子节点中执行ps aux/top可以查看父节点的PID,那是因为还没有对文件系统挂载点进行隔离,与其他namespace不同,为了实现一个稳定安全的容器,PID namespace还需要进行一些额外的工作才能确保进程的顺利运行。

PID namespace中的init进程,相当于管理进程,作为所有进程的父进程,维护一张进程表,这样的树状结构有利于资源监控与回收。所以:如果确实需要在Docker容器中运行多个进程,最先启动的进程应该有资源管理能力,如bash。

注意:PID namespace与其它namespace不同的地方是:unshare()和setns()调用会允许用户在原有进程中创建名字空间进行隔离,但创建了PID namespace后,原先的unshare()或setns()的调用者程序并不会进入新的namespace,接下来的子进程会进入,并作为新namespace中的init进程。(原因是默认为进程的PID为一个常量,不会改变。)所以在Docker中,docker exec会使用setns()函数加入已经存在的namespace,但最终还是要调用clone()函数。

(4)mount namespace

mount namespace通过隔离文件系统挂载点对隔离文件系统提供支持,可以通过

$ cat /proc/[pid]/mountstats  # 或/mounts

查看所有挂载在当前namespac中的文件系统

进程在创建mount namespace时,会把当前的文件结构复制给新的namespace

挂载传播(mount propagation)解决了挂载的共享问题,其定义了挂载对象之间的关系:

<1>共享关系:一个挂载对象中的挂载事件会传播到其他挂载对象,反之亦然

<2>从属关系:一个挂载对象中的挂载事件会传播到其他挂载对象,反正不行;这种关系中,从属对象是事件的接受者。

挂在状态有如下几种:

<1>共享挂载(shared):传播事件的挂载对象

<2>从属挂载(slave):接收传播事件的挂载对象

<3>共享/从属挂载(shared/slave)

<4>私有挂载(private):既不传播也不接收传播事件

<5>不可绑定挂载(unbindable):与私有挂载类似,但不允许执行挂载绑定。

默认情况下,挂载都是私有的,设置共享的命令如下:

mount --make-shared <mount-object>

将共享挂载设置为从属挂载的命令如下:

mount --make-slave <shared-mount-object>

将从属挂载设置为共享/从属的命令如下(或者将其移动到一个共享挂载对象下):

mount --make-shared

将修改过的挂载对象重新标记为私有:

mount --make-private <mount-object>

将挂载对象标记为不可绑定:

mount --make-unbindable <mount-object>

(5)nework namespace

network namespace主要提供关于网络资源的隔离,如:网络设备、IPv4和IPv6协议栈、IP路由器、防火墙、/proc/net目录、/sys/class/net目录、套接字等。

一个物理的网络设备最多存在于一个network namespace中,可以通过创建veth pair(虚拟网络设备对:有两端,类似管道,数据可以在两端传输)在不同的network namespace之间创建通道,以达到通信目的。

在建立veth pair之前,新旧namespace通过pipe(管道)进行通信

(6)user namespace

user namespace主要是隔离了安全相关的标识符(identifiers)和属性(attributes),包括用户组ID、root目录、key(密钥)以及特殊权限。

即一个普通用户的进程通过clone()创建的新进程在新user namespace中可以拥有不同的用户和用户组(如超级用户)。