docker进阶教程一:namespace之障眼法

Namespace是什么

Namespace技术则是用来修改进程视图的主要方法。先通过一个实例来看是讲解namespace。

以下运行一个busybox的容器,使用ps可以看到,我们在Docker里最开始执行的/bin/sh,就是这个容器内部的第1号进程(PID=1),而这个容器里一共只有两个进程在运行。这就意味着,前面执行的/bin/sh,以及我们刚刚执行的ps,已经被Docker隔离在了一个跟宿主机完全不同的世界当中。

1
2
3
4
5
[root@linjx ~]# docker run --name=busybox -it busybox:latest /bin/sh
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 /bin/sh
6 root 0:00 ps aux

但是在宿主机里面,是可以看到这个/bin/sh的进程的,其PID为27220。

1
2
3
[root@linjx ~]# ps aux |grep busy
root 27220 0.0 13.0 332052 49544 pts/0 Sl+ 01:40 0:00 docker run --name=busybox -it busybox:latest /bin/sh
root 27314 0.0 0.6 112672 2296 pts/1 R+ 01:43 0:00 grep --color=auto busy

这种机制,其实就是对被隔离应用的进程空间做了手脚,使得这些进程只能看到重新计算过的进程编号,比如PID=1。可实际上,他们在宿主机的操作系统里,还是原来的进程。这种技术,就是Linux里面的Namespace机制。

而Namespace的使用方式也非常有意思:它其实只是Linux创建新进程的一个可选参数。我们知道,在Linux系统中创建线程的系统调用是clone(),当我们用clone()系统调用创建一个新进程时,就可以在参数中指定CLONE_NEWPID参数int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);;这时,新创建的这个进程将会“看到”一个全新的进程空间,在这个进程空间里,它的PID是1。之所以说“看到”,是因为这只是一个“障眼法”,在宿主机真实的进程空间里,这个进程的PID还是真实的数值。

除了我们刚刚用到的PID Namespace,Linux操作系统还提供了Mount、UTS、IPC、Network和User这些Namespace,用来对各种不同的进程上下文进行“障眼法”操作。

比如,Mount Namespace,用于让被隔离进程只看到当前Namespace里的挂载点信息;Network Namespace,用于让被隔离进程看到当前Namespace里的网络设备和配置。

namespaces简介

分类

目前 linux 内核主要实现了一下几种不同的资源 namespace:

名称 宏定义 隔离的内容
IPC CLONE_NEWIPC 实现容器与宿主机、容器与容器之间的IPC隔离。IPC资源包括信号量、消息队列和共享内存。(since Linux 2.6.19)
Network CLONE_NEWNET 提供了关于网络资源的隔离,包括网络设备、IPv4和IPv6协议栈、IP路由表、防火墙、/proc/net目录、/sys/class/net目录、套接字(socket)等。(since Linux 2.6.24)
Mount CLONE_NEWNS 通过隔离文件系统挂载点隔离文件系统,是第一个实现的Linux namespace(since Linux 2.4.19)
PID CLONE_NEWPID 两个不同namespace下的进程没有关系,因此PID也可以相同。内核为所有的PID namespace维护了一个树状结构。(since Linux 2.6.24)
User CLONE_NEWUSER 隔离了安全相关的标识符和属性(用户ID、用户组ID、root目录、key(密钥)、特殊权限)。(started in Linux 2.6.23 and completed in Linux 3.8)
UTS CLONE_NEWUTS 提供了主机名和域名的隔离,使每个Docker容器可以拥有独立的主机名和域名,在网络上可以视为独立的节点。 (since Linux 2.6.19)
Cgroup CLONE_NEWCGROUP Cgroup root directory (since Linux 4.6)

查看进程所属的namespace

从版本号为 3.8 的内核开始,/proc/[pid]/ns 目录下会包含进程所属的 namespace 信息,使用下面的命令可以查看当前进程所属的 namespace 信息:

1
2
3
4
5
6
7
8
[root@localhost ~]# ll /proc/self/ns/
total 0
lrwxrwxrwx. 1 root root 0 Oct 12 03:57 ipc -> ipc:[4026531839]
lrwxrwxrwx. 1 root root 0 Oct 12 03:57 mnt -> mnt:[4026531840]
lrwxrwxrwx. 1 root root 0 Oct 12 03:57 net -> net:[4026531956]
lrwxrwxrwx. 1 root root 0 Oct 12 03:57 pid -> pid:[4026531836]
lrwxrwxrwx. 1 root root 0 Oct 12 03:57 user -> user:[4026531837]
lrwxrwxrwx. 1 root root 0 Oct 12 03:57 uts -> uts:[4026531838]
  • 这些 namespace 文件都是链接文件。链接文件的内容的格式为 xxx:[inode number]。其中的 xxx 为 namespace 的类型,inode number 则用来标识一个 namespace,我们也可以把它理解为 namespace 的 ID。如果两个进程的某个 namespace 文件指向同一个链接文件,说明其相关资源在同一个 namespace 中。
  • 在 /proc/[pid]/ns 里放置这些链接文件的另外一个作用是,一旦这些链接文件被打开,只要打开的文件描述符(fd)存在,那么就算该 namespace 下的所有进程都结束了,但这个 namespace 也会一直存在,后续的进程还可以再加入进来。

