先开个坑,把 docker 容器虚拟化给学一下。

docker 其实我一直都想好好学一下,之前只是简单地略了一次,这次趁着寒假,把容器和 CI 这块好好搞一下。

so,start dash!

# What is docker?

docker 是一种虚拟化技术,和虚拟机不同的是,docker 是直接运行在操作系统内核上的用户控件。容器技术可以让多个独立的用户空间运行在一台宿主机。

image-20220106103908215

容器提供了在计算机上的隔离环境中安装和运行应用程序的方法。在容器内运行的应用程序仅可使用于为该容器分配的资源,例如:CPU,内存,磁盘,进程空间,用户,网络,卷等。在使用有限的容器资源的同时,并不与其他容器冲突。您可以将容器视为简易计算机上运行应用程序的隔离沙箱。

说白了,就是 linux 划分出一个隔离的空间,这个空间有内存有计算资源。

Docker 技术使用 Linux 内核和内核功能(例如 Cgroupsnamespaces)来分隔进程,以便各进程相互独立运行。这种独立性正是采用容器的目的所在;它可以独立运行多种进程、多个应用,更加充分地发挥基础设施的作用,同时保持各个独立系统的安全性

容器通过 Namespace 进行资源隔离,通过 Cgroup 进行资源控制,通过 rootfs 进行文件系统隔离,容器引擎自身的特性来管理容器的生命周期。

# Namespace 进行资源隔离

chroot 命令: change root directory(更改 root 目录),因为系统默认目录结构是以 / 开始的,即 root 目录就是 / ,修改 root 目录之后,系统会通过指定的位置作为 / 。比如我用 chroot target /bin/sh

chroot 实现的就是一种隔离。

Namespace 的作用之一:实现轻量级的虚拟化服务。

Linux namespaces 是对全局系统资源的一种封装隔离,使得处于不同 namespace 的进程拥有独立的全局系统资源,改变一个 namespace 中的系统资源只会影响当前 namespace 里的进程,对其他 namespace 中的进程没有影响

我们可以从 docker 实现者的角度考虑该如何实现一个资源隔离的容器。比如是不是可以通过 chroot 命令切换根目录的挂载点,从而隔离文件系统。为了在分布式的环境下进行通信和定位,容器必须要有独立的 IP、端口和路由等,这就需要对网络进行隔离。同时容器还需要一个独立的主机名以便在网络中标识自己。接下来还需要进程间的通信、用户权限等的隔离。最后,运行在容器中的应用需要有进程号 (PID),自然也需要与宿主机中的 PID 进行隔离。也就是说这六种隔离能力是实现一个容器的基础,让我们看看 linux 内核的 namespace 特性为我们提供了什么样的隔离能力:

image-20220106110757050

容器需要的六种 namespace。

# 进行 namespace API 操作

# clone

clone 就是加强版的 vfork,因为 vfork 调用生成的子进程实际上是和父进程共享内存地址空间的,而 clone 就可自定义需要共享的是啥。

int clone (int (*child_func)(void *), void *child_stack,int flags, void *args)

flag 参数可以对 namespace 进行操作。

# /proc/[pid]/ns

image-20220107115332250

$$ 指当前进程

可以看到这些文件实际上都是软连接,指向 namespace 文件的 inode。

mount --bind /proc/$$/ns/uts ~/uts

就是用 ~/uts 来作为一个新的软连接。

# setns

setns 可以是进程加入到新的 namespace。

int setns(int fd, int nstype)

fd 是 namespace 的文件描述符, nstype 指定是否需要检查 namespace 类型是否符合实际要求。

fd = open(argv[1], O_RDONLY);
setns(fd, 0); // 调用 setns
execvp(argv[2], &argv[2]); // 执行命令

./setns ~/uts /bin/bash

setns 执行后会执行 clone 创建子进程,而原来的父进程会终止。

# UTS namespace

uts 是域名和主机名的隔离,容器和主机应该具有不同的 hostname

代码:

#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* args){
    printf("在子进程中!\n");
    sethostname("NewNamespce", 12);
    execv(child_args[0],child_args);
    return 1;
}
int main(){
    printf("------begin----------\n");
    int child_pid = clone(child_main, child_stack + STACK_SIZE, CLONE_NEWUTS| SIGCHLD, NULL); // 这里设置了 CLONE_NEWUTS,表示子进程克隆了父进程的 uts。
    waitpid(child_pid, NULL, 0);
    printf("------end------------\n");
    return 0;
}

image-20220107170432303

可以看到新的进程内,hostname 发生了改变,我说当时用 docker 的时候,怎么 hostname 是一串奇奇怪怪的数字。

现在明白了,这就是 uts 隔离。

# IPC 隔离

IPC 隔离就是进程间通信隔离。因为进程间通信有多种,有消息队列,共享内存、管道等方式。所以 IPC 隔离的作用的就是 docker 内的进程使用容器内的消息队列和共享内存等等,IPC namespace 下的进程彼此可以通信,不同 IPC namespace 进程不能通信。

IPC 可以看👉 PIC:进程间通信

修改语句: int child_pid = clone(child_main, child_stack + STACK_SIZE, CLONE_NEWUTS| SIGCHLD, NULL);

ipcmk -Q : 创建 ipc 消息队列

ipcs -q : 查看消息队列

image-20220107171751280

可以看到 ipc namespace 发生了隔离。

# PID 隔离

pid 就是 process id 进程 id。 pid 隔离就是不同 pid namespace 可以允许有相同的 pid。

Linux 会维护一个 pid namespace 树,最顶层的 root 节点是系统初始化时创建的,原先的 pid namespace 是新创建的 pid namespace 的父节点。子节点不能看到父节点的进程,但父节点可以看到子节点。

pid 1 是 init 进程,通常为 bashzshsh 。init 进程负责回收所有的孤儿进程(发生错误导致没有父进程)。父节点 pid namespace 的进程可以强行杀死子节点 pid namespace 节点的 init 进程,一旦 init 进程被杀死,该 pid namespace 下的所有进程都会被终止。

问题:子节点仍然可以看到父节点进程。

image-20220107173053305

解决:重新挂载 proc mount -t proc proc /proc

image-20220107173731876

对于进程来说 pid namespace 应该是一个常量。

# mount 隔离

挂载隔离就是隔离文件系统挂载点实现对隔离文件系统支持。

挂载传播:共享挂载、私有挂载。

挂载:

指的就是将设备文件中的顶级目录连接到 Linux 根目录下的某一目录(最好是空目录),访问此目录就等同于访问设备文件。

Linux 系统中 “一切皆文件”,所有文件都放置在以根目录为树根的树形目录结构中。在 inux 看来,任何硬件设备也都是文件,它们各有自己的一套文件系统(文件目录结构)。

因此产生的问题是,当在 Linux 系统中使用这些硬件设备时,只有将 Linux 本身的文件目录与硬件设备的文件目录合二为一,硬件设备才能为我们所用。合二为一的过程称为 “挂载”。

这里 Unix 目录结构来历值得一看。

这里真有点脑子不够用的感觉。。。

# mount 文件 or 目录绑定

这里有一篇好文章 mount 命令进阶

sudo mount --bind [DirFile-1] [DirFile-2]

sudo umount [DirFile-2]

解绑用绝对路径。

bind 不会增加 inode 的引用次数

# network 隔离

进行网络资源的隔离。

img

docker 可以实现网络隔离,这部分之后再细看。

这里有一篇博客,写的很好一分钟看懂 Docker 的网络模式和跨主机通信

# user 隔离

通过映射可以将 container 内的用户映射为外部用户,这样就具有了外部用户的权限

# Cgroup

待填坑