Docker Namespace资源隔离源码深度剖析-Docker商业环境实战

专一于大数据及容器云核心技术解密,可提供全栈的大数据+云原平生台咨询方案,请持续关注本套博客。若有任何学术交流,可随时联系。更多内容请关注《数据云技术社区》公众号。 linux

1 Namespace 概述

  • Namespace是将内核的全局资源作封装,使得每一个Namespace都有一份独立的资源,所以不一样的进程在各自的Namespace内对同一种资源的使用不会互相干扰。实际上,Linux内核实现namespace的主要目的就是为了实现轻量级虚拟化(容器)服务。
  • 在同一个namespace下的进程能够感知彼此的变化,而对外界的进程一无所知。这样就可让容器中的进程产生错觉,仿佛本身置身于一个独立的系统环境中,以此达到独立和隔离的目的。
  • namespace的API包括clone()、setns()以及unshare(),还有/proc下的部分文件。为了肯定隔离的究竟是哪一种namespace,在使用这些API时,一般须要指定如下六个常数的一个或多个,经过|(位或)操做来实现。这六个参数分别是CLONE_NEWIPC、CLONE_NEWNS、CLONE_NEWNET、CLONE_NEWPID、CLONE_NEWUSER和CLONE_NEWUTS。
  • 经过clone()建立新进程的同时建立namespace
IPC:隔离System V IPC和POSIX消息队列。
Network:隔离网络资源。
Mount:隔离文件系统挂载点。每一个容器能看到不一样的文件系统层次结构。
PID:隔离进程ID。
UTS:隔离主机名和域名。
User:隔离用户ID和组ID。

int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);

参数child_func传入子进程运行的程序主函数。
参数child_stack传入子进程使用的栈空间
参数flags表示使用哪些CLONE_*标志位
参数args则可用于传入用户参数

clone()其实是传统UNIX系统调用fork()的一种更通用的实现方式,它能够经过flags来控制使用多
少功能。一共有二十多种CLONE_*的flag(标志位)参数用来控制clone进程的方方面面(如是否与父
进程共享虚拟内存等等)。
复制代码
  • 经过setns()加入一个已经存在的namespace
在进程都结束的状况下,也能够经过挂载的形式把namespace保留下来,保留namespace的目的天然是
为之后有进程加入作准备。经过setns()系统调用,你的进程从原先的namespace加入咱们准备好的新
namespace,使用方法以下:

int setns(int fd, int nstype)
参数fd表示咱们要加入的namespace的文件描述符。上文已经提到,它是一个指向/proc/[pid]/ns目录
的文件描述符,能够经过直接打开该目录下的连接或者打开一个挂载了该目录下连接的文件获得。

参数nstype让调用者能够去检查fd指向的namespace类型是否符合咱们实际的要求。若是填0表示不检查。
复制代码
  • 经过unshare()在原先进程上进行namespace隔离
后要提的系统调用是unshare(),它跟clone()很像,不一样的是,unshare()运行在原先的进程上,
不须要启动一个新进程,使用方法以下:

int unshare(int flags);
调用unshare()的主要做用就是不启动一个新进程就能够起到隔离的效果,至关于跳出原先的
namespace进行操做。这样,你就能够在原进程进行一些须要隔离的操做。Linux中自带的
unshare命令,就是经过unshare()系统调用实现的。
复制代码
  • 以下Docker源码,呈现了namespace的建立过程。

2 Namespace源码执行流程

2.1 容器对象建立阶段

  • 具体流程请参考《Docker源码解析》
startContainer() => createContainer() => loadFactory() => libcontainer.New() 
复制代码

2.2 容器对象运行阶段(nsexec)

  • 总体流程以下
startContainer() => runner.run() => newProcess() => runner.container.Run(process) 
=> linuxContainer.strat() => linuxContainer.newParentProcess(process) 
=>linuxContainer.commandTemplate() => linuxContaine.newInitProcess() =>parent.start() 
=> initProcess.start()
复制代码
  • linuxContainer.strat()
  • 首先建立newParentProcess,生成InitProcess,实现对container的process进行Namespace相关设置如uid/gid、pid、uts、ns、cgroup等。
func (c *linuxContainer) newParentProcess(p *Process, doInit bool) (parentProcess, error) {
	parentPipe, childPipe, err := newPipe()
	if err != nil {
		return nil, newSystemError(err)
	}
	cmd, err := c.commandTemplate(p, childPipe)
	if err != nil {
		return nil, newSystemError(err)
	}
	if !doInit {
		return c.newSetnsProcess(p, cmd, parentPipe, childPipe), nil
	}
	return c.newInitProcess(p, cmd, parentPipe, childPipe)
}
复制代码
  • 建立容器的 init 进程时相关namespace 配置项(newInitProcess)
  • initProcess.start()。
  • InitProcess.start() 容器的初始化配置,此处 cmd.start() 调用实则是 runC init命令执行
  • InitProcess.start() 容器的初始化配置,此处 cmd.start() 调用实则是 runC init命令执行:
  • Init() 完成容器的相关初始化配置(网络/路由、rootfs、selinux、console、主机名、apparmor、Sysctl、seccomp、capability 等)

func (l *LinuxFactory) StartInitialization() (err error) {
  //...
    i, err := newContainerInit(it, pipe, consoleSocket, fifofd) 
  //...
  // newContainerInit()返回的initer实现对象的Init()方法调用 "linuxStandardInit.Init()"
  return i.Init()                    
}

func (l *linuxStandardInit) Init() error {
  //...
  // 配置network,
  //  配置路由
  // selinux配置
  // + 准备rootfs
  // 配置console
  // 完成rootfs设置
  // 主机名设置
  // 应用apparmor配置
  // Sysctl系统参数调节
  // path只读属性配置
  // 告诉runC进程,咱们已经完成了初始化工做
  // 进程标签设置
  // seccomp配置
  // 设置正确的capability,用户以及工做目录
  // 肯定用户指定的容器进程在容器文件系统中的路径
  // 关闭管道,告诉runC进程,咱们已经完成了初始化工做
  // 在exec用户进程以前等待exec.fifo管道在另外一端被打开
  // 咱们经过/proc/self/fd/$fd打开它
  // ......
  // 向exec.fifo管道写数据,阻塞,直到用户调用`runc start`,读取管道中的数据
  // 此时当前进程已处于阻塞状态,等待信号执行后面代码
  //
    if _, err := unix.Write(fd, []byte("0")); err != nil {
        return newSystemErrorWithCause(err, "write 0 exec fifo")
    }
  // 关闭fifofd管道 fix CVE-2016-9962
  // 初始化Seccomp配置
  // 调用系统exec()命令,执行entrypoint
    if err := syscall.Exec(name, l.config.Args[0:], os.Environ()); err != nil {
        return newSystemErrorWithCause(err, "exec user process")
    }
    return nil
}


复制代码

3 nsenter源码执行流程

  • Nsexec() 为 nsenter 主干执行逻辑代码,全部 namespaces 配置都在此 func 内执行完成,clone_parent就是实现Namespace建立的基本。
void nsexec()
{
	char *namespaces[] = { "ipc", "uts", "net", "pid", "mnt" };
	const int num = sizeof(namespaces) / sizeof(char *);
	jmp_buf env;
	char buf[PATH_MAX], *val;
	int i, tfd, child, len, pipenum, consolefd = -1;
	pid_t pid;
	char *console;

	val = getenv("_LIBCONTAINER_INITPID");
	if (val == NULL)
		return;

	pid = atoi(val);
	snprintf(buf, sizeof(buf), "%d", pid);
	if (strcmp(val, buf)) {
		pr_perror("Unable to parse _LIBCONTAINER_INITPID");
		exit(1);
	}

	val = getenv("_LIBCONTAINER_INITPIPE");
	if (val == NULL) {
		pr_perror("Child pipe not found");
		exit(1);
	}

	pipenum = atoi(val);
	snprintf(buf, sizeof(buf), "%d", pipenum);
	if (strcmp(val, buf)) {
		pr_perror("Unable to parse _LIBCONTAINER_INITPIPE");
		exit(1);
	}

	console = getenv("_LIBCONTAINER_CONSOLE_PATH");
	if (console != NULL) {
		consolefd = open(console, O_RDWR);
		if (consolefd < 0) {
			pr_perror("Failed to open console %s", console);
			exit(1);
		}
	}

	/* Check that the specified process exists */
	snprintf(buf, PATH_MAX - 1, "/proc/%d/ns", pid);
	tfd = open(buf, O_DIRECTORY | O_RDONLY);
	if (tfd == -1) {
		pr_perror("Failed to open \"%s\"", buf);
		exit(1);
	}

	for (i = 0; i < num; i++) {
		struct stat st;
		int fd;

		/* Symlinks on all namespaces exist for dead processes, but they can't be opened */ if (fstatat(tfd, namespaces[i], &st, AT_SYMLINK_NOFOLLOW) == -1) { // Ignore nonexistent namespaces. if (errno == ENOENT) continue; } fd = openat(tfd, namespaces[i], O_RDONLY); if (fd == -1) { pr_perror("Failed to open ns file %s for ns %s", buf, namespaces[i]); exit(1); } // Set the namespace. if (setns(fd, 0) == -1) { pr_perror("Failed to setns for %s", namespaces[i]); exit(1); } close(fd); } if (setjmp(env) == 1) { // Child if (setsid() == -1) { pr_perror("setsid failed"); exit(1); } if (consolefd != -1) { if (ioctl(consolefd, TIOCSCTTY, 0) == -1) { pr_perror("ioctl TIOCSCTTY failed"); exit(1); } if (dup3(consolefd, STDIN_FILENO, 0) != STDIN_FILENO) { pr_perror("Failed to dup 0"); exit(1); } if (dup3(consolefd, STDOUT_FILENO, 0) != STDOUT_FILENO) { pr_perror("Failed to dup 1"); exit(1); } if (dup3(consolefd, STDERR_FILENO, 0) != STDERR_FILENO) { pr_perror("Failed to dup 2"); exit(1); } } // Finish executing, let the Go runtime take over. return; } // Parent // We must fork to actually enter the PID namespace, use CLONE_PARENT // so the child can have the right parent, and we don't need to forward
	// the child's exit code or resend its death signal. child = clone_parent(&env); if (child < 0) { pr_perror("Unable to fork"); exit(1); } len = snprintf(buf, sizeof(buf), "{ \"pid\" : %d }\n", child); if (write(pipenum, buf, len) != len) { pr_perror("Unable to send a child pid"); kill(child, SIGKILL); exit(1); } exit(0); } 复制代码

4 总结

Docker Namespace源码隔离过于高深,本文先浅析于此。bash

专一于大数据及容器云核心技术解密,可提供全栈的大数据+云原平生台咨询方案,请持续关注本套博客。若有任何学术交流,可随时联系。更多内容请关注《数据云技术社区》公众号。 网络

相关文章
相关标签/搜索