赞
踩
按照Docker官网,容器是运行在宿主机上的一个进程,但与宿主机上的其他进程相隔离;
这种隔离机制使用了内核中的namespace和cgroups功能;
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);
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
# 查看当前所有进程
# 在linux中一切皆文件,如下图我们可以看出进程本身其实也只是一个文件
ls /proc
# 查看pid=1的进程的namespace
ls -al /proc/1/ns
mount: 文件挂载
// source: 挂载源地址
// target: 挂载目标地址
// filesystemtype: 系统类型
// mountflags: 挂载源文件访问标志
// data: 文件系统特有的参数
int mount( const char* source, const char* target, const char* filesystemtype, unsigned long mountflags, const void * data);
// 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; }
# 进入自定义的容器中
gcc container.c -o container && ./conatiner
# 检查不同命名空间下资源的隔离
宿主机上
容器中
通过比较上述宿主机以及容器间的这些资源,我们可以发现容器间的这些资源相互隔离了;
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
对进程设置资源(cpu, memory, 磁盘,带宽)限制
cgroup可对进程使用的以下资源进行限制
资源 | 解释 |
---|---|
blkio | 磁盘吞吐量 |
cpu | cpu使用量 |
cpuacct | cpu使用率的统计报告 |
cpuset | 分配独立CPU和内存节点 |
devices | 控制设备访问 |
freezer | 挂起或恢复进程 |
memory | 内存使用量 |
net_cls | 标记网络数据包从而限制带宽 |
net_prio | 网络数据包优先级 |
perf_event | 使用perf工具监控进程 |
pids | 任务数量 |
systemd | 控制管理系统资源 |
使用cgroup给某个进程可以生成的任务数量进行限制(pids)
mkdir /sys/fs/cgroup/pids/container
cd /sys/fs/cgroup/pids/container
# 最多只能运行一个进程
echo 1 > pids.max
# 对当前进程进行限制
echo $$ > cgroup.procs
# 再次运行ls发现无法执行子进程
ls
Cgroup Usage
Cgroup Memory & Pids Usage
当我们进入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;
(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
到这一步,我们就将应用在容器中运行所需的文件准备好了;
如下图我们可以看到新创建的进程将~/test作为了根目录,且与宿主机文件隔离;
# 以 ~/test 作为新进程(容器)的根目录
chroot ~/test
/bin/ls -al
容器只是一个进程,不过容器中文件与宿主机文件相隔离
# 将上述准备好的~/test作为新进程的根目录
chroot ~/test
/bin/ls -al /helloworld/HelloWorld-Golang/
# 为你的宿主机所能执行的文件添加可执行权限
/bin/chmod +x /helloworld/HelloWorld-Golang/main-linux-arm
# 运行
/helloworld/HelloWorld-Golang/main-linux-arm
由于此处我们并未做网络相关的隔离,因此该进程(容器)共享宿主机网络资源;
接下来我们使用clone函数创建一个新的进程(容器),对其进行资源隔离,并运行上述HelloWorld程序;
(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;
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
(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; }
gcc container.c && ./a.out
/bin/chmod +x main-linux-arm && ./main-linux-arm
# 检查该进程(容器)资源隔离是否生效
/bin/hostname
/usr/sbin/ip a
当我们运行一个nginx容器时,总共分为以下几个步骤;
在该步骤,我们会将nginx镜像拉取下来,其实这个镜像就是一个文件夹,里面包含运行nginx所需要的文件,然后使用chroot将该文件设置为新进程的根目录,通过docker inspect 我们也可以看出;
docker pull nginx
docker run -itd nginx
# 查看该nginx容器详细信息,如下图所示
docker inspect ${containerID} | grep MergedDir
# 该目录即为当前运行的nginx容器的根目录
ls -al ${MergedDir}
(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
docker使用了UnionFS,可以将多个不同位置的目录联合挂载到同一个目录下,如下图,nginx运行的容器由多个lowerDir,UpperDir,WorkDir一起被合并挂载到了 MergeDir上;
在该步骤,我们相当于首先将lowerDir, upperDir, workDir通过UnionFS方式挂载至MergedDir中,然后运行 chroot 将该MergedDir作为新进程根目录, 然后主动运行应用;
容器只是运行在宿主机上的进程,其本质只是将文件等资源与宿主机相隔离,但共享整个操作系统内核,因此你对操作系统内核配置的修改会影响到运行在该宿主机上的所有容器;
tip: 如果你有任何疑问,欢迎留言,也欢迎关注我的公众号 “从零开始的Go学习”
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。