当前位置:   article > 正文

运维开发实践 - Docker - 容器实现原理_docker mergeddir

docker mergeddir

1.Docker容器是什么

按照Docker官网,容器是运行在宿主机上的一个进程,但与宿主机上的其他进程相隔离;

2.容器实现原理

这种隔离机制使用了内核中的namespace和cgroups功能;

2.1.Linux namespace

Linux通过将系统的资源放置在不同的namespace下,实现资源的隔离;

类型解释
Network隔离网络资源
Mount隔离文件系统的挂载点
UTS隔离主机名和域名信息
IPC隔离进程间通信
PID隔离进程ID
User隔离用户和用户组ID

clone系统调用:创建子进程

# flags: 控制新创建的进程隔离的资源
int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);
  • 1
  • 2
flag隔离资源描述
CLONE_NEWCGROUP子进程的cgroup资源和当前进程隔离隔离Cgroup根目录下不同层级目录的权限
CLONE_NEWIPC子进程的ipc资源和当前进程隔离隔离当前在不同进程间传递和交换信息的范围
CLONE_NEWNET…network…隔离子进程的网络栈,路由表,防火墙规则等
CLONE_NEWNS…mount…隔离文件系统的挂载点
CLONE_NEWPID…pid…隔离进程的ID空间
CLONE_NEWUSER…user…隔离用户uid,gid在宿主机中的权限
CLONE_NEWUTS…UTS…隔离子进程的主机名,hostname和NIS域名

(NIS域名:Network information service,用共享网络信息的集中存储)

# 查看当前进程树
pstree -p
  • 1
  • 2

在这里插入图片描述

# 查看当前所有进程
# 在linux中一切皆文件,如下图我们可以看出进程本身其实也只是一个文件
ls /proc
  • 1
  • 2
  • 3

在这里插入图片描述

# 查看pid=1的进程的namespace
ls -al /proc/1/ns
  • 1
  • 2

在这里插入图片描述

mount: 文件挂载

// source: 挂载源地址
// target: 挂载目标地址
// filesystemtype: 系统类型
// mountflags: 挂载源文件访问标志
// data: 文件系统特有的参数
int mount( const char* source, const char* target, const char* filesystemtype, unsigned long mountflags, const void * data);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

// container.c

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <sys/capability.h>
#include <stdio.h>
#include <sched.h>
#include <linux/sched.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];
char* const container_args[] = {
        "/bin/bash",
        NULL
};

int container_main(void* arg){
        printf("container_main\n");
        sethostname("container0",10);
        mount("proc", "/proc", "proc", 0, NULL);
        mount("none", "/tmp", "tmpfs", 0, "");
        printf("Info: %s\n",strerror(errno));
        chroot("./");
        perror("chroot");
        chdir("/");
        perror("chdir");
        execv(container_args[0], container_args);
        return 1;
}

int main(){
    int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWNS|CLONE_NEWCGROUP|CLONE_NEWIPC|CLONE_NEWUTS|CLONE_NEWPID|CLONE_NEWUSER|CLONE_NEWNET| SIGCHLD , NULL);
    waitpid(container_pid, NULL, 0);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
# 进入自定义的容器中
gcc container.c -o container && ./conatiner

# 检查不同命名空间下资源的隔离
  • 1
  • 2
  • 3
  • 4

宿主机上
在这里插入图片描述
容器中
在这里插入图片描述

通过比较上述宿主机以及容器间的这些资源,我们可以发现容器间的这些资源相互隔离了;

CLONE_NEWCGROUP
在这里插入图片描述

# CLONE_NEWNS
# 默认情况下clone方法会拷贝宿主机的所有挂载点
# 因此我们可以看到容器比宿主机多出一个新增的挂载点
mount | grep proc
# CLONE_NEWCGROUP
ls /sys/fs/cgroup
cat /sys/fs/cgroup/cpu/tasks
# CLONE_NEWIPC
# 宿主机和容器的ipcs队列均为空因此无法比较
# ipcs用于查看当前系统进程间通信的状况
ipcs
# CLONE_NEWUTS
# 主机名称
hostname
# CLONE_NEWPID
# 查看当前进程pid
ps
# CLONE_NEWUSER
# 查看当前用户
whoami
# CLONE_NEWNET
# 查看当前网络
ip a
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