当一个namespace中的所有进程都退出时,该namespace将会被销毁。当然还有其他方法让namespace一直存在,假设我们有一个进程号为1000的进程,以ipc namespace为例:

  1. 通过mount —bind命令。例如mount --bind /proc/1000/ns/ipc /other/file,就算属于这个ipc namespace的所有进程都退出了,只要/other/file还在,这个ipc namespace就一直存在,其他进程就可以利用/other/file,通过setns函数加入到这个namespace
  2. 在其他namespace的进程中打开/proc/1000/ns/ipc文件,并一直持有这个文件描述符不关闭,以后就可以用setns函数加入这个namespace。

跟namespace相关的API

clone

clone:创建一个新的进程并把他放到新的namespace中

1
2
3
4
5
int clone(int (*child_func)(void *), void *child_stack
, int flags, void *arg);

flags:
指定一个或者多个上面的CLONE_NEW*(当然也可以包含跟namespace无关的flags),这样就会创建一个或多个新的不同类型的namespace,并把新创建的子进程加入新创建的这些namespace中。

setns

setns将当前进程加入到已有的namespace中

1
2
3
4
5
6
7
8
int setns(int fd, int nstype);

fd:
指向/proc/[pid]/ns/目录里相应namespace对应的文件,表示要加入哪个namespace

nstype:指定namespace的类型(上面的任意一个CLONE_NEW*):
1. 如果当前进程不能根据fd得到它的类型,如fd由其他进程创建,并通过UNIX domain socket传给当前进程,那么就需要通过nstype来指定fd指向的namespace的类型
2. 如果进程能根据fd得到namespace类型,比如这个fd是由当前进程打开的,那么nstype设置为0即可

unshare

unshare: 使当前进程退出指定类型的namespace,并加入到新创建的namespace(相当于创建并加入新的namespace)

1
2
3
4
int unshare(int flags);

flags:
指定一个或者多个上面的CLONE_NEW*,这样当前进程就退出了当前指定类型的namespace并加入到新创建的namespace

clone和unshare区别

clone和unshare的功能都是创建并加入新的namespace, 他们的区别是:

  • unshare是使当前进程加入新的namespace
  • clone是创建一个新的子进程,然后让子进程加入新的namespace,而当前进程保持不变

相关命令

在linux上,有 nsenter unshare 这2个命令可以直接使用来实现不同的功能,具体如下:

  • nsenter:加入指定进程的指定类型的namespace,然后执行参数中指定的命令。详情请参考帮助文档代码
  • unshare:离开当前指定类型的namespace,创建且加入新的namespace,然后执行参数中指定的命令。详情请参考帮助文档代码

IPC & UTS namespace

IPC namespace用来隔离System V IPC objectsPOSIX message queues。其中System V IPC objects包含消息列表Message queues、信号量Semaphore sets和共享内存Shared memory segments.

在实例之前,先介绍2个跟ipc相关的命令:

  • ipcmk:创建shared memory segments, message queues, 和semaphore arrays
  • ipcs:查看shared memory segments, message queues, 和semaphore arrays的相关信息
    • 参数-a:显示全部可显示的信息
    • 参数-q:显示活动的消息队列信息
    • 参数-m:显示活动的共享内存信息
    • 参数-s:显示活动的信号量信息

这里将以消息队列为例,演示一下隔离效果,为了使演示更直观,我们在创建新的ipc namespace的时候,同时也创建新的uts namespace,然后为新的utsnamespace设置新hostname,这样就能通过shell提示符一眼看出这是属于新的namespace的bash,后面的文章中也采取这种方式启动新的bash。

在这个示例中,我们将用到两个shell窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#--------------------------第一个shell窗口----------------------
#记下默认的uts和ipc namespace number
dev@ubuntu:~$ readlink /proc/$$/ns/uts /proc/$$/ns/ipc
uts:[4026531838]
ipc:[4026531839]

#确认hostname
dev@ubuntu:~$ hostname
ubuntu

