简介

在上篇中我们实现了将容器后台运行,本篇中我们将实现docker的ps命令,查看当前正在运行中的容器列表

源码说明

同时放到了Gitee和Github上,都可进行获取

  • Gitee: https://gitee.com/free-love/docker-demo
  • GitHub: https://github.com/lw1243925457/dockerDemo

本章节对应的版本标签是:5.4,防止后面代码过多,不好查看,可切换到标签版本进行查看

代码实现

这一部分实现起来就有点麻烦了,其中的一个nsenter始终不能运行正常,折腾了好一阵子发现,需要导出包才行,相关的会在代码中详细的说明

首先我们是需要使用setns去再次进入到我们容器的namespace中:

setns是一个系统调用,可以根据提供的PID再次进入到指定的Namespace 中。它需要先打开/proc/[pid]/ns/文件夹下对应的文件,然后使当前进程进入到指定的Namespace 中

但是一个具有多线程的进程是无法使用setns调用进入到对应的命名空间的,而Go启动一个程序就会进入多线程状态,所以无法简单使用命令调用去实现这个功能,需要借助C来实现

Cgo是一个很炫酷的功能,允许Go程序去调用C的函数与标准库。你只需要以一种特殊的方式在Go的源代码里写出需要调用的C的代码,Cgo就可以把你的C源码文件和Go文件整合成一个包

Cgo代码实现

新建一个文件夹 nsenter,新建文件:nsenter.go

编写Cgo的进入命名空间的代码,如下:

package nsenter/*
#define _GNU_SOURCE
#include <unistd.h>
#include <errno.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>// 构造函数:这里作用是在被引用的时候,这段代码就会执行
__attribute__((constructor)) static void enter_namespace(void) {char *mydocker_pid;// 从环境变量中获取需要进入的PID// 如果没有PID,直接退出,不执行后面的处理逻辑mydocker_pid = getenv("mydocker_pid");if (mydocker_pid) {fprintf(stdout, "got mydocker_pid=%s\n", mydocker_pid);} else {fprintf(stdout, "missing mydocker_pid env skip nsenter");return;}char *mydocker_cmd;// 从环境变量中获取需要执行的命令,没有命令,直接退出mydocker_cmd = getenv("mydocker_cmd");if (mydocker_cmd) {fprintf(stdout, "got mydocker_cmd=%s\n", mydocker_cmd);} else {fprintf(stdout, "missing mydocker_cmd env skip nsenter");return;}int i;char nspath[1024];char *namespaces[] = { "ipc", "uts", "net", "pid", "mnt" };for (i=0; i<5; i++) {sprintf(nspath, "/proc/%s/ns/%s", mydocker_pid, namespaces[i]);int fd = open(nspath, O_RDONLY);/ 调用setns进入对应的namespaceif (setns(fd, 0) == -1) {fprintf(stderr, "setns on %s namespace failed: %s\n", namespaces[i], strerror(errno));} else {fprintf(stdout, "setns on %s namespace succeeded\n", namespaces[i]);}close(fd);}// 进入后执行指定的命令int res = system(mydocker_cmd);exit(0);return;
}
*/
import "C"

如上所示,这样就把进入命名空间的Cgo文件写好了,具体使用在后面会详细说明

Exec命令实现

我们在main中增加exec命令:

func main() {......app.Commands = []cli.Command{command.InitCommand,command.RunCommand,command.CommitCommand,command.ListCommand,command.LogCommand,command.ExecCommand,}......
}

在main_command.go文件,增加Exec指令解析

var ExecCommand = cli.Command{Name:  "exec",Usage: "exec a command into container",Action: func(context *cli.Context) {if os.Getenv(run.EnvExecPid) != "" {log.Infof("pid callback pid %d", os.Getgid())return}// 我们希望命令格式是docker exec 容器名 命令if len(context.Args()) < 2 {log.Errorf("missing container name or command")return}containerName := context.Args().Get(0)var commandArray []stringfor _, arg := range context.Args().Tail() {commandArray = append(commandArray, arg)}// 执行命令if err := run.ExecContainer(containerName, commandArray); err != nil {log.Errorf("%v", err)}},
}

新增exec.go文件,编写具体的exec逻辑

import (// 这个很关键,引入而不使用,但其在启动的时候后自动调用_ "dockerDemo/mydocker/nsenter""fmt"log "github.com/sirupsen/logrus""os""os/exec""strings"
)// EnvExecPid
/**
前面的C代码中已经出现了mydocker_pid 和 mydocker_cmd 这两个key
主要是为了控制是否执行c代码里面的setns
*/
const EnvExecPid = "mydocker_pid"
const EnvExecCmd = "mydocker_cmd"func ExecContainer(containerName string, commandArray []string) error {// 根据传过来的容器名获取宿主机对应的pidpid, err := getContainerPidByName(containerName)if err != nil {return err}// 把命令以空格为分隔符拼接成一个字符串,便于传递cmdStr := strings.Join(commandArray, " ")log.Infof("container pid %s", pid)log.Infof("command %s", cmdStr)cmd := exec.Command("/proc/self/exe", "exec")cmd.Stdin = os.Stdincmd.Stdout = os.Stdoutcmd.Stderr = os.Stderrif err := os.Setenv(EnvExecPid, pid); err != nil {return fmt.Errorf("setenv %s err: %v", EnvExecPid, err)}if err := os.Setenv(EnvExecCmd, cmdStr); err != nil {return fmt.Errorf("setenv %s err: %v", EnvExecCmd, err)}envs, err := getEnvsByPid(pid)if err != nil {return err}cmd.Env = append(os.Environ(), envs...)if err := cmd.Run(); err != nil {return fmt.Errorf("exec container %s err: %v", containerName, err)}return nil
}

这里又遇到熟悉的/proc/self/exe,只不过是换了后面的参数,由原来的init 变成了现在的exec。这么做的目的就是为了那段C代码的执行。
因为一旦程序启动,那段C代码就会运行,那么对于我们使用exec来说,当容器名和对应的命令传递进来以后,程序已经执行了,而且那段C代码也应该运行完毕。
那么,怎么指定环境变量让它再执行一遍呢?这里就用到了这个/proc/self/exe。
这里又创建了一个command,只不过这次只是简单地fork出来一个进程,不需要这个进程拥有什么命名空间的隔离,然后把这个进程的标准输入输出都绑定到宿主机上。
这样去run这里的进程时,实际上就是又运行了一遍自己的程序,但是这时有一点不同的就是,再一次运行的时候已经指定了环境变量,所以C代码执行的时候就能拿到对应的环境变量,便可以进入到指定的Namespace中进行操作了。这时应该就可以明白前面一段代码的意义了

简单来说,就是需要再次触发下我们相关的docker命令运行

其中这句:_ “dockerDemo/mydocker/nsenter”

这个一定要加上,不然的话,不能在exec运行的时候触发Cgo文件的运行,进入命名空间就会失败

测试运行

命令如下:

# 启动一个后台进行root@lw-Code-01-Series-PF5NU1G  ~/code/go/dockerDemo   main ●  ./main run --name bird -d top                                                                                       ✔  ⚡  395  05:50:30
{"level":"info","msg":"memory cgroup path: /sys/fs/cgroup/memory/mydocker-cgroup","time":"2022-04-08T05:50:45+08:00"}
{"level":"info","msg":"memory cgroup path: /sys/fs/cgroup/memory/mydocker-cgroup","time":"2022-04-08T05:50:45+08:00"}
{"level":"info","msg":"all command is : top","time":"2022-04-08T05:50:45+08:00"}
{"level":"info","msg":"parent process run","time":"2022-04-08T05:50:45+08:00"}# 查看其正在运行中root@lw-Code-01-Series-PF5NU1G  ~/code/go/dockerDemo   main ●  ./main ps                                                                                                  SIG(127) ↵  ⚡  396  05:50:45
ID           NAME        PID         STATUS      COMMAND     CREATED
0462374057   bird        35641       running     top         8000-04-04 00:00:00# 运行exec进入容器中root@lw-Code-01-Series-PF5NU1G  ~/code/go/dockerDemo   main ●  ./main exec bird sh                                                                                                 ✔  ⚡  397  05:50:52
{"level":"info","msg":"container pid 35641","time":"2022-04-08T05:51:02+08:00"}
{"level":"info","msg":"command sh","time":"2022-04-08T05:51:02+08:00"}
got mydocker_pid=35641
got mydocker_cmd=sh
setns on ipc namespace succeeded
setns on uts namespace succeeded
setns on net namespace succeeded
setns on pid namespace succeeded
setns on mnt namespace succeeded
# 查看当前的进程,看到和宿主机不一样,明显是容器的,成功进入到容器中
/ # ps -ef
PID   USER     TIME  COMMAND1 root      0:00 top7 root      0:00 sh8 root      0:00 ps -ef
/ # exit# 运行一个立即退出的命令,可以看到也成功输出root@lw-Code-01-Series-PF5NU1G  ~/code/go/dockerDemo   main ●  ./main exec bird "ls -l"                                                                                            ✔  ⚡  398  05:51:28
{"level":"info","msg":"container pid 35641","time":"2022-04-08T05:51:41+08:00"}
{"level":"info","msg":"command ls -l","time":"2022-04-08T05:51:41+08:00"}
got mydocker_pid=35641
got mydocker_cmd=ls -l
setns on ipc namespace succeeded
setns on uts namespace succeeded
setns on net namespace succeeded
setns on pid namespace succeeded
setns on mnt namespace succeeded
total 4
drwxr-xr-x   15 root     root          4096 Apr  7 21:50 bird

自己动手写Docker系列 -- 5.4实现进入容器的namespace,exec命令相关推荐

  1. 自己动手写Docker系列 -- 5.7实现通过容器制作镜像

    简介 在上篇中我们实现了rm命令,删除存在的容器,本篇中,将完善之前的文件系统隔离,实现容器与容器之间的文件系统隔离 源码说明 同时放到了Gitee和Github上,都可进行获取 Gitee: htt ...

  2. 自己动手写Docker系列 -- 6.3 手动配置容器网络(下)

    简介 网络部分较为复杂,本篇先利用之前写好的基础容器和网桥部分,加上手工给容器配置网络,让其容器与外部网络部分功能正常,为后面程序编写打下基础 源码说明 同时放到了Gitee和Github上,都可进行 ...

  3. 【无标题】自己动手写Docker系列 -- 6.3 手动配置容器网络(上)

    简介 网络部分较为复杂,本篇先利用之前写好的基础容器和网桥部分,加上手工给容器配置网络,让其容器与宿主机网络部分功能正常,为后面程序编写打下基础 源码说明 同时放到了Gitee和Github上,都可进 ...

  4. 自己动手写Docker系列 -- 3.1构造实现run命令版本的容器

    简介 通过对前面Linux的Namespace.Cgroups.Union File System的学习,对Docker实现的基础知识有了一点点了解,接下来就跟着作者开始编写 源码说明 同时放到了Gi ...

  5. 自己动手写Docker系列 -- 5.1实现容器的后台运行

    简介 在前几篇中,我们已经构建了一个基础的镜像,本篇开始做一些进阶的功能,下面就是实现docker中的-d命令,让容器能够后台运行 源码说明 同时放到了Gitee和Github上,都可进行获取 Git ...

  6. 自己动手写Docker系列 -- 4.1使用busybox创建容器

    简介 目前docker demo中还是使用的系统原有proc,不怎么纯净,本篇中使用busybox来更换docker demo的系统挂载点 源码说明 同时放到了Gitee和Github上,都可进行获取 ...

  7. 自己动手写Docker系列 -- 5.2实现查看运行中的容器

    简介 在上篇中我们实现了将容器后台运行,本篇中我们将实现docker的ps命令,查看当前正在运行中的容器列表 源码说明 同时放到了Gitee和Github上,都可进行获取 Gitee: https:/ ...

  8. 自己动手写Docker系列 -- 4.2使用AUFS包装busybox

    简介 在上篇中实现了使用宿主机busybox目录作为文件的根目录,但在容器内的对文件的操作仍会影响到宿主机的目录,本篇中实现进一步的容器和镜像隔离 源码说明 同时放到了Gitee和Github上,都可 ...

  9. 自己动手写Docker系列 -- 4.3实现volume数据卷

    简介 在上篇中对容器和镜像实现了进一步的文件隔离,是容器内的修改不影响到宿主机.本篇中将实现docker中的volume,提供持久化存储能力 源码说明 同时放到了Gitee和Github上,都可进行获 ...

最新文章

  1. 牛客网(剑指offer) 第十二题 数值的整数次方
  2. Java——集合的概述
  3. windows怎么将图片变为单色图片_印刷丨单色黑与四色黑
  4. 利用mysql5.6 的st_distance 实现按照距离远近排序。 (转载)
  5. android利用AudioRecord实现录音功能(kotlin语言)
  6. java数据结构编程问题_Java语言程序设计与数据结构第十一版(基础篇)第一章编程练习题答案...
  7. JDK API 1.6.0中文版直接下载
  8. 零基础入门Jetson Nano——软件篇
  9. 华为公司如何开好经营分析会(战略落地的核心抓手)?
  10. 香港进入5G时代!多功能智能灯柱试验计划为5G建设作配合
  11. Shell编程-02-正则表达式
  12. APP攻防——微信小程序解包反编译数据抓包APK资源提取
  13. 深入了解:HTML5下载属性
  14. 小学生python游戏编程arcade----灯光示例
  15. android进度条已加载部分闪光,Android进度条ProgressBar的实现代码
  16. Get To The Point: Summarization with Pointer-Generator Networks
  17. 由《骗子伪装成10086发送诈骗短信》联想到程序猿的内在修炼
  18. 平时总结的一些前端网址
  19. mac 系统office软件
  20. 最详细的基于R语言的Logistic Regression(Logistic回归)源码,包括拟合优度,Recall,Precision的计算

热门文章

  1. Mac下Intellij IDea发布Web项目详解一
  2. php 数据处理--合并,拆分,追加,去重
  3. Intelij IDEA解决Dependency无法更新问题
  4. spring.net结合普通三层(实现IOC 及AOP中的异常记录功能)
  5. 解决ubuntu下的firefox无法在线播放音频和视频的问题
  6. 如何自定义一个异常类
  7. 【白皮书分享】2020中国人工智能API经济白皮书.pdf(附下载链接)
  8. 16篇最新推荐系统论文送你(文末附打包下载链接)
  9. cmake命令的python库的位置参数-DTORCH_PATH
  10. 浙大 PAT 乙级1055