简介
在上篇中我们实现了将容器后台运行,本篇中我们将实现 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 []string
for _, 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 {
// 根据传过来的容器名获取宿主机对应的pid
pid, 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.Stdin
cmd.Stdout = os.Stdout
cmd.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: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 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=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
复制代码
评论