如題,為什麼在討論 shell mode 的時候要一直提醒我們實驗的 base image 是 Ubuntu 呢?如果換成 alpine 會怎麼樣呢?
現在先來複習一下: 如果用 shell mode 的方式啟動 container,那會透過 /bin/sh -c 去執行 command,當我們透過 docker ps 查看時,會看到 "/bin/sh -c 'sleep 1…",container 中的 PID 1 會是 /bin/sh -c ...,不會是 sleep。在昨天的討論,也發現 /bin/sh
不會去處理 SIGTERM
訊號,所以即便我們的 script 中有捕捉且處理 SIGTERM
訊號,用 shell mode 的方式,仍然要等上十秒鐘。
現在就讓我們改用 alpine 來做實驗看看:
Dockerfile-trap-shell-alpine:
From alpine:latest
WORKDIR /app
COPY trap-ash.sh trap.sh
CMD /app/trap.sh
跟昨天的差別是,我們的 base image 改成用 alpine:latest
,我們把這個 Dockerfile build 成 trapshell:alpine
image,來啟動且觀察看看:
ubuntu@ip-xxx:~/signal$ docker run -d trapshell:alpine
a6437783b39dc2cd62ec03104e8923e6649390b0bbb091b1d1cbcc329a7ce557
ubuntu@ip-xxx:~/signal$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a6437783b39d trapshell:alpine "/bin/sh -c /app/tra…" ...略
ubuntu@ip-ㄌ:~/signal$ docker top a6437783b39d -o pid,ppid,args
PID PPID COMMAND
105652 105630 /bin/ash /app/trap.sh
105711 105652 sleep 1
ubuntu@ip-xxx:~/signal$ docker stop a6437783b39d
a6437783b39d
跟 Ubuntu 有什麼不一樣呢?在 shell mode,透過 docker ps
可以看到 command 仍是透過 /bin/sh -c
去執行,這跟 Ubuntu 那邊沒有什麼不一樣,但如果查看 container 中的 processes,會發現 PID 1 的 process 不再是 /bin/sh -c
了,而是我們的 trap.sh 了。所以當我們執行 docker stop
時,SIGTERM
訊號會被送到我們的 trap.sh 去,這個 container 也會很快地被關閉,不需要等上 10 秒鐘。
大家可以試試看其他的 image 看看結果如何,網路上有些文章在討論這邊的時候,如果只有用 Ubuntu 來做實驗,可能就會有一個稍微有點偏差的結論,會認為只要是用 shell mode 就一定會有沒有處理 SIGTERM
的情況發生,但透過我們上述的實驗知道,至少 alpine 是不會有這樣的情況的。
那到底是什麼情況造成這樣的差異呢?明明在 shell mode,不管是 ubuntu 或是 alpine,都是用 /bin/sh -c
去執行命令的,看來就是這裡有差異了,那讓我們來看一下在 Ubuntu 跟 apline 中的 /bin/sh
到底是誰:
ubuntu@ip-xxx:~/signal$ docker run --rm ubuntu:20.04 ls -al /bin/sh
lrwxrwxrwx 1 root root 4 Jul 18 2019 /bin/sh -> dash
ubuntu@ip-xxx:~/signal$ docker run --rm alpine ls -al /bin/sh
lrwxrwxrwx 1 root root 12 Aug 9 08:47 /bin/sh -> /bin/busybox
在 ubuntu 裡,/bin/sh
是連結到 dash
,而 alpine 裡是連結到 /bin/busybox
,那 dash
怎麼了嗎?
再來做一個實驗,我們用 ubuntu:20.04
啟動一個 container,在這個 container 裡分別作以下動作:
bash -c
來啟動一個 process,並且用 ps -eaf
觀察一下root@65fb79724581:/# bash -c "sleep 1000"
用另外一個 terminal 執行 docker exec
進入這個 container 觀察:
root@65fb79724581:/# ps -eaf
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 13:19 pts/0 00:00:00 /bin/bash
root 15 0 0 13:19 pts/1 00:00:00 /bin/bash
root 28 1 0 13:23 pts/0 00:00:00 sleep 1000
root 29 15 0 13:24 pts/1 00:00:00 ps -eaf
dash -c
來啟動一個 process,並且用 ps -eaf
觀察一下root@65fb79724581:/# dash -c "sleep 2000"
用另外一個 terminal 執行 docker exec
進入這個 container 觀察:
root@65fb79724581:/# ps -eaf
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 13:19 pts/0 00:00:00 /bin/bash
root 15 0 0 13:19 pts/1 00:00:00 /bin/bash
root 32 1 0 13:24 pts/0 00:00:00 dash -c sleep 2000
root 33 32 0 13:24 pts/0 00:00:00 sleep 2000
root 34 15 0 13:24 pts/1 00:00:00 ps -eaf
登愣,果然是 dash
的坑!
關於這點我有查到一篇文章「你用sh -c ‘command‘时踩过坑吗?」,雖然不能完全理解這篇文章里提到的內容,但也試著去下載了 bash 的原始碼下來看看(下載處),我下載的是 bash-5.2.tar.gz 這個版本,在這個版本的程式碼裡頭搜尋 ONESHOT
,果然搜到文章中有提到的:
config-top.h
/* Define ONESHOT if you want sh -c 'command' to avoid forking to execute
`command' whenever possible. This is a big efficiency improvement. */
#define ONESHOT
看來這是一個 bash 的優化,可以讓我們在用 bash -c
執行程式時,避免掉 fork
這個動作,根據以上註解,會大幅地改善效能。
根據該文的分析,在 execute_disk_command
這個函式中(execute_cmd.c),如果判斷是不 fork,那就會執行 shell_execute
,這會去呼叫 execve
API,根據 man page:
execve() executes the program referred to by pathname. This causes the program that is currently being run by the calling process to be replaced with a new program, with newly initialized stack, heap, and (initialized and uninitialized) data segments.
看來 execve
這個 API 是會去起動一個程式,並且取代掉原本的 process,他在 NOTES 中還特別強調:
One sometimes sees execve() (and the related functions described in exec(3)) described as "executing a new process" (or similar). This is a highly misleading description: there is no new process; many attributes of the calling process remain unchanged (in particular, its PID). All that execve() does is arrange for an existing process (the calling process) to execute a new program.
如果是要 fork 那就會執行 make_child
(jobs.c),這裡頭會呼叫 fork
API,顯然就是會建立一個新的 process 出來。
經過這樣的分析,就可以理解為什麼用 bash -c "sleep 1000"
,會只留下 sleep 1000
了,如上所述,他會去呼叫 execve
來啟動 sleep 1000
,並且取代掉 bash -c
這個 process。
那 dash 就沒有做這樣的優化嗎?根據該篇文章所述,dash 早在 2011 年就做了一樣的優化,但 Ubuntu 裡的 dash 卻禁用了這個優化,所以在 ubuntu 上的 dash -c
還是用 fork 的方式來啟動程式。這也一連串導致了以 ubunut 為 base image 啟動出來的 container,若是以 shell mode 來執行 command,PID 1 會是 /bin/sh -c
的原因了。
這邊再補充一點,我們能不能殺掉 container 中的 PID 1 process?甚至,我們能不能殺掉 host 中的 PID 1 process?來看一下 kill 的 man page,裡頭有提到這段:
The only signals that can be sent to process ID 1, the init process, are those for which init has explicitly installed signal handlers. This is done to assure the system is not brought down accidentally.
看來是讓 init process 自己決定能不能被殺掉,我們來試試看 container 裡的:
A. exec mode:
ubuntu@ip-xxx:~$ docker run --rm -d trap:exec
b942f014e1932af3b01092e084c97162db485f880ab807ce90a90a1b81320a74
ubuntu@ip-xxx:~$ docker exec -it b942f014e1 /bin/bash
# 因為是 exec mode, PID 1 process 就是我們的 trap
root@b942f014e193:/app# ps -eaf
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 01:37 ? 00:00:00 /bin/bash /app/trap.sh
root 24 0 0 01:37 pts/0 00:00:00 /bin/bash
root 51 1 0 01:38 ? 00:00:00 sleep 1
root 52 24 0 01:38 pts/0 00:00:00 ps -eaf
# 建立一個新的 process
root@b942f014e193:/app# sleep 1000 &
[1] 56
root@b942f014e193:/app# ps -eaf
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 01:37 ? 00:00:00 /bin/bash /app/trap.sh
root 24 0 0 01:37 pts/0 00:00:00 /bin/bash
root 56 24 0 01:38 pts/0 00:00:00 sleep 1000
root 61 1 0 01:38 ? 00:00:00 sleep 1
root 62 24 0 01:38 pts/0 00:00:00 ps -eaf
# 嘗試殺掉 56
root@b942f014e193:/app# kill -9 56
# 56 有被殺掉
root@b942f014e193:/app# ps -eaf
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 01:37 ? 00:00:00 /bin/bash /app/trap.sh
root 24 0 0 01:37 pts/0 00:00:00 /bin/bash
root 68 1 0 01:38 ? 00:00:00 sleep 1
root 69 24 0 01:38 pts/0 00:00:00 ps -eaf
[1]+ Killed sleep 1000
# 嘗試殺掉 1
root@b942f014e193:/app# kill -9 1
# PID 1 process 仍然存在,沒有被殺掉
root@b942f014e193:/app# ps -eaf
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 01:37 ? 00:00:00 /bin/bash /app/trap.sh
root 24 0 0 01:37 pts/0 00:00:00 /bin/bash
root 77 1 0 01:38 ? 00:00:00 sleep 1
root 78 24 0 01:38 pts/0 00:00:00 ps -eaf
B. shell mode
ubuntu@ip-xxx:~$ docker run --rm -d trap:shell
466c8dcd3cb19ca4c52e5a9d74dd5aca0455058ecaca794d685c2f738dc9e6e6
ubuntu@ip-172-31-58-247:~$ docker exec -it 466c8dcd3cb19 /bin/bash
# 因為是 shell mode, PID 1 process 會是 /bin/sh,我們的 trap 在 PID 7
root@466c8dcd3cb1:/app# ps -eaf
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 01:41 ? 00:00:00 /bin/sh -c /app/trap.sh
root 7 1 0 01:41 ? 00:00:00 /bin/bash /app/trap.sh
root 17 0 0 01:41 pts/0 00:00:00 /bin/bash
root 36 7 0 01:41 ? 00:00:00 sleep 1
root 37 17 0 01:41 pts/0 00:00:00 ps -eaf
# 建立一個新的 process
root@466c8dcd3cb1:/app# sleep 1000 &
[1] 44
root@466c8dcd3cb1:/app# ps -eaf
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 01:41 ? 00:00:00 /bin/sh -c /app/trap.sh
root 7 1 0 01:41 ? 00:00:00 /bin/bash /app/trap.sh
root 17 0 0 01:41 pts/0 00:00:00 /bin/bash
root 44 17 0 01:41 pts/0 00:00:00 sleep 1000
root 48 7 0 01:42 ? 00:00:00 sleep 1
root 49 17 0 01:42 pts/0 00:00:00 ps -eaf
# 嘗試殺掉 44
root@466c8dcd3cb1:/app# kill -9 44
# 44 有被殺掉
root@466c8dcd3cb1:/app# ps -eaf
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 01:41 ? 00:00:00 /bin/sh -c /app/trap.sh
root 7 1 0 01:41 ? 00:00:00 /bin/bash /app/trap.sh
root 17 0 0 01:41 pts/0 00:00:00 /bin/bash
root 62 7 0 01:42 ? 00:00:00 sleep 1
root 63 17 0 01:42 pts/0 00:00:00 ps -eaf
[1]+ Killed sleep 1000
# 嘗試殺掉 1
root@466c8dcd3cb1:/app# kill -9 1
# PID 1 process 仍然存在,沒有被殺掉
root@466c8dcd3cb1:/app# ps -eaf
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 01:41 ? 00:00:00 /bin/sh -c /app/trap.sh
root 7 1 0 01:41 ? 00:00:00 /bin/bash /app/trap.sh
root 17 0 0 01:41 pts/0 00:00:00 /bin/bash
root 70 7 0 01:42 ? 00:00:00 sleep 1
root 71 17 0 01:42 pts/0 00:00:00 ps -eaf
# 那如果我們殺掉 PID 7 呢?
root@466c8dcd3cb1:/app# kill -9 7
# PID 7 可以成功被殺掉,也因此終止了整個 container...
root@466c8dcd3cb1:/app# ubuntu@ip-xxx:~$
為什麼會這樣呢?container 是靠 PID 1 去維持運行,但在 shell mode,PID 1 是 /bin/sh -c
,/bin/sh -c
又執行了 trap.sh,當 trap.sh 結束離開,/bin/sh -c
也就跟著結束了,就變成雖然 PID 1 殺不掉,但殺掉其他 Process 也導致 PID 1 關掉了,以這個角度看來,用 shell mode 似乎比較危險一點?
好啦,關於 process 的部分告一個段落了(應該吧),明天讓我們...由於沒有存稿,也不知道明天會寫什麼,明天就知道了。