本文為從零開始寫 Docker 系列第十二篇,實現類似 docker stop
的功能,使得我們能夠停止指定容器。
完整程式碼見:https://github.com/lixd/mydocker
歡迎 Star
推薦閱讀以下文章對 docker 基本實現有一個大致認識:
- 核心原理:深入理解 Docker 核心原理:Namespace、Cgroups 和 Rootfs
- 基於 namespace 的檢視隔離:探索 Linux Namespace:Docker 隔離的神奇背後
- 基於 cgroups 的資源限制
- 初探 Linux Cgroups:資源控制的奇妙世界
- 深入剖析 Linux Cgroups 子系統:資源精細管理
- Docker 與 Linux Cgroups:資源隔離的魔法之旅
- 基於 overlayfs 的檔案系統:Docker 魔法解密:探索 UnionFS 與 OverlayFS
- 基於 veth pair、bridge、iptables 等等技術的 Docker 網路:揭秘 Docker 網路:手動實現 Docker 橋接網路
開發環境如下:
root@mydocker:~# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.2 LTS
Release: 20.04
Codename: focal
root@mydocker:~# uname -r
5.4.0-74-generic
注意:需要使用 root 使用者
1. 概述
之前實現了 mydocker run -d
讓容器能夠後臺執行,但是沒有實現停止功能,導致無法停止後臺執行的容器。
本篇則是實現mydocker stop
命令,讓我們能夠直接停止後臺執行的容器。
2. 實現
容器的本質是程序,那麼停止容器就可以看做是結束程序。因此 mydocker stop 的實現思路就是先根據 containerId 查詢到它的主程序 PID,然後 Kill 傳送 SIGTERM 訊號,等待程序結束就好。
整個流程如下圖所示:
stopCommand
首先在 main_command.go 中增加 stopCommand:
var stopCommand = cli.Command{
Name: "stop",
Usage: "stop a container,e.g. mydocker stop 1234567890",
Action: func(context *cli.Context) error {
// 期望輸入是:mydocker stop 容器Id,如果沒有指定引數直接列印錯誤
if len(context.Args()) < 1 {
return fmt.Errorf("missing container id")
}
containerName := context.Args().Get(0)
stopContainer(containerName)
return nil
},
}
然後在 main 函式中加入該命令:
func main(){
// 省略其他內容
app.Commands = []cli.Command{
initCommand,
runCommand,
commitCommand,
listCommand,
logCommand,
execCommand,
stopCommand,
}
}
核心邏輯都在 stopContainer 中,command 這邊只需要解析並傳遞引數即可。
stopContainer
stopContainer 中就是停止容器的具體實現了。實現也很簡單,大致可以分為 3 步:
- 1)首先根據 ContainerId 找到之前記錄的容器資訊的檔案並拿到容器具體資訊,主要是 PID
- 2)然後呼叫 Kill 命令,給指定 PID 傳送 SIGTERM
- 3)最後更新容器狀態為 stop 並寫回記錄容器資訊的檔案;
具體程式碼如下:
func stopContainer(containerId string) {
// 1. 根據容器Id查詢容器資訊
containerInfo, err := getInfoByContainerId(containerId)
if err != nil {
log.Errorf("Get container %s info error %v", containerId, err)
return
}
pidInt, err := strconv.Atoi(containerInfo.Pid)
if err != nil {
log.Errorf("Conver pid from string to int error %v", err)
return
}
// 2.傳送SIGTERM訊號
if err = syscall.Kill(pidInt, syscall.SIGTERM); err != nil {
log.Errorf("Stop container %s error %v", containerId, err)
return
}
// 3.修改容器資訊,將容器置為STOP狀態,並清空PID
containerInfo.Status = container.STOP
containerInfo.Pid = " "
newContentBytes, err := json.Marshal(containerInfo)
if err != nil {
log.Errorf("Json marshal %s error %v", containerId, err)
return
}
// 4.重新寫回儲存容器資訊的檔案
dirPath := fmt.Sprintf(container.InfoLocFormat, containerId)
configFilePath := path.Join(dirPath, container.ConfigName)
if err := os.WriteFile(configFilePath, newContentBytes, constant.Perm0622); err != nil {
log.Errorf("Write file %s error:%v", configFilePath, err)
}
}
getInfoByContainerId 如下,根據 containerId 拼接出具體 path,讀取檔案內容拿到啟動時記錄的容器資訊,其中就包括 PID。
func getInfoByContainerId(containerId string) (*container.Info, error) {
dirPath := fmt.Sprintf(container.InfoLocFormat, containerId)
configFilePath := path.Join(dirPath, container.ConfigName)
contentBytes, err := os.ReadFile(configFilePath)
if err != nil {
return nil, errors.Wrapf(err, "read file %s", configFilePath)
}
var containerInfo container.Info
if err = json.Unmarshal(contentBytes, &containerInfo); err != nil {
return nil, err
}
return &containerInfo, nil
}
3. 測試
測試流程為:
- 1)mydocker run -d建立一個 detach 的後臺容器
- 2)mydocker stop 該容器
- 3)mydocker ps 檢視容器狀態是否變更,ps 檢視容器程序是否消失
建立一個 detach 容器:
root@mydocker:~/feat-stop/mydocker# go build .
root@mydocker:~/feat-stop/mydocker# ./mydocker run -d -name bird top
{"level":"info","msg":"createTty false","time":"2024-01-30T14:04:13+08:00"}
{"level":"info","msg":"resConf:\u0026{ 0 }","time":"2024-01-30T14:04:13+08:00"}
{"level":"info","msg":"busybox:/root/busybox busybox.tar:/root/busybox.tar","time":"2024-01-30T14:04:13+08:00"}
{"level":"error","msg":"mkdir dir /root/merged error. mkdir /root/merged: file exists","time":"2024-01-30T14:04:13+08:00"}
{"level":"error","msg":"mkdir dir /root/upper error. mkdir /root/upper: file exists","time":"2024-01-30T14:04:13+08:00"}
{"level":"error","msg":"mkdir dir /root/work error. mkdir /root/work: file exists","time":"2024-01-30T14:04:13+08:00"}
{"level":"info","msg":"mount overlayfs: [/usr/bin/mount -t overlay overlay -o lowerdir=/root/busybox,upperdir=/root/upper,workdir=/root/work /root/merged]","time":"2024-01-30T14:04:13+08:00"}
{"level":"info","msg":"command all is top","time":"2024-01-30T14:04:13+08:00"}
分別使用 ps 命令和 mydocker ps 命令查詢一下 PID
root@mydocker:~/feat-stop/mydocker# ./mydocker ps
ID NAME PID STATUS COMMAND CREATED
3184421796 bird 180831 running top 2024-01-30 14:04:1
root@mydocker:~/feat-stop/mydocker# ps -ef|grep top
root 180831 1 0 14:04 pts/10 00:00:00 top
可以看到,PID 為 180831 的程序就是我們的容器程序。
現在執行 stop 命令停止該容器
root@mydocker:~/feat-stop/mydocker# ./mydocker stop 3184421796
再透過 mydocker ps 命令檢視一下
root@mydocker:~/feat-stop/mydocker# ./mydocker ps
ID NAME PID STATUS COMMAND CREATED
3184421796 bird stopped top 2024-01-30 14:04:13
可以看到,狀態變成了 stopped,並且 PID 一欄也是空的。
最後執行 ps 檢視一下是不是真的停掉了
root@mydocker:~/feat-stop/mydocker# ps -ef|grep top
root 180869 177607 0 14:06 pts/10 00:00:00 grep --color=auto top
可以看到,原來容器的程序已經退出了,說明 stop 是成功的。
4. 小結
本篇主要實現 mydocker stop
命令,根據 ContainerId 找到容器程序 PID,然後 Kill 並更新容器狀態資訊。
完整程式碼見:https://github.com/lixd/mydocker
歡迎關注~
【從零開始寫 Docker 系列】持續更新中,搜尋公眾號【探索雲原生】訂閱,閱讀更多文章。
相關程式碼見 feat-stop
分支,測試指令碼如下:
需要提前在 /root 目錄準備好 busybox.tar 檔案,具體見第四篇第二節。
# 克隆程式碼
git clone -b feat-stop https://github.com/lixd/mydocker.git
cd mydocker
# 拉取依賴並編譯
go mod tidy
go build .
# 測試
./mydocker run -d -name c1 top
# 檢視容器 Id
./mydocker ps
# stop 停止指定容器
./mydocker stop ${containerId}