深入篇(4):docker exec 底层实现

292 阅读1分钟

docker exec 是怎么做到进入容器里的呢?

Linux Namespace 创建的隔离空间虽然看不见摸不着,但一个进程的 Namespace 信息在宿主机上是确确实实存在的,并且是以一个文件的方式存在。

通过如下指令,可以看到当前正在运行的 Docker 容器的进程号 :

$ docker inspect --format '{{ .State.Pid }}'  4ddf4638572d

25686

可以通过查看宿主机的 proc 文件,看到 25686 进程的所有 Namespace 对应的文件:

image.png

进程的每种 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 中:

image.png

执行 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。