go setns 的一些问题记录

记录一些关于 golang setns 的问题。

关于 mnt ns

setns 中提到:

A process can’t join a new mount namespace if it is sharing
filesystem-related attributes (the attributes whose sharing is
controlled by the clone(2) CLONE_FS flag) with another
process.

如果当前进程与其他进程共享了文件系统相关属性,就无法直接改变当前进程的 mntns,这些属性是在 clone 时,使用了 CLONE_FS 设置的。当设置了 CLONE_FS,则当前进程使用 chroot、chdir、umask 这些函数时,会同时作用与这个线程组内的进程。
与之相同的还有 User namespaces , umaske 相关。

关于 pid ns

当我们使用 setns 设置了 pidns 时,当前进程无法使用 clone CLONE_THREAD ,参考文档(搜索 CLONE_THREAD was specified)。我们可以先 clone 一个子进程,子进程会继承当前进程的 ns,然后在子进程中可以使用 CLONE_THREAD。关于为什么后续需要再跟进一下。

关于 go

关于这段话,是通过一些介绍 golang 调度的文章,和自己代码测试得出的结论,不一定准确。golang 是多线程,一段程序运行在一个协程中,这个协程在运行时可能运行在不同的线程上,就是这个协程可能现在运行在这个线程上,待会可能就运行在另一个线程上,这是 golang 调度决定的。但是我们 setns 是作用与当前线程的,所以我们需要将我们需要特殊 ns 的代码程序,绑定在 setns 后的线程上,让他一直在这个特殊的线程上运行。我们可以使用 runtime.LockOSThread()。这就完成了协程与线程的绑定。
如果我们需要使用的 ns 包括 mntns、utsns,那么我们无法纯 go 代码来完成,因为这两个 ns 在线程的情况下都设置,都可能会出错,上面有给出原因。所以需要借助 cgo 来完成,在 cgo 完成对主线程 ns 的设置。主要参考了 runc 的实现,文章 参考1参考2。自己根据 runc 源码进行提取的demo 贴在后面(就是粘贴复制)。关于 pid ns 的问题也能介绍为什么 runc cgo 中 clone 了两次进程,因为后续 golang 会 pthread_create 线程。
使用 cgo 我们无法直接获取传递给程序的参数,可用的做法是,通过 go exec 创建一个自身运行进程,然后通过传递环境变量的方式,传递给 cgo 参数值。体现在 runc 中就是 runc create → runc init ,runc 中有很多细节,他通过环境变量传递 netlink fd,然后进行通信。我们可以简单的使用 环境变量来传递 docker pid 的值。即 go -pid xxx → go init。

当时获取 netstat -anpt,因为需要考虑 mnt 后没有 netstat 的问题,所以手动实现一下 netstat,去遍历 /proc/pid/net 下的数据,因为当时使用的是 runtime.LockOSThread() 和 golang 的 syscall.RawSyscall 308 去调用 setns 来设置 netns,会发现执行结果不稳定,一会是设置的 netns 的结果,一会是当前 netns 的结果。解决了很久,后面才发现,/proc/pid/net 是一个 link,他是指向 pid 线程的主线程 /proc/pid/net,所以只有当 runtime.LockOSThread() 正好绑定在主线程上,setns 后得到的结果才是正确的,如果没有绑定在主线程上,那么即使在当前线程 setns ,然后去读取 /proc/pid/net 得到的结果仍然是主线程没有 setns 的结果。所以需要修改为 /proc/self/task/pid/net

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
//nsenter.c
#include <setjmp.h>
#include <stdio.h>
#include <linux/sched.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

#define JUMP_PARENT 0x00
#define JUMP_CHILD 0xA0
#define JUMP_INIT 0xA1

#define STACK_SIZE 65536
#define DOCKER_PID_EVN_STR "DOCKER_PID"

struct clone_t
{
jmp_buf *env;
int jmpval;
};
void joinNamespace(char *p)
{
int i;
char nspath[1024];
char *namespaces[] = {
"ipc",
"uts",
"net",
"pid",
"mnt",
};
for (i = 0; i < 5; i++)
{
sprintf(nspath, "/proc/%s/ns/%s", p, namespaces[i]);
int fd = open(nspath, O_RDONLY);
if (setns(fd, 0) == -1)
{
fprintf(stderr, "setns on %s namespace failed\n", namespaces[i]);
exit(0);
}
else
{
fprintf(stdout, "setns on %s namespace succeeded\n", namespaces[i]);
}
close(fd);
}
return;
}
static int childFn(void *args)
{
// printf("child thread start\n");
struct clone_t *ca = (struct clone_t *)args;
longjmp(*ca->env, ca->jmpval);
}
void cloneChild(jmp_buf *env, int jmpval)
{
int pid;
void *stack = malloc(STACK_SIZE);
struct clone_t ca = {
.jmpval = jmpval,
.env = env,
};
pid = clone(childFn, (char *)stack + STACK_SIZE, SIGCHLD | CLONE_PARENT, &ca);
if (pid == -1)
{
free(stack);
printf("clone child error\n");
return;
}
}
void nsenter()
{
jmp_buf env;
char *dockerPIDENV = getenv(DOCKER_PID_EVN_STR);
if (!dockerPIDENV)
{
return;
}
switch (setjmp(env))
{
case JUMP_PARENT:
cloneChild(&env, JUMP_CHILD);
exit(0);
case JUMP_CHILD:
joinNamespace(dockerPIDENV);
cloneChild(&env, JUMP_INIT);
exit(0);
case JUMP_INIT:
break;
}
}

//main.go
package main

/*
#include "nsenter.c"
__attribute__((constructor)) void init(void) {
nsenter();
}
*/
import "C"
import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"runtime"
)
const (
DOCKER_PID_ENV_STR = "DOCKER_PID"
)
func main() {
initFlag := flag.Bool("init", false, "runc init")
dockerPid := flag.Int("pid", 0, "set docker pid")
flag.Parse()
if *initFlag {
initProcess()
} else {
if *dockerPid == 0 {
flag.Usage()
return
}
cmd := exec.Command("/proc/self/exe", "-init")
cmd.Args[0] = os.Args[0]
cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%d", DOCKER_PID_ENV_STR, *dockerPid))
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Fatal(err)
}
stderr, err := cmd.StderrPipe()
if err != nil {
log.Fatal(err)
}
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
stdoutStr, _ := ioutil.ReadAll(stdout)
fmt.Printf("%s\n", stdoutStr)
stderrStr, _ := ioutil.ReadAll(stderr)
fmt.Printf("%s\n", stderrStr)
if err := cmd.Wait(); err != nil {
log.Fatal(err)
}
}
}
func initProcess() {
runtime.LockOSThread()
cmd := exec.Command("netstat", "-anpt")
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Fatal(err)
}
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
slurp, _ := ioutil.ReadAll(stdout)
fmt.Printf("%s\n", slurp)
if err := cmd.Wait(); err != nil {
log.Fatal(err)
}
runtime.UnlockOSThread()
}