您的位置:首页 > 运维架构 > Docker

Docker 入门笔记 8 - Namespace 简介(中)

2018-01-01 21:13 489 查看

Namespace 简介(中)

PID Namespace

对于 Docker 来说,PID Namespace可以说非常重要,它可以使容器之间的进程树互不可见。 通过PID Namespace, 每个容器中都会有一个进程号计数器, 容器内所有进程号会被重新编号。宿主机内核会维护各个容器中的进程树,在树的最顶端的进程号变为1, 也就是 Init 进程。 此进程会作为容器内其他所有进程的父进程来执行容器环境的初始化工作。

接下来,我们仍然用 unshare 命令测试一下PID Namespace的作用

查看当前状态

在当前bash中运行

# echo $$
4682

#  ll /proc/$$/ns
总用量 0
dr-x--x--x 2 root root 0 Dec 29 16:04 ./
dr-xr-xr-x 9 root root 0 Dec 29 14:01 ../
lrwxrwxrwx 1 root root 0 Jan  1 20:41 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Jan  1 20:41 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Jan  1 20:41 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Jan  1 20:41 net -> net:[4026531957]
lrwxrwxrwx 1 root root 0 Jan  1 20:41 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Jan  1 20:41 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Jan  1 20:41 uts -> uts:[4026531838]


创建一个小的能打印自己pid 的c程序如下,

//getpid.c
#include<unistd.h>
#include<stdio.h>
main()
{
printf("pid=%d  ppid=%d\n",getpid(), getppid());
}


编译运行 getppid.c

#gcc getpid.c -o getpid
# ./getpid
pid=4715  ppid=4682


运行一个新的bash并将它加入到新的pid namespace中

我们可以看到新的bash 在自己的名空间中获得的ID为1

# unshare --fork --pid /bin/bash
# echo $$
1


在这个新的bash中,如果我们运行小程序 getpid, 可以发现新进程的pid也是从属于这个namespace的, 而bash进程起到了宿主机中init的作用。

#./getpid
pid=14  ppid=1


那么原来的init进程呢?我们grep一下可以发现其实它一直都在。

# ps -ef |grep init |grep -v grep
root         1     0  0 08:21 ?        00:00:02 /sbin/init splash


再次在新的bash中运行ps命令

# ps
PID TTY          TIME CMD
4681 pts/2    00:00:00 su
4682 pts/2    00:00:00 bash
4717 pts/2    00:00:00 unshare
4718 pts/2    00:00:00 bash
4733 pts/2    00:00:00 ps

# ps -ef |grep unshare
root      9644  8010  0 16:32 pts/23   00:00:00 unshare --fork --pid /bin/bash
root      9663  9645  0 16:33 pts/23   00:00:00 grep --color=auto unshare

# ps -ef |egrep '8010|9644|9645'|grep -v 'grep'
root      8010  8009  0 13:40 pts/23   00:00:00 bash
root      9644  8010  0 16:32 pts/23   00:00:00 unshare --fork --pid /bin/bash
root      9645  9644  0 16:32 pt
4000
s/23   00:00:00 /bin/bash
root      9711  9645  0 16:38 pts/23   00:00:00 ps -ef

# pstree 8009
su───bash───unshare───bash───pstree


可以发现虽然新的bash程序以及它的子进程在新的名空间中被分配了新的pid,但并不影响它在parent namesapce中获得自己的pid。如图:



我们再做一个实验。在parent pid namespace的shell 中启动一个随意一个进程

# gedit &
[1] 4877


进程启动后我们可以发现它的pid是4887, 然后在新的pid namespace的bash中尝试kill

# kill -9 4887
bash: kill: (4887) - no such process


实际上在child pid namespace中,我们无法访问或者操纵parent pid namespace的进程。这就是PID namespace的隔离性质。

那么为什么我们在新的bash里输入ps,top等命令,还是可以看得到所有进程呢。这是因为,像ps,top这些命令会去读/proc文件系统,而在上面的实验中所有shell都访问的是同一个/proc文件系统,所以这些命令显示的东西都是一样的。