#查看现有的ipc Message Queues,默认情况下没有message queue
dev@ubuntu:~$ ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages

#创建一个message queue
dev@ubuntu:~$ ipcmk -Q
Message queue id: 0
dev@ubuntu:~$ ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x12aa0de5 0 dev 644 0 0


#--------------------------第二个shell窗口----------------------
#重新打开一个shell窗口,确认和上面的shell是在同一个namespace,
#能看到上面创建的message queue
dev@ubuntu:~$ readlink /proc/$$/ns/uts /proc/$$/ns/ipc
uts:[4026531838]
ipc:[4026531839]
dev@ubuntu:~$ ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x12aa0de5 0 dev 644 0 0

#运行unshare创建新的ipc和uts namespace,并且在新的namespace中启动bash
#这里-i(--ipc)表示启动新的ipc namespace,-u(--uts)表示启动新的utsnamespace
dev@ubuntu:~$ sudo unshare -iu /bin/bash
root@ubuntu:~#

#确认新的bash已经属于新的ipc和uts namespace了
root@ubuntu:~# readlink /proc/$$/ns/uts /proc/$$/ns/ipc
uts:[4026532455]
ipc:[4026532456]

#设置新的hostname以便和第一个shell里面的bash做区分
root@ubuntu:~# hostname container001
root@ubuntu:~# hostname
container001

#当hostname改变后,bash不会自动修改它的命令行提示符
#所以运行exec bash重新加载bash
root@ubuntu:~# exec bash
root@container001:~#
root@container001:~# hostname
container001

#现在各个bash进程间的关系如下
#bash(24429)是shell窗口打开时的bash
#bash(27668)是运行sudo unshare创建的bash,和bash(24429)不在同一个namespace
root@container001:~# pstree -pl
├──sshd(24351)───sshd(24428)───bash(24429)───sudo(27667)───bash(27668)───pstree(27695)

#查看message queues,看不到原来namespace里面的消息,说明已经被隔离了
root@container001:~# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages

#创建一条新的message queue
root@container001:~# ipcmk -Q
Message queue id: 0
root@container001:~# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x54b08fc2 0 root 644 0 0

#--------------------------第一个shell窗口----------------------
#回到第一个shell窗口,看看有没有受到新namespace的影响
dev@ubuntu:~$ ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x12aa0de5 0 dev 644 0 0
#完全无影响,还是原来的信息

#试着加入第二个shell窗口里面bash的uts和ipc namespace
#-t后面跟pid用来指定加入哪个进程所在的namespace
#这里27668是第二个shell中正在运行的bash的pid
#加入成功后将运行/bin/bash
dev@ubuntu:~$ sudo nsenter -t 27668 -u -i /bin/bash

#加入成功,bash的提示符也自动变过来了
root@container001:~# readlink /proc/$$/ns/uts /proc/$$/ns/ipc
uts:[4026532455]
ipc:[4026532456]

#显示的是新namespace里的message queues
root@container001:~# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x54b08fc2 0 root 644 0 0

这样就实现了 hostname 以及 ipc 的隔离了。

此内容全部来自:Linux Namespace系列(03):IPC namespace (CLONE_NEWIPC)

Mount namespace

Mount namespaces是第一个被加入Linux的namespace,由于当时没想到还会引入其它的namespace,所以取名为CLONE_NEWNS,而没有叫CLONE_NEWMOUNT

Mount namespace用来隔离文件系统的挂载点, 使得不同的mount namespace拥有自己独立的挂载点信息,不同的namespace之间不会相互影响,这对于构建用户或者容器自己的文件系统目录非常有用。

当前进程所在mount namespace里的所有挂载信息可以在/proc/[pid]/mounts、/proc/[pid]/mountinfo和/proc/[pid]/mountstats里面找到。

每个mount namespace都拥有一份自己的挂载点列表,当用clone或者unshare函数创建新的mount namespace时,新创建的namespace将拷贝一份老namespace里的挂载点列表,但从这之后,他们就没有关系了,通过mount和umount增加和删除各自namespace里面的挂载点都不会相互影响。

以下例子就实现了mount的隔离了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#--------------------------第一个shell窗口----------------------
#先准备两个iso文件,用于后面的mount测试
dev@ubuntu:~$ mkdir iso
dev@ubuntu:~$ cd iso/
dev@ubuntu:~/iso$ mkdir -p iso01/subdir01
dev@ubuntu:~/iso$ mkdir -p iso02/subdir02
dev@ubuntu:~/iso$ mkisofs -o ./001.iso ./iso01
dev@ubuntu:~/iso$ mkisofs -o ./002.iso ./iso02
dev@ubuntu:~/iso$ ls
001.iso 002.iso iso01 iso02
#准备目录用于mount
dev@ubuntu:~/iso$ sudo mkdir /mnt/iso1 /mnt/iso2

#查看当前所在的mount namespace
dev@ubuntu:~/iso$ readlink /proc/$$/ns/mnt
mnt:[4026531840]

#mount 001.iso 到 /mnt/iso1/
dev@ubuntu:~/iso$ sudo mount ./001.iso /mnt/iso1/
mount: /dev/loop1 is write-protected, mounting read-only

#mount成功
dev@ubuntu:~/iso$ mount |grep /001.iso
/home/dev/iso/001.iso on /mnt/iso1 type iso9660 (ro,relatime)

#创建并进入新的mount和uts namespace
dev@ubuntu:~/iso$ sudo unshare --mount --uts /bin/bash
#更改hostname并重新加载bash
root@ubuntu:~/iso# hostname container001
root@ubuntu:~/iso# exec bash
root@container001:~/iso#

#查看新的mount namespace
root@container001:~/iso# readlink /proc/$$/ns/mnt
mnt:[4026532455]

#老namespace里的挂载点的信息已经拷贝到新的namespace里面来了
root@container001:~/iso# mount |grep /001.iso
/home/dev/iso/001.iso on /mnt/iso1 type iso9660 (ro,relatime)

#在新namespace中mount 002.iso
root@container001:~/iso# mount ./002.iso /mnt/iso2/
mount: /dev/loop0 is write-protected, mounting read-only
root@container001:~/iso# mount |grep iso
/home/dev/iso/001.iso on /mnt/iso1 type iso9660 (ro,relatime)
/home/dev/iso/002.iso on /mnt/iso2 type iso9660 (ro,relatime)

#umount 001.iso
root@container001:~/iso# umount /mnt/iso1
root@container001:~/iso# mount |grep iso
/home/dev/iso/002.iso on /mnt/iso2 type iso9660 (ro,relatime)

#/mnt/iso1目录变为空
root@container001:~/iso# ls /mnt/iso1
root@container001:~/iso#

#--------------------------第二个shell窗口----------------------
#打开新的shell窗口,老namespace中001.iso的挂载信息还在
#而在新namespace里面mount的002.iso这里看不到
dev@ubuntu:~$ mount |grep iso
/home/dev/iso/001.iso on /mnt/iso1 type iso9660 (ro,relatime)
#iso1目录里面也有内容
dev@ubuntu:~$ ls /mnt/iso1
subdir01
#说明两个namespace中的mount信息是隔离的

在这篇文章中, Linux Namespace系列(04):mount namespaces (CLONE_NEWNS) 还有一个实例,是关于Shared subtrees,有兴趣的同学可以看再下。

PID namespace

PID namespaces用来隔离进程的ID空间,使得不同pid namespace里的进程ID可以重复且相互之间不影响。

PID namespace可以嵌套,也就是说有父子关系,在当前namespace里面创建的所有新的namespace都是当前namespace的子namespace。父namespace里面可以看到所有子孙后代namespace里的进程信息,而子namespace里看不到祖先或者兄弟namespace里的进程信息。目前PID namespace最多可以嵌套32层,由内核中的宏MAX_PID_NS_LEVEL来定义。

Linux下的每个进程都有一个对应的/proc/PID目录,该目录包含了大量的有关当前进程的信息。 对一个PID namespace而言,/proc目录只包含当前namespace和它所有子孙后代namespace里的进程的信息。

在Linux系统中,进程ID从1开始往后不断增加,并且不能重复(当然进程退出后,ID会被回收再利用),进程ID为1的进程是内核启动的第一个应用层进程,一般是init进程(现在采用systemd的系统第一个进程是systemd),具有特殊意义,当系统中一个进程的父进程退出时,内核会指定init进程成为这个进程的新父进程,而当init进程退出时,系统也将退出。

除了在init进程里指定了handler的信号外,内核会帮init进程屏蔽掉其他任何信号,这样可以防止其他进程不小心kill掉init进程导致系统挂掉。不过有了PID namespace后,可以通过在父namespace中发送SIGKILL或者SIGSTOP信号来终止子namespace中的ID为1的进程。

由于ID为1的进程的特殊性,所以每个PID namespace的第一个进程的ID都是1。当这个进程运行停止后,内核将会给这个namespace里的所有其他进程发送SIGKILL信号,致使其他所有进程都停止,于是namespace被销毁掉。

简单示例

我们经常在docker下面看到pid为1的进程,他是怎么样实现的呢?如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#查看当前pid namespace的ID
dev@ubuntu:~$ readlink /proc/self/ns/pid
pid:[4026531836]