2.2.Linux cgroups

对进程设置资源(cpu, memory, 磁盘,带宽)限制
在这里插入图片描述
cgroup可对进程使用的以下资源进行限制

资源解释
blkio磁盘吞吐量
cpucpu使用量
cpuacctcpu使用率的统计报告
cpuset分配独立CPU和内存节点
devices控制设备访问
freezer挂起或恢复进程
memory内存使用量
net_cls标记网络数据包从而限制带宽
net_prio网络数据包优先级
perf_event使用perf工具监控进程
pids任务数量
systemd控制管理系统资源

Cgroup net_cls&net_prio

使用cgroup给某个进程可以生成的任务数量进行限制(pids)

mkdir /sys/fs/cgroup/pids/container
cd /sys/fs/cgroup/pids/container

# 最多只能运行一个进程
echo 1 > pids.max

# 对当前进程进行限制
echo $$ > cgroup.procs
# 再次运行ls发现无法执行子进程
ls
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

在这里插入图片描述
Cgroup Usage
Cgroup Memory & Pids Usage

2.3.文件挂载隔离实践

当我们进入docker容器时,发现其文件与宿主机中的相互独立…
这使用的是mount namespace,使得容器文件和宿主机文件相隔离

chroot: 将指定的目录设为新进程的根目录;

  • 限制用户权限
  • 用户环境与宿主机环境隔离

(1) 执行以下脚本在 ~/test 目录下拷入基本命令

