难点

进入容器 Namespace 可以用 setns 系统调用。它需要先打开 /proc/[pid]/ns/ 文件夹下对应的文件,然后使当前进程进入到指定的 Namespace 中。系统调用描述非常简单,但是有一点对于 Go 来说很麻烦。对于 Mount Namespace 来说,一个具有多线程的进程是无法使用 setns 调用进入到对应的命名空间的。但是,Go 每启动一个程序就会进入多线程状态,因此无法简简单单地在 Go 里面直接调用系统调用,使当前的进程进入对应的 Mount Namespace。这里需要借助 Cgo 来实现这个功能。Cgo 允许 Go 程序去调用 C 的函数与标准库。

功能实现

我们写了一个构造函数,这个构造函数会在程序一启动的时候运行。构造函数的流程如下:

  1. 首先,从环境变量中获取需要进入的 PID 和 需要执行的命令,如果没有,就退出。
  2. 根据 Namespace 拼接出对应的路径,比如 /proc/pid/ns/ipc。
  3. 然后调用 setns 系统调用进入对应的 Namespace 并执行指定的命令。

问题及解决方案

这里主要使用了构造函数,然后导入了 C 模块,一旦这个包被引用,它就会在所有 Go 运行的环境启动之前执行,这样就避免了 Go 多线程导致的无法进入 mnt Namespace 的问题。这段程序执行完毕后,Go 程序才会执行。
但是这会带来一个问题,就是只要这个包被导入,它就会在所有 Go 代码前执行,那么即使那些不需要使用 exec 这段代码的地方也会运行这段程序。因此,需要在这段 C 代码前面一开始的位置就指定环境变量, 对于不使用 exec 功能的 Go 代码,只要不设置对应的环境变量,那么当 C 程序检测到没有这个环境变量时,就会直接退出,继续执行原来的代码,并不会影响原来的逻辑。

一旦程序启动,那段 C 代码就会运行,那么对于我们使用 exec 来说,当容器名和对应的命令传递进来以后,程序己经执行了,而且那段 C 代码也应该运行完毕。那么,怎么指定环境变量让它再执行一遍呢?创建了一个 command,传入 /proc/self/exe,实际上就是又运行了一遍自己的程序,但是这时有一点不同的就是,再一次运行的时候已经指定了环境变量,所以 C 代码执行的时候就能拿到对应的环境变量,便可以进入到指定的Namespace 中进行操作了。
e97b7808fa399ee37daf63da6722f56.png