從零開始寫 Docker(十二)---實現 mydocker stop 停止容器

探索云原生發表於2024-04-25

mydocker-stop.png

本文為從零開始寫 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 訊號,等待程序結束就好。

整個流程如下圖所示:

mydocker-stop-process.png

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}

相關文章