# test.sh
basedir=~/test
commands=(/bin/ls /bin/bash /bin/cat /bin/chmod)
mkdir -p $basedir
cd $basedir
for(( i=0;i<${#commands[@]};i++ ));do
        list=$(ldd ${commands[i]} | egrep -o '/(.*?) ')
        mkdir -p $basedir`dirname ${commands[i]}`;
        cp ${commands[i]} $basedir${commands[i]};
        for dependency in $list;do
               # echo ${commands[i]} - $dependency;
                mkdir -p $basedir`dirname $dependency`;
                cp -v $dependency $basedir$dependency
        done;
done;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

(2)


mkdir -p ~/test && cd ~/test
# 下载hello world项目,里面已经打好了一个helloworld程序
# 目前支持 linux-arm, linux-arm64, linux-arm64
git clone https://gitee.com/Liyuan-1/helloworld.git

# 运行上述test.sh脚本
chmod +x test.sh && ./test.sh

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

到这一步,我们就将应用在容器中运行所需的文件准备好了;
在这里插入图片描述
如下图我们可以看到新创建的进程将~/test作为了根目录,且与宿主机文件隔离;

# 以 ~/test 作为新进程(容器)的根目录
chroot ~/test
/bin/ls -al
  • 1
  • 2
  • 3

在这里插入图片描述

2.4.容器运行实践

容器只是一个进程,不过容器中文件与宿主机文件相隔离

# 将上述准备好的~/test作为新进程的根目录
chroot ~/test
/bin/ls -al /helloworld/HelloWorld-Golang/

# 为你的宿主机所能执行的文件添加可执行权限
/bin/chmod +x /helloworld/HelloWorld-Golang/main-linux-arm

# 运行
/helloworld/HelloWorld-Golang/main-linux-arm
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在这里插入图片描述
由于此处我们并未做网络相关的隔离,因此该进程(容器)共享宿主机网络资源;
在这里插入图片描述

3.容器资源隔离运行实践

接下来我们使用clone函数创建一个新的进程(容器),对其进行资源隔离,并运行上述HelloWorld程序;

3.1. 准备可执行文件

(1) 首先我们拷贝一些基础命令以及启动HelloWorld应用所需要的文件到~/test目录下

# init_commands.sh
basedir=~/test
commands=(/usr/sbin/ip /bin/ps /bin/hostname /bin/whoami /bin/ls /bin/bash /bin/cat /bin/chmod)
mkdir -p $basedir
cd $basedir
for(( i=0;i<${#commands[@]};i++ ));do
        list=$(ldd ${commands[i]} | egrep -o '/(.*?) ')
        mkdir -p $basedir`dirname ${commands[i]}`;
        cp ${commands[i]} $basedir${commands[i]};
        for dependency in $list;do
               # echo ${commands[i]} - $dependency;
                mkdir -p $basedir`dirname $dependency`;
                cp -v $dependency $basedir$dependency
        done;
done;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
mkdir -p ~/test && cd ~/test

# 执行上述shell脚本
# 拷贝基础命令到~/test目录下
chmod +x ./init_commands.sh && ./init_commands.sh

# 拷贝HelloWorld程序
git clone https://gitee.com/Liyuan-1/helloworld.git

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在这里插入图片描述

3.2. 运行容器

(2) 使用clone创建一个和宿主机资源相隔离的进程,并将上述生成的dir作为容器运行的根目录
// container.c

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <sys/capability.h>
#include <stdio.h>
#include <sched.h>
#include <linux/sched.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];
char* const container_args[] = {
        "/bin/bash",
        NULL
};

int container_main(void* arg){
        printf("container_main\n");
        sethostname("container0",10);
        mount("proc", "/proc", "proc", 0, NULL);
        mount("none", "/tmp", "tmpfs", 0, "");
        chroot("./");
        perror("chroot");
        chdir("/");
        perror("chdir");
        printf("mount:  %s\n",strerror(errno));
        execv(container_args[0], container_args);
        return 1;
}

int main(){
    int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWNS|CLONE_NEWCGROUP|CLONE_NEWIPC|CLONE_NEWUTS|CLONE_NEWPID|CLONE_NEWUSER|CLONE_NEWNET| SIGCHLD , NULL);
    waitpid(container_pid, NULL, 0);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
gcc container.c && ./a.out

/bin/chmod +x main-linux-arm && ./main-linux-arm

# 检查该进程(容器)资源隔离是否生效
/bin/hostname
/usr/sbin/ip a
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在这里插入图片描述
在这里插入图片描述

4.docker容器运行分析

当我们运行一个nginx容器时,总共分为以下几个步骤;

4.1. docker pull nginx

在该步骤,我们会将nginx镜像拉取下来,其实这个镜像就是一个文件夹,里面包含运行nginx所需要的文件,然后使用chroot将该文件设置为新进程的根目录,通过docker inspect 我们也可以看出;

docker pull nginx
docker run -itd nginx
# 查看该nginx容器详细信息,如下图所示
docker inspect ${containerID} | grep MergedDir
# 该目录即为当前运行的nginx容器的根目录
ls -al ${MergedDir}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在这里插入图片描述

(1) UnionFS (Union File System)

UnionFS: 将多个文件夹挂载到同一个目录上

假设我有2个目录,下面分别有一些文件;
在这里插入图片描述

# 将 ./test00  ./test01 ./test02 挂载至 ./liyuan 目录下
# 上层的目录的文件会覆盖下层的目录
# lowerdir: 指定用户需要挂载的lower层目录,可使用`:`分隔
# upperdir: 指定用户需要挂载的upper目录
# workdir: 指定文件系统的工作基础目录,挂载后内容会被清空,且在使用过程中其内容用户不可见
mkdir -p ./liyuan &&  mount -t overlay overlay -o lowerdir=./test00,upperdir=./test01,workdir=./test02  ./liyuan
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在这里插入图片描述
docker使用了UnionFS,可以将多个不同位置的目录联合挂载到同一个目录下,如下图,nginx运行的容器由多个lowerDir,UpperDir,WorkDir一起被合并挂载到了 MergeDir上;
在这里插入图片描述
在这里插入图片描述

4.2. docker run -it nginx

在该步骤,我们相当于首先将lowerDir, upperDir, workDir通过UnionFS方式挂载至MergedDir中,然后运行 chroot 将该MergedDir作为新进程根目录, 然后主动运行应用;

3. 总结

容器只是运行在宿主机上的进程,其本质只是将文件等资源与宿主机相隔离,但共享整个操作系统内核,因此你对操作系统内核配置的修改会影响到运行在该宿主机上的所有容器;

tip: 如果你有任何疑问,欢迎留言,也欢迎关注我的公众号 “从零开始的Go学习”

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/578179
推荐阅读
相关标签
  

闽ICP备14008679号