因此,要想达到充分的隔离,我们还要对文件系统进行隔离,这就是下一节的内容。

Mount Namespace

Mount命名空间是第一个在Linux上实现的命名空间类型,出现在2002年。这个事实说明了相当通用的“NEWNS”绰号(简称“新命名空间”):那时没有人似乎认为其他 ,将来可能需要不同类型的名称空间

Mount namespace隔离了名称空间中的进程所看到的挂载点列表。或者,换一种说法,每个挂载名称空间都有自己的挂载点列表,这意味着不同名称空间中的进程可以看到并能够操作单个目录层次结构的不同视图。

在2000 年,Al Viro 为 Linux 引入了绑定挂载和文件系统名称空间:

- 绑定挂载(bind mount)允许从任何其他位置访问任何文件或目录。

- 文件系统名称空间(filesystem namespace)是与不同进程相关联的完全独立的文件系统树。

系统第一次启动时,会有一个单独的挂载名称空间,即所谓的“初始名称空间”。通过使用CLONE_NEWNS标志与clone()系统调用(在新的名称空间中创建一个新的子进程)或unshare()系统调用(将调用者移入新的名称空间)来创建新的Mount Namespace。当创建一个新的Mount Namespace时,它会收到从clone()或unshare()调用方的名称空间复制的挂载点列表的副本。

在clone() 或unshare() 调用之后,可以在每个名称空间(通过mount()和umount()))独立添加和删除挂载点。对安装点列表的更改(默认情况下)仅对进程所在的安装名称空间中的进程可见;其他安装名称空间中的更改不可见。

尽管每个进程使用单独的文件系统名称空间在理论上非常有意义,但是在实践中,完全隔离它们会造成过度限制。例如,假设将一个磁盘加载到一个光盘驱动器中。在最初的实现中,使所有mount namespace都可以看见这个磁盘唯一方法是在每个命名空间中单独挂载磁盘。,因为在最初的文件系统名称空间中执行的挂载无法影响用户的拷贝。而在许多情况下,用户希望是执行一个单独的挂载操作,使得磁盘在系统上的所有挂载命名空间(或者可能是某些子集)中都可见。

为解决这一问题,Linux 2.6.15中又引入了共享子树功能(2006年初,大约挂载命名空间的初始实现大约三年后)。共享子树的主要优点是允许自动控制名称空间之间挂载和卸载事件的传播(mount propagation )。这意味着,例如,将光盘安装在一个安装名称空间中,可以在所有其他名称空间中触发安装该磁盘。

在共享子树功能下,每个安装点都会标记一个“传播类型”,该传播类型确定在此安装点下创建和删除的安装点是否传播到其他安装点。有四种不同的传播类型:

MS_SHARED:此挂载点与其他“挂载点”中的其他挂载点共享mount和unmount事件。在此挂载点下添加或删除挂载点时,此更改将传播到==peer group== ,以便mount或unmount也能在每个peer mount points发生。传播也发生在相反的方向上,所以挂载和卸载事件在同级装载也将传播到此挂载点。

MS_PRIVATE:与shared mount point相反。挂载点不会将事件传播给任何对等点,也不会从任何对等点接收传播事件。

