当然unshare只是个简单和namespace有关的的例子,如果要深入了解容器,这就有点不够用了。 因为我们可能还需要更近一步的控制。因此我们会从程序语言层面来探索namespace,而不只是命令,本系列采用golang
众所周知,go是容器的实现语言最流行的语言之一,比如大名鼎鼎的docker,kubernetes等。 go也被称作互联网时代的c语言,主要还是因为go的性能相当不错,而且还十分地简洁。 docker的开发者也曾表示他们才有go来开发的原因之一就是看上它能静态边缘,优秀的异步编程模式和方便的跨平台等特性。
对应我而言,如果能用一简洁的语言来实现,那就再好不过。 我也曾经写过python,C#等不同的语言,现在会更习惯使用go,因为确实挺方便的,相比python,没有动态语言那种runtime的坑,相比C#,没有一堆的dll依赖。
撸起袖子
本系列的目标是使用go来理解Linux namespace,本章节,会编写一个小应用叫ns-proc。 故名思义,这个是namespace process的demo。
代码如下:
// +build linux
package main
import (
"fmt"
"os"
"os/exec"
"syscall"
)
func main() {
cmd := exec.Command("sh")
//set identify for this demo
cmd.Env = []string{"PS1=-[namespace-process]-# "}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS,
}
if err := cmd.Run(); err != nil {
fmt.Printf("Error running the /bin/sh command - %s\n", err)
os.Exit(1)
}
}
如你所见,代码是很简单,需要注意的是,如果你在非Linux下编写的,比如我是在macOS。
请在文件头部加上// +build linux
并且在build的时候不能直接运行go build ns-proc.go
,你需要指定OS为Linux。
比如 GOOS=linux go build ns-proc.go
这是因为&syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS,
}这个golang的API只属于Linux平台才有。
运行这个命令后(直接在Linux平台build也是可以,那就不需要指定参数)得到ns-proc文件,上传到Linux平台执行,我这里测试的是CentOS
[root@vm43 gons]# vim ns-proc.go
[root@vm43 gons]# go build ns-proc.go
[root@vm43 gons]# ./ns-proc
-[namespace-process]-#
-[namespace-process]-#
再回头看下代码,其实也没什么特别操作,代码exec.Command
创建了一个*Cmd
对象,并调用了系统sh命令。
为了方便辨认,这里设置了环境变量PS1,可以在上面输出结果看到效果。
然后把程序的IO通过管道重定向到标志输入输出流和标志错误流。
代码里关键的一个操作是还添加了SysProcAttr
参数,这个参数有什么用,要先了解了namespaces这个API
关于namespaces API
查看namespaces(7) 手册页面。 可以找到关键的几个调用:
The namespaces API As well as various /proc files described below, the namespaces API includes the following system calls:
clone(2)
The clone(2) system call creates a new process. If the
flags argument of the call specifies one or more of the
CLONE_NEW* flags listed above, then new namespaces are
created for each flag, and the child process is made a
member of those namespaces. (This system call also
implements a number of features unrelated to namespaces.)
setns(2)
The setns(2) system call allows the calling process to
join an existing namespace. The namespace to join is
specified via a file descriptor that refers to one of the
/proc/[pid]/ns files described below.
unshare(2)
The unshare(2) system call moves the calling process to a
new namespace. If the flags argument of the call
specifies one or more of the CLONE_NEW* flags listed
above, then new namespaces are created for each flag, and
the calling process is made a member of those namespaces.
(This system call also implements a number of features
unrelated to namespaces.)
ioctl(2)
Various ioctl(2) operations can be used to discover
information about namespaces. These operations are
described in ioctl_ns(2).
有三个关键API
clone(2),创建新进程
setns(2) 调用的进程加入一个已存在的命名空间
unshare(2) 调用进程脱离某个命名空间
首先,unshare()
这个比较熟悉,因为我们在上一章介绍过这个API。
当我们在终端执行unshare这个命令的时候其实就是利用了这个linux API。
然后我们现在把关注点放在clone()
,
上面go代码cmd.Run()
执行的时候,其实就会调用系统的clone()
另外clone()这个api还能接受其他的参数,比如一个或者多个的CLONE_*
参数(flag),从golang来看,这里可以传递flag有以下:
CLONE_CHILD_CLEARTID = 0x200000
CLONE_CHILD_SETTID = 0x1000000
CLONE_DETACHED = 0x400000
CLONE_FILES = 0x400
CLONE_FS = 0x200
CLONE_IO = 0x80000000
CLONE_NEWIPC = 0x8000000
CLONE_NEWNET = 0x40000000
CLONE_NEWNS = 0x20000
CLONE_NEWPID = 0x20000000
CLONE_NEWUSER = 0x10000000
CLONE_NEWUTS = 0x4000000
CLONE_PARENT = 0x8000
CLONE_PARENT_SETTID = 0x100000
CLONE_PTRACE = 0x2000
CLONE_SETTLS = 0x80000
CLONE_SIGHAND = 0x800
CLONE_SYSVSEM = 0x40000
CLONE_THREAD = 0x10000
CLONE_UNTRACED = 0x800000
CLONE_VFORK = 0x4000
CLONE_VM = 0x100
我们前面的demo代码通过SysProcAttr
传递了syscall.CLONE_NEWUTS
这个flag,最后在Linux下,代码编译成可执行程序,会转化成调用Linux namespace的clone()
并传递了CLONE_*
这些参数来创建一个新进程(这里demo创建的是/bin/sh)。
可以看到在CentOS下执行结果如下:
[root@vm43 gons]# go build ns-proc.go
[root@vm43 gons]# ./ns-proc
-[namespace-process]-#
-[namespace-process]-#
另外如果用strace命令是可以看到我们编译出来的ns-proc执行过程,其实就是调用clone(),并把CLONE_NEWUTS作为flag参数传递进去
[root@vm43 gons]# strace ./ns-proc
execve("./ns-proc", ["./ns-proc"], [/* 28 vars */]) = 0
arch_prctl(ARCH_SET_FS, 0x58d490) = 0
...
clone(child_stack=0, flags=CLONE_VM|CLONE_VFORK|CLONE_NEWUTS|SIGCHLD) = 26478
...
waitid(P_PID, 26478, -[namespace-process]-#
-[namespace-process]-#
-[namespace-process]-#
理论上,这个demo已经是在新的UTS命名空间在运行启动的进程。 不信可以再验证下:
-[namespace-process]-#
-[namespace-process]-# readlink /proc/self/ns/uts
uts:[4026532707]
-[namespace-process]-# exit
exit
[root@vm43 gons]# readlink /proc/self/ns/uts
uts:[4026531838]
[root@vm43 gons]#
可以通过/proc/self/ns/uts
查看当前的linux namespace类型(uts)和inode号码。
结果显示,在shell -[namespace-process]中看到的结果为
uts:[4026532707],而退出shell后看到的是uts:[4026531838]。
这就表示了两个进程的确是在不同的namespace下。
看上去还可以,但是目前我们只是在新进程只使用了单个新的namespace,如果需要指定多个。 Cloneflags是可以接受多个flag的,通过位或操作(|)添加多个flag试下,比如修改:
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags:
syscall.CLONE_NEWNS |
syscall.CLONE_NEWUTS |
syscall.CLONE_NEWIPC |
syscall.CLONE_NEWPID |
syscall.CLONE_NEWNET |
syscall.CLONE_NEWUSER,
}
重新编译运行,这次启动的sh进程则运行于新的Mount, UTS, IPC, PID, Network和User命名空间下。
注意:CentOS7貌似不支持CLONE_NEWUSER这个flag,因此,重新编译的程序是在Ubuntu下测试的,参考bug https://bugzilla.redhat.com/show_bug.cgi?id=1168776
截止目前,我们的确用go演示了使用新的命名空间来运行进程,但是也仅仅是创建了出来,好像还缺了点东西。 比如说,我们创建了Mount空间,(CLONE_NEWNS) ,但是也还没涉及到其他mount操作。 此外我们也创建了网络空间(CLONE_NEWNET),但是也并没有在新的namespace下设置过网卡之类的, CLONE_NEWUSER 也是。。
完整的工程代码可以访问git仓库获取,鉴于国内网络,暂时先托管国内gitee仓库
接下来做什么
本次主要是使用go演示了创建不同的namespace,并用个简单的sh进程在Linux下做个演示。 这创建是创建了,剩下的初始化以及配置之路的操作会放在后面再慢慢介绍。如果有兴趣的可关注后面的文章。