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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM