来源 | 多选参数

责编 | 程序锅

头图 | 下载于视觉中国

容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”也就是独立的“运行环境”。下面我们使用 C 语言和 Namespace 技术来手动创建一个容器,演示 Linux 容器最基本的实现原理。

什么是容器?容器其实是一种特殊的进程而已,只是这个进程运行在自己的 “运行环境” 中,比如有自己的文件系统而不是使用主机的文件系统(文件系统这个对我来说印象是最深刻的,也是让人对容器很更好理解的一个切入点)。

有一个计算数值总和的小程序,这个程序的输入来自一个文件,计算完成后的结果则输出到另一个文件中。为了让这个程序可以正常运行,除了程序本身的二进制文件之外还需要数据,而这两个东西放在磁盘上,就是我们平常所说的一个“程序”,也就是代码的可执行镜像。

当“程序”被执行起来之后,它就从磁盘上的二进制文件变成了计算机内存中的数据、寄存器里的值、堆栈中的指令、被打开的文件,以及各种设备的状态信息的一个集合。像这样一个程序运行起来后的计算机执行环境的总和,就是进程,而计算机执行环境的总和就是它的动态表现。

而容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”也就是独立的“运行环境”。那么怎么去造成这个边界呢?

  • 对于 Docker 等大多数 Linux 容器来说,Cgroups 技术是用来制造约束的主要手段;

  • Namespace 技术则是用来修改进程视图的主要方法;

下面我们使用 C 语言和 Namespace 技术来手动创建一个容器,演示 Linux 容器最基本的实现原理。

自己实现一个容器

Linux 中关于 Namespace 的系统调用主要有这么三个:

  • clone()---实现线程的系统调用,用来创建一个新的进程,同时可以设置 Namespace 的一些参数。

  • unshare()---使某个进程脱离某个 namespace。

  • setns()---把某进程加入到某个 namespace。

我们使用 clone 来创建一个子进程,通过创建出来的效果可以看到,子进程的 PID 是跟在父亲节点后面的,而不是 1。

 1#define _GNU_SOURCE2#include <sys/types.h>3#include <sys/wait.h>4#include <sys/mount.h>5#include <stdio.h>6#include <sched.h>7#include <signal.h>8#include <unistd.h>9
10#define STACK_SIZE (1024 * 1024)
11static char container_stack[STACK_SIZE];
12
13char* const container_args[] = {
14    "/bin/bash",
15    NULL
16};
17
18int container_main(void* arg) {
19    printf("Container [%5d] - inside the container!\n", getpid());
20    execv(container_args[0], container_args);
21    printf("Something's wrong!\n");
22    return 1;
23}
24
25int main() {
26    printf("Parent [%5d] - start a container!\n", getpid());
27    int container_id = clone(container_main, container_stack + STACK_SIZE, SIGCHLD, NULL);
28    waitpid(container_id, NULL, 0);
29    printf("Parent - container stopped!\n");
30    return 0;
31}

接下去这段代码我们给创建出来的进程设置 PID namespace 和 UTS namespace。从实际的效果我们可以看到子进程的 pid 为 1,而子进程中打开的 bash shell 显示的主机名为 container_dawn。是不是有点容器那味了?这里子进程在自己的 PID Namespace 中的 PID 为 1,因为 Namespace 的隔离机制,让这个子进程误以为自己是第 1 号进程,相当于整了个障眼法。但是,实际上这个进程在宿主机的进程空间中的编号不为 1,是一个真实的数值,比如 14624。

 1int container_main(void* arg) {2    printf("Container [%5d] - inside the container!\n", getpid());3    sethostname("container_dawn", 15);4    execv(container_args[0], container_args);5    printf("Something's wrong!\n");6    return 1;7}89int main() {
10    printf("Parent [%5d] - start a container!\n", getpid());
11    int container_id = clone(container_main, container_stack + STACK_SIZE,
12                                CLONE_NEWUTS | CLONE_NEWPID | SIGCHLD, NULL);
13    waitpid(container_id, NULL, 0);
14    printf("Parent - container stopped!\n");
15    return 0;
16}

最后我们改变一下这个进程可以看到的文件系统,我们首先使用 docker export 将 busybox 镜像导出成一个 rootfs 目录,这个 rootfs 目录的情况如图所示,已经包含了 /proc、/sys 等特殊的目录。

