iT邦幫忙

2022 iThome 鐵人賽

DAY 15
1
DevOps

那些關於 docker 你知道與不知道的事系列 第 15

Day 15: docker exec 是怎麼辦到的呢?

  • 分享至 

  • xImage
  •  

有用過 docker 的人相信對 docker exec 這個指令並不陌生,這個指令可以讓我們「進入」一個 container,但 container 不是已經透過各種新建立的 namespaces 達成了隔離,那 docker exec 是怎麼做到的呢?今天就讓我們試著來討論看看這個問題。


在示範之前,我們先整理一下昨天我們用 alpine image 啟動的那個 container,我們參數用的是 -it,執行的命令是 ash,也就是用 ash 啟動一個 process,這個新的 container id 是 ffb4953e6750,這個 ash process 在 Host 的 PID 是 58127。

在 container 中:

$ docker run -it alpine ash
/ # sleep 2000 &
/ # ps -eaf
PID   USER     TIME  COMMAND
    1 root      0:00 ash
    7 root      0:00 sleep 2000
    8 root      0:00 ps -eaf
    
/ # lsns
        NS TYPE   NPROCS PID USER COMMAND
4026531834 time        3   1 root ash
4026531835 cgroup      3   1 root ash
4026531837 user        3   1 root ash
4026532276 mnt         3   1 root ash
4026532277 uts         3   1 root ash
4026532278 ipc         3   1 root ash
4026532279 pid         3   1 root ash
4026532280 net         3   1 root ash

在 Host:

$ docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED         STATUS         PORTS     NAMES
ffb4953e6750   alpine    "ash"     6 minutes ago   Up 6 minutes             fervent_albattani

$ ps -eaf
...略
root       58127   58107  0 14:13 pts/0    00:00:00 ash
root       58155   58127  0 14:13 pts/0    00:00:00 sleep 2000

這邊先示範一下 docker exec 的用法,他的參數跟 docker run 其實非常地像,只是將 image id 換成 container id:

$ docker exec -it ffb4953e6750 ash
/ # ps -eaf
PID   USER     TIME  COMMAND
    1 root      0:00 ash
    7 root      0:00 [sleep]
   24 root      0:00 ash
   29 root      0:00 ps -eaf
/ # lsns
        NS TYPE   NPROCS PID USER COMMAND
4026531834 time        3   1 root ash
4026531835 cgroup      3   1 root ash
4026531837 user        4   1 root ash
4026532276 mnt         3   1 root ash
4026532277 uts         3   1 root ash
4026532278 ipc         3   1 root ash
4026532279 pid         4   1 root ash
4026532280 net         3   1 root ash

嘿,我們是不是成功地進入了 ffb4953e6750 這個 container 中了呢,而且利用 lsns 也看到了一模一樣的 namespace。這邊也可以觀察到,docker exec 指令最後的 COMMAND ash,其實就是這次要啟動的 process,而這個 process 也出現在 container 裡了(PID 24)。

那到底這是怎麼辦到呢?


我們之前看過 namespaces man page ,裡頭有列出一些關於 namespace 的 API:
https://ithelp.ithome.com.tw/upload/images/20220929/20151857nIVHazGAEG.png

當時我被他的 unshare 指令給吸引到(見Day 10),後來我們也用了 clone 去建立新的 namespace,除了這兩個 API 之外,他還有一個 API 是 setns

The setns(2) system call allows the calling process to join an existing namespace. The namespace to join is specified via a file descriptor that refers to one of the /proc/[pid]/ns files described below.

看完這個描述,是不是開始有感覺了呢?setns 可以讓我們把一個 process 透過指定 namespace 的 file descriptor 加入目前已經存在的 namespace,而我們昨天討論過了,這些 file descriptor 就存在 /proc/[pid]/ns 中。

讓我們來試試看吧,這次我們來試 Network Namespace 好了,先到 container 裡去查看一下網路資訊:

/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
61: eth0@if62: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
    link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