#启动新的pid namespace
#这里同时也启动了新的uts和mount namespace,新的uts是为了设置一个新的hostname,便于和老的namespace区分
#新的mount namespace是为了方便我们修改新namespace里面的mount信息,因为这样不会对老namespace造成影响
#这里--fork是为了让unshare进程fork一个新的进程出来,然后再用bash替换掉新的进程
#这是pid namespace本身的限制,进程所属的pid namespace在它创建的时候就确定了,不能更改,
#所以调用unshare和nsenter后,原来的进程还是属于老的namespace,
#而新fork出来的进程才属于新的namespace
dev@ubuntu:~$ sudo unshare --uts --pid --mount --fork /bin/bash
root@ubuntu:~# hostname container001
root@ubuntu:~# exec bash
root@container001:~#

#查看进程间关系,当前bash(31646)确实是unshare的子进程
root@container001:~# pstree -pl
├─sshd(955)─┬─sshd(17810)───sshd(17891)───bash(17892)───sudo(31644)──
─unshare(31645)───bash(31646)───pstree(31677)
#他们属于不同的pid namespace
root@container001:~# readlink /proc/31645/ns/pid
pid:[4026531836]
root@container001:~# readlink /proc/31646/ns/pid
pid:[4026532469]

#但为什么通过这种方式查看到的namespace还是老的呢?
root@container001:~# readlink /proc/$$/ns/pid
pid:[4026531836]

#由于我们实际上已经是在新的namespace里了,并且当前bash是当前namespace的第一个进程
#所以在新的namespace里看到的他的进程ID是1
root@container001:~# echo $$
1
#但由于我们新的namespace的挂载信息是从老的namespace拷贝过来的,
#所以这里看到的还是老namespace里面的进程号为1的信息
root@container001:~# readlink /proc/1/ns/pid
pid:[4026531836]
#ps命令依赖/proc目录,所以ps的输出还是老namespace的视图
root@container001:~# ps ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 7月07 ? 00:00:06 /sbin/init
root 2 0 0 7月07 ? 00:00:00 [kthreadd]
...
root 31644 17892 0 7月14 pts/0 00:00:00 sudo unshare --uts --pid --mount --fork /bin/bash
root 31645 31644 0 7月14 pts/0 00:00:00 unshare --uts --pid --mount --fork /bin/bash

#所以我们需要重新挂载我们的/proc目录
root@container001:~# mount -t proc proc /proc

#重新挂载后,能看到我们新的pid namespace ID了
root@container001:~# readlink /proc/$$/ns/pid
pid:[4026532469]
#ps的输出也正常了
root@container001:~# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 7月14 pts/0 00:00:00 bash
root 44 1 0 00:06 pts/0 00:00:00 ps -ef

嵌套示例

  • 调用unshare或者setns函数后,当前进程的namespace不会发生变化,不会加入到新的namespace,而它的子进程会加入到新的namespace。也就是说进程属于哪个namespace是在进程创建的时候决定的,并且以后再也无法更改。
  • 在一个PID namespace里的进程,它的父进程可能不在当前namespace中,而是在外面的namespace里面(这里外面的namespace指当前namespace的祖先namespace),这类进程的ppid都是0。比如新namespace里面的第一个进程,他的父进程就在外面的namespace里。通过setns的方式加入到新namespace中的进程的父进程也在外面的namespace中。
  • 可以在祖先namespace中看到子namespace的所有进程信息,且可以发信号给子namespace的进程,但进程在不同namespace中的PID是不一样的。
  • 以下示例是在ubuntu测试的,如果在centos上,大体是一样的,但/proc/[pid]/status没有pid信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#--------------------------第一个shell窗口----------------------
#记下最外层的namespace ID
dev@ubuntu:~$ readlink /proc/$$/ns/pid
pid:[4026531836]

#创建新的pid namespace, 这里--mount-proc参数是让unshare自动重新mount /proc目录
dev@ubuntu:~$ sudo unshare --uts --pid --mount --fork --mount-proc /bin/bash
root@ubuntu:~# hostname container001
root@ubuntu:~# exec bash
root@container001:~# readlink /proc/$$/ns/pid
pid:[4026532469]

#再创建新的pid namespace
root@container001:~# unshare --uts --pid --mount --fork --mount-proc /bin/bash
root@container001:~# hostname container002
root@container001:~# exec bash
root@container002:~# readlink /proc/$$/ns/pid
pid:[4026532472]

