5 分钟
容器核心技术(二) Namespace 概述
全局系统资源
操作系统的一个主要作用就是将硬件抽象成一个个可以操作的资源给上层应用使用。这些资源可以简单分为两类:
- 独占资源:如页表、内存空间(堆、栈)、寄存器、CPU 时间片等,这些资源的是按照进程隔离,在进程看来这些资源都是自己独占的。
- 全局资源:如网络、文件系统、设备等,这些资源的特性是在进程间共享的,不同进程的操作会影响到其他进程。
全局系统资源给进程带来相互通讯协调的能力,但是也带来一些问题,即进程间相互影响。
Namespace 列表
而 Namespace 就是 Linux 提供的一种对全局系统资源进程分组隔离的机制,即:同一个 Namespace 的进程看到的全局系统资源是共享的,而不同 Namespace 的进程全局系统资源是隔离的。截止到 Linux Kernel 5.6,Linux 提供了 8 种全局资源的 Namespace :
Namespace | Flag | man 手册 | 内核版本 | 说明 |
---|---|---|---|---|
Mount | CLONE_NEWNS | mount_namespaces(7) | Kernel 2.4.19, 2002 | 挂载命名空间(mount namespaces),隔离挂载点等信息,子挂载命名空间的挂载不会向上传递到父挂载命名空间,是 Linux 内核历史上第一个命名空间的概念。 |
UTS | CLONE_NEWUTS | uts_namespaces(7) | Kernel 2.6.19, 2006 | Unix 主机命名空间(UTS namespaces, UNIX Time-Sharing),隔离主机名与域名等信息,不同的 UTS 命名空间可以拥有不同的主机名,在网络上呈现为多个主机。 |
IPC | CLONE_NEWIPC | ipc_namespaces(7) | Kernel 2.6.19, 2006 | 进程间通信命名空间(IPC namespaces, Inter-Process Communication),隔离 System V IPC,不同 IPC 命名空间中的进程不能使用传统的 System V 风格的进程间通信方式,如共享内存(SHM)等。 |
PID | CLONE_NEWNET | pid_namespaces(7) | Kernel 2.6.24, 2008 | 进程 ID 命名空间(PID namespaces),隔离进程的 PID 空间,不同的 PID 命名空间中的 PID 可以重复,互不影响。 |
Network | CLONE_NEWNET | network_namespaces(7) | Kernel 2.6.29, 2009 | 网络命名空间(network namespaces),虚拟化一个完整的网络栈,每个网络栈拥有一套完整的网络资源,包括网络设备(interfaces)、路由表与防火墙等。与其他命名空间不同,网络命名空间没有层次结构,所有的网络命名空间互相独立,每个进程只能属于一个网络命名空间,并且网络命名空间在没有进程属于它的时候不会自动消失。 |
User | CLONE_NEWUSER | user_namespaces(7) | Kernel 3.8, 2013 | 用户命名空间(user namespaces),隔离用户与组信息,子用户命名空间中的每个用户和组(UID / GID)均映射到父用户命名空间中的一个用户和组,提供一种更好的权限隔离方式。通过将容器中的 root 用户映射到主机上的一个非特权用户,可以提升容器的安全性,这也是 LXC / LXD 实现「非特权容器」的方法。 |
Cgroup | CLONE_NEWCGROUP | cgroup_namespaces(7) | Kernel 4.6, 2016 | Cgroup 命名空间,类似 chroot,隔离 cgroup 层次结构,子命名空间看到的根 cgroup 结构实际上是父命名空间的一个子树。 |
Time | CLONE_NEWTIME | time_namespaces(7) | Kernel 5.6, 2020 | 系统时间命名空间,与 UTS 命名空间类似,允许不同的进程看到不同的系统时间。 |
Linux 创建进程
在 Linux 中,创建进程众所周知的就是 fork
函数。实际上,创建进程的库函数有:
fork
函数:通过复制当前进程的方式,创建一个新进程,返回新进程的进程 ID,父进程返回 0。注意:- 页表会进行全量复制,内存写时复制。
- Linux kernel 2.3.3 之前,fork 是一个系统调用包装
- Linux kernel 2.3.3 之后,fork 只是一个 glibc 的库函数,最终调用
clone
系统调用(使用SIGCHLD
标志)
vfork
函数:类似于fork
性能略优于fork
,不会复制页表。编写跨 Unix 平台程序时,不建议使用。注意:- 不会复制页表,因此新进程不应该修改内存而是直接调用
exec
相关函数 - 在 Linux 中
vfork
不是一个系统调用,只是一个 glibc 的库函数,最终调用clone
系统调用(使用CLONE_VM | CLONE_VFORK | SIGCHLD
标志)
- 不会复制页表,因此新进程不应该修改内存而是直接调用
clone
函数:创建一个新进程(线程),与fork
和vfork
相比:- 可以更精确的控制,哪些执行上下文在之间共享,可以做到
fork
、vfork
、pthread_create
类似的效果 - 可以控制进程的Namespace(容器的核心技术) 需要注意的是:
clone
函数 是一个 glibc 函数,其也是clone
系统调用的封装。clone
系统调用本身并不接受一个函数指针作为参数,其真实声明类似于,long clone(unsigned long flags, void *child_stack, void *ptid, void *ctid, struct pt_regs *regs);
,参见:stackoverflow。- 在 Linux 5.3 之后,
clone
函数 开始使用 clone3 系统调用 - 编写跨 Unix 平台程序时,不建议使用。
- 可以更精确的控制,哪些执行上下文在之间共享,可以做到
Namespace 实际上就是 Linux 在进程层面提供的一系列对全局系统资源进行隔离的机制。
系统调用和命令
Namespace 在 Linux 中是进程的属性和进程组紧密相关:一个进程的 Namespace 默认是和其父进程保持一致的。Linux 提供了几个系统调用,来创建、加入观察 Namespace:
- 创建:通过
clone(2) 系统调用
的 flag 来为新创建的进程创建新的 Namespace - 加入:通过
setns(2) 系统调用
将当前线程(注意当前进程不允许有多个线程)加入某个其他进程的 Namespace,docker exec
就是通过这个系统调用实现的(PID Namespace 是个例外,参见后续文章) - 创建:通过
unshare(2) 系统调用
为当前进程创建新的 Namespace(PID Namespace 是个例外,参见后续文章) - 查看:通过
ioctl_ns(2) 系统调用
来查看命名空间的关系(主要是 user namespace 和 pid namespace)
除了系统调用外,Linux 也提供了相应的命令来创建、加入 Namespace:
- 创建:通过
unshare(1) 命令
启动一个进程,然后再为该进程,创建新的 Namespace(PID Namespace 是个例外,参见后续文章),该命令的实现为:先调用unshare(2) 系统调用
,然后exec
执行命令 - 加入:通过
nsenter(1) 命令
启动一个进程,然后再将该进程,加入一个 Namespace(PID Namespace 是个例外,参见后续文章),该命令的实现为:先调用setns(2) 系统调用
,然后fork-exec
执行命令
官方手册
关于 Namespace 的描述,Linux 手册非常详细的手册说明:
- namespaces(7) - 整体描述
- mount_namespaces(7)
- uts_namespaces(7)
- ipc_namespaces(7)
- pid_namespaces(7)
- network_namespaces(7)
- user_namespaces(7)
- cgroup_namespaces(7)
- time_namespaces(7)
实验说明
后续文章,将以 Go 语言、 C 语言、Shell 命令三种形式,来介绍这些 Namespace。实验环境说明参见:容器核心技术(一) 实验环境准备 & Linux 基础知识