全局系统资源

操作系统的一个主要作用就是将硬件抽象成一个个可以操作的资源给上层应用使用。这些资源可以简单分为两类:

  • 独占资源:如页表、内存空间(堆、栈)、寄存器、CPU 时间片等,这些资源的是按照进程隔离,在进程看来这些资源都是自己独占的。
  • 全局资源:如网络、文件系统、设备等,这些资源的特性是在进程间共享的,不同进程的操作会影响到其他进程。

全局系统资源给进程带来相互通讯协调的能力,但是也带来一些问题,即进程间相互影响。

Namespace 列表

而 Namespace 就是 Linux 提供的一种对全局系统资源进程分组隔离的机制,即:同一个 Namespace 的进程看到的全局系统资源是共享的,而不同 Namespace 的进程全局系统资源是隔离的。截止到 Linux Kernel 5.6,Linux 提供了 8 种全局资源的 Namespace

NamespaceFlagman 手册内核版本说明
MountCLONE_NEWNSmount_namespaces(7)Kernel 2.4.19, 2002挂载命名空间(mount namespaces),隔离挂载点等信息,子挂载命名空间的挂载不会向上传递到父挂载命名空间,是 Linux 内核历史上第一个命名空间的概念。
UTSCLONE_NEWUTSuts_namespaces(7)Kernel 2.6.19, 2006Unix 主机命名空间(UTS namespaces, UNIX Time-Sharing),隔离主机名与域名等信息,不同的 UTS 命名空间可以拥有不同的主机名,在网络上呈现为多个主机。
IPCCLONE_NEWIPCipc_namespaces(7)Kernel 2.6.19, 2006进程间通信命名空间(IPC namespaces, Inter-Process Communication),隔离 System V IPC,不同 IPC 命名空间中的进程不能使用传统的 System V 风格的进程间通信方式,如共享内存(SHM)等。
PIDCLONE_NEWNETpid_namespaces(7)Kernel 2.6.24, 2008进程 ID 命名空间(PID namespaces),隔离进程的 PID 空间,不同的 PID 命名空间中的 PID 可以重复,互不影响。
NetworkCLONE_NEWNETnetwork_namespaces(7)Kernel 2.6.29, 2009网络命名空间(network namespaces),虚拟化一个完整的网络栈,每个网络栈拥有一套完整的网络资源,包括网络设备(interfaces)、路由表与防火墙等。与其他命名空间不同,网络命名空间没有层次结构,所有的网络命名空间互相独立,每个进程只能属于一个网络命名空间,并且网络命名空间在没有进程属于它的时候不会自动消失。
UserCLONE_NEWUSERuser_namespaces(7)Kernel 3.8, 2013用户命名空间(user namespaces),隔离用户与组信息,子用户命名空间中的每个用户和组(UID / GID)均映射到父用户命名空间中的一个用户和组,提供一种更好的权限隔离方式。通过将容器中的 root 用户映射到主机上的一个非特权用户,可以提升容器的安全性,这也是 LXC / LXD 实现「非特权容器」的方法。
CgroupCLONE_NEWCGROUPcgroup_namespaces(7)Kernel 4.6, 2016Cgroup 命名空间,类似 chroot,隔离 cgroup 层次结构,子命名空间看到的根 cgroup 结构实际上是父命名空间的一个子树。
TimeCLONE_NEWTIMEtime_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 函数:创建一个新进程(线程),与 forkvfork 相比:
    • 可以更精确的控制,哪些执行上下文在之间共享,可以做到 forkvforkpthread_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 手册非常详细的手册说明:

实验说明

后续文章,将以 Go 语言、 C 语言、Shell 命令三种形式,来介绍这些 Namespace。实验环境说明参见:容器核心技术(一) 实验环境准备 & Linux 基础知识