#再创建新的pid namespace
root@container002:~# unshare --uts --pid --mount --fork --mount-proc /bin/bash
root@container002:~# hostname container003
root@container002:~# exec bash
root@container003:~# readlink /proc/$$/ns/pid
pid:[4026532475]

#目前namespace container003里面就一个bash进程
root@container003:~# pstree -p
bash(1)───pstree(22)
#这样我们就有了三层pid namespace,
#他们的父子关系为container001->container002->container003

#--------------------------第二个shell窗口----------------------
#在最外层的namespace中查看上面新创建的三个namespace中的bash进程
#从这里可以看出,这里显示的bash进程的PID和上面container003里看到的bash(1)不一样
dev@ubuntu:~$ pstree -pl|grep bash|grep unshare
|-sshd(955)-+-sshd(17810)---sshd(17891)---bash(17892)---sudo(31814)--
-unshare(31815)---bash(31816)---unshare(31842)---bash(31843)--
-unshare(31864)---bash(31865)
#各个unshare进程的子bash进程分别属于上面的三个pid namespace
dev@ubuntu:~$ sudo readlink /proc/31816/ns/pid
pid:[4026532469]
dev@ubuntu:~$ sudo readlink /proc/31843/ns/pid
pid:[4026532472]
dev@ubuntu:~$ sudo readlink /proc/31865/ns/pid
pid:[4026532475]

#PID在各个namespace里的映射关系可以通过/proc/[pid]/status查看到
#这里31865是在最外面namespace中看到的pid
#45,23,1分别是在container001,container002和container003中的pid
dev@ubuntu:~$ grep pid /proc/31865/status
NSpid: 31865 45 23 1

#创建一个新的bash并加入container002
dev@ubuntu:~$ sudo nsenter --uts --mount --pid -t 31843 /bin/bash
root@container002:/#

#这里bash(23)就是container003里面的pid 1对应的bash
root@container002:/# pstree -p
bash(1)───unshare(22)───bash(23)
#unshare(22)属于container002
root@container002:/# readlink /proc/22/ns/pid
pid:[4026532472]
#bash(23)属于container003
root@container002:/# readlink /proc/23/ns/pid
pid:[4026532475]

#为什么上面pstree的结果里面没看到nsenter加进来的bash呢?
#通过ps命令我们发现,我们新加进来的那个/bin/bash的ppid是0,难怪pstree里面显示不出来
#从这里可以看出,跟最外层namespace不一样的地方就是,这里可以有多个进程的ppid为0
#从这里的TTY也可以看出哪些命令是在哪些窗口执行的,
#pts/0对应第一个shell窗口,pts/1对应第二个shell窗口
root@container002:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 04:39 pts/0 00:00:00 bash
root 22 1 0 04:39 pts/0 00:00:00 unshare --uts --pid --mount --fork --mount-proc /bin/bash
root 23 22 0 04:39 pts/0 00:00:00 bash
root 46 0 0 04:52 pts/1 00:00:00 /bin/bash
root 59 46 0 04:53 pts/1 00:00:00 ps -ef

#--------------------------第三个shell窗口----------------------
#创建一个新的bash并加入container001
dev@ubuntu:~$ sudo nsenter --uts --mount --pid -t 31816 /bin/bash
root@container001:/#

#通过pstree和ps -ef我们可看到所有三个namespace中的进程及他们的关系
#bash(1)───unshare(22)属于container001
#bash(23)───unshare(44)属于container002
#bash(45)属于container003,而68和84两个进程分别是上面两次通过nsenter加进来的bash
#同上面ps的结果比较我们可以看出,同样的进程在不同的namespace里面拥有不同的PID
root@container001:/# pstree -pl
bash(1)───unshare(22)───bash(23)───unshare(44)───bash(45)
root@container001:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 04:37 pts/0 00:00:00 bash
root 22 1 0 04:39 pts/0 00:00:00 unshare --uts --pid --mount --fork --mount-proc /bin/bash
root 23 22 0 04:39 pts/0 00:00:00 bash
root 44 23 0 04:39 pts/0 00:00:00 unshare --uts --pid --mount --fork --mount-proc /bin/bash
root 45 44 0 04:39 pts/0 00:00:00 bash
root 68 0 0 04:52 pts/1 00:00:00 /bin/bash
root 84 0 0 05:00 pts/2 00:00:00 /bin/bash
root 95 84 0 05:00 pts/2 00:00:00 ps -ef

#发送信号给contain002中的bash
root@container001:/# kill 68

#--------------------------第二个shell窗口----------------------
#回到第二个窗口,发现bash已经被kill掉了,说明父namespace是可以发信号给子namespace中的进程的
root@container002:/# exit
dev@ubuntu:~$

