docker exec 是怎么做到进入容器里的呢?
Linux Namespace 创建的隔离空间虽然看不见摸不着,但一个进程的 Namespace 信息在宿主机上是确确实实存在的,并且是以一个文件的方式存在。
通过如下指令,可以看到当前正在运行的 Docker 容器的进程号 :
$ docker inspect --format '{{ .State.Pid }}' 4ddf4638572d
25686
可以通过查看宿主机的 proc 文件,看到 25686 进程的所有 Namespace 对应的文件:
进程的每种 Linux Namespace,都在它对应的 /proc/[进程号]/ns 下有一个对应的虚拟文件,并且链接到一个真实的 Namespace 文件上。
这意味着:一个进程,可以选择加入到某个进程已有的 Namespace 当中,从而达到“进入”这个进程所在容器的目的,这正是 docker exec 的实现原理。
而这个操作所依赖的,是一个名叫 setns() 的 Linux 系统调用。
用如下一段小程序说明调用方式
#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE);} while (0)
int main(int argc, char *argv[]) {
int fd;
fd = open(argv[1], O_RDONLY);
if (setns(fd, 0) == -1) {
errExit("setns");
}
execvp(argv[2], &argv[2]);
errExit("execvp");
}
它接收两个参数,第一个参数是当前进程要加入的 Namespace 文件的路径,比如 /proc/25686/ns/net;第二个参数是你要在这个 Namespace 里运行的进程,比如 /bin/bash。
这段代码的的核心操作,则是通过 open() 系统调用打开了指定的 Namespace 文件,并把这个文件的描述符 fd 交给 setns() 使用。在 setns() 执行后,当前进程就加入了这个文件对应的 Linux Namespace 中。
编译执行这个程序,加入到容器进程(PID=25686)的 Network Namespace 中:
执行 ifconfig 查看网络设备时会发现网卡“变少”了,在 setns() 之后看到的这两个网卡是我在前面启动的容器里的网卡。新创建的 /bin/bash 进程,由于加入了该容器进程的 Network Namepace,它看到的网络设备与容器里是一样的,即:/bin/bash 进程的网络设备视图,也被修改了。
而一旦一个进程加入到了另一个 Namespace 当中,在宿主机的 Namespace 文件上,也会有所体现。
在宿主机上用 ps 指令找到这个 set_ns 程序执行的 /bin/bash 进程,其真实的 PID 是 28499:
# 在宿主机上
ps aux | grep /bin/bash
root 28499 0.0 0.0 19944 3612 pts/0 S 14:15 0:00 /bin/bash
这时,如果按照前面介绍过的方法,查看一下这个 PID=28499 的进程的 Namespace,你就会发现这样一个事实:
$ ls -l /proc/28499/ns/net
lrwxrwxrwx 1 root root 0 Aug 13 14:18 /proc/28499/ns/net -> net:[4026532281]
$ ls -l /proc/25686/ns/net
lrwxrwxrwx 1 root root 0 Aug 13 14:05 /proc/25686/ns/net -> net:[4026532281]
这两个进程共享了这个名叫 net:[4026532281] 的 Network Namespace。