iT邦幫忙

2022 iThome 鐵人賽

DAY 22
0
DevOps

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

Day 22: Ubuntu 的 /bin/sh 怎麼了?

  • 分享至 

  • xImage
  •  

如題,為什麼在討論 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 的部分告一個段落了(應該吧),明天讓我們...由於沒有存稿,也不知道明天會寫什麼,明天就知道了。


上一篇
Day 21: container 裡誰是 PID 1 有差嗎?
下一篇
Day 23: container 怎麼跟別人溝通呢?
系列文
那些關於 docker 你知道與不知道的事32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言