接下去,我们在代码中使用 chroot() 函数将创建出来的子进程的根目录改变成上述的 rootfs 目录。从实现的效果来看,创建出来的子进程的 PID 为 1,并且这个子进程将上述提到的 rootfs 目录当成了自己的根目录。

 1char* const container_args[] = {2    "/bin/sh",3    NULL4};56int container_main(void* arg) {7    printf("Container [%5d] - inside the container!\n", getpid());89    if (chdir("./rootfs") || chroot("./") != 0) {
10        perror("chdir/chroot");
11    }
12
13    execv(container_args[0], container_args);
14    printf("Something's wrong!\n");
15    return 1;
16}
17
18int main() {
19    printf("Parent [%5d] - start a container!\n", getpid());
20    int container_id = clone(container_main, container_stack + STACK_SIZE,
21                                CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);
22    waitpid(container_id, NULL, 0);
23    printf("Parent - container stopped!\n");
24    return 0;
25}

需要注意的是所使用的 shell 需要改一下,因为 busybox 中没有 /bin/bash,假如还是 /bin/bash 的话是会报错的,因为 chroot 改变子进程的根目录视图之后,最终是从 rootfs/bin/  中找 bash 这个程序的。

上面其实已经基本实现了一个容器,接下去我们实现一下 Docker 卷的基本原理(假设你已经知道卷是什么了)。在代码中,我们将 /tmp/t1 这个目录挂载到 rootfs/mnt 这个目录中,并采用 MS_BIND 的方式,这种方式使得 rootfs/mnt (进入容器之后就是 mnt 目录)的视图其实就是 /tmp/t1 的视图,你对 rootfs/mnt 的修改其实就是对 /tmp/t1 修改,rootfs/mnt 相当于 /tmp/t1 的另一个入口而已。当然,在实验之前,你先确保 /tmp/t1 和 rootfs/mnt 这两个目录都已经被创建好了。实验效果见代码之后的那张图。

 1char* const container_args[] = {2    "/bin/sh",3    NULL4};56int container_main(void* arg) {7    printf("Container [%5d] - inside the container!\n", getpid());89    /*模仿 docker 中的 volume*/
10    if (mount("/tmp/t1", "rootfs/mnt", "none", MS_BIND, NULL)!=0) {
11        perror("mnt");
12    }
13
14    /* 隔离目录 */
15    if (chdir("./rootfs") || chroot("./") != 0) {
16        perror("chdir/chroot");
17    }
18
19    execv(container_args[0], container_args);
20    printf("Something's wrong!\n");
21    return 1;
22}
23
24int main() {
25    printf("Parent [%5d] - start a container!\n", getpid());
26    int container_id = clone(container_main, container_stack + STACK_SIZE,
27                                CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);
28    waitpid(container_id, NULL, 0);
29    printf("Parent - container stopped!\n");
30    return 0;
31}

除了上述所使用的 PID、UTS、Mount namespace,Linux 操作系统还提供了 IPC、Network 和 User 这些 Namespace。

总结

通过上面我们可以看到,容器的创建和普通进程创建没什么区别。都是父进程先创建一个子进程,只是对于容器来说,这个子进程接下去通过内核提供的隔离机制再给自己创建一个独立的资源环境。

同理,在使用 Docker 的时候,其实也并没有一个真正的 Docker 容器运行在宿主机里面。Docker 项目启动还是用户原来的应用进程,只是在创建进程的时候,Docker 为这个进程指定了它所需要启用的一组 Namespace 参数。这样,这个进程只能“看”到当前 Namespace 所限定的资源、文件、设备、状态或者配置。而对于宿主机以及其他不相关的程序,这个进程就完全看不到了。这时,进程就会以为自己是 PID Namespace 里面 1 号进程,只能看到各自 Mount Namespace 里面挂载的目录和文件,只能访问 Network Namespace 里的网络设备。这种就使得进程运行在一个独立的“运行环境”里,也就是容器里面。

因此,对接一开始所说的,还想再唠叨一句:容器其实就是一种特殊的进程而已。**只是这个进程和它运行所需的所有资源都打包在了一起,进程执行时所使用的资源也都是打包中的。相比虚拟机的方式,本质是进程的容器则仅仅是在操作系统上划分出了不同的“运行环境”,从而使得占用资源更少,部署速度更快。

更多阅读推荐

  • 用三国杀讲分布式算法,舒适了吧?

  • 云原生体系下的技海浮沉

  • 如何通过 Serverless 轻松识别验证码?

  • 5G与金融行业融合应用的场景探索

  • 打破“打工人”魔咒,RPA 来狙击!