注意,kill的动作,类似进程的概念,父进程被kill之后,所有的子进程都会自动消亡。

本小节来源于:Linux Namespace系列(05):pid namespace (CLONE_NEWPID)

Network namespace

network namespace用来隔离网络设备, IP地址, 端口等. 每个namespace将会有自己独立的网络栈,路由表,防火墙规则,socket等。

每个新的network namespace默认有一个本地环回接口,除了lo接口外,所有的其他网络设备(物理/虚拟网络接口,网桥等)只能属于一个network namespace。每个socket也只能属于一个network namespace。

当新的network namespace被创建时,lo接口默认是关闭的,需要自己手动启动起。

标记为”local devices”的设备不能从一个namespace移动到另一个namespace,比如loopback, bridge, ppp等,我们可以通过ethtool -k命令来查看设备的netns-local属性。

1
2
3
#这里“on”表示该设备不能被移动到其他network namespace
dev@ubuntu:~$ ethtool -k lo|grep netns-local
netns-local: on [fixed]

实例演示

本示例将演示如何创建新的network namespace并同外面的namespace进行通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#--------------------------第一个shell窗口----------------------
#记录默认network namespace ID
dev@ubuntu:~$ readlink /proc/$$/ns/net
net:[4026531957]

#创建新的network namespace
dev@ubuntu:~$ sudo unshare --uts --net /bin/bash
root@ubuntu:~# hostname container001
root@ubuntu:~# exec bash
root@container001:~# readlink /proc/$$/ns/net
net:[4026532478]

#运行ifconfig啥都没有
root@container001:~# ifconfig
root@container001:~#

#启动lo (这里不详细介绍ip这个tool的用法,请参考man ip)
root@container001:~# ip link set lo up
root@container001:~# ifconfig
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

root@container001:~# ping 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.070 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.015 ms

#获取当前bash进程的PID
root@container001:~# echo $$
15812

#--------------------------第二个shell窗口----------------------
#创建新的虚拟以太网设备,让两个namespace能通讯
dev@ubuntu:~$ sudo ip link add veth0 type veth peer name veth1

#将veth1移动到上面第一个窗口中的namespace
#这里15812是上面bash的PID
dev@ubuntu:~$ sudo ip link set veth1 netns 15812

#为veth0分配IP并启动veth0
dev@ubuntu:~$ sudo ip address add dev veth0 192.168.8.1/24
dev@ubuntu:~$ sudo ip link set veth0 up
dev@ubuntu:~$ ifconfig veth0
veth0 Link encap:Ethernet HWaddr 9a:4d:d5:96:b5:36
inet addr:192.168.8.1 Bcast:0.0.0.0 Mask:255.255.255.0
inet6 addr: fe80::984d:d5ff:fe96:b536/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:8 errors:0 dropped:0 overruns:0 frame:0
TX packets:8 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:648 (648.0 B) TX bytes:648 (648.0 B)

#--------------------------第一个shell窗口----------------------
#为veth1分配IP地址并启动它
root@container001:~# ip address add dev veth1 192.168.8.2/24
root@container001:~# ip link set veth1 up
root@container001:~# ifconfig veth1
veth1 Link encap:Ethernet HWaddr 6a:dc:59:79:3c:8b
inet addr:192.168.8.2 Bcast:0.0.0.0 Mask:255.255.255.0
inet6 addr: fe80::68dc:59ff:fe79:3c8b/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:8 errors:0 dropped:0 overruns:0 frame:0
TX packets:8 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:648 (648.0 B) TX bytes:648 (648.0 B)

#连接成功
root@container001:~# ping 192.168.8.1
PING 192.168.8.1 (192.168.8.1) 56(84) bytes of data.
64 bytes from 192.168.8.1: icmp_seq=1 ttl=64 time=0.098 ms
64 bytes from 192.168.8.1: icmp_seq=2 ttl=64 time=0.023 ms

到目前为止,两个namespace之间可以网络通信了,但在container001里还是不能访问外网。下面将通过NAT的方式让container001能够上外网。这部分内容完全是网络相关的知识,跟namespace已经没什么关系了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#--------------------------第二个shell窗口----------------------
#回到上面示例中的第二个窗口

#确认IP forward是否已经开通,这里1表示开通了
#如果你的机器上是0,请运行这个命令将它改为1: sudo sysctl -w net.ipv4.ip_forward=1
dev@ubuntu:~$ cat /proc/sys/net/ipv4/ip_forward
1

#添加NAT规则,这里ens32是机器上连接外网的网卡
#关于iptables和nat都比较复杂,这里不做解释
dev@ubuntu:~$ sudo iptables -t nat -A POSTROUTING -o ens32 -j MASQUERADE

