手动创建类docker环境

手动创建类docker环境

最近docker特别的火,但是细究docker的原理机制。其实就是使用了cgroups+namespace完成资源的限制与隔离。现在我们手动创建一个namespace的容器,做到资源的隔离。

之前我们已经讨论了namespace的组成,现在我们通过手动的方式创建每种不同namespace的环境。创建不同的namespace主要使用clone()+特定关键字的方式进行。我们可以把clone返回的pid,所以container也是一种特殊的进程!

Mount namespaces CLONE_NEWNS Linux 2.4.19
UTS namespaces CLONE_NEWUTS Linux 2.6.19
IPC namespaces CLONE_NEWIPC Linux 2.6.19
PID namespaces CLONE_NEWPID Linux 2.6.24
Network namespaces CLONE_NEWNET 始于Linux 2.6.24 完成于 Linux 2.6.29
User namespaces CLONE_NEWUSER 始于 Linux 2.6.23 完成于 Linux 3.8

通过这个表格我们看到每个namespace完成时间不同,但是基于目前kernel版本已经为4.0.我们可以理解namespace部分基本完成。首先我们先定义一个模板:


#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");
  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, SIGCHLD, NULL);
  waitpid(child_pid, NULL, 0);
  return 0;
}

这里我们发现clone()的第二个参数是child_stack+STACK_SIZE,这里要说明下栈是从高地址往低地址走的,所以给出最后一个地址,也就是给出了栈的首地址。

##1.UTS namespace[1]

使用这个UTS namespace可以使得容器拥有自己的主机名,程序是要是使用clone+sethostname()配合,这个与fork一个进程特别相似。


// (needs root privileges (or appropriate capabilities))
//[...]
int child_main(void* arg)
{
  printf(" - World !\n");
  sethostname("In 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;
}

与fork()函数类似,fork()函数创建一个新的child_pid时,进程中的内容与父进程完全相同,当后续执行子进程功能时,使用execv()族函数覆盖子进程各个程序段,包括代码段数据段等。这里我们注意到clone()函数中的CLONE_NEWUTS关键字,然后在子函数中调用execv(child_args[0], child_args);

注:child_args省略在运行程序添加参数

运行结果:


lzz@localhost:~/container$ gcc -Wall main.c -o ns && sudo ./ns
 - Hello ?
 - World !
root@In Namespace:~/container$ # inside the container
root@In Namespace:~/container$ exit
lzz@localhost:~/container$ # outside the container

上面的这个namespace只做到主机名的隔离,其他子系统都没有还没有隔离,我们在proc下还是可以看到全局的信息。

##2.IPC namespace[2]

这里我们使用pipe进行同步,当创建child_pid时, checkpoint[0]为管道里的读取端,checkpoint[1]则为管道的写入端。当管道没有数据时,read()调用将默认的被阻塞,等待某些数据写入,从而达到同步的目的。


...
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;
}

这里在父进程下关闭close(checkpoint[1]);意味着父进程结束,子进程才能继续。

##3. PID namespace[3]

PID namespace可以做到容器内部的pid与容器外的隔离,也就是说都可以有pid 1的进程,当然容器内pid 1 的进程映射到容器外,拥有其他的pid 号。


...
int child_main(void* arg)
{
  char c;
 
  // init sync primitive
  close(checkpoint[1]);
  // wait...
  read(checkpoint[0], &c, 1);
 
  printf(" - [%5d] World !\n", getpid());
  sethostname("In Namespace", 12);
  execv(child_args[0], child_args);
  printf("Ooops\n");
  return 1;
}
 
int main()
{
  // init sync primitive
  pipe(checkpoint);
 
  printf(" - [%5d] Hello ?\n", getpid());
 
  int child_pid = clone(child_main, child_stack+STACK_SIZE,
      CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | SIGCHLD, NULL);
 
  // further init here (nothing yet)
 
  // signal "done"
  close(checkpoint[1]);
 
  waitpid(child_pid, NULL, 0);
  return 0;
}

