如果我们想像 Docker 一样实现一个简陋的资源隔离的容器,我们需要隔离文件系统、需要隔离网络、需要隔离主机名、需要隔离进程间的通信,需要隔离PID,需要隔离用户和用户组。
Namespace 是 Linux 提供的资源隔离机制。只有在同一个 Namespace 下的进程可以相互联系,但无法感受到外部进程的存在,营造出处于一个独立的系统环境中的错觉,从而实现了隔离。
Linux内核中就提供了这六种 namespace 隔离的系统调用,如下表所示。
Linux提供的操作 namespace 相关 API 介绍如下:
API包括 clone()、setns( )以及 unshare(),还有 /proc 下的部分文件。为了确定隔离的到底是哪种 namespace,在使用这些 API 时,通常需要指定上面图中的参数的六个常数的一个或多个,通过 |(位或)操作来实现。
- clone()
clone()是fork()的一种实现方式。调用fork()函数时系统会创建新进程,为其分配资源,并把原来进程中的值复制到新进程中,可通过 fpid 区分新进程和父进程
int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);
参数:
child_func 传入子进程运行的程序主函数。
child_stack 传入子进程使用的栈空间
flags 表示使用哪些 CLONE_* 标志位
args 则可用于传入用户参数
- setns()
setns()用于加入已存在的namespace
int setns(int fd, int nstype);
参数:
fd:要加入namespace的文件描述符(指向/proc/[pid]/ns目录)。
nstype:要加入namespace的类型,用于检查,0表示不检查。
- unshare()
unshare()在原进程上进行namespace隔离。
int unshare(int flags)
unshare()不启动新进程,但是跳出了原来的namespace。
UTS(UNIX Time-sharing System)namespace
分时系统(Time-sharing System)中一台主机连接了若干个终端,每个终端有一个用户在使用。
用户交互式地向系统提出命令请求,系统接受每个用户的命令,采用时间片轮转方式处理服务请求,并通过交互方式在终端上向用户显示结果。用户根据上步结果发出下道命令。
分时操作系统将CPU的时间划分成若干个片段,称为时间片。操作系统以时间片为单位,轮流为每个终端用户服务。每个用户轮流使用一个时间片而使每个用户并不感到有别的用户存在。
UTS(Unix Time-sharing System) namespace提供了主机名和域名的隔离,使每个Docker容器可以拥有独立的主机名和域名,在网络上可以视为独立的节点。
下面通过 go 来简单实现 UTS 隔离
package main
import (
"log"
"os"
"os/exec"
"syscall"
)
func main() {
cmd := exec.Command("sh")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS,
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
在 Linux 环境下执行
go run uts.go
此时在一个新的进程中执行了sh命令,由于指定了 flag syscall.CLONE_NEWUTS, 此时已经与之前的进程不在同一个 UTS namespace中了。在新 sh 和原 sh 中分别执行 ls -l /proc/$$/ns 进行验证,可以看到这里两个只有 uts 所指向的 ID 不同,说明已经隔离成功
IPC(Interprocess Communication)namespace
容器中进程间通信采用的方法包括常见的信号量、消息队列和共享内存。然而与虚拟机不同的是,容器内部进程间通信对宿主机来说,实际上是具有相同 PID namespace 中的进程间通信,因此需要一个唯一的标识符来进行区别。申请 IPC 资源就申请了这样一个全局唯一的 32 位 ID,所以 IPC namespace 中实际上包含了系统 IPC 标识符以及实现POSIX消息队列的文件系统。在同一个 IPC namespace下的进程彼此可见,而与其他的IPC namespace下的进程则互相不可见。
IPC namespace在代码上的变化与UTS namespace相似,只是标识位有所变化,需要加上 CLONE_NEWIPC 参数。
我们首先在 shell 中使用 ipcmk -Q 命令创建一个message queue。
使用 ipcs -q 查看已经有一个 id 为 0 message queue
我们可以运行 ipd.go 新建的子进程中执行 ipcs -q 查看 message queue。
上面的结果显示中可以发现,已经找不到原先声明的message queue,实现了IPC的隔离。
Process identifier(PID) namespace
PID是大多数操作系统的内核用于唯一标识进程的一个数值。
PID为1的进程是init,作为所有进程的父进程,不会处理来自其他进程的信号(信号屏蔽),并维护一张进程表,当有子进程变成孤儿时会回收其资源并结束进程。
PID namespace隔离对进程PID重新编号,两个不同namespace下的进程没有关系,因此PID也可以相同。内核为所有的PID namespace维护了一个树状结构。
其中:
1)每个PID namespace中的第一个进程拥有特权。
2)一个namespace中的进程不能影响父节点或兄弟节点namespace中的进程。
3)root namespace中可以看到所有的进程,包括所有后代节点中的namespace。
4)在外部可以通过监控Docker daemon所在的PID namespace中的所有进程和子进程来实现对Docker中运行的程序的监控。
同样修改 flag 运行代码看 pid 隔离的效果:
Mount namespace
mount namespace 是历史上第一个Linux namespace,通过隔离文件系统挂载点隔离文件系统,标识位为CLONE_NEWNS。隔离之后不同的mount namespace中的文件结构互不影响。
可以通过/proc/[pid]/mounts查看所有挂载在当前namespace中的文件系统。进程创建mount namespace时把当前文件结构复制给新的namespace。
挂载传播(mount propagation)定义了挂载对象之间的关系,解决了文件结构复制过程中子节点namespace影响父节点namespace文件系统的问题。
共享关系(share relationship):存在挂载关系的两个挂载对象中的事件会双向传播
从属关系(slave relationship):挂载对象中的事件只能按指向从属对象的方向传播(共享挂载—>从属挂载)
Network namespace
network namespace提供了关于网络资源的隔离,包括网络设备、IPv4和IPv6协议栈、IP路由表、防火墙、/proc/net目录、/sys/class/net目录、套接字(socket)等。
注意:
一个物理的网络设备最多存在于一个network namespace中
可以通过veth pair在不同的network namespace中创建通道进行通信。
一般情况下,物理网络设备都分配在最初的root namespace中。
User namespace
user namespace主要隔离了安全相关的标识符和属性(用户ID、用户组ID、root目录、key(密钥)、特殊权限)。
因此用 clone() 创建的新进程在新的 user namespace 中可以拥有不同的用户和用户组,在新进程创建的容器中,它是超级用户,但在容器之外只是普通用户。
Linux中,特权用户的 user ID 是 0,user ID 非 0 的进程启动 user namespace后 user ID 可以变为 0
参考:
http://www.sel.zju.edu.cn/?p=556
http://lionheartwang.github.io/blog/2018/03/18/dockerzi-yuan-ge-chi-he-xian-zhi-shi-xian-yuan-li/