最近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