本文为从零开始写 Docker 系列第九篇,实现类似 docker ps 的功能,使得我们能够查询到后台运行中的所有容器。


完整代码见:https://github.com/lixd/mydocker

欢迎 Star

推荐阅读以下文章对 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 ps 命令了。其实 mydocker ps 命令比较简单,主要是去约定好的位置查询一下容器的信息数据,然后显示出来,因此数据准备就显得尤为重要。

因此整个实现分为两部分:

  • 1)容器运行时记录数据
  • 2)mydocker ps 查询数据

对于 docker 来说,他会把容器信息存储在var/lib/docker/containers 目录下。

  • 读取 var/lib/docker/containers 目录下的所有文件夹就能拿到当前系统中的容器
  • 读取/var/lib/docker/containers/{containerID}/config.v2.json 文件即可拿到对应容器的详细信息。

我们也参考着 Docker 实现即可。

2. 记录容器信息

在前面章节创建的容器中,所有关于容器的信息,比如PID、容器创建时间、容器运行命令等,都没有记录,这导致容器运行完后就再也不知道它的信息了,因此需要把这部分信息保留下来。

具体实现则是创建容器时将相关信息写入/var/lib/mydocker/containers/{containerId}/config.json 文件中。

具体流程如下图所示:

提供 -name flag

首先,要在 runCommand flag 里面增加一个 name 标签,方便用户启动容器时指定容器的名字。

var runCommand = cli.Command{
Name: "run",
Usage: `Create a container with namespace and cgroups limit
mydocker run -it [command]`,
Flags: []cli.Flag{
// 省略其他内容
cli.StringFlag{
Name: "name",
Usage: "container name",
},
},
Action: func(context *cli.Context) error {
// 把namne传递给Run方法
containerName := context.String("name")
Run(tty, cmdArray, resConf, volume, containerName)
return nil
},

recordContainerInfo

然后,需要增加一个 record 方法记录容器的相关信息。在增加之前,需要一个 ID 生成器,用来唯一标识容器。

使用过 Docker 的都知道,每个容器都会有一个 ID,为了方便起见,mydocker 中就用 10 位数字来表示一个容器的 ID。

func randStringBytes(n int) string {
letterBytes := "1234567890"
rand.Seed(time.Now().UnixNano())
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}

另外就是记录容器信息这个重要的环节,我们先定义了一个容器的一些基本信息,比如 PID 和创建时间等,然后默认把容器的信息以 json 的形式存储在宿主机的/var/run/mydocker/容器名/config.json文件里面。

容器完整信息的基本格式如下:

type Info struct {
Pid string `json:"pid"` // 容器的init进程在宿主机上的 PID
Id string `json:"id"` // 容器Id
Name string `json:"name"` // 容器名
Command string `json:"command"` // 容器内init运行命令
CreatedTime string `json:"createTime"` // 创建时间
Status string `json:"status"` // 容器的状态
}

然后就开始记录容器信息:

func RecordContainerInfo(containerPID int, commandArray []string, containerName, containerId string) error {
// 如果未指定容器名,则使用随机生成的containerID
if containerName == "" {
containerName = containerId
}
command := strings.Join(commandArray, "")
containerInfo := &Info{
Id: containerId,
Pid: strconv.Itoa(containerPID),
Command: command,
CreatedTime: time.Now().Format("2006-01-02 15:04:05"),
Status: RUNNING,
Name: containerName,
} jsonBytes, err := json.Marshal(containerInfo)
if err != nil {
return errors.WithMessage(err, "container info marshal failed")
}
jsonStr := string(jsonBytes)
// 拼接出存储容器信息文件的路径,如果目录不存在则级联创建
dirPath := fmt.Sprintf(InfoLocFormat, containerId)
if err := os.MkdirAll(dirPath, constant.Perm0622); err != nil {
return errors.WithMessagef(err, "mkdir %s failed", dirPath)
}
// 将容器信息写入文件
fileName := path.Join(dirPath, ConfigName)
file, err := os.Create(fileName)
defer file.Close()
if err != nil {
return errors.WithMessagef(err, "create file %s failed", fileName)
}
if _, err = file.WriteString(jsonStr); err != nil {
return errors.WithMessagef(err, "write container info to file %s failed", fileName)
}
return nil
}

实际就是把容器的信息序列化之后持久化到磁盘的/var/run/{containerID}/config.json文件里。


如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。

搜索公众号【探索云原生】即可订阅


Run 方法修改

最后,在 Run 函数上加上对于这个函数的调用,代码如下:

func Run(tty bool, comArray []string, res *subsystems.ResourceConfig, volume, containerName string) {
containerId := container.GenerateContainerID() // 生成 10 位容器 id parent, writePipe := container.NewParentProcess(tty, volume)
if parent == nil {
log.Errorf("New parent process error")
return
}
if err := parent.Start(); err != nil {
log.Errorf("Run parent.Start err:%v", err)
return
} // record container info
err := container.RecordContainerInfo(parent.Process.Pid, comArray, containerName, containerId)
if err != nil {
log.Errorf("Record container info error %v", err)
return
} // 创建cgroup manager, 并通过调用set和apply设置资源限制并使限制在容器上生效
cgroupManager := cgroups.NewCgroupManager("mydocker-cgroup")
defer cgroupManager.Destroy()
_ = cgroupManager.Set(res)
_ = cgroupManager.Apply(parent.Process.Pid, res) // 在子进程创建后才能通过pipe来发送参数
sendInitCommand(comArray, writePipe)
if tty { // 如果是tty,那么父进程等待,就是前台运行,否则就是跳过,实现后台运行
_ = parent.Wait()
container.DeleteWorkSpace("/root/", volume)
container.DeleteContainerInfo(containerId)
}
}

另外再容器退出后,就需要删除容器的相关信息,实现也很简单,把对应目录的信息都删除就好了。

func DeleteContainerInfo(containerID string) {
dirPath := fmt.Sprintf(InfoLocFormat, containerID)
if err := os.RemoveAll(dirPath); err != nil {
log.Errorf("Remove dir %s error %v", dirPath, err)
}
}

到此为止,就完成了信息的收集。容器创建后,所有需要的信息都被存储到/var/lib/mydocker/containers/{containerID}下,下面就可以通过读取并遍历这个目录下的容器去实现 mydocker ps 命令了。

3. 实现 mydocker ps

具体实现则是遍历 /var/lib/mydocker/containers/ 目录,解析得到容器信息并汇总后以表格形式打印出来。

具体流程如下图所示:

listCommand

首先在 main_command.go 中增加 ps 命令:

var listCommand = cli.Command{
Name: "ps",
Usage: "list all the containers",
Action: func(context *cli.Context) error {
ListContainers()
return nil
},
}

在 main.go 中引用该命令:

func main {
// 省略其他内容
app.Commands = []cli.Command{
initCommand,
runCommand,
commitCommand,
listCommand,
}
}

具体实现见 ListContainers 方法。

ListContainers

整体实现也比较简单:

  • 首先遍历存放容器数据的/var/lib/mydocker/containers/目录,里面每一个子目录都是一个容器。
  • 然后使用 getContainerInfo 方法解析子目录中的 config.json 文件拿到容器信息
  • 最后格式化成 table 形式打印出来即可
func ListContainers() {
// 读取存放容器信息目录下的所有文件
files, err := os.ReadDir(container.InfoLoc)
if err != nil {
log.Errorf("read dir %s error %v", container.InfoLoc, err)
return
}
containers := make([]*container.Info, 0, len(files))
for _, file := range files {
tmpContainer, err := getContainerInfo(file)
if err != nil {
log.Errorf("get container info error %v", err)
continue
}
containers = append(containers, tmpContainer)
}
// 使用tabwriter.NewWriter在控制台打印出容器信息
// tabwriter 是引用的text/tabwriter类库,用于在控制台打印对齐的表格
w := tabwriter.NewWriter(os.Stdout, 12, 1, 3, ' ', 0)
_, err = fmt.Fprint(w, "ID\tNAME\tPID\tSTATUS\tCOMMAND\tCREATED\n")
if err != nil {
log.Errorf("Fprint error %v", err)
}
for _, item := range containers {
_, err = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
item.Id,
item.Name,
item.Pid,
item.Status,
item.Command,
item.CreatedTime)
if err != nil {
log.Errorf("Fprint error %v", err)
}
}
if err = w.Flush(); err != nil {
log.Errorf("Flush error %v", err)
}
}

getContainerInfo

具体的解析方法则提取到了 getContainerInfo

读取文件内容,并反序列化得到容器信息。

func getContainerInfo(file os.DirEntry) (*container.Info, error) {
// 根据文件名拼接出完整路径
configFileDir := fmt.Sprintf(container.InfoLocFormat, file.Name())
configFileDir = path.Join(configFileDir, container.ConfigName)
// 读取容器配置文件
content, err := os.ReadFile(configFileDir)
if err != nil {
log.Errorf("read file %s error %v", configFileDir, err)
return nil, err
}
info := new(container.Info)
if err = json.Unmarshal(content, info); err != nil {
log.Errorf("json unmarshal error %v", err)
return nil, err
} return info, nil
}

4. 测试

测试以下功能:

  • 创建容器后能否记录信息到文件
  • mydocker ps 能否正常读取并展示容器信息

记录容器信息

分别测试指定容器名称和不知道名称两种情况。

指定名称

通过--name 指定容器名称,并通过-d 指定后台运行:

root@mydocker:~/feat-ps/mydocker# ./mydocker run -d -name runtop top
{"level":"info","msg":"createTty false","time":"2024-01-25T14:20:11+08:00"}
{"level":"info","msg":"resConf:\u0026{ 0 }","time":"2024-01-25T14:20:11+08:00"}
{"level":"info","msg":"busybox:/root/busybox busybox.tar:/root/busybox.tar","time":"2024-01-25T14:20:11+08:00"}
{"level":"error","msg":"mkdir dir /root/merged error. mkdir /root/merged: file exists","time":"2024-01-25T14:20:11+08:00"}
{"level":"error","msg":"mkdir dir /root/upper error. mkdir /root/upper: file exists","time":"2024-01-25T14:20:11+08:00"}
{"level":"error","msg":"mkdir dir /root/work error. mkdir /root/work: file exists","time":"2024-01-25T14:20:11+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-25T14:20:11+08:00"}
{"level":"info","msg":"command all is top","time":"2024-01-25T14:20:11+08:00"}

可以看到此时,命令已经退出了,查询容器(top 命令)是否在后台运行。

root@mydocker:~/feat-ps/mydocker# ps -ef|grep -e PPID -e top
UID PID PPID C STIME TTY TIME CMD
root 169514 1 0 14:20 pts/8 00:00:00 top

后台确实有一个 top 命令在运行,PID 为 169514。

查看 /var/lib/mydocker/containers 目录,是否新增了容器信息记录文件

root@mydocker:~/feat-ps/mydocker# ls /var/lib/mydocker/containers
5633481844
root@mydocker:~/feat-ps/mydocker# ls /var/lib/mydocker/containers/5633481844/
config.json
root@mydocker:~/feat-ps/mydocker# cat /var/lib/mydocker/containers/5633481844/config.json
{"pid":"169514","id":"5633481844","name":"runtop","command":"top","createTime":"2024-01-25 14:20:11","status":"running"}

可以看到,config.json 文件记录了容器名称,id、pid、command 等信息,基于这些信息,我们执行 mydocker ps 时就可以列出当前正在运行的容器信息了。

不指定名称

在测试一下不指定名称的容器,能否正常记录。

root@mydocker:~/feat-ps/mydocker# ./mydocker run -d  top
{"level":"info","msg":"createTty false","time":"2024-01-25T14:22:28+08:00"}
{"level":"info","msg":"resConf:\u0026{ 0 }","time":"2024-01-25T14:22:28+08:00"}
{"level":"info","msg":"busybox:/root/busybox busybox.tar:/root/busybox.tar","time":"2024-01-25T14:22:28+08:00"}
{"level":"error","msg":"mkdir dir /root/merged error. mkdir /root/merged: file exists","time":"2024-01-25T14:22:28+08:00"}
{"level":"error","msg":"mkdir dir /root/upper error. mkdir /root/upper: file exists","time":"2024-01-25T14:22:28+08:00"}
{"level":"error","msg":"mkdir dir /root/work error. mkdir /root/work: file exists","time":"2024-01-25T14:22:28+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-25T14:22:28+08:00"}
{"level":"info","msg":"command all is top","time":"2024-01-25T14:22:28+08:00"}

查看 /var/lib/mydocker/containers 目录是否新增记录文件

root@mydocker:~/feat-ps/mydocker# ls /var/lib/mydocker/containers
5633481844 8636128862
root@mydocker:~/feat-ps/mydocker# ls /var/lib/mydocker/containers/8636128862/
config.json
root@mydocker:~/feat-ps/mydocker# cat /var/lib/mydocker/containers/8636128862/config.json
{"pid":"169707","id":"8636128862","name":"8636128862","command":"top","createTime":"2024-01-25 14:22:28","status":"running"

可以看到,新增了 8636128862 目录,其中 8636128862 就是容器 ID,对于未指定名称的容器,会使用生成的 id 作为名称。

接着查看一下/var/lib/mydocker/containers目录结构:

root@mydocker:/var/lib/mydocker/containers# tree .
.
├── 5633481844
│ └── config.json
└── 8636128862
└── config.json

可以看到,mydocker 分别在该路径下创建了两个文件夹,分别以容器的ID命名。

子目录里面的config.json 存储了容器的详细信息。

至此,说明我们的容器信息记录功能是正常的。

mydocker ps

最后测试 mydocker ps 命令能否正常展示,容器信息。

root@mydocker:~/feat-ps/mydocker# ./mydocker ps
ID NAME PID STATUS COMMAND CREATED
5633481844 runtop 169514 running top 2024-01-25 14:20:11
8636128862 8636128862 169707 running top 2024-01-25 14:22:28

成功打印出了当前运行中的两个容器,说明 mydocker ps 命令是 ok 的。

5. 总结

本篇实现的 mydocker ps 比较简单,和 docker 实现基本类似:

  • 容器启动把信息存储在var/lib/mydocker/containers 目录下

  • 读取 var/lib/mydocker/containers 目录下的所有文件夹就能拿到当前系统中的容器

  • 读取/var/lib/mydocker/containers/{containerID}/config.json 文件即可拿到对应容器的详细信息。

不过现在由于没有隔离每个容器的 rootfs,因此启动多个容器时会出现一些问题,不过不是本篇重点,暂时先不关注,等后续统一处理。


【从零开始写 Docker 系列】持续更新中,搜索公众号【探索云原生】订阅,阅读更多文章。



完整代码见:https://github.com/lixd/mydocker

欢迎关注~

相关代码见 feat-volume 分支,测试脚本如下:

需要提前在 /root 目录准备好 busybox.tar 文件,具体见第四篇第二节。

# 克隆代码
git clone -b feat-ps https://github.com/lixd/mydocker.git
cd mydocker
# 拉取依赖并编译
go mod tidy
go build .
# 测试
./mydocker run -d -name c1 top

从零开始写 Docker(九)---实现 mydocker ps 查看运行中的容器的更多相关文章

  1. Docker查看运行中容器并进入容器

    一.简述 Docker查看运行中容器并进入容器. 二.方法 $ sudo docker ps $ sudo docker exec -it 775c7c9ee1e1 /bin/bash 将黄色文字替换 ...

  2. Docker学习笔记 - 在运行中的容器内启动新进程

    docker psdoker top dc1 # 容器情况# 在运行中的容器内启动新进程docker exec [-d] [-i] [-t] 容器名 [command] [args]docker ex ...

  3. Docker给运行中的容器添加映射端口

    方法一: 1.获得容器IP将container_name 换成实际环境中的容器名docker inspect `container_name` | grep IPAddress 2. iptables ...

  4. Docker 给运行中的容器添加映射端口

    方法1 1.获得容器IP 将container_name 换成实际环境中的容器名 docker inspect `container_name` | grep IPAddress 2. iptable ...

  5. docker登录运行中的容器的4方案

    目前容器云非常的成熟,也有很多的使用案例,可以说不是什么高大上的东西了,可以说整个云计算也不是什么奢侈品,而是基础设施.但是如何使用,就成了必须的技术. 今天记录下,基于docker的容器登录技术. ...

  6. 使用 top instance 命令查看运行中 MaxCompute 作业

    我们都知道,在 MaxCompute Console 里,可以使用下面的命令来列出运行完成的 instance 列表. show p|proc|processlist [from <yyyy-M ...

  7. Docker使用exec进入正在运行中的容器

    docker在1.3.X版本之后提供了一个新的命令exec用于进入容器,这种方式相对简单一些,下面我们来看一下该命令的使用: docker exec --help 接下来我们使用该命令进入一个已经在运 ...

  8. Docker 为 ASP.NET Core Web 应用程序生成 Docker 映像,创建并运行多个容器

    1.为 ASP.NET Core 应用程序生成 Docker 映像 下载这个事例项目:https://github.com/dotnet/dotnet-docker/tree/master/sampl ...

  9. 查看运行中的Java其配置的堆大小

    一.背景 有题目中的需求,也不是空穴来风:前一阵给公司搭建了一个持续集成服务器,Jenkins.最近发现,运行一段时间后,就变慢了. 随便一个操作,cpu就飙高了.然后就思考会不会是内存不够用,频繁G ...

  10. docker 笔记--运行中的容器如何添加端口映射

    解决: iptables -t nat -A DOCKER -p tcp --dport ${YOURPORT_1} -j DNAT --to-destination ${CONTAINERIP}:$ ...

随机推荐

  1. Codeforces Round #824 (Div. 2) A-E

    比赛链接 A 题解 知识点:贪心,数学. 注意到三段工作时间一共 \(n-3\) 天,且天数实际上可以随意分配到任意一段,每段至少有一天,现在目的就是最大化段差最小值. 不妨设 \(l_1<l_ ...

  2. Linux dmesg命令使用方法详解

    一.命令简介  dmesg(display message)命令用于显示开机信息.kernel 会将开机信息存储在 ring buffer 中.您若是开机时来不及查看信息,可利用 dmesg 来查看. ...

  3. 从零开始手写缓存框架(二)redis expire 过期原理及实现

    前言 我们在 从零手写 cache 框架(一)实现固定大小的缓存 中已经初步实现了我们的 cache. 本节,让我们来一起学习一下如何实现类似 redis 中的 expire 过期功能. 过期是一个非 ...

  4. Wireguard笔记(三) lan-to-lan子网穿透和多网段并存

    目录 Wireguard笔记(一) 节点安装配置和参数说明 Wireguard笔记(二) 命令行操作 Wireguard笔记(三) lan-to-lan子网穿透和多网段并存 多 Wireguard 服 ...

  5. 【Android】屏幕旋转时数据丢失问题解决方案

    1 问题描述 ​ 在旋转屏幕时,记录旋转屏幕次数的计数器(count)一直为 0,不能实现累加效果.主要因为在旋转屏幕时,会销毁原来的变量,重新构建界面. 2 解决思路 ​ 在 Activity 销毁 ...

  6. SpringCloud OpenFeign服务接口调用

    介绍 OpenFeign是一种声明式.模板化的HTTP客户端.在Spring Cloud中使用OpenFeign,可以做到使用HTTP请求访问远程服务,就像调用本地方法一样的,开发者完全感知不到这是在 ...

  7. Java并发编程实例--1.创建和运行一个线程

    从这一篇开始写Java并发编程实例,内容都翻译整理自书籍:<Java 7 Concurrency Cookbook> 谈到线程,无法逃避的一个问题就是: 并发(concurrency)和并 ...

  8. Spring rce CVE-2022-22965

    原理大致是这样:spring框架在传参的时候会与对应实体类自动参数绑定,通过"."还可以访问对应实体类的引用类型变量.使用getClass方法,通过反射机制最终获取tomcat的日 ...

  9. Direct2D CreateHwndRenderTarget 和 CreateDCRenderTarget

    前段时间稍微看了点Direct3D, 觉得挺有意思的,但是想着要有3D得先从2D开始.故开始了D2D旅行. 如标题所示,CreateHwndRenderTarget 是在用来创建一个渲染到窗口的渲染目 ...

  10. 深入理解String

    深入理解String String是Java中的一个类,是一个引用类型,用于表示字符串.它是不可变的(immutable),即一旦创建,其值就不能被修改.任何对String对象的修改操作都会创建一个新 ...