什么是 Namespace

Namespace 是 Linux 内核中实现的特性,本质上是一种资源隔离方案

Namespace,顾名思义,为不同的进程集合提供不同的「命名空间」,不同进程集合彼此不能访问其对应的「命名空间」,而「命名空间」其实就是其资源集合。

Namespace 提供了一种抽象机制,将原本全局共享的资源隔离成不同的集合,集合中的成员独享其原本全局共享的资源。

举个例子:进程 A 和进程 B 分别属于两个不同的 Namespace,那么进程 A 将可以使用 Linux 内核提供的所有 Namespace 资源:如独立的主机名,独立的文件系统,独立的进程编号等等。同样地,进程 B 也可以使用同类资源,但其资源与进程 A 使用的资源相互隔离,彼此无法感知。从用户层面来看,进程 A 读写属于 A 的 Namespace 资源,进程 B 读写属于 B 的 Namespace 资源,彼此之间安全隔离

Docker 就是利用 Namespace 这个特性,实现了容器之间的资源隔离。本质上来看,每一个 Docker 容器就是宿主机进程,不同 Docker 容器就对应不同的宿主机进程,这样,不同容器(即不同进程)就可以采用 Namespace 资源隔离,使得每一个容器看起来都像是一个独立的小虚拟机。

Linux 内核提供了 7 种不同的 Namespace,如下所示:

Namespace clone() 使用的 flag 所隔离的资源
Cgroup CLONE_NEWCGROUP Cgroup 根目录
IPC CLONE_NEWIPC System V IPC,POSIX 消息队列
Network CLONE_NEWNET 网络设备、协议栈、端口等
Mount CLONE_NEWNS 挂载点
PID CLONE_NEWPID 进程 ID
User CLONE_NEWUSER 用户和组 ID
UTS CLONE_NEWUTS 主机名和域名

怎么使用 Namespace

3 个系统调用

Namespace 乍一看似乎是非常复杂的特性,但其使用方式非常简单,只需要 3 个 API 即可,这 3 个 API 分别对应 3 个系统调用:

clone()

创建一个新的进程,通过不同的 flag(如上表格所示)将同时创建不同资源类型的 namespace,并将该进程作为其 namespace 的一个成员。

clone() 的原型为:

1
2
3
4
 #define _GNU_SOURCE
 #include <sched.h>
   	
 int clone(int (*child_fn)(void *), void *child_stack, int flags, void *arg);

其中:

  • child_fn:子进程运行的程序子函数;
  • child_stack:子进程所使用的栈空间;
  • flags:使用哪些 CLONE_* 的标志量,不同的 flag 可以使用布尔或的方式进行叠加。该变量的低位的几个 bytes 代表着当子进程终结之后,将给父进程发送什么样的信号。如果是 SIGCHLD,则当子进程结束后,会向父进程发送 SIGCHLD 信号,父进程此时可用 waitpid() 来等待子进程结束。如果不指定信号,则子进程结束后将不向父进程发送任何信号;
  • arg:可传入的用户参数;

函数执行成功将返回子进程的 PID,否则(发生错误)返回 -1。

setns()

让调用进程加入某个已存在的 namespace。

setns() 的原型是:

1
2
3
4
#define _GNU_SOURCE
#include <sched.h>
   
int setns(int fd, int nstype);

其中:

  • fd:要加入的 namespace 的文件描述符,一般为 /proc/[pid]/ns 下某个对应类型 namespace 的软链接;

  • nstype:调用进程想要加入的 namesapce 的类型,其类型对应上文的表格中的 7 种 Namespace 类型:

    • 0:允许加入任何类型的 namespace;
    • CLONE_NEWCGROUPfd 必须指向一个 cgroup 的 namespace;
    • CLONE_NEWIPCfd 必须指向一个 IPC 的 namespace;
    • CLONE_NEWNETfd 必须指向一个 network 的 namespace;
    • CLONE_NEWNSfd 必须指向一个 mount 的 namespace;
    • CLONE_NEWPIDfd 必须指向一个 pid 的 namespace;
    • CLONE_NEWUSERfd 必须指向一个 user 的 namespace;
    • CLONE_NEWUTSfd 必须指向一个 UTS 的 namespace;

unshare()

让调用进程移入一个新的 namespace。这个调用用的不是很多,有空再过来补充一下。

备注:使用 clone()unshare() 在大多数情况下都需要 CAP_SYS_ADMIN 权限(即 root 权限)。

/proc 目录与 Namespace

除了 3 个 API 之外,还需要关注 /proc 目录。

自 Linux 3.8 开始,用户就可以在 /proc/[pid]/ns 目录下看到指向不同 namespace 号的文件,如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# ls -l /proc/$$/ns
total 0
lrwxrwxrwx 1 root root 0 Apr 14 13:26 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Apr 14 13:26 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Apr 14 13:26 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Apr 14 13:26 net -> net:[4026531993]
lrwxrwxrwx 1 root root 0 Apr 14 13:26 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Apr 14 13:26 pid_for_children -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Apr 14 13:26 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Apr 14 13:26 uts -> uts:[4026531838]

每一个进程在其对应的 /proc/[pid]/ns 下都有其 namespace 信息,该目录下每一个文件都是一个软链接,setns() 可以通过打开对应进程下的 ns 目录下的软链接文件来加入到对应的 namespace 中。

该目录下的每个软链接的内容其实是一串字符,由 namespace 类型和 inode number 组成:

1
2
# readlink /proc/$$/ns/uts
uts:[4026531838]

且满足以下几个条件:

  • 若几个进程中对应 namespace 软链接内容一致,则这几个进程同属于同一个 namespace;

  • 即使 namespace 中的进程全部终结了,只要其软链接文件一直处于 open 状态,则 namespace 将一直存在;