53. reexec and namespace

在查看namespace的用法时, 发现reexec包比较难理解 [1]

代码仓库:

package main

import (
    "fmt"
    "os"
    "log"
    "os/exec"
    "syscall"

    "github.com/docker/docker/pkg/reexec"
)


func init() {
    log.SetFlags(log.LstdFlags | log.Lshortfile)
    log.Println("run func init()")
    reexec.Register("nsInitialisation", nsInitialisation)
    log.Println("finish reexec.Register()")
    if reexec.Init() {
        log.Println("reexec.init() have been init()")
        os.Exit(0)
    }
    log.Println("run func init() finish")
}

func nsInitialisation() {
    log.Println(">> namespace setup code goes here <<")
    nsRun()
}

func nsRun() {
    cmd := exec.Command("/bin/sh")

    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    cmd.Env = []string{"PS1=-[ns-process]- # "}

    if err := cmd.Run(); err != nil {
        fmt.Printf("Error running the /bin/sh command - %s\n", err)
        os.Exit(1)
    }
}

func main() {
    log.Println("main() begin in first line")
    cmd := reexec.Command("nsInitialisation")
    log.Println("main() construct  reexec.Command()")
    log.Println(cmd.Path)
    log.Println(cmd.Args[0])
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWNS |
            syscall.CLONE_NEWUTS |
            syscall.CLONE_NEWIPC |
            syscall.CLONE_NEWPID |
            syscall.CLONE_NEWNET |
            syscall.CLONE_NEWUSER,
        UidMappings: []syscall.SysProcIDMap{
            {
                ContainerID: 0,
                HostID:      os.Getuid(),
                Size:        1,
            },
        },
        GidMappings: []syscall.SysProcIDMap{
            {
                ContainerID: 0,
                HostID:      os.Getgid(),
                Size:        1,
            },
        },
    }

    if err := cmd.Run(); err != nil {
        fmt.Printf("Error running the reexec.Command - %s\n", err)
        os.Exit(1)
    }
}

运行结果是:

user1@intel6248:~/go/src/github.com/Lylelee/ns-process$ ./ns-process
2020/08/28 17:14:46 ns_process.go:16: run func init()
2020/08/28 17:14:46 ns_process.go:18: finish reexec.Register()
2020/08/28 17:14:46 ns_process.go:23: run func init() finish
2020/08/28 17:14:46 ns_process.go:47: main() begin in first line
2020/08/28 17:14:46 ns_process.go:49: main() construct  reexec.Command()
2020/08/28 17:14:46 ns_process.go:50: /proc/self/exe
2020/08/28 17:14:46 ns_process.go:51: nsInitialisation
2020/08/28 17:14:46 ns_process.go:16: run func init()
2020/08/28 17:14:46 ns_process.go:18: finish reexec.Register()
2020/08/28 17:14:46 ns_process.go:27: >> namespace setup code goes here <<
-[ns-process]- # exit
2020/08/28 17:14:50 ns_process.go:20: reexec.init() have been init()

这里解析一下执行过程

./ns-process    执行可执行程序
                注意此时os.Args[0]是空
func init()     在main参数执行前会执行软件包中的init()函数
        2020/08/28 17:14:46 ns_process.go:16: run func init()
    reexec.Register("nsInitialisation", nsInitialisation)
        2020/08/28 17:14:46 ns_process.go:18: finish reexec.Register()
    if reexec.Init() == false   尝试运行新注册的nsInitialisation,失败,因为os.Args[0]是空
        2020/08/28 17:14:46 ns_process.go:23: run func init() finish
func main()     执行main函数
        2020/08/28 17:14:46 ns_process.go:47: main() begin in first line
    cmd := reexec.Command("nsInitialisation")   构造新命令,设置参数为nsInitialisation
        2020/08/28 17:14:46 ns_process.go:49: main() construct  reexec.Command()
        2020/08/28 17:14:46 ns_process.go:50: /proc/self/exe    将要执行的命令是自己,也就是当前进程 ns-process ?
        2020/08/28 17:14:46 ns_process.go:51: nsInitialisation  将要执行进程的参数
    cmd.run()
        /proc/self/exec nsInitialisation 执行命令
        func init()     在main参数执行前会执行软件包中的init()函数
                2020/08/28 17:14:46 ns_process.go:16: run func init()
            reexec.Register("nsInitialisation", nsInitialisation)   进程中也是需要register的
                2020/08/28 17:14:46 ns_process.go:18: finish reexec.Register()
            if reexec.Init() == true   尝试运行新注册的nsInitialisation,成功,因为os.Args[0]已经设置伪nsInitialisation,查看上一句 cmd := reexec.Command("nsInitialisation")

                func nsInitialisation() {
                    func nsRun() {

                        2020/08/28 17:14:46 ns_process.go:27: >> namespace setup code goes here <<

                        cmd := exec.Command("/bin/sh") 进入命令行


                        -[ns-process]- # exit

                2020/08/28 17:14:50 ns_process.go:20: reexec.init() have been init() 执行注册的函数成功

这里花了不少事件去理解他是怎么自己执行自己的。 做法是, 自己在执行时检查自己的参数os.Args[0]是否设置, 如果设置了就执行注册在这个参数下的函数或者时命令。 如果没有设置这个参数,或者设置了这个参数但是没有注册,那么直接退出就好了。

所以, 第一次在命令行输入命令开始执行时, 参数未设置,尝试调用注册的函数失败。 执行到main函数,用rexec.Command设置参数,再调用run, 这个时候就会fork自己。 以设置的参数查找注册函数,执行注册函数。这个时候注册号的函数就再新的命名空间中执行了。

这种设计方法, 感觉会让人比较费解。

据说这种fork自己的办法还可以把内存中可执行的二进制保存到硬盘, 这个样可以实现自己更新自己。

为了简化问题重写了一份比较好理解的代码 [2]

package main

import (
    "github.com/docker/docker/pkg/reexec"
    "log"
    "os"
)

func init() {
    log.SetFlags(log.LstdFlags | log.Lshortfile)
}

func CalmDown() {
    pid := os.Getpid()
    log.Println(pid,"CalmDown() Take a deep breath...")
    // do somthing more
    log.Println(pid, "CalmDown() Yes, I am calmdown already!")
}

func main() {

    pid := os.Getpid()
    log.Println(pid, "os argument: ", os.Args)

    reexec.Register("func1", CalmDown)

    log.Println(pid , "register func1")

    if reexec.Init() {
        log.Println(pid, "reexec have init")
        os.Exit(0)
    }

    log.Println(pid, "test init")

    cmd := reexec.Command("func1")

    log.Println(pid,cmd.Path)
    log.Println(pid,cmd.Args)

    output, err := cmd.CombinedOutput()

    if err != nil {
        log.Println(pid, "cmd run with error: ", err.Error())
        os.Exit(10)
    }
    log.Println(pid, "cmd output: ")
    log.Println(pid, string(output))
    log.Println(pid, "rexec demo finish")
}

输出结果

2020/08/29 11:01:29 reexec_usage.go:23: 65180 os argument:  [./reexec_usage]
2020/08/29 11:01:29 reexec_usage.go:27: 65180 register func1
2020/08/29 11:01:29 reexec_usage.go:34: 65180 test init
2020/08/29 11:01:29 reexec_usage.go:38: 65180 /proc/self/exe
2020/08/29 11:01:29 reexec_usage.go:39: 65180 [func1]
2020/08/29 11:01:29 reexec_usage.go:47: 65180 cmd output:
2020/08/29 11:01:29 reexec_usage.go:48: 65180 2020/08/29 11:01:29 reexec_usage.go:23: 65185 os argument:  [func1]
2020/08/29 11:01:29 reexec_usage.go:27: 65185 register func1
2020/08/29 11:01:29 reexec_usage.go:15: 65185 CalmDown() Take a deep breath...
2020/08/29 11:01:29 reexec_usage.go:17: 65185 CalmDown() Yes, I am calmdown already!
2020/08/29 11:01:29 reexec_usage.go:30: 65185 reexec have init

2020/08/29 11:01:29 reexec_usage.go:49: 65180 rexec demo finish
[1]https://medium.com/@teddyking/namespaces-in-go-reexec-3d1295b91af8
[2]https://play.golang.org/p/ArHJfulbgrO