Namespace 是由 Linux 内核提供的一种特性,它能够将一些系统资源包装到一个抽象的空间中,并使得该空间中的进程以为这些资源是系统中仅有的资源。Namespace 是构建容器技术的基石,它使得容器内的进程只能看到容器内的进程和资源,实现与宿主系统以及其他容器的进程和资源隔离。
Namespace 按操作的系统资源不同有很多种类,比如 cgroup namespace,mount namespace 等等,接下来我们仅以 pid namespace 为例,以 runC
作为容器运行时实现,来演示当我们执行对容器的操作时,namespace 是如何工作的。
在上一篇文章中我们已经介绍过,绝大部分容器系统都使用 runC
作为底层的运行时实现,如果你是在 Linux 发行版系统中使用 docker
,甚至不需要专门安装就能使用 runc
命令。
runC
只能从 filesystem bundle
中执行容器(filesystem bundle
顾名思义就是一个满足特定结构的文件夹),但是我们可以使用 docker
来准备一个可用的 bundle
:
1
2
3
4
5
6
7
8
9
10
11
12
| # 创建 bundle 的顶层目录
$ mkdir /mycontainer
$ cd /mycontainer
# 创建用于存放 root filesystem 的 rootfs 目录
$ mkdir rootfs
# 利用 Docker 导出 busybox 容器的 root filesystem
$ docker export $(docker create busybox) | tar -C rootfs -xvf -
# 创建一个 config.json 作为整个 bundle 的 spec
$ runc spec
|
此时整个 bundle
的目录结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| $ tree -L 2 /mycontainer
/mycontainer
├── config.json
└── rootfs
├── bin
├── dev
├── etc
├── home
├── proc
├── root
├── sys
├── tmp
├── usr
└── var
|
为了完成演示,我们需要一些第三方的系统监测工具作为辅助:
监测进程的启动以获得容器中运行进程的 PID,如 ubuntu 中的 forkstat
,它可以实时地监测 fork()
, exec()
和 exit()
等系统调用,安装方式如下:
查看 namespace 信息,如 [cinf](https://github.com/mhausenblas/cinf)
,它是一个能够方便地列出系统中所有 namespace 或查看某个 namespce 详细信息的命令行工具,安装方式如下:
1
2
3
4
5
| $ curl -s -L https://github.com/mhausenblas/cinf/releases/latest/download/cinf_linux_amd64.tar.gz \
-o cinf.tar.gz && \
tar xvzf cinf.tar.gz cinf && \
mv cinf /usr/local/bin && \
rm cinf*
|
首先我们需要在一个窗口中运行 forkstat
:
接着另外新建一个终端窗口,切换到 /mycontainer
目录,使用 runC
运行容器:
执行后会直接进入到新创建的容器中,运行 ps
命令:
1
2
3
| PID USER TIME COMMAND
1 root 0:00 sh
7 root 0:00 ps
|
forkstat
窗口将会有以下输出:
1
2
3
4
5
6
7
8
| Time Event PID Info Duration Process
12:35:22 exec 33040 runc run mybox
12:35:22 exec 33047 runc init
12:35:22 exec 33049 dumpe2fs -h /dev/sdb3
12:35:22 exec 33050 dumpe2fs -h /dev/sdb3
12:35:22 exec 33047 runc init
12:35:22 exec 33052 sh
12:35:37 exec 33062 ps
|
从同步打印的结果可以判断, ps
和 forkstat
所分别输出的 sh
或 ps
实际上是同一个进程,但由于容器中的进程位于一个单独的 pid namespace 中,它们在容器中拥有另外的 PID,而且它们以为自己是容器中唯一存在的进程,因此 PID 会从 1 开始。
现在来找出容器所使用的 pid namespace,为此需要调整一下 ps
命令的输出格式:
1
2
3
| $ ps -p 33052 -o pid,pidns
PID PIDNS
33052 4026532395
|
PIDNS 即 pid namespace,以上命令可得到 PID 为 33052 的 sh
进程属于 4026532395 这个 pid namespace。既然已经有了容器中进程的 PID,实际上我们可以通过宿主机的 /proc
文件系统获得该进程所属的所有 namespace:
1
2
3
4
5
6
7
8
9
10
11
| $ ll /proc/33052/ns
lrwxrwxrwx 1 root root 0 7月 21 12:37 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 7月 21 12:36 ipc -> 'ipc:[4026532394]'
lrwxrwxrwx 1 root root 0 7月 21 12:36 mnt -> 'mnt:[4026532383]'
lrwxrwxrwx 1 root root 0 7月 21 12:36 net -> 'net:[4026532397]'
lrwxrwxrwx 1 root root 0 7月 21 12:36 pid -> 'pid:[4026532395]'
lrwxrwxrwx 1 root root 0 7月 21 12:37 pid_for_children -> 'pid:[4026532395]'
lrwxrwxrwx 1 root root 0 7月 21 12:37 time -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 7月 21 12:37 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 7月 21 12:36 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 7月 21 12:36 uts -> 'uts:[4026532393]'
|
打印结果展示了一个进程所属的 namespace:
- 每个 namespace 都是一个软链接,软链接的名称指示了 namespace 的类型,如 cgroup 表示 cgroup namespace, pid 表示 pid namespace。
- 每个软链接指向该进程所属的真正 namespace 对象,该对象用
inode
号码表示,每个 inode
号码在宿主系统中都是唯一的。 - 如果有两个进程的同一类型 namespace 软链接都指向同一个
inode
,说明他们属于同一个 namespace。
实际上所有的进程都会属于至少一个 namespace,Linux 系统在启动时就会为所有类型创建一个默认的 namespace 供进程使用。
我们也可以尝试在容器内获得 sh
所属的 namespace,此时需要在容器内使用 1 这个 PID:
1
2
3
4
5
6
7
8
9
10
11
| $ ls -l /proc/1/ns
lrwxrwxrwx 1 root root 0 Jul 21 04:37 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Jul 21 04:37 ipc -> ipc:[4026532394]
lrwxrwxrwx 1 root root 0 Jul 21 04:37 mnt -> mnt:[4026532383]
lrwxrwxrwx 1 root root 0 Jul 21 04:37 net -> net:[4026532397]
lrwxrwxrwx 1 root root 0 Jul 21 04:37 pid -> pid:[4026532395]
lrwxrwxrwx 1 root root 0 Jul 21 04:37 pid_for_children -> pid:[4026532395]
lrwxrwxrwx 1 root root 0 Jul 21 04:37 time -> time:[4026531834]
lrwxrwxrwx 1 root root 0 Jul 21 04:37 time_for_children -> time:[4026531834]
lrwxrwxrwx 1 root root 0 Jul 21 04:37 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Jul 21 04:37 uts -> uts:[4026532393]
|
下面我们将从 namespace 的角度,来观测 pid namespace 中的所有进程。Linux 系统并未提供类似的功能,因此需要借助上文安装的 cinf
工具来实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
| $ cinf -namespace 4026532395
PID PPID NAME CMD NTHREADS CGROUPS STATE
33052 33052 sh sh 1 12:devices:/user.slice/mybox S (sleeping)
11:blkio:/user.slice/mybox 10:rdma:/
9:memory:/user.slice/user-0.slice/session-590.scope/mybox
8:net_cls,net_prio:/mybox 7:freezer:/mybox
6:pids:/user.slice/user-0.slice/session-590.scope/mybox
5:cpu,cpuacct:/user.slice/mybox 4:cpuset:/mybox
3:perf_event:/mybox 2:hugetlb:/mybox
1:name=systemd:/user.slice/user-0.slice/session-590.scope/mybox
0::/user.slice/user-0.slice/session-590.scope
|
目前这个 namespace 中只有一个进程,这个进程也是我们所创建容器的 init
进程。当一个新的容器被创建时,系统将创建一些新的 namespace,容器的 init
进程将被加入到这些 namespace。
对于 pid namespace 来说,容器中运行的所有进程只能看到位于同一 pid namespace 即 pid:[4026532395]
中的其他进程。sh
进程在容器中被认为是系统运行的第一个进程,PID 为 1,但在宿主机中只是一个 PID 为 33052 的普通进程,同一个进程在不同 namespace 中拥有不同的 PID,这就是 pid namespace 的作用。某种程度上,容器就意味着一个新的 namespace 集合。
创建一个新的终端窗口,在已运行的容器中运行一个新的进程:
1
| $ runc exec mybox /bin/top -b
|
从 forkstat
窗口中,我们可以看到新创建进程的 PID:
1
2
3
4
5
| Time Event PID Info Duration Process
12:40:23 exec 33132 runc exec mybox /bin/top -b
12:40:23 exec 33140 runc init
12:40:23 exec 33140 runc init
12:40:23 exec 33142 /bin/top -b
|
实际上还有更直接的方式从宿主机中查看容器中运行的进程,我们可以使用 runC
提供的 ps
子命令:
1
2
3
4
| $ runc ps mybox
UID PID PPID C STIME TTY TIME CMD
root 33052 33040 0 12:35 pts/0 00:00:00 sh
root 33142 33132 0 12:40 pts/1 00:00:00 /bin/top -b
|
接下来依然使用 cinf
来找出新创建进程所属的 namespace:
1
2
3
4
5
6
7
8
9
10
| $ cinf --pid 33142
NAMESPACE TYPE
4026532383 mnt
4026532393 uts
4026532394 ipc
4026532395 pid
4026532397 net
4026531837 user
|
从结果来看,并没有新的命名空间被创建,32608 进程的 namespace 和 mybox 容器的 init
进程- sh
所属的 namespace 是完全相同的。也就是说,在容器中创建一个新的进程,只是将这个进程加入到了容器 init
进程所属的 namespace。
下面来列出 4026532395 namespace 所拥有的所有进程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| $ cinf --namespace 4026532395
PID PPID NAME CMD NTHREADS CGROUPS STATE
33052 33040 sh sh 1 12:devices:/user.slice/mybox S (sleeping)
11:blkio:/user.slice/mybox 10:rdma:/
9:memory:/user.slice/user-0.slice/session-590.scope/mybox
8:net_cls,net_prio:/mybox 7:freezer:/mybox
6:pids:/user.slice/user-0.slice/session-590.scope/mybox
5:cpu,cpuacct:/user.slice/mybox 4:cpuset:/mybox
3:perf_event:/mybox 2:hugetlb:/mybox
1:name=systemd:/user.slice/user-0.slice/session-590.scope/mybox
0::/user.slice/user-0.slice/session-590.scope
33142 33132 top top -b 1 12:devices:/user.slice/mybox S (sleeping)
11:blkio:/user.slice/mybox 10:rdma:/
9:memory:/user.slice/user-0.slice/session-590.scope/mybox
8:net_cls,net_prio:/mybox 7:freezer:/mybox
6:pids:/user.slice/user-0.slice/session-590.scope/mybox
5:cpu,cpuacct:/user.slice/mybox 4:cpuset:/mybox
3:perf_event:/mybox 2:hugetlb:/mybox
1:name=systemd:/user.slice/user-0.slice/session-590.scope/mybox
0::/user.slice/user-0.slice/session-590.scope
|
如果在容器内运行 ps -ef
,我们也能看到这些进程,由于 pid namespace 的原因它们的 PID 将会有所不同:
1
2
3
4
| PID USER TIME COMMAND
1 root 0:00 sh
19 root 0:00 top -b
20 root 0:00 ps -ef
|
现在我们知道,docker/runc exec
实际上就是在已创建容器的 namespace 中运行一个新的进程。
运行一个容器时,将创建一些新的 namespace, init
进程将被加入到这些 namespace;在一个容器中运行一个新进程时,新进程将加入创建容器时所创建的 namespace。
实际上创建容器时新建 namespace 这种行为是可以改变的,我们可以指定新建的容器使用已有的 namespace。