本页目录

命名空间

Namespace 是 Linux 容器的基础原语——每种 namespace 为进程提供一类资源的隔离幻觉:PID namespace 让容器里的进程以为自己是 PID 1,user namespace 把容器内的 root 映射为宿主机上的普通用户,network namespace 让每个容器看到独立的网卡和路由表。nsproxy 把多个 namespace 绑定为一个进程的"隔离视图"。 内核版本: 2.6 ~ 6.x

概述

Namespace 是 Linux 容器的基础原语——每个 namespace 为其中的进程提供资源隔离的幻觉⁠。一个进程在 PID namespace 中看到自己是 PID 1,在 mount namespace 中看到独立的文件系统树,在 network namespace 中看到独立的网卡和路由表。

Namespace 与 cgroup 的互补关系:namespace 隔离可见性⁠(看到什么),cgroup 限制资源量⁠(能用多少)。Docker/LXC/Podman 就是在 namespace + cgroup + seccomp 堆叠之上构建的。

8 种 Namespace

TypeClone Flag隔离内容引入
MountCLONE_NEWNS文件系统挂载点,各自可以 mount/umount2.4.19
UTSCLONE_NEWUTShostname, domainname2.6.19
IPCCLONE_NEWIPCSysV IPC (shm/sem/msg), POSIX mq2.6.19
PIDCLONE_NEWPID进程 ID,ns 内 PID 1 看到自己为 init2.6.24
NetworkCLONE_NEWNET网卡, 路由, netfilter, socket2.6.24 (完成 2.6.29)
UserCLONE_NEWUSERUID/GID 映射 (ns 内 root = host 普通用户)3.8
CgroupCLONE_NEWCGROUPcgroup 根目录 (容器内看不到 host cgroup 树)4.6
TimeCLONE_NEWTIMECLOCK_MONOTONIC/BOOTTIME 偏移 (每个 ns 独立时间)5.6

内核实现: nsproxy

// include/linux/nsproxy.h
struct nsproxy {
    atomic_t count;                          // 引用计数 (fork 共享)
    struct uts_namespace *uts_ns;
    struct ipc_namespace *ipc_ns;
    struct mnt_namespace *mnt_ns;
    struct pid_namespace *pid_ns_for_children; // 注意: 不是自己的 PID ns!
    struct net *net_ns;
    struct cgroup_namespace *cgroup_ns;
    struct time_namespace *time_ns;
};

每个 task_struct → nsproxy 指向该进程所属的各 namespace。fork() 时子进程共享父进程的 nsproxy(count++)。unshare() 时克隆 nsproxy 并将指定类型的 namespace 替换为新创建的。setns(fd) 将当前进程加入已有的 namespace(fd 来自 /proc/<pid>/ns/<type>)。

PID Namespace: 嵌套与 init 进程

PID namespace 形成嵌套树——父 ns 可以看到子 ns 的进程(在父 ns 中有普通 PID),子 ns 看不到父 ns 的进程。

每个 PID namespace 有自己的 init 进程⁠(PID 1)。PID 1 的特殊责任:

  1. 孤儿收养⁠: 父进程在子进程之前退出 → 子进程被 re-parent 到 PID 1
  2. wait() 循环⁠: PID 1 必须主动 wait() 回收僵尸,否则僵尸在 ns 内积累直到 PID 耗尽
  3. 不能被 kill: SIGKILL 对 PID 1 无效(除非从祖先 ns 发)。但 PID 1 可以被 SIGSTOP 暂停
  4. PID 1 退出 → 整个 ns 被销毁⁠: 内核给 ns 中所有进程发 SIGKILL

容器中的 PID 1 通常是应用本身(如 CMD ["nginx", "-g", "daemon off;"])——它必须正确处理信号(SIGTERM)和孤儿回收。如果不做,僵尸进程会积累。

User Namespace: root 在 ns 内 = nobody 在 host

User namespace 是容器安全的基础——它允许非特权用户在 ns 内拥有 root 身份:

/proc/<pid>/uid_map:
  ns_uid 0 → host_uid 1000           # ns 内 root = host 普通用户
  ns_uid 1 → host_uid 100000         # ns 内普通用户 = host 从 100000 开始的 subuid

# 非特权用户创建 user ns (kernel.unprivileged_userns_clone=1):
$ unshare --user --map-root-user
# → ns 内你看到自己是 root, 但在 host 上你还是普通用户

关键安全特性:user ns 内的"root"只能操作该 user ns 拥有的资源⁠——不能访问 host 文件、不能加载内核模块、不能 kill host 进程。这种"seeming root, actually jailed"使得 rootless 容器成为可能(Podman rootless mode)。

Mount Namespace: 独立的挂载树

每个 mount namespace 有独立的文件系统挂载点列表。一个 ns 中的 mount 不影响其他 ns。CLONE_NEWNS 创建时,子 ns 接收父 ns 的挂载点副本⁠(不是共享)——之后各自独立修改。

共享子树 (shared subtrees, 2.6.15+): mount --make-shared /path 使得挂载事件在多个 ns 之间传播。Systemd 用这个机制在 host 和容器间共享 /run

Network Namespace: 独立的网络栈

netns 是最常用的 namespace——每个容器需要一个。每 netns 有独立的:网卡列表、路由表、netfilter 规则、socket 绑定。ip netns add myns 创建新 netns(实际是 bind mount /var/run/netns/myns)。ip link set eth0 netns myns 把网卡移入 netns。

用户态 API

# 创建新 PID + mount + net namespace:
unshare --pid --fork --mount --net --mount-proc /bin/bash
# --fork: 必须 — 否则新 PID ns 的 init 还是原来的 shell
# --mount-proc: 在新 ns 中 mount procfs → ps 只显示 ns 内进程

# 进入已有 namespace:
nsenter -t <pid> --all

# 列出 namespace:
lsns

参考

  • 源码⁠: kernel/nsproxy.c, kernel/pid_namespace.c, kernel/user_namespace.c, net/core/net_namespace.c, fs/namespace.c
  • 内核文档⁠: Documentation/admin-guide/namespaces/
  • LWN: "Namespaces in Linux" 系列

Keywords: namespace, nsproxy, PID namespace, user namespace, UID mapping, mount namespace, network namespace, CLONE_NEWNS, container, setns, unshare