当前位置: 首页 > news >正文

手写简易docker

在极客时间上学完了 手把手带你写个最精简的 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);// 设置 cgroupcfcgroup(pid);printf("父进程设置 cgroup 完毕\n");// 设置网络cfnet(pid);close(pipefd[1]);// 等待子进程结束的信号waitpid(pid, NULL, 0);umount(container_rootfs);systemf("rm -rf %s", container_rootfs);

到了最重要的核心。该函数进行大部分工作:

  1. 在宿主机创建子进程 由子进程执行想要在容器里执行的任务 并且创建的子进程使用对各种资源使用自己的命名空间,如pid、网络、uts、IPC,从而和宿主机以及其他容器隔离开来。
  2. 在宿主机设置 cgroup,限制子进程使用的资源量。
  3. 在宿主机设置网络

创建子进程

    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/bashsetenv("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);

解除与容器根目录的挂载 ,最后删除容器文件。

总结

容器其实是隔离的进程,之所以实现了隔离,不是造了个壳再往里面塞东西,而是先启动一个子进程 ,然后进行各种限制,再根据需要开放部分限制。这就像面向对象的封装,先将全部封起来,然后根据需要开放部分限制,尽可能避免其内部受到影响。

http://www.jsqmd.com/news/613772/

相关文章:

  • 杰理之蓝牙空闲状态时间设置【篇】
  • CKKS 同态加密数学基础推导技
  • ctfileGet:突破网盘限速的本地解析方案
  • 写段代码教会你什么是HOOK技术?HOOK技术能干什么?肯
  • 失业期PHP程序员,你用什么态度对事,事就用什么结果对你。
  • 青海纯玩小团选型与实用指南:西北/青甘大环线旅行全攻略 - 深度智识库
  • 深度理解算法和数据结构:栈并非天生存在,而是数组的「思想封装」|4 道经典题从本质吃透栈与单调栈
  • 前端使用AI试水报告本
  • 2026年B2B工业制造企业GEO服务商排名深度解析:径硕科技(JINGdigital)与JINGEO为何稳居首选
  • Vim 中执行 :set mouse=a
  • 科研级时间序列解析:从 ARIMA 到 Mamba,深度学习与频域分析的全栈技术方案
  • 实测H3C s5500-52C-SI 交换机破解密码
  • 杰理之蓝牙名修改【篇】
  • 视觉生产力的觉醒:萤火AI如何让电商人从“工具”变回“人”
  • 微波管参数全解析:什么是增益、带宽?看懂这张图就够了!
  • 万字拆解 LLM 运行机制:Token、上下文与采样参数载
  • [管理进阶] 别把核心 SOP 变成“公开的秘密”:店群规模化如何用定制 RPA 与底层群控筑牢数字堡垒?
  • 从零开始的机器学习实战:带你打通数据训练的“任督二脉”
  • 法国展位设计搭建:哪些优质公司值得信赖?
  • 4月8日TRO最新案件预警
  • 万象视界灵坛实战案例:新闻配图政治倾向性语义初筛辅助系统
  • 2026年阿里企业邮箱购买联系电话:快速咨询与开通解析 - 品牌2025
  • 2026贵阳工程门窗配套,发布推荐5家源头工厂直营名单 - 精选优质企业推荐榜
  • Ventoy主题定制:打造属于你的个性化启动界面
  • MATLAB十字路口车辆通行情况模拟系统
  • 失业期PHP程序员,每天花十分钟想想,今天哪件事干得还行,哪件事可以更好。
  • MATLAB雷达图避坑指南:从基础绘制到高级美化的完整流程
  • 不满意Oh My Zsh启动卡顿,来试试Starship吧绕
  • 健康一体机哪家好?2026年五大优质健康一体机品牌推荐 - 品牌2026
  • 穿越鳌太线