Docker容器本质上是宿主机上的进程。Docker通过namespace实现了资源隔离,通过cgroups实现了资源限制,通过写时复制机制(copy-on-write)实现了高效的文件操作。
namespace资源隔离
完成一个基本容器需要六项隔离,Linux内核中提供了这六种隔离的系统调用:
namespace | 系统调用参数 | 隔离内容 |
---|---|---|
UTS | CLONE_NEWUTS | 主机名与域名 |
IPC | CLONE_NEWIPC | 信号量、消息队列和共享内存 |
PID | CLONE_NEWPID | 进程编号 |
Network | CLONE_NEWNET | 网络设备、网络栈、端口等 |
Mount | CLONE_NEWNS | 挂载点(文件系统) |
User | CLONE_NEWUSER | 用户组和用户组 |
linux内核实现namespace的一个主要目的,就是为了实现轻量级虚拟化(容器)技术服务。在同一个namespace下的进程可以感知彼此的变化,而对外界的进程一无所知。这样就可以让容器中的进程产生错觉,仿佛自己置身一个独立的系统环境中,以达到隔离的目的。(这里讨论的namespace实现针对的是linux内核3.8及以后版本)
namespace API的4种操作
clone()
通过 clone() 在创建新进程的同时创建 namespace
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
Clone() 其实是 linux 系统调用 fork() 的一种更通用的实现方式,它可以通过 flags 来控制使用多少功能。一共有 20 多种 CLONE_ 开头的 falg(标志位) 参数用来控制 clone 进程的方方面面(比如是否与父进程共享虚拟内存等),下面我们只介绍与 namespace 相关的 4 个参数:
- fn:指定一个由新进程执行的函数。当这个函数返回时,子进程终止。该函数返回一个整数,表示子进程的退出代码。
- child_stack:传入子进程使用的栈空间,也就是把用户态堆栈指针赋给子进程的 esp 寄存器。调用进程(指调用 clone() 的进程)应该总是为子进程分配新的堆栈。
- flags:表示使用哪些 CLONE_ 开头的标志位,与 namespace 相关的有CLONE_NEWIPC、CLONE_NEWNET、CLONE_NEWNS、CLONE_NEWPID、CLONE_NEWUSER、CLONE_NEWUTS 和 CLONE_NEWCGROUP。
- arg:指向传递给 fn() 函数的参数。
setns()
通过 setns() 函数可以将当前进程加入到已有的 namespace 中。(在docker中,使用docker exec命令在已经运行的容器中执行新的命令,就需要用到该方法。)
#include <sched.h>
int setns(int fd, int nstype);
和 clone() 函数一样,C 语言库中的 setns() 函数也是对 setns() 系统调用的封装:
- fd:表示要加入 namespace 的文件描述符。它是一个指向 /proc/[pid]/ns 目录中文件的文件描述符,可以通过直接打开该目录下的链接文件或者打开一个挂载了该目录下链接文件的文件得到。
- nstype:参数 nstype 让调用者可以检查 fd 指向的 namespace 类型是否符合实际要求。若把该参数设置为 0 表示不检查。
unshare()
通过 unshare 函数可以在原进程上进行 namespace 隔离。也就是创建并加入新的 namespace 。
#include <sched.h>
int unshare(int flags);
和前面两个函数一样,C 语言库中的 unshare() 函数也是对 unshare() 系统调用的封装。调用 unshare() 的主要作用就是:不启动新的进程就可以起到资源隔离的效果,相当于跳出原先的 namespace 进行操作。
fork()
系统调用函数fork()并不属于namespace的API,当程序调用fork()函数时,系统会创建新的进程,为其分配资源,例如存储数据和代码的空间,然后把原来进程的所有值复制到新的进程中,只有少量数值与原来的进程不同,相当于复制了本身。
fork()的神奇之处在于它被调用一次,却能返回两次(父进程与子进程各返回一次),通过返回值的不同就可以区分父进程与子进程。他可能有以下3种不同的返回值:
- 在父进程中,fork()返回新创建子进程的进程id;
- 在子进程中,fork()返回0;
- 如果出现问题,fork()返回一个负值。
使用fork()后,父进程有义务监控子进程的运行状态,并在子进程推出后才能正常退出,否则子进程就会成为“孤儿”进程。
下面将根据docker内部对namespace资源隔离使用方式分别对6种namespace进行解析。
UTS namespace
UTS (UNIX TIme-sharing System) namespace 提供了主机和域名的隔离,这样每个Docker容器就可以拥有独立的主机名和域名,在网络上可以被视作一个独立的节点,而不是宿主机上的一个进程。Docker中,每个镜像基本都以自身提供的服务名称来命名hostname,且不会对宿主机产生任何影响,其原理就是利用了UTS namespace。
IPC namespace
进程间通信(Inter-Process Communication,IPC)设计的IPC资源包括常见的信号量、消息队列和共享内存。
申请IPC资源就申请了一个全局唯一的32位ID。所以IPC namespace中实际上包含了系统IPC标识符以及实现POSIX消息队列的文件系统。在同一个IPC namespace中的进程彼此可见,不同的namespace中的进程则互不可见。
PID namespace
PID namespace的隔离非常实用,他对进程PID重新编号,即两个不同namespace下的进程可以拥有相同的PID,每个PID namespace都有自己的计数程序。内核为所有的PID namespace维护了一个树状结构,最顶层的是系统初始时创建的,被称为root namespace。它创建的新PID namespace被称为child namespace,而原先的PID namespace就是新创建的PID namespace的child namespace,而原来的PID namespace就是新创建的PID namespace的 parent namespace。
通过这种方式,不同的PID namespace会形成一个层级体系,所属的父节点可以看到子节点中的进程,并可以通过信号等方式对子节点中的进程产生影响。但是子节点却看不到父节点PID namespace中的任何内容。
mount namespace
mount namespace通过隔离文件系统挂载点对隔离文件系统提供支持。隔离后,不同的mount namespace中的文件结构发生变化也互不影响。
network namespace
network namespace主要提供了关于网络资源的隔离,包括网络设备,IPv4,IPv6协议栈、IP路由表、防火墙、/proc/net目录、/sys/class/net目录、套接字等。
user namespace
user namespace隔离了安装相关的标识符和属性
cgroups资源限制
它不但可以限制被namespace隔离起来的资源,还可以为资源设置权重、计算使用量、操控任务(进程或线程)启停等。
cgroups 的作用
cgroups 为不同用户层面的资源管理提供了一个统一接口,从单个的资源控制到操作系统层面的虚拟化,cgroups提供了4大功能。
- 资源限制
- cgroups可以对任务使用的资源总额进行限制。如设定应用运行时使用的内存上限,一旦超过配额就发出OOM提示
- 优先级分配
- 通过分配的CPU时间片数量以及磁盘IO带宽大小,实际上就相当于控制了任务运行的优先级
- 资源统计
- cgroups可以统计系统的资源使用量如CPU使用时长,内存用量等,这个功能非常适用于计费
- 任务控制
- cgroups 可以对任务进行挂起、恢复等操作