在 Host 也查看一下:
https://ithelp.ithome.com.tw/upload/images/20220929/20151857cn3ywgKxZg.png

很明顯,差異很大。

再來我們根據 setns man page 裡提供的範例,稍微做點修改以進行測試:

#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

char* const container_args[] = {
  "/bin/bash",
  NULL
};

int main(int argc, char *argv[])
{
  int fd = open(argv[1], O_RDONLY | O_CLOEXEC);
  if(fd == -1) {
    printf("Failed to open!\n");
    return 1;
  }

  if (setns(fd, 0) == -1) {
    printf("setns failed!\n");
  }

  execvp(container_args[0], container_args);

  return 0;
}

在這個程式中,我會透過參數傳入想要加入的 namespace 的 file descriptor,程式中會試著開啟這個 file descriptor,開啟成功的話,就會透過 setns 來設定這個 namespace,最後我們執行一個 /bin/bash 的 process,到時候就可以在這個 bash 中,檢查看看 Network namespace 的情況。

$ gcc -o setns setns.c

# 58127 是 container 中 PID 1 的 process 在 Host 中的 PID,所以其資料在 /proc/58127 中
ubuntu@ip-xxx:~/setns-test$ sudo ./setns /proc/58127/ns/net
root@ip-xxx:/home/ubuntu/setns-test#  ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
61: eth0@if62: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

怎麼樣,是不是跟在 container 中看到的一模一樣呢?

再讓我們去 /proc/self/ns 看看:

root@ip-xxx:/home/ubuntu/setns-test# ls -al /proc/self/ns
total 0
dr-x--x--x 2 root root 0 Sep 29 15:40 .
dr-xr-xr-x 9 root root 0 Sep 29 15:40 ..
lrwxrwxrwx 1 root root 0 Sep 29 15:40 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Sep 29 15:40 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 Sep 29 15:40 mnt -> 'mnt:[4026531841]'
lrwxrwxrwx 1 root root 0 Sep 29 15:40 net -> 'net:[4026532280]'
lrwxrwxrwx 1 root root 0 Sep 29 15:40 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Sep 29 15:40 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Sep 29 15:40 time -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Sep 29 15:40 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Sep 29 15:40 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Sep 29 15:40 uts -> 'uts:[4026531838]'

整理幾個 namespace 出來看看:
https://ithelp.ithome.com.tw/upload/images/20220929/20151857hDp5kQpNN3.png

實驗做出來的這個 bash 的 namespaces 中,只有 Network namespace 跟 container 是一樣的,因為我們在 setns 時,也是只有設定加入 /proc/58127/ns/net,其他 namespace 自然就都跟其 parent namespace 一樣了。


這樣應該可以推想到,docker exec 指令最後的 COMMAND,例如這邊的 ash,用這個 COMMAND 啟動的這個新的 process,會透過 setns 加入為這個 container 所建立出來的各種新的 namespace 裡,讓我們看起來像是「進入」了 container 裡一樣。如果以加入 namespace 這個角度來說,的確是「進入」,另外一個角度則是,就是讓這個新的 ash process 與在 container 中的那些 processes 看到一樣的世界,一樣去被「欺騙」了。

很好玩吧~ (似乎每個實驗結束都要問一次好不好玩,沒辦法,好玩對我來說太重要了,希望你們也能覺得有趣!)


Day 13 一樣,留個小實驗給大家自己動手做一下,如果利用剛剛的程式加入的 namespace 是 PID namespace 的話:

  1. 那新啟動的這個 /bin/bash 可以在 container 中被看到嗎?
  2. 在新啟動的這個 /bin/bash 裡,執行 sleep 3000 &,那這個 sleep process 可以在 container 中被看到嗎?

以上兩題的答案是什麼?又是為什麼呢?


上一篇
Day 14: container 與 namespace
下一篇
Day 16: process 的族譜
系列文
那些關於 docker 你知道與不知道的事32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言