#--------------------------第一个shell窗口----------------------
#回到第一个窗口,添加默认网关
root@container001:~# ip route add default via 192.168.8.1
root@container001:~# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.8.1 0.0.0.0 UG 0 0 0 veth1
192.168.8.0 0.0.0.0 255.255.255.0 U 0 0 0 veth1

#这样就可以访问外网了
#由于测试环境的限制,所以采用下面的方式检测网络是否畅通
#如果网络没有什么限制的话,随便ping一个外部的IP测试就可以了
root@container001:~# curl -I www.google.com
HTTP/1.1 200 OK
Date: Fri, 15 Jul 2016 08:12:03 GMT

network namespace的概念比较简单,但如何做好网络的隔离和连通却比较难,包括性能和安全相关的考虑,需要很好的Linux网络知识。

ip netns

在单独操作network namespace时,ip netns是一个很方便的工具,并且它可以给namespace取一个名字,然后根据名字来操作namespace。那么给namespace取名字并且根据名字来管理namespace里面的进程是怎么实现的呢?请看下面的脚本演示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#开始之前,获取一下默认network namespace的ID
dev@ubuntu:~$ readlink /proc/$$/ns/net
net:[4026531957]

#创建一个用于绑定network namespace的文件,
#ip netns将所有的文件放到了目录/var/run/netns下,
#所以我们这里重用这个目录,并且创建一个我们自己的文件netnamespace1
dev@ubuntu:~$ sudo mkdir -p /var/run/netns
dev@ubuntu:~$ sudo touch /var/run/netns/netnamespace1

#创建新的network namespace,并在新的namespace中启动新的bash
dev@ubuntu:~$ sudo unshare --net bash
#查看新的namespace ID
root@ubuntu:~# readlink /proc/$$/ns/net
net:[4026532448]

#bind当前bash的namespace文件到上面创建的文件上
root@ubuntu:~# mount --bind /proc/$$/ns/net /var/run/netns/netnamespace1
#通过ls -i命令可以看到文件netnamespace1的inode号和namespace的编号相同,说明绑定成功
root@ubuntu:~# ls -i /var/run/netns/netnamespace1
4026532448 /var/run/netns/netnamespace1

#退出新创建的bash
root@ubuntu:~# exit
exit
#可以看出netnamespace1的inode没变,说明我们使用了bind mount后
#虽然新的namespace中已经没有进程了,但这个新的namespace还存在
dev@ubuntu:~$ ls -i /var/run/netns/netnamespace1
4026532448 /var/run/netns/netnamespace1

#上面的这一系列操作等同于执行了命令: ip netns add netnamespace1
#下面的nsenter命令等同于执行了命令: ip netns exec netnamespace1 bash

#我们可以通过nsenter命令再创建一个新的bash,并将它加入netnamespace1所关联的namespace(net:[4026532448])
dev@ubuntu:~$ sudo nsenter --net=/var/run/netns/netnamespace1 bash
root@ubuntu:~# readlink /proc/$$/ns/net
net:[4026532448]

从上面可以看出,给namespace取名字其实就是创建一个文件,然后通过mount --bind将新创建的namespace文件和该文件绑定,就算该namespace里的所有进程都退出了,内核还是会保留该namespace,以后我们还可以通过这个绑定的文件来加入该namespace。

本小节内容来源:Linux Namespace系列(06):network namespace (CLONE_NEWNET)

User namespace

Linux Namespace系列(07):user namespace (CLONE_NEWUSER) (第一部分)

Linux Namespace系列(08):user namespace (CLONE_NEWUSER) (第二部分)

总结

Docker容器这个听起来玄而又玄的概念,实际上是在创建容器进程时,指定了这个进程所需要启用的一组Namespace参数。这样,容器就只能“看”到当前Namespace所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。

Namespace技术实际上修改了应用进程看待整个计算机“视图”,即它的“视线”被操作系统做了限制,只能“看到”某些指定的内容。但对于宿主机来说,这些被“隔离”了的进程跟其他进程并没有太大区别。

默认 主要 成功 信息 警告 危险

参考资料

Linux Namespace : 简介

Linux Namespace和Cgroup系列教程

Docker资源隔离和限制实现原理

DOCKER基础技术:LINUX NAMESPACE(上)

  • 本文作者: wumingx
  • 本文链接: https://www.wumingx.com/k8s/docker-namespace.html
  • 本文主题: docker进阶教程一:namespace之障眼法
  • 版权声明: 本博客所有文章除特别声明外,转载请注明出处!如有侵权,请联系我删除。
0%