看穿容器的外表,Linux容器实现原理演示相关推荐

  1. Linux容器:cgroup,namespace原理与实现

    转自 <容器三把斧之 | cgroup原理与实现> <容器三把斧之 | namespace原理与实现> 目录 容器三把斧之 | cgroup原理与实现 cgroup 结构体 c ...

  2. linux原理 培训,Linux容器技术原理和使用

    1.1 隔离和共享 在一个多员共用的开发环境或者一台服务器运行多个逻辑隔离的服务器进程.谁的运行环境也不希望影响到另一个谁.也就是一个物理机器需要虚拟化出多个环境或者容器.通过提供一种创建和进入容器的 ...

  3. Linux容器化原理笔记

    一.容器 1. 从一台物理机虚拟化出很多虚拟机这种方式,一定程度上实现了资源创建的灵活性.但是同时会发现,虚拟化的方式还是非常复杂的, CPU.内存.网络.硬盘全部需要虚拟化,还有性能损失.那有没有一 ...

  4. 在docker的Linux容器搭建前端开发环境

    随着开发的深入,前端开发已经不局限于简单的本地开发坏境的搭建与调试.运维方面,目前的服务器使用的基本上都是linux系统,了解下Linux系统原理与一些常用的配置和指令,对我们的开发和部署以及排除线上 ...

  5. 课时 15-深入解析 Linux 容器 (华敏)

    今天的内容主要分成以下三个部分 资源隔离和限制: 容器镜像的构成: 容器引擎的构成: 前两个部分就是资源隔离和限制还有容器镜像的构成,第三部分会以一个业界比较成熟的容器引擎为例去讲解一下容器引擎的构成 ...

  6. 从零开始入门 K8s | 深入剖析 Linux 容器

    作者 | 唐华敏(华敏)  阿里云容器平台技术专家 本文整理自<CNCF x Alibaba 云原生技术公开课>第 15 讲. 关注"阿里巴巴云原生"公众号,回复关键词 ...

  7. 从零开始入门 K8s:深入剖析 Linux 容器

    Linux 容器是一种轻量级的虚拟化技术,在共享内核的基础上,基于 namespace 和 cgroup 技术做到进程的资源隔离和限制.本文将会以 docker 为例,介绍容器镜像和容器引擎的基本知识 ...

  8. linux 容器_Linux容器的幕后花絮

    linux 容器 您可以拥有没有Docker的 Linux容器吗? 没有OpenShift ? 没有Kubernetes ? 是的你可以. 在Docker成为家喻户晓的容器的几年前(也就是说,如果您生 ...

  9. Linux 容器化技术详解(虚拟化、容器化、Docker)

    虚拟化是过去用来充分利用物理资源的最常用方法.早年间,我们可以用一台服务器运行一个操作系统,处理一个任务,带来的问题是资源利用率极其不足,计算机的潜能并不能完全发挥,而后多道批处理系统.分时系统相继出 ...

最新文章

  1. SAP S/4HANA现金管理之变
  2. Linux后台运行命令 nohup command myout.file 21
  3. 数据结构之二叉搜索树/二叉查找数/有序二叉树/排序二叉树
  4. Linux_ISCSI服务器
  5. [C] 跨平台使用Intrinsic函数范例3——使用MMX、SSE2指令集 处理 32位整数数组求和...
  6. sql 2012先分离迁移mdf mlf 文件到别的机器后附加 数据库成只读的修复方法
  7. 【笔记】MATLAB中的图形(2)
  8. 计算机三级网络技术上机,计算机三级网络技术上机部分(南开100题题库)
  9. CAJ格式文献转成PDF格式
  10. 基于PLC的搬运机器手控制系统设计
  11. pip3 install -U qcloud-python-sts 安装失败解决方法
  12. 【转】一生必看的成功学书(转载)
  13. kubernetes资源控制器【一】- ReplicaSet控制器
  14. 实验11-1-7 藏头诗 (15分)
  15. 初步认识地图布局和指北针 - SuperMap iDesktop 8C
  16. JZOJ1383. 奇怪的问题 (2017.8B组)
  17. vue H5app plus调取手机相册,限制图片大小,图片转base64
  18. 网络空间安全导论期末复习题
  19. 一键呼叫可视对讲用于路灯杆
  20. 红芯浏览器真的“安全”吗? - 杰洛特的文章 - 知乎 https://zhuanlan.zhihu.com/p/42482349

热门文章

  1. php字符串分割tp模板,ThinkPHP 模板substr的截取字符串函数详解
  2. python视窗编程_[PYTHON] 核心编程笔记(19.图形用户界面编程)
  3. opencv机器学习线性回归_Python机器学习之?线性回归入门(二)
  4. vue项目android,Android与Vue项目交互
  5. vue登录如何存储cookie_vue项目实现表单登录页保存账号和密码到cookie功能
  6. android 上下数字滚动_原来PPT数字还有这么高大上的展示方式
  7. mysql游标的概述_MySQL游标简介
  8. 双用户windows linux系统,Windows与Linux合二为一?终于能在windows上运行Linux了!
  9. css文本行高是哪个属性_CSS中的line-height行高属性的使用技巧小结
  10. 前端websocket获取数据后需要存本地吗_是什么让我放弃了Restful API?了解清楚后我全面拥抱GraphQL!...