这里我们看到在clone的标志里又加入了CLONE_NEWPID,然后在child_main中加入getpid(),我们可以发现容器的pid号。运行结果:


lzz@localhost:~/container$ gcc -Wall main-3-pid.c -o ns && sudo ./ns
 - [ 7823] Hello ?
 - [    1] World !
root@In Namespace:~/blog# echo "=> My PID: $$"
=> My PID: 1
root@In Namespace:~/blog# exit

这里我们发现在容器中,我们没有挂载proc文件系统,这里有一个问题,如果我们在容器里面挂载一个proc,在容器外使用top、ps -aux会提示出错。需要重新挂载proc在根目录下,因为这里我们并没有隔离文件系统。

##4.CLONE_NEWNS[4]

这个clone选项,可以保证在容器内的文件挂载操作,不影响父容器的使用,也就解决了上面proc挂载损坏父容器空间的问题。


...
// sync primitive
int checkpoint[2];
....
int child_main(void* arg)
{
  char c;
 
  // init sync primitive
  close(checkpoint[1]);
 
  // setup hostname
  printf(" - [%5d] World !\n", getpid());
  sethostname("In Namespace", 12);
 
  // remount "/proc" to get accurate "top" && "ps" output
  mount("proc", "/proc", "proc", 0, NULL);
 
  // wait...
  read(checkpoint[0], &c, 1);
 
  execv(child_args[0], child_args);
  printf("Ooops\n");
  return 1;
}
 
int main()
{
  // init sync primitive
  pipe(checkpoint);
 
  printf(" - [%5d] Hello ?\n", getpid());
 
  int child_pid = clone(child_main, child_stack+STACK_SIZE,
      CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);
 
  // further init here (nothing yet)
 
  // signal "done"
  close(checkpoint[1]);
 
  waitpid(child_pid, NULL, 0);
  return 0;
}

这个时候我们运行这个程序:


lzz@localhost:~/container$ gcc -Wall ns.c -o ns && sudo ./ns
 - [14472] Hello ?
 - [    1] World !
root@In Namespace:~/blog# mount -t proc proc /proc
root@In Namespace:~/blog# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  1.0  0.0  23620  4680 pts/4    S    00:07   0:00 /bin/bash
root        79  0.0  0.0  18492  1328 pts/4    R+   00:07   0:00 ps aux
root@In Namespace:~/blog# exit

可以发现容器内的ps读取的是容器内部的proc文件系统!

这里我们就要考虑docker中,比如我们从docker hub下pull下一个镜像,在这个镜像中必然存在一个文件系统rootfs,包括各种配置信息,基本的链接库。各种特殊的文件系统:/dev,/sysfs等。这个就需要我们进行裁剪! 这里主要存在的目录有:


bin etc lib lib64 mnt proc run sbin sys tmp usr/bin

我们需要根据自己的architecture配置这些目录。最后使用mount,将容器中的文件系统,都挂载到这些目录下。挂载完毕后使用chroot与chdir隔离目录:


if ( chdir("./rootfs") != 0 || chroot("./") != 0 ){
    perror("chdir/chroot");
}

##5.User namespace[5]

这个namespace主要是管理用户的UID,GID,主要原理通过读写/proc//uid_map 和 /proc//gid_map 这两个文件。这两个文件的格式为:ID-inside-ns ID-outside-ns length 。 核心函数:


void set_map(char* file, int inside_id, int outside_id, int len) {
    FILE* mapfd = fopen(file, "w");
    if (NULL == mapfd) {
        perror("open file error");
        return;
    }
    fprintf(mapfd, "%d %d %d", inside_id, outside_id, len);
    fclose(mapfd);
}
 
void set_uid_map(pid_t pid, int inside_id, int outside_id, int len) {
    char file[256];
    sprintf(file, "/proc/%d/uid_map", pid);
    set_map(file, inside_id, outside_id, len);
}
 