MS_SLAVE:这种传播类型位于共享和私有之间。一个slave mount 从属于一个master(这个master一般是一个shared peer group,master的所有成员会传播mount和unmount事件到slave mount。但是,slave mount不会将事件传播到master 对等组。

MS_UNBINDABLE:此mount point 是不可绑定的。与私有mount point 一样,此挂载点不会将事件传播到对等节点或从对等节点传播事件。另外,此mount point不能成为 bind mount 操作的源。

测试 shared peer group

在根目录创建三个文件夹 X 、 Y、 Z

#cd /; mkdir X Y Z


将根目录标记为MS_PRIVATE

#mount --make-private /


关于默认传播类型

这里要说明的是,虽然从内核的角度来看,创建新设备的默认设置如下:

如果装载点具有父级(即,它是非根装入点),并且父级的传播类型是MS_SHARED,则新装入的传播类型也是MS_SHARED。否则,新挂载的传播类型是MS_PRIVATE。

根据这些规则,根挂载将是MS_PRIVATE,所有的后代挂载默认也是MS_PRIVATE。然而,MS_SHARED可以说是一个更好的默认值,因为它是更常用的传播类型。为此,systemd将所有安装点的传播类型设置为MS_SHARED。因此,在大多数现代Linux发行版中,默认的传播类型实际上是MS_SHARED。

以shared传播类型挂载 X、Y

# mount --make-shared /dev/sda2 /X
# mount --make-shared /dev/sda6 /Y


查看挂载情况

# cat /proc/self/mountinfo
25 0 8:6 / / rw,relatime - ext4 /dev/sda6 rw,errors=remount-ro,data=ordered
171 25 8:2 / /X rw,relatime shared:1 - fuseblk /dev/sda2 rw,user_id=0,group_id=0,allow_other,blksize=4096
174 25 8:6 / /Y rw,relatime shared:126 - ext4 /dev/sda6 rw,errors=remount-ro,data=ordered


5,在另一consol 用unshare新起一个bash并将起放入新的mount namespace, 并查看挂载情况

# unshare -m bash
# cat /proc/self/mountinfo
178 170 8:6 / / rw,relatime - ext4 /dev/sda6 rw,errors=remount-ro,data=ordered
212 178 8:2 / /X rw,relatime - fuseblk /dev/sda2 rw,user_id=0,group_id=0,allow_other,blksize=4096
223 178 8:6 / /Y rw,relatime - ext4 /dev/sda6 rw,errors=remount-ro,data=ordered


奇怪的是 X,Y在新的mount namespace中的传播类型变成了 private, 前面不是说 ==”当创建一个新的Mount Namespace时,它会收到从clone()或unshare()调用方的名称空间复制的挂载点列表的副本“== 吗,怎么传播类型没有复制过来?

这是因为,当创建一个新的挂载名称空间时,unshare假定用户需要一个完全隔离的名称空间,并通过执行以下命令(将根目录下的所有挂载以递归方式标记为private)的等效命令使所有挂载点保持私有状态:

mount --make-rprivate /


为了防止这种情况,我们可以在创建新的名称空间时使用一个额外的选项:

unshare -m --propagation unchanged <cmd>


以propagation unchange方式重做第五步

# unshare -m --propagation unchanged bash
# cat /proc/self/mountinfo
178 170 8:6 / / rw,relatime - ext4 /dev/sda6 rw,errors=remount-ro,data=ordered
212 178 8:2 / /X rw,relatime shared:1 - fuseblk /dev/sda2 rw,user_id=0,group_id=0,allow_other,blksize=4096
223 178 8:6 / /Y rw,relatime shared:126 - ext4 /dev/sda6 rw,errors=remount-ro,data=ordered


回到第一个bash,从X mount point创建一个bind mount

# mount --bind /X /Z
# cat /proc/self/mountinfo 25 0 8:6 / / rw,relatime - ext4 /dev/sda6 rw,errors=remount-ro,data=ordered 171 25 8:2 / /X rw,relatime shared:1 - fuseblk /dev/sda2 rw,user_id=0,group_id=0,allow_other,blksize=4096 174 25 8:6 / /Y rw,relatime shared:126 - ext4 /dev/sda6 rw,errors=remount-ro,data=ordered
224 25 8:2 / /Z rw,relatime shared:1 - fuseblk /dev/sda2 rw,user_id=0,group_id=0,allow_other,blksize=4096


对比两个bash的 mount info我们可以发现

- 上述试验中生成了两个 shared peer group。 bash1中的X,Z 以及bash2 中的X’(X的副本) 同属于 shared peer group 1, 而 bash1 中的Y以及 bash2中的Y’ 同属于 shared peer group 126.

- bash1 中的挂载点Z, 由于是在bash2 所属的名空间被创建后挂载的所以未被复制,这是由于它的parent mount(/)标记为私有。



测试对比 MS_SHARED 和 MS_PRIVATE

在根目录下创建两个文件夹 mntP 和 mntS

cd /;mkdir mntP mntS


以不同传播方式挂载这两个目录到tmpfs文件系统

# mount -t tmpfs -o size=20m tmpfs --make-shared /mntS
# mount -t tmpfs -o size=20m tmpfs --make-private /mntP
# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
170 25 0:47 / /mntS rw,relatime shared:1
171 25 0:52 / /mntP rw,relatime


在另一consol 用unshare新起一个bash

# unshare -m --propagation unchanged bash
# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
212 178 0:47 / /mntS rw,relatime shared:1
223 178 0:52 / /mntP rw,relatime


在第二个bash中执行

# mkdir /mntS/a
# mount /dev/sda2 /mntS/a
# mkdir /mntP/b
# mount /dev/sda6 /mntP/b
# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
212 178 0:47 / /mntS rw,relatime shared:1
223 178 0:52 / /mntP rw,relatime
224 212 8:2 / /mntS/a rw,relatime shared:126
258 223 8:6 / /mntP/b rw,relatime


从上面可以看出,/mntS/a被创建为共享(从其父装载继承此设置),/mntP/b被创建为私有装载。

返回到第一个bash并检查设置,我们看到在共享挂载点/ mntS下创建的新挂载传播到其对等挂载,但在专用挂载点/ mntP没有传播:

# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
170 25 0:47 / /mntS rw,relatime shared:1
171 25 0:52 / /mntP rw,relatime
257 170 8:2 / /mntS/a rw,relatime shared:126


测试MS_SLAVE

在根目录下创建两个文件夹 mntX 和 mntY

cd /;mkdir mntX mntY


挂载这两个目录到tmpfs文件系统

# mount -t tmpfs -o size=20m tmpfs --make-shared /mntX
# mount -t tmpfs -o size=20m tmpfs --make-shared /mntY
# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
170 25 0:47 / /mntX rw,relatime shared:1
171 25 0:52 / /mntY rw,relatime shared:126


在另一consol 用unshare新起一个bash

# unshare -m --propagation unchanged bash
# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
212 178 0:47 / /mntX rw,relatime shared:1
223 178 0:52 / /mntY rw,relatime shared:126


在bash2 设置/mntY为slave

# mount --make-slave /mntY
# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
212 178 0:47 / /mntX rw,relatime shared:1
223 178 0:52 / /mntY rw,relatime master:126


可以看见 /mntY 已经变成了 shared peer group 126的slave mount 了

在bash2 执行

# mkdir /mntX/a
# mount /dev/sda2 /mntX/a
# mkdir /mntY/b
# mount /dev/sda6 /mntY/b
# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
212 178 0:47 / /mntX rw,relatime shared:1
223 178 0:52 / /mntY rw,relatime master:126
224 212 8:2 / /mntX/a rw,relatime shared:127
258 223 8:6 / /mntY/b rw,relatime


可以看见此时 /mntY/b 被创建为 private状态

在bash1中检查,可发现bash2 中 /mntY/b的挂载未被传播

# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
170 25 0:47 / /mntX rw,relatime shared:1
171 25 0:52 / /mntY rw,relatime shared:126
257 170 8:2 / /mntX/a rw,relatime shared:127


注意,此时bash1 中的 /mntY仍属于 shared peer group 126, 它其实是 bash2 中的/mntY的master

在bash1中执行以下命令

# mkdir /mntY/c
# mount /dev/sda6 /mntY/c
# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//' 170 25 0:47 / /mntX rw,relatime shared:1 171 25 0:52 / /mntY rw,relatime shared:126 257 170 8:2 / /mntX/a rw,relatime shared:127
284 171 8:6 / /mntY/c rw,relatime shared:128


在bash2中检查可发现 /mntY/c的挂载被传播到了 bash2的 mount namesapce

# cat /proc/self/mountinfo | grep '/mnt' | sed 's/ - .*//'
212 178 0:47 / /mntX rw,relatime shared:1
223 178 0:52 / /mntY rw,relatime master:126
224 212 8:2 / /mntX/a rw,relatime shared:127
258 223 8:6 / /mntY/b rw,relatime
290 223 8:6 / /mntY/c rw,relatime master:128


测试 MS_UNBINDABLE

Unbindable mounts用于解决所谓的 “mount point explosion”的问题 .这一问题发生在文件树的一个低级别的挂载点重复执行递归绑定更高级子树时发生的问题.

创建两个挂载点 /mntX 和 /mntX/mntY

# mkdir /mntX
# mount -t tmpfs -o size=20m tmpfs --make-shared /mntX
# mount | awk '{print $1, $2, $3}'
/dev/sda6 on /
tmpfs on /mntX


在另一终端用unshare 新创建一个bash, 并将/ 连同所有子挂载点递归设为 slave(防止在其他mount namespace产生副作用). 然后尝试将几个用户的home目录递归的绑定到/mnt

# unshare -m bash
# mount --make-rslave /
# mkdir -p /home/user1 /home/user2
# mount --rbind / /home/user1
# mount | awk '{print $1, $2, $3}'
/dev/sda6 on /
tmpfs on /mntX
/dev/sda6 on /home/user1
tmpfs on /home/user1/mntX

# mount --rbind / /home/user2
root@chic-X450LD:/# mount | awk '{print $1, $2, $3}'
/dev/sda6 on /
tmpfs on /mntX
/dev/sda6 on /home/user1
tmpfs on /home/user1/mntX
/dev/sda6 on /home/user2
tmpfs on /home/user2/mntX
/dev/sda6 on /home/user2/home/user1
tmpfs on /home/user2/home/user1/mntX


通过 –make-unbindable 可以阻断这种情况

# mount --rbind --make-unbindable / /home/user1
# mount --rbind --make-unbindable / /home/user2


重做步骤1,2

# mount | awk '{print $1, $2, $3}'
/dev/sda6 on /
tmpfs on /mntX
/dev/sda6 on /home/user1
tmpfs on /home/user1/mntX
/dev/sda6 on /home/user2
tmpfs on /home/user2/mntX




chroot 与mount namespace

早期Linux可以用chroot改变一个进程的根目录,也就是通过该命令可将一个子目录设置为该应用程序的“/”,如果要正常执行命令和程序等,必须还将系统中/bin, /usr/bin,/usr/lib等目录拷贝到该“/”下。



mount namespace进一步丰富了chroot的功能,它chroot更灵活。它属于内核名字空间的一部分。 mount namespace可以对系统真正根目录下的子目录做到共享、独享(也就是将拥有一个子目录的副本)等。

制作一个简单的小容器

接下来我们再结合chroot做一个小实验。 假定有一个用户user1

现在我们希望把user1的目录局限在它的主目录 /home/user1

用unshare启动新的bash并为其指定新的pid 和 mount 名空间

# unshare --mount --fork --pid /bin/bash


在user1主目录创建三个子目录作为挂载点

# mkdir -p /home/user1/bin /home/user1/lib /home/user1/lib64
# tree /home/user1/
/home/user1/
├── bin
├── lib
└── lib64


挂载三个目录,并用chroot将/home/user1变为根目录

# mount --rbind /bin/ /home/user1/bin
# mount --rbind /lib/ /home/user1/lib
# mount --rbind /lib64/ /home/user1/lib64
# chroot /home/user1/
bash-4.3# pwd
/


挂载proc 目录后再次运行ps 命令,现在我们可以看见进程少多了

# mkdir /proc
# mount -t proc proc /proc
# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
0            1     0  0 10:05 ?        00:00:00 /bin/bash
0           20     1  0 10:10 ?        00:00:00 /bin/bash -i
0           25    20  0 10:12 ?        00:00:00 ps -ef
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  namespace docker