当你在一个容器里执行 ps aux,只看到自己的进程;执行 ifconfig,只看到自己的网卡;执行 hostname,只看到自己的主机名——这不是魔法,而是 Linux Namespace 在工作。Namespace 让每个进程拥有独立的系统资源视图,仿佛运行在专属的操作系统中。
Namespace 的概念并非 Linux 首创。1992 年,Bell Labs 的 Plan 9 操作系统率先提出了”per-process namespace”的理念——每个进程可以拥有独立的文件系统命名空间。这一思想深刻影响了后来的 Linux 内核设计。2002 年,Linux 2.4.19 引入了第一个 Namespace——Mount Namespace,允许进程拥有独立的文件系统挂载视图。此后十余年间,内核逐步添加了 PID(2006,2.6.24)、Network(2009,2.6.29)、IPC(2006,2.6.19)、UTS(2006,2.6.19)、User(2013,3.8)、Cgroup(2016,4.6)、Time(2020,5.6)等 Namespace。2013 年 Docker 诞生后,Namespace 从”内核开发者的小众特性”一跃成为容器技术的基石——容器的”视图隔离”全部由 Namespace 实现。理解 Namespace 的演进脉络,有助于理解为什么不同 Namespace 的行为差异如此之大(例如 User Namespace 支持”非特权创建”,而 Network Namespace 需要 CAP_NET_ADMIN),因为这些差异根植于它们各自的历史背景和设计目标。
前置知识
- Linux 系统编程基础:
clone()、unshare()、setns()三个系统调用是操作 Namespace 的核心接口 - Ch01 容器全景:从 chroot 到 OCI:建立容器技术的全景认知,理解 Namespace 在容器架构中的位置
- Linux
/proc文件系统:Namespace 的信息通过/proc/[pid]/ns/目录暴露
如果你对 Linux 进程和文件系统的基础概念还不熟悉,推荐先阅读姊妹系列「从零剖析 Linux 操作系统」中的 Cgroups 与 Namespaces 和 VFS 与文件系统 章节。
本章将深入 8 种 Linux Namespace 的原理与实现。不仅要知道”Namespace 能隔离什么”,更要理解”Namespace 怎么隔离”、“隔离的边界在哪里”、“哪些场景可以逃逸”。
一、Namespace 核心概念
1.1 什么是 Namespace?
Namespace 的核心思想是资源隔离:将全局资源包装为一个抽象,让 Namespace 内的进程看起来拥有独立的资源实例。内核为每种资源维护了一个映射表,将 Namespace 内的虚拟 ID 映射到全局的实际 ID。
// Linux 内核中 Namespace 的核心数据结构(简化)struct nsproxy { struct uts_namespace *uts_ns; // UTS: 主机名 struct ipc_namespace *ipc_ns; // IPC: 进程间通信 struct mnt_namespace *mnt_ns; // Mount: 挂载点 struct pid_namespace *pid_ns; // PID: 进程 ID struct net *net_ns; // Network: 网络栈 struct cgroup_namespace *cgroup_ns; // Cgroup: Cgroup 视图 struct user_namespace *user_ns; // User: 用户 ID struct time_namespace *time_ns; // Time: 时钟};
// 每个进程的 task_struct 包含 nsproxy 指针struct task_struct { struct nsproxy *nsproxy; // ...};1.2 三个系统调用
Linux 提供了三个系统调用来操作 Namespace:
| 系统调用 | 功能 | 关键参数 |
|---|---|---|
clone() | 创建新进程并指定新 Namespace | CLONE_NEWPID, CLONE_NEWNET, … |
unshare() | 将当前进程移入新 Namespace | 同上 |
setns() | 将当前进程加入已有 Namespace | fd(指向 /proc/PID/ns/ 的文件描述符) |
// clone: 创建新进程并加入新 Namespace#define _GNU_SOURCE#include <sched.h>#include <stdio.h>#include <stdlib.h>
static int child_func(void *arg) { printf("Child PID: %d\n", getpid()); // 在新 PID Namespace 中,PID 为 1 return 0;}
int main() { // 分配子进程栈 char *stack = malloc(1024 * 1024); char *stack_top = stack + 1024 * 1024;
// 创建子进程,同时创建新的 PID 和 Mount Namespace pid_t pid = clone(child_func, stack_top, CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL); if (pid == -1) { perror("clone"); return 1; }
printf("Parent PID: %d, Child PID: %d\n", getpid(), pid); wait(NULL); free(stack); return 0;}# unshare: 将当前进程移入新 Namespace# 创建新的 Mount Namespace 并挂载 procsudo unshare --mount --pid --fork --mount-proc /bin/bash
# setns: 加入已有的 Namespace# 进入 Docker 容器的 Network Namespacesudo nsenter -t $(docker inspect -f '{{.State.Pid}}' mycontainer) -n ip addr1.3 Namespace 的继承与嵌套
Namespace 支持嵌套——一个 Namespace 可以是另一个 Namespace 的子 Namespace。子 Namespace 中的资源对父 Namespace 可见(反之不一定),这取决于具体的 Namespace 类型。
二、PID Namespace
2.1 原理
PID Namespace 隔离进程 ID 号空间。在新的 PID Namespace 中,第一个进程的 PID 为 1,后续进程依次递增。宿主机上,这些进程仍然有全局唯一的 PID。
# 在容器中看到的 PIDdocker exec mycontainer ps aux# PID USER COMMAND# 1 root nginx: master process# 10 nginx nginx: worker process
# 在宿主机上看到的同一进程ps aux | grep nginx# root 12345 ... nginx: master process# 101 12346 ... nginx: worker process2.2 PID 1 的特殊性
PID 1 在 Linux 中有特殊地位:
| 特性 | 说明 |
|---|---|
| 信号处理 | PID 1 默认忽略 SIGINT 和 SIGTERM,除非显式注册处理函数 |
| 僵尸回收 | PID 1 负责回收所有子进程的退出状态(wait) |
| 优雅退出 | 容器停止时,PID 1 收到 SIGTERM,应优雅关闭子进程 |
很多容器因为 PID 1 进程选择不当(如使用 shell 脚本启动应用),导致无法优雅退出或僵尸进程堆积。推荐使用 tini 或 dumb-init 作为 PID 1 进程。
2.3 PID Namespace 的限制
- PID Namespace 嵌套最多 32 层
- 从父 Namespace 可以看到子 Namespace 的进程(但 PID 不同)
- 从子 Namespace 无法看到父 Namespace 的进程
kill系统调用只能发送信号给同一 Namespace 内的进程(除非有 CAP_KILL)
三、Mount Namespace
3.1 原理
Mount Namespace 隔离文件系统挂载点视图。不同 Mount Namespace 中的进程可以看到不同的挂载点列表。
# 创建新的 Mount Namespacesudo unshare --mount /bin/bash
# 在新 Namespace 中挂载 tmpfsmount -t tmpfs tmpfs /mntmount | grep /mnt# tmpfs on /mnt type tmpfs
# 在另一个终端(宿主 Namespace)查看mount | grep /mnt# 看不到 /mnt 的挂载——因为 Mount Namespace 隔离了挂载点视图3.2 共享子树(Shared Subtrees)
Mount Namespace 的关键特性是共享子树传播类型,它决定了挂载事件如何在 Namespace 之间传播:
| 传播类型 | 说明 | 典型用途 |
|---|---|---|
shared | 挂载/卸载事件传播到对等组 | 系统默认,USB 热插拔 |
slave | 只接收对等组的传播,不反向传播 | 容器只读共享宿主挂载 |
private | 不传播也不接收 | 容器独立挂载 |
unbindable | 不能被 bind mount | 防止递归挂载 |
# 查看挂载点的传播类型findmnt -o TARGET,PROPAGATION
# 将挂载点设为 private(容器常用)mount --make-private /
# 将挂载点设为 slavemount --make-slave /sys3.3 容器中的 Mount 操作
runc 在创建容器时,会执行一系列 mount 操作:
# runc 的典型 mount 操作(简化)mount -t proc proc /proc # 挂载 procfsmount -t sysfs sysfs /sys # 挂载 sysfsmount -t devtmpfs devtmpfs /dev # 挂载 devtmpfsmount -t tmpfs tmpfs /dev/shm # 挂载共享内存mount -t tmpfs tmpfs /run # 挂载运行时目录mount -t cgroup2 cgroup2 /sys/fs/cgroup # 挂载 Cgroup v2四、Network Namespace
4.1 原理
Network Namespace 隔离网络栈——包括网络设备、IP 地址、路由表、端口号、iptables 规则等。
# 创建 Network Namespacesudo ip netns add ns1sudo ip netns add ns2
# 查看 Namespace 中的网络设备sudo ip netns exec ns1 ip link# 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN# 只有 loopback 设备,且处于 DOWN 状态
# 创建 veth pair 连接两个 Namespacesudo ip link add veth1 type veth peer name veth2sudo ip link set veth1 netns ns1sudo ip link set veth2 netns ns2
# 配置 IP 地址sudo ip netns exec ns1 ip addr add 10.0.0.1/24 dev veth1sudo ip netns exec ns1 ip link set veth1 upsudo ip netns exec ns1 ip link set lo up
sudo ip netns exec ns2 ip addr add 10.0.0.2/24 dev veth2sudo ip netns exec ns2 ip link set veth2 upsudo ip netns exec ns2 ip link set lo up
# 测试连通性sudo ip netns exec ns1 ping -c 3 10.0.0.24.2 容器网络模式
Docker 支持多种网络模式,每种模式对 Network Namespace 的使用不同:
| 网络模式 | Namespace 策略 | 特点 |
|---|---|---|
| bridge | 独立 Network NS + veth pair | 默认模式,容器有独立 IP |
| host | 共享宿主 Network NS | 性能最好,无网络隔离 |
| none | 独立 Network NS,仅 loopback | 无网络,用于安全隔离 |
| container | 共享另一容器的 Network NS | Pod 内容器共享网络栈 |
4.3 Network Namespace 与 CNI
Kubernetes 通过 CNI(Container Network Interface)插件配置 Pod 的网络:
{ "cniVersion": "0.4.0", "name": "bridge-network", "type": "bridge", "bridge": "cni0", "ipam": { "type": "host-local", "subnet": "10.244.0.0/16", "routes": [{"dst": "0.0.0.0/0"}] }}Network Namespace 是容器网络的基础,但 CNI 插件负责了 veth pair 创建、bridge 连接、IP 分配、路由配置等复杂工作。详见 Ch12 容器网络。
五、User Namespace
5.1 原理
User Namespace 隔离用户和组 ID。最强大的特性是UID 映射:容器内的 root(UID 0)可以映射到宿主机上的普通用户(UID 100000),实现 rootless 容器。
# 创建 User Namespace,映射 UIDsudo unshare --user --map-root-user /bin/bash
# 在新 User Namespace 中id# uid=0(root) gid=0(root)
# 但在宿主机上,这个进程的实际 UID 是普通用户# 从另一个终端查看ps -o pid,uid,ruid,comm -p $(pgrep -f "unshare")5.2 UID/GID 映射
User Namespace 通过 /proc/PID/uid_map 和 /proc/PID/gid_map 定义映射关系:
# 查看 Docker 容器的 UID 映射cat /proc/$(docker inspect -f '{{.State.Pid}}' mycontainer)/uid_map# 0 100000 65536# 含义:容器内 UID 0-65535 → 宿主机 UID 100000-165535
# 手动配置 UID 映射echo "0 100000 65536" > /proc/$PID/uid_mapecho "0 100000 65536" > /proc/$PID/gid_map5.3 User Namespace 与 Capability
User Namespace 的一个关键特性:在新的 User Namespace 中,进程拥有全部 Capability,但这些 Capability 只在该 Namespace 内有效。
六、IPC / UTS / Cgroup / Time Namespace
6.1 IPC Namespace
IPC Namespace 隔离 System V IPC 对象和 POSIX 消息队列:
# 查看 System V IPC 对象ipcs -q # 消息队列ipcs -m # 共享内存ipcs -s # 信号量
# 在新 IPC Namespace 中,看不到宿主的 IPC 对象sudo unshare --ipc /bin/bashipcs -q # 空的6.2 UTS Namespace
UTS Namespace 隔离主机名和域名(源自 UNIX Time-Sharing System):
# 创建新 UTS Namespace 并设置主机名sudo unshare --uts /bin/bashhostname mycontainerhostname# mycontainer
# 宿主机的主机名不受影响6.3 Cgroup Namespace
Cgroup Namespace 隔离 Cgroup 根目录视图。在容器内,进程只能看到自己的 Cgroup 子树:
# 不使用 Cgroup Namespacecat /proc/self/cgroup# 0::/system.slice/docker-abc123.scope → 看到完整路径
# 使用 Cgroup Namespacesudo unshare --cgroup /bin/bashcat /proc/self/cgroup# 0::/ → 看到的是根目录,不知道自己在子树中6.4 Time Namespace
Time Namespace(Linux 5.6+)隔离 CLOCK_BOOTTIME 和 CLOCK_MONOTONIC 时钟,主要用于容器迁移场景(如 checkpoint/restore):
# 创建 Time Namespacesudo unshare --time /bin/bash
# 修改 Time Namespace 的偏移# /proc/PID/timens_offsets七、Namespace 组合与容器配置
7.1 runc 的默认 Namespace 配置
runc 创建容器时,默认配置 6 种 Namespace:
{ "linux": { "namespaces": [ { "type": "pid" }, { "type": "mount" }, { "type": "ipc" }, { "type": "uts" }, { "type": "network" }, { "type": "cgroup" } ] }}7.2 Namespace 组合对比
| 组合方式 | 用途 | 示例 |
|---|---|---|
| 全部独立 | 标准容器 | docker run |
| 共享 Network NS | Pod 内容器 | Kubernetes Pod |
| 共享 IPC NS | 进程间通信 | System V 共享内存 |
| 共享 PID NS | 进程可见 | 调试 sidecar |
| User NS + 其他 NS | Rootless 容器 | Podman rootless |
Kubernetes Pod 中多个容器共享 Network Namespace 时,应用之间可以通过 localhost 直接通信,但端口不能冲突。设计 Pod 时要为每个容器规划好监听端口,避免端口抢占导致启动失败。
7.3 Namespace 与安全
八、动手实践
8.1 用 Go 创建隔离进程
package main
import ( "fmt" "os" "os/exec" "syscall")
func main() { switch os.Args[1] { case "run": run() case "child": child() default: panic("invalid command") }}
func run() { cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr
// 创建新的 Namespace cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC, }
must(cmd.Run())}
func child() { fmt.Printf("Running %v as PID %d\n", os.Args[2:], os.Getpid())
// 挂载 proc(在新 Mount Namespace 中) must(syscall.Mount("proc", "/proc", "proc", 0, ""))
cmd := exec.Command(os.Args[2], os.Args[3:]...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr must(cmd.Run())}
func must(err error) { if err != nil { panic(err) }}8.2 Namespace 可视化脚本
#!/bin/bash# 可视化进程的 Namespace 关系
echo "=== 容器进程的 Namespace ==="for pid in $(docker top mycontainer -o pid | tail -n +2); do echo "PID $pid:" ls -la /proc/$pid/ns/ 2>/dev/null | awk '{print " " $NF}'done
echo ""echo "=== 宿主进程的 Namespace ==="echo "PID 1 (systemd):"ls -la /proc/1/ns/ | awk '{print " " $NF}'
echo ""echo "=== Namespace 差异 ==="echo "容器进程与宿主进程的 Namespace 不同 = 隔离生效"附、实践:用 unshare 手工创建隔离环境
本节用
unshare命令手工创建各种 Namespace,观察隔离效果。所有命令在 Linux 系统上以 root 权限运行。
附.1 观察宿主进程的 Namespace
每个进程的 Namespace 信息通过 /proc/[pid]/ns/ 目录暴露:
ls -la /proc/self/ns/cgroup:[4026531835]ipc:[4026532213]mnt:[4026532224]net:[4026531840]pid:[4026532226]time:[4026531834]user:[4026531837]uts:[4026532225]方括号中的数字是 Namespace 的 inode 号。同一 inode 号表示同一 Namespace。
附.2 UTS Namespace:独立主机名
# 在新 UTS Namespace 中设置主机名unshare --uts sh -c 'hostname isolated-host && hostname'# isolated-host
# 宿主主机名未变hostname# LAPTOP-NMOAUL8EUTS Namespace 让每个隔离环境拥有独立的主机名,容器中的 hostname 命令只影响自己的 Namespace。
附.3 PID Namespace:独立进程树
# 创建新的 PID Namespace,--fork 必须,--mount-proc 让 ps 只看到新 Namespace 的进程unshare --pid --fork --mount-proc sh -c 'echo "容器内 PID: $$" && ps aux'容器内 PID: 1USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMANDroot 1 0.0 0.0 2800 1664 ? S 01:32 0:00 sh -c echo "容器内 PID: $$" && ps auxroot 2 0.0 0.0 11320 4352 ? R 01:32 0:00 ps aux关键观察:在新 PID Namespace 中,进程的 PID 从 1 开始,ps aux 只显示同一 Namespace 中的进程。宿主进程的 PID 完全不同:
echo "宿主 PID: $$"# 宿主 PID: 3361650注意:
--mount-proc会重新挂载/proc,这是ps命令能正确显示新 PID Namespace 进程的前提。如果不加--mount-proc,ps仍会读取宿主的/proc,显示所有进程。
附.4 Network Namespace:独立网络栈
# 创建新的 Network Namespaceunshare --net sh -c 'ip link show'1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00新 Network Namespace 只有 loopback 接口,且状态为 DOWN——没有 eth0、没有 IP 地址、无法通信。这正是容器网络需要 CNI 插件配置的原因(详见 Ch12 容器网络)。
附.5 用 setns 加入已有 Namespace
nsenter 命令可以进入一个正在运行的容器的 Namespace:
# 找到容器的 PIDCONTAINER_PID=$(docker inspect -f '{{.State.Pid}}' mycontainer)
# 进入容器的 Namespacensenter -t $CONTAINER_PID -m -p -u -i -n -- /bin/sh这等价于 docker exec,但 nsenter 更底层——它直接调用 setns() 系统调用,不经过 Docker API。
九、本章小结
上一章从全景视角介绍了容器全景与三大内核基石。
| Namespace | 隔离内容 | 容器默认启用 | 关键特性 |
|---|---|---|---|
| PID | 进程 ID | PID 1 特殊性、嵌套限制 32 层 | |
| Mount | 挂载点 | 共享子树传播、pivot_root | |
| Network | 网络栈 | veth pair、bridge、CNI | |
| User | 用户/组 ID | (可选) | UID 映射、rootless 容器 |
| IPC | 进程间通信 | System V IPC、POSIX MQ | |
| UTS | 主机名 | 简单但重要 | |
| Cgroup | Cgroup 视图 | 隐藏 Cgroup 路径 | |
| Time | 系统时钟 | 容器迁移场景 |
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






