使用golang理解Linux namespace(二)- clone()

上篇文章中,通过了unshare命令引出了命名空间的概念和实际使用例子,

当然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

[[email protected] gons]# vim ns-proc.go
[[email protected] gons]# go build ns-proc.go
[[email protected] 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 below, 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 below, 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下执行结果如下:

[[email protected] gons]# go build ns-proc.go
[[email protected] gons]# ./ns-proc
-[namespace-process]-#
-[namespace-process]-#

另外如果用strace命令是可以看到我们编译出来的ns-proc执行过程,其实就是调用clone(),并把CLONE_NEWUTS作为flag参数传递进去

[[email protected] 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
[[email protected] gons]# readlink /proc/self/ns/uts
uts:[4026531838]
[[email protected] 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下做个演示。 这创建是创建了,剩下的初始化以及配置之路的操作会放在后面再慢慢介绍。如果有兴趣的可关注后面的文章。