Linux命名空间学习教程(二) IPC


【编者的话】Docker核心解决的问题是利用LXC来实现类似VM的功能,从而利用更加节省的硬件资源提供给用户更多的计算资源。而 LXC所实现的隔离性主要是来自内核的命名空间, 其中pid、net、ipc、mnt、uts 等命名空间将容器的进程、网络、消息、文件系统和hostname 隔离开。本文是Linux命名空间系列教程的第二篇,重点介绍了IPC命名空间。DockerOne在撸代码的基础上进行了校对和整理。

继上一篇 关于UTS namespace的文章(hostname隔离),我们将继续学习面向安全的namespace:IPC(Inter-Process Communications,进程间通信)。如果你尚未阅读UTS部分,我强烈建议你先阅读这个系列的第一篇文章,了解一下linux namespace的隔离机制。

要激活IPC namespace,只需要把“CLONE_NEWPIC”标记添加到“clone”的调用中,而不需要其它额外的步骤。IPC namespace也能和其他namespace组合使用。

一旦激活,你将能够如同往常一样创建IPC,甚至是命名一个,并且不会与其他任何应用冲突。

但是,请先等一下!我的“父进程”和“子进程”现在不是隔离的吗?那么如果我需要让它们通信,应该如何解决这一问题?

好问题,一个常规方案是在child全权控制之前,在parent中增加一些额外的步骤。幸运的是,并非所有的东西都是隔离的,clone和它的parent分享内存空间,因此你依旧可以使用:
  • 信号(signal)
  • poll内存
  • 套接字(sockets)
  • 使用文件(file)和文件描述符(fd)


由于上下文(context)的改变,使用signal也许不是最佳方案。而使用poll memory来做通信则太TM低效了!

如果你并不计划完全隔离网络栈,你可以使用sockets,而文件系统也是类似。但是,在这个系列的例子里,这正是我们要做的事:一步步隔离一切

一个鲜有人知/鲜有人用的方案是监视一对pipe上的事件。事实上,这是Lennart Poettering在Systemd's "nspawn"中使用过的技术。我会在这里引入这种极其强大的技术。这也正是我们在下一篇文章中所依赖的技术。

首先,我们需要初始化一对pipe。让我们称之为“检查点”。
checkpoint-global-init.c
// required headers:

include <unistd.h>

// global status:
int checkpoint[2];
// [parent] init:
pipe(checkpoint);

这里的思想是,在parent中触发一个“close”事件,并且在child的读取端等待“EOF”被接收。当EOF被接收后,所有写fd的文件必须被关闭,这是我们需要理解的最重要的东西。因此,在child等待前,应先关闭我们写fd的副本。
checkpoint-child-init.c
// required headers:

include <unistd.h>

// [child] init:
close(checkpoint[1]);

现在,“信号”是很直接的:
1. 在parent中关闭写fd
2. 在child中等待EOF

checkpoint-signal.c
// required headers:

include <unistd.h>

// [child] wait:
char c; // stub char
read(checkpoint[0], &amp;c, 1);
// [parent] signal ready code:
close(checkpoint[1]);


如果我们将它和第一个例子中的UTS namespace放在一起,将是如下样子:
main-2-ipc.c

define _GNU_SOURCE

include <sys/types.h>

include <sys/wait.h>

include <stdio.h>

include <sched.h>

include <signal.h>

include <unistd.h>

define STACK_SIZE (1024 * 1024)

// sync primitive
int checkpoint[2];
static char child_stack[STACK_SIZE];
char* const child_args[] = {
"/bin/bash",
NULL
};
int child_main(void* arg) {
char c;
// init sync primitive
close(checkpoint[1]);
// wait...
read(checkpoint[0], &c, 1);
printf(" - World !\n");
sethostname("In Namespace", 12);
execv(child_args[0], child_args);
printf("Ooops\n");
return 1;

int main() {
// init sync primitive
pipe(checkpoint);
printf(" - Hello ?\n");
int child_pid = clone(child_main, child_stack+STACK_SIZE,
  CLONE_NEWUTS | CLONE_NEWIPC | SIGCHLD, NULL);
// some damn long init job
sleep(4);
// signal "done"
close(checkpoint[1]);
waitpid(child_pid, NULL, 0);
return 0;
}

由于需要高级特性,我们需要root或者对等的权限来运行这段代码。很显然,没有必要在这个例子中保留“CLONE_NEWUTS”。我保留它仅仅是为了展示多个namespace也许可以一起使用。

这就是IPC的所有东西,它本身并不复杂。正如我们接下来要做的,它只有在parent/child同步时才变得复杂,而这正需要“pipe”技术来作为一个方便的解决方案。产品中已经使用了这个技术。

下一篇,是我(作为系统管理员)最喜欢的部分:PID namespaces。

原文链接:Introduction to Linux namespaces – Part 2: IPC(翻译:孙科 审校:李颖杰)

-----------------------------------------
Linux命名空间学习教程(一) UTS
Linux命名空间学习教程(二) IPC
Linux命名空间学习教程(三) PID
Linux命名空间学习教程(四)NS(FS)
Linux命名空间学习教程(五)NET

0 个评论

要回复文章请先登录注册