如何构造你自己的容器---1
金蝶云社区-墨家总院
墨家总院
18人赞赏了该文章 727次浏览 未经作者许可,禁止转载编辑于2018年12月04日 20:04:32

(本文独家发布在金蝶云社区上)

简单介绍

开源项目 LXC 是Linux平台上著名的虚拟化工具。在内部,LXC是依赖了Linux内核中三个主要的隔离方案:

  • Chroot,即根目录改变

  • Cgroups,即群组控制

  • Namespaces,即命名空间

在这篇文章里,我主要着重描述第三个隔离方案:命名空间(Namespace)。Linux内核版本3.12之后,Linux支持6种命名空间:

  1. UTS( UNIX Time-sharing System): 基于UTS的主机名命名空间。

  2. IPC: 基于进程间通信的命名空间。

  3. PID: 基于chroot而形成新的进程树的命名空间。

  4. NS: 基于挂载点的命名空间。

  5. NET: 网络访问,也包括网络接口。

  6. USER: 将虚拟的本地的用户id映射到真实的本地用户上。

另外,这篇文章的主要内容来自Yet another enthusiast blog!, 包括代码实例。写这篇文章的主要目的就是加深自己对虚拟化的理解。这篇文章先介绍第一个UTS。


Namespace: UTS

clone 系统调用

当我们使用clone系统调用从母进程创建新的进程的时候,有一个标记参数。因此,先看下clone系统调用的定义:

/* Prototype for the glibc wrapper function */
#define _GNU_SOURCE       
#include <sched.h>       

int clone(int (*fn)(void *), void *child_stack,                 
          int flags, void *arg, ...                 
          /* pid_t *ptid, void *newtls, pid_t *ctid */ );

首先,简要说明一下一些重要的参数是很有必要的。clone 
函数的第一个参数是用来运行新进程的函数指针,就像我们在做C语言编程时的入口函数main。第二个参数是新进程运行时需要的堆栈内存。第三个参数是标记变量,它对于命名空间技术很重要,因为上面6种命名空间需要被这个标记变量配置。

CLONE_NEWUTS

基于上面的描述,如果我们想要使用任何一种命名空间的功能,我们只需要在调用clone函数的时候制定相应的标记。因此在这个章节里,CLONE_NEWUTS是我们的目标。下面是来自于Linux手册的关于CLONE_NEWUTS的解释:


CLONE_NEWUTS (since Linux 2.6.19):

  • If CLONE_NEWUTS is set, then create the process in a new UTS namespace, whose identifiers are initialized by duplicating the identifiers from the UTS namespace of the calling process. If this flag is not set, then (as with fork(2)) the process is created in the same UTS namespace as the calling process. This flag is intended for the implementation of containers.

  • A UTS namespace is the set of identifiers returned by uname(2); among these, the domain name and the hostname can be modified by setdomainname(2) and sethostname(2), respectively. Changes made to the identifiers in a UTS namespace are visible to all other processes in the same namespace, but are not visible to processes in other UTS namespaces.

  • Only a privileged process (CAP_SYS_ADMIN) can employ CLONE_NEWUTS.


大体意思是当调用clone时,设置了CLONE_NEWUTS后,新的进程将在新的命名空间下,具体看下面的代码实例。

简单的代码实例情景

如何使用它?

当这个标记被设置以后,新生成的进程再调用sethostname设置一个新的主机名字的时候,新进程将处在一个新的主机名字下,与其母进程截然不同的主机名字。代码如下:

#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)

static char child_stack[STACK_SIZE];

char* const child_args[] = {    
    "/bin/bash",
    NULL
};

int child_main(void* arg){    
    printf(" - World !\n");    
    sethostname("Alexander Namespace", 12);    
    execv(child_args[0], child_args);    
    printf("Ooops\n");    
    return 1;
}

int main(){    
    printf(" - Hello ?\n");    
    int child_pid = clone(child_main, child_stack+STACK_SIZE,        
        CLONE_NEWUTS | SIGCHLD, NULL);    
    
    waitpid(child_pid, NULL, 0);    
    return 0;
}


以上代码表明,在新进程里运行了child_main函数后,新生成的basg在新的命名空间下。当再运行exit命令后,原来的命名空间的bash就又回来了。

Namespace: IPC

如果想要启用IPC命名空间,只要在调用clone的时候将第三个参数的标记位设成“CLONE_NEWIPC即可(当然多个标记位是可以以或的方式共存,这里先单独设置上)。我们就可以在新生成的进程里做任何进程间通信的事情。

但是有个问题?我如何与母进程进行通信?因为貌似子进程和母进程已经做了一个了断了。有一个通用的办法就是在让子进程全面分离出去前做一些微小的工作即可。幸运的是,不是所有事物都做了彻底的分割。clone系统调用将内存分享给了母进程。因此你可以用以下方式与母进程做进程间通信(IPC):

  • signal

  • poll

  • 套接字(socket)

  • 文件或者文件描述符

当然因为进程上下文都已经改变,这些方式不是很高效。另外我们其实要做的就是完全隔离,而不是藕断丝连。所以管道是更好的方式。


首先我们先初始化一对管道,我们叫他们“检查点”:

// required headers:
#include <unistd.h>

// global status:
int checkpoint[2];

// [parent] init:
pipe(checkpoint);


这个主要想法是在父进程端触发一个关闭事件,然后等待并确保子进程read到“EOF”消息。理解的关键是在收到“EOF”消息后,所有的操作写的文件描述符必须关闭。因此,在子进程里,等待之前做的第一件事就是关掉自己的写的文件描述符。

// required headers: 
#include <unistd.h>

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


实际的发送信号很直接:

  1. 在父进程里关掉写的文件描述符(fd)

  2. 等待子进程发送EOF信号。

// required headers: 
#include <unistd.h>

// [child] wait:
char c;

// stub char
read(checkpoint[0], &c, 1);

// [parent] signal ready code:
close(checkpoint[1]);


如果我们把这一切放到UTS命名空间的例子中,它是这样的:


#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”,但是我故意留着他来表明,多个命名空间方法可以一起使用。

这就是所有有关ipc的东西。其实ipc本身没什么复杂的。但是一旦涉及到我们之后将要处理的父进程与子进程之间的同步,ipc将会变得微妙起来。这是管道技术作为一个确实可行的方案的一个实例。而且在实际产品种,这种技术也是经常被用到。


参考

  1. https://blog.yadutaf.fr/2013/12/22/introduction-to-linux-namespaces-part-1-uts/

  2. Linux Maunal.


赞 18