在使用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/
(全文完)
參考: