写点什么

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

作者:
  • 2022 年 4 月 10 日
  • 本文字数:4137 字

    阅读完需:约 14 分钟

简介

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

源码说明

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



本章节对应的版本标签是: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进入对应的namespace if (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.Stderr
if 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:45ID NAME PID STATUS COMMAND CREATED0462374057 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=35641got mydocker_cmd=shsetns on ipc namespace succeededsetns on uts namespace succeededsetns on net namespace succeededsetns on pid namespace succeededsetns on mnt namespace succeeded# 查看当前的进程,看到和宿主机不一样,明显是容器的,成功进入到容器中/ # ps -efPID USER TIME COMMAND 1 root 0:00 top 7 root 0:00 sh 8 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=35641got mydocker_cmd=ls -lsetns on ipc namespace succeededsetns on uts namespace succeededsetns on net namespace succeededsetns on pid namespace succeededsetns on mnt namespace succeededtotal 4drwxr-xr-x 15 root root 4096 Apr 7 21:50 bird
复制代码


发布于: 刚刚阅读数: 2
用户头像

关注

还未添加个人签名 2018.09.09 加入

代码是门手艺活,也是门艺术活

评论

发布
暂无评论
自己动手写Docker系列 -- 5.4实现进入容器的namespace,exec命令_Docker_萧_InfoQ写作平台