






















在极客时间上学完了 手把手带你写个最精简的 docker 这门课程 学着写了个简易docker。
容器其实是一组隔离的进程。运行一个容器,其实是启动一个进程,然后对它做出各项限制,隔离它与外界以及外界对它的影响。就像给它套了个壳,让它在里面静静的运行。
1.主函数
int main(int argc, char *argv[]) {
if (strcmp(argv[1], "run") == 0) {
minidocker_run(&argv[2]);
} else if (strcmp(argv[1], "exec") == 0) {
minidocker_exec(&argv[2]);
} else if (strcmp(argv[1], "ps") == 0) {
system("tree -L 1 containers");
} else {
fprintf(stderr, "Usage: minidocker run|exec|ps\n");
return EXIT_FAILURE;
}
return 0;
}
对输入的指令进行判断 对应于docker的run exec以及ps方法
2.minidocker_run函数
void minidocker_run(char **argv) {
char image_file[256];
char container_rootfs[256];
char timestamp_str[20];
get_current_timestamp(timestamp_str, sizeof(timestamp_str));
systemf("mkdir containers/%s", timestamp_str);
systemf("mkdir runtime/%s", timestamp_str);
systemf("mount -t overlay overlay -o lowerdir=images/%s,upperdir=containers/%s,workdir=runtime/tmpwork runtime/%s", argv[0], timestamp_str, timestamp_str);
systemf("mkdir -p containers/%s/etc", timestamp_str);
systemf("cp /etc/resolv.conf containers/%s/etc/resolv.conf", timestamp_str);
argv[0] = timestamp_str;
minidocker_exec(argv);
}
linux里的联合文件系统 (overlay) 可以允许多个文件系统或目录以只读或读写的形式合并。像
systemf("mount -t overlay overlay -o lowerdir=images/%s,upperdir=containers/%s,workdir=runtime/tmpwork runtime/%s", argv[0], timestamp_str, timestamp_str);
里指定下层目录为images里的镜像,上层目录为containers里的容器目录,runtime/tmpwork为合并后的目录。在合并目录里可以看到镜像目录和容器目录的文件,如果有相同的,那么取上层目录里的,也就是容器目录的,因此合并目录其实只是个虚拟目录,像个视图。下层目录是只读的,因此在合并目录里增加文件其实是在上层的容器目录里增加,而如果想修改的文件来自下层,那么将该文件复制到上层的容器目录里 ,然后修改。因为优先看到上层的文件,所以下层的该文件看不到了,这就是写时复制。这样,镜像文件是不可变的,可以由所有需要的容器共享,而上层的容器可以通过写时复制进行了自己想要的修改,达成目标的同时极大减省了资源。
3.minidocker_exec函数
pipe(pipefd);
// 创建新进程的命名空间标识
int flags = CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET | CLONE_NEWIPC;
// 创建新进程,新进程在上述独立的命名空间下
char container_rootfs[256];
snprintf(container_rootfs, sizeof(container_rootfs), "runtime/%s", argv[0]);
argv[0] = container_rootfs;
int pid = clone(child, malloc(4096) + 4096, flags | SIGCHLD, argv);
printf("父进程创建子进程完毕,子进程 pid = %d\n", pid);
// 设置 cgroup
cfcgroup(pid);
printf("父进程设置 cgroup 完毕\n");
// 设置网络
cfnet(pid);
close(pipefd[1]);
// 等待子进程结束的信号
waitpid(pid, NULL, 0);
umount(container_rootfs);
systemf("rm -rf %s", container_rootfs);
到了最重要的核心。该函数进行大部分工作:
创建子进程
int pid = clone(child, malloc(4096) + 4096, flags | SIGCHLD, argv);
clone用于创建子进程,其中参数child是在子进程中执行的函数,flags是创建的子进程的命名空间参数,CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET | CLONE_NEWIPC表示该子进程的ufs、pid、ns、net以及IPC等资源使用全新的命名空间,而不从父进程中继承,就像java里的在不同包下。因此子进程像套了个密不透风的壳,跟外界的联系断得干干净净。而child函数的代码如下:
int child(void *arg) {
char **argv = (char **)arg;
char ch;
close(pipefd[1]);
read(pipefd[0], &ch, 1);
// 把 mount 挂载的目录从共享状态变成私有状态,这样子进程改变 mount 不影响父进程
mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL);
// 设置根目录
chroot(argv[0]);
// 把当前目录设置为根目录
chdir("/");
// 挂载 proc 文件系统
mount("proc", "/proc", "proc", MS_NOEXEC | MS_NOSUID | MS_NODEV, "");
// 替换对应的程序,比如调用者传来的 /bin/bash
setenv("PATH", "/bin", 1);
// 设置容器的网络
child_cfnet();
// 替换并执行指定的程序
execvp(argv[1], argv + 1);
perror(argv[1]);
return 1;
}
进入子进程后后执行该函数。该函数是为容器启动后做一些初始化工作。
chroot(argv[0]);
chdir("/");
首先要设置子进程的根目录,对应于前面containers/容器ID 目录。设置之后 子进程的 / 目录其实是宿主机的containers/容器ID 目录。因为该目录只有该容器操作,所以容器像拥有了只有自己能操作的整套目录。
mount("proc", "/proc", "proc", MS_NOEXEC | MS_NOSUID | MS_NODEV, "");
接着要挂载proc文件系统。linux里万物皆文件,因此进程信息放在proc目录下。就行互联网通过url访问文件一样,proc目录其实是虚拟目录,去访问该目录的内容其实是内核将相应的进程信息返回。但前面子进程已经跟宿主机断得干干净净了,所以只能重新搭上关系,将宿主机的proc目录挂载进子进程里,让子进程通过proc目录也能去访问进程信息。另外为了子进程的操作不影响宿主机,挂载后将相应的目录从共享改为私有,子进程的后续改动只对自己起效。
void cfnet(pid_t container_pid) {
systemf("ip link add veth-host-%d type veth peer name veth-container", container_pid);
systemf("ip link set veth-container netns %d", container_pid);
systemf("ip link set veth-host-%d up", container_pid);
systemf("ip link set veth-host-%d master docker0", container_pid);
}
void child_cfnet() {
srand(time(NULL));
int random_num = rand() % (254 - 2 + 1) + 2;
system("ip link set lo up");
system("ip link set veth-container up");
systemf("ip addr add 172.17.0.%d/16 dev veth-container", random_num);
system("ip route add default via 172.17.0.1");
}
容器还是需要访问网络的,上面两个函数,cfnet函数在minidocker_exec函数里由宿主机执行,child_cfnet在child函数里由子进程执行。因为子进程跟父进程使用不同命名空间断开了网络,因此要将他们的网络连接起来。在linux里,一对veth设备像一根网线 其中一个设备发送的数据 另一个设备都会接收到。因为
systemf("ip link add veth-host-%d type veth peer name veth-container", container_pid);
systemf("ip link set veth-container netns %d", container_pid);
创建了一对veth设备,veth-host在宿主机上,但随后veth-container被移到容器的网络命名空间里。像一根网线连通了宿主机和容器,容器里发的消息通过该设备流到宿主机上。
systemf("ip link set veth-host-%d master docker0", container_pid);
但如果这样做的话 宿主机上太多veth-host设备了,都要给它分配IP的话太多了,因此将所有veth-host都插入docker0网桥,并且在宿主机上给网桥分配IP,这样容器发送数据都到了网桥,然后发到其他容器或者真正的网络上。
system("ip link set lo up");
system("ip link set veth-container up");
systemf("ip addr add 172.17.0.%d/16 dev veth-container", random_num);
system("ip route add default via 172.17.0.1");
第一条启用容器内部网络的本地回环接口,第二条在容器内部启动移进来的veth-container设备 并且给分配内部IP地址,最后是设置默认网关。默认网关即是宿主机的docker0网桥。因为给veth-container分配ip与默认网关子网掩码一样,处于同一子网,所以容器内部发出的包,docker0都收的到。这样容器实现了和宿主机以及其他容器的联网。
execvp(argv[1], argv + 1);
回到child函数,这行代码设置了容器真正要执行的操作,一般是/bin/sh。接着child函数执行完 回到minidocker_exec函数,下一步是设置资源限制:
void cfcgroup(pid_t pid) {
system("mkdir -p /sys/fs/cgroup/cpu/minidocker");
systemf("mkdir -p /sys/fs/cgroup/cpu/minidocker/%d", pid);
systemf("echo %d >> /sys/fs/cgroup/cpu/minidocker/%d/tasks", pid, pid);
systemf("echo 20000 > /sys/fs/cgroup/cpu/minidocker/%d/cpu.cfs_quota_us", pid);
}
linux里万物皆文件,进程信息可以通过读取proc目录得到,其对进程的资源限制也是写入目录里,像上面就是把要限制的进程号写入 再写入对该进程使用CPU的限制。
minidocker_exec函数下一步是设置网络 ,这个上面讲了,而之后就是等待子进程结束然后执行清理工作了
waitpid(pid, NULL, 0);
umount(container_rootfs);
systemf("rm -rf %s", container_rootfs);
解除与容器根目录的挂载 ,最后删除容器文件。
容器其实是隔离的进程,之所以实现了隔离,不是造了个壳再往里面塞东西,而是先启动一个子进程 ,然后进行各种限制,再根据需要开放部分限制。这就像面向对象的封装,先将全部封起来,然后根据需要开放部分限制,尽可能避免其内部受到影响。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。