• docker exec实现原理


    使用Docker部署应用以及容器数据卷Volume中,已经了解了Docker的基本操作。其中有一个很神奇的操作,即docker exec,这个命令允许我们从外部进入一个容器中。本文主要剖析这个命令背后的原理,借此回顾Linux Namespace的一些实现原理。

    (1)通过如下命令启动一个容器

    root@ubuntu:~# docker run -d kkbill/helloworld:v1.0
    664562841f30f29c04577763e09ac6db393afde9bf435c5d30c11ce654e8eb8b
    

    可以看到,该容器正在正常运行

    root@ubuntu:~# docker ps
    CONTAINER ID        IMAGE                  COMMAND        ...   PORTS          NAMES
    664562841f30  kkbill/helloworld:v1.0   "python app.py"    ...   80/tcp       zen_mahavira
    

    (2)可以通过如下指令得到当前容器进程对应的 PID

    root@ubuntu:~# docker inspect 664562841f30
    [
        {
            ...
            "State": {
                ...
                "Pid": 10444,
                ...
            },
    ...
    

    或者,加上--format参数:

    root@ubuntu:~# docker inspect --format '{{ .State.Pid }}' 664562841f30
    10444
    

    (3)这时,可以通过查看宿主机的 proc 文件,看到这个 10444 进程的所有 Namespace 对应的文件:

    root@ubuntu:~# ls -l /proc/10444/ns
    total 0
    lrwxrwxrwx 1 root root 0 May 17 16:05 cgroup -> 'cgroup:[4026531835]'
    lrwxrwxrwx 1 root root 0 May 17 16:05 ipc -> 'ipc:[4026532219]'
    lrwxrwxrwx 1 root root 0 May 17 16:05 mnt -> 'mnt:[4026532217]'
    lrwxrwxrwx 1 root root 0 May 17 15:52 net -> 'net:[4026532222]'
    lrwxrwxrwx 1 root root 0 May 17 16:05 pid -> 'pid:[4026532220]'
    lrwxrwxrwx 1 root root 0 May 17 16:05 pid_for_children -> 'pid:[4026532220]'
    lrwxrwxrwx 1 root root 0 May 17 16:05 user -> 'user:[4026531837]'
    lrwxrwxrwx 1 root root 0 May 17 16:05 uts -> 'uts:[4026532218]'
    

    可以看到,一个进程的每种 Linux Namespace,都在它对应的 /proc/[PID]/ns 下有一个对应的虚拟文件,并且链接到一个真实的 Namespace 文件上。

    现在看来,Namespace 不再是虚无缥缈的概念,而是一个个实实在在存在的文件。前面所说的“进入”一个容器,应该就是对这些文件做一些操作。在 Linux 的系统调用中,有一个setns()函数,可以实现这样的功能。

    该系统调用的说明如下:

    int setns(int fd, int nstype);
    
    DESCRIPTION:
    Given a file descriptor referring to a namespace, reassociate the calling thread with that namespace.
    The <fd> argument is a file descriptor referring to one of the namespace entries in a  /proc/[pid]/ns/ directory; The calling thread will be reassociated with the corresponding namespace, subject to any constraints imposed by the nstype argument.
    The  <nstype>  argument specifies which type of namespace the calling thread may be reassociated with. nstype = 0 means allow any type of namespace to be joined.
    

    setns()系统调用的作用就是,把调用该函数的进程关联到指定的namespace中,确切的说就是关联到指定的 /proc/[pid]/ns/目录中。

    下面以一小段代码进行说明:

    #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");
    }
    

    这段代码功能非常简单:它一共接收两个参数,第一个参数是 argv[1],即当前进程(calling thread)要加入的 Namespace 文件的路径,比如 /proc/10444/ns/net;而第二个参数,则是你要在这个 Namespace 里运行的进程,比如 /bin/bash。

    这段代码的核心操作,则是通过 open() 系统调用打开了指定的 Namespace 文件,并把这个文件的描述符 fd 交给 setns() 使用。在setns() 执行后,当前进程就加入了这个文件对应的 Linux Namespace 当中了。

    现在,你可以编译执行一下这个程序,加入到容器进程(PID=10444)的 Network Namespace 中:

    root@ubuntu:~/work/container# gcc -o set_ns set_ns.c 
    root@ubuntu:~/work/container# ./set_ns /proc/10444/ns/net /bin/bash
    root@ubuntu:~/work/container# ifconfig
    eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
            inet 172.18.0.2  netmask 255.255.0.0  broadcast 172.18.255.255
            ether 02:42:ac:12:00:02  txqueuelen 0  (Ethernet)
            RX packets 0  bytes 0 (0.0 B)
            RX errors 0  dropped 0  overruns 0  frame 0
            TX packets 0  bytes 0 (0.0 B)
            TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
    
    lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
            inet 127.0.0.1  netmask 255.0.0.0
            loop  txqueuelen 1000  (Local Loopback)
            RX packets 0  bytes 0 (0.0 B)
            RX errors 0  dropped 0  overruns 0  frame 0
            TX packets 0  bytes 0 (0.0 B)
            TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
    

    如上所示,当执行 ifconfig 命令查看网络设备时,发现只有 2 个,而我的宿主机上其实有4个网络设备。

    实际上,在 setns() 之后我看到的这两个网卡,正是我在前面启动的 Docker 容器里的网卡。也就是说,新创建的这个 /bin/bash 进程,由于加入了该容器进程(PID=10444)的 Network Namepace,它看到的网络设备与这个容器里是一样的,即:/bin/bash 进程的网络设备视图,也被修改了

    一旦一个进程加入到了另一个 Namespace 当中,在宿主机的 Namespace 文件上,也会有所体现。

    在宿主机上,通过 ps 命令找到这个 set_ns 程序对应的 PID,其值为 10667:

    root@ubuntu:~# ps aux | grep /bin/bash
    root     10667  0.0  0.1  21604  3960 pts/0    S+   16:24   0:00 /bin/bash
    

    按照前面的方法,查看一下 PID = 10667 这个进程对应的Network Namespace,会发现:

    // 外部程序,即执行 set_ns 的进程 
    root@ubuntu:~# ls -l /proc/10667/ns/net 
    lrwxrwxrwx 1 root root 0 May 17 16:37 /proc/10667/ns/net -> 'net:[4026532222]'
    // 容器进程
    root@ubuntu:~# ls -l /proc/10444/ns/net 
    lrwxrwxrwx 1 root root 0 May 17 15:52 /proc/10444/ns/net -> 'net:[4026532222]'
    

    可以看到,这两者指向的Network Namespace文件是一致的,也就是说,这两个进程,共享了这个名叫 net:[4026532222]的 Network Namespace。

    此外,Docker 还专门提供了一个参数,可以让你启动一个容器并“加入”到另一个容器的 Network Namespace 里,这个参数就是 -net,比如:

    root@ubuntu:~# docker run -ti --net container:664562841f30 busybox ifconfig
    

    新启动的这个容器,就会直接加入到 ID=664562841f30的容器,也就是我们前面的创建的 Python 应用容器(PID=10444)的 Network Namespace 中。所以,这里 ifconfig 返回的网卡信息,跟我前面那个程序返回的结果一模一样。

    而如果我指定–net=host,就意味着这个容器不会为进程启用 Network Namespace。这就意味着,这个容器拆除了 Network Namespace 的“隔离墙”,所以,它会和宿主机上的其他普通进程一样,直接共享宿主机的网络栈。这就为容器直接操作和使用宿主机网络提供了一个渠道。

    -net 参数实际讲的就是容器网络模型相关的,容器的网络模型有4种,分别是none模式,container模式,host模式和bridge模式,默认使用的是bridge模式,关于这一主题已经在容器网络实现中讨论过了。

    另外,docker exec 更详细的使用可参考:https://docs.docker.com/engine/reference/commandline/exec/

    (全文完)


    参考:

    1. 极客时间专栏:https://time.geekbang.org/column/article/18119
  • 相关阅读:
    用javascript实现简单的用户登录验证
    JS创建数组的三种方法
    JS中的数据类型
    原始值和引用值
    html为什么用雪碧图的优缺点
    html,将元素水平,垂直居中的四种方式
    使用display:none和visibility:hidden隐藏的区别
    jsonview注解、RequestBody 、拦截
    02.实现图形验证码
    spring注解使用
  • 原文地址:https://www.cnblogs.com/kkbill/p/12950757.html
Copyright © 2020-2023  润新知