void set_gid_map(pid_t pid, int inside_id, int outside_id, int len) {
    char file[256];
    sprintf(file, "/proc/%d/gid_map", pid);
    set_map(file, inside_id, outside_id, len);
}

##6.Network namespace[6]

这个namespace主要完成的是将一块物理网卡虚拟出多快虚拟网卡,主要命令:


# Create a "demo" namespace
ip netns add demo
 
# create a "veth" pair
ip link add veth0 type veth peer name veth1
 
# and move one to the namespace
ip link set veth1 netns demo
 
# configure the interfaces (up + IP)
ip netns exec demo ip link set lo up
ip netns exec demo ip link set veth1 up
ip netns exec demo ip addr add xxx.xxx.xxx.xxx/30 dev veth1
ip link set veth0 up
ip addr add xxx.xxx.xxx.xxx/30 dev veth0

运用在代码中就是:


...
int child_main(void* arg)
{
  char c;
 
  // init sync primitive
  close(checkpoint[1]);
 
  // setup hostname
  printf(" - [%5d] World !\n", getpid());
  sethostname("In Namespace", 12);
 
  // remount "/proc" to get accurate "top" && "ps" output
  mount("proc", "/proc", "proc", 0, NULL);
 
  // wait for network setup in parent
  read(checkpoint[0], &c, 1);
 
  // setup network
  system("ip link set lo up");
  system("ip link set veth1 up");
  system("ip addr add 169.254.1.2/30 dev veth1");
 
  execv(child_args[0], child_args);
  printf("Ooops\n");
  return 1;
}
 
int main()
{
  // init sync primitive
  pipe(checkpoint);
 
  printf(" - [%5d] Hello ?\n", getpid());
 
  int child_pid = clone(child_main, child_stack+STACK_SIZE,
      CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET | SIGCHLD, NULL);
 
  // further init: create a veth pair
  char* cmd;
  asprintf(&cmd, "ip link set veth1 netns %d", child_pid);
  system("ip link add veth0 type veth peer name veth1");
  system(cmd);
  system("ip link set veth0 up");
  system("ip addr add 169.254.1.1/30 dev veth0");
  free(cmd);
 
  // signal "done"
  close(checkpoint[1]);
 
  waitpid(child_pid, NULL, 0);
  return 0;
}

对于网络这一块,要想使得容器内的进程与容器外的进程通信,需要架设网桥,具体可以查看相关文档。

##Final

我们这里看一下父子容器的namespace:

父:


lzz@localhost:~$ sudo ls -l /proc/4599/ns
total 0
lrwxrwxrwx 1 root root 0  4月  7 22:01 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0  4月  7 22:01 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0  4月  7 22:01 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0  4月  7 22:01 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0  4月  7 22:01 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0  4月  7 22:01 uts -> uts:[4026531838]

子:


lzz@localhost:~$ sudo ls -l /proc/4600/ns
total 0
lrwxrwxrwx 1 root root 0  4月  7 22:01 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0  4月  7 22:01 mnt -> mnt:[4026532520]
lrwxrwxrwx 1 root root 0  4月  7 22:01 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0  4月  7 22:01 pid -> pid:[4026532522]
lrwxrwxrwx 1 root root 0  4月  7 22:01 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0  4月  7 22:01 uts -> uts:[4026532521]

我们可以发现,其中的ipc,net,user是相同的ID,而mnt,pid,uts都是不同的。如果两个进程指向的namespace编号相同,就说明他们在同一个namespace下,否则则在不同namespace里面。

参考:

[1] http://lwn.net/Articles/531245/

[2] http://coolshell.cn/articles/17010.html

[3] http://lwn.net/Articles/532741/

[4] http://coolshell.cn/articles/17029.html

[5] http://lwn.net/Articles/539941/

[6] http://lwn.net/Articles/580893/

[7]

https://blog.jtlebi.fr/2014/01/12/introduction-to-linux-namespaces-part-4-ns-fs/ http://www.cnblogs.com/nufangrensheng/p/3579378.html