Namespace 是 Linux 内核提供的资源隔离机制,让进程"看到"的系统资源是独立的,从而实现容器间的隔离
核心思想
Namespace 的作用是隔离视图——不同 Namespace 中的进程看到不同的系统资源(进程列表、网络设备、文件系统等),但它们实际上运行在同一个内核上。这就是容器"轻量级隔离"的根基。
| Namespace | 隔离内容 | 系统调用标志 | 内核版本 |
|---|---|---|---|
| PID | 进程 ID | CLONE_NEWPID | 2.6.24 |
| Network | 网络设备、IP、端口、路由表 | CLONE_NEWNET | 2.6.29 |
| Mount | 文件系统挂载点 | CLONE_NEWNS | 2.4.19 |
| UTS | 主机名和域名 | CLONE_NEWUTS | 2.6.19 |
| IPC | 进程间通信(信号量、消息队列、共享内存) | CLONE_NEWIPC | 2.6.19 |
| User | 用户和用户组 ID | CLONE_NEWUSER | 3.8 |
| Cgroup | Cgroup 根目录视图 | CLONE_NEWCGROUP | 4.6 |
| Time | 系统时钟 | CLONE_NEWTIME | 5.6 |
其中前六种是 Docker 容器使用的核心 Namespace,后两种(Cgroup / Time)较新,容器场景中使用较少。
ps aux 只能看到容器内的进程,就是 PID Namespace 的功劳类比:PID Namespace 就像公寓楼的各个单元——每个单元内部的房间编号从 1 开始,互不冲突。你在自己单元里看不到隔壁的房间。
# 创建新的 PID Namespace 并在其中运行 bash
sudo unshare --pid --fork --mount-proc bash
# 在新 Namespace 中查看进程
ps aux
# 你会发现只有 bash 和 ps 两个进程,PID 从 1 开始
为什么需要 --fork?
进程的 PID Namespace 归属在它被创建(fork)的那一刻就固定了,之后不能更改——就像你出生在哪个国家,国籍就是哪个。unshare 进程本身属于旧 NS,它不能把自己"传送"到新 NS 里。加了 --fork 后,unshare 会 fork 一个子进程,这个子进程"出生"在新 NS 中、PID 为 1,然后由它来执行 bash。
为什么需要 --mount-proc?
ps 命令查看进程列表时,并不是直接询问内核内存,而是读取 /proc 这个特殊的虚拟文件系统(每个数字子目录对应一个进程,如 /proc/1/、/proc/1234/)。如果不重新挂载,新 NS 里的 /proc 仍然是宿主机的那份,ps 看到的还是宿主机全部进程。--mount-proc 在新 NS 中重新挂载 /proc,让它只反映当前 NS 的进程。
每个 Network Namespace 拥有独立的:
容器间网络通信通常通过 veth pair(虚拟以太网对)+ bridge(网桥)实现(详见第 04 篇)。
# 创建两个 Network Namespace
sudo ip netns add ns1
sudo ip netns add ns2
# 创建 veth pair(一对虚拟网线)
sudo ip link add veth1 type veth peer name veth2
# 将 veth 分别放入两个 Namespace
sudo ip link set veth1 netns ns1
sudo ip link set veth2 netns ns2
# 配置 IP 并启动
sudo ip netns exec ns1 ip addr add 10.0.0.1/24 dev veth1
sudo ip netns exec ns1 ip link set veth1 up
sudo ip netns exec ns2 ip addr add 10.0.0.2/24 dev veth2
sudo ip netns exec ns2 ip link set veth2 up
# 测试连通性
sudo ip netns exec ns1 ping 10.0.0.2
# 清理
sudo ip netns del ns1
sudo ip netns del ns2
关键理解
Docker 容器的网络隔离就是 Network Namespace 的直接应用。每个容器有自己的网络栈,通过 veth pair 连接到宿主机的 docker0 网桥,实现容器间和容器与外网的通信。
# 创建新的 Mount Namespace
sudo unshare --mount bash
# 在新 Namespace 中挂载 tmpfs,不影响宿主机
mount -t tmpfs tmpfs /mnt
echo "hello from container" > /mnt/test.txt
cat /mnt/test.txt # 可以读到
# 退出后宿主机的 /mnt 不受影响
类比:Mount Namespace 就像给每个容器配了一套独立的"文件柜"。容器里新放的文件、修改的文件,外面完全看不到,也不会影响其他容器的文件柜。
# 创建新的 UTS Namespace 并修改主机名
sudo unshare --uts bash
hostname my-container
hostname # 输出: my-container
# 退出后宿主机的 hostname 不受影响
# 查看当前 IPC 资源
ipcs
# 创建新的 IPC Namespace
sudo unshare --ipc bash
ipcs # 在新 Namespace 中看到的 IPC 资源是空的
# 创建新的 User Namespace(无需 sudo!)
unshare --user --map-root-user bash
id # 输出: uid=0(root) gid=0(root)
# 看起来是 root,但实际上在宿主机上仍是普通用户
安全意义
User Namespace 是容器安全的重要防线。即使攻击者在容器内获取了 root 权限,由于 UID 映射,在宿主机上他只是一个普通用户,无法造成真正的破坏。这就是 Rootless Container 的核心原理。
| 系统调用 | 作用 | 场景 |
|---|---|---|
clone() |
创建新进程时同时创建新的 Namespace | 容器运行时(runc)创建容器进程 |
unshare() |
将当前进程移入新的 Namespace | 命令行工具 unshare、调试 |
setns() |
将当前进程加入已存在的 Namespace | docker exec 进入容器、nsenter 命令 |
# 查看某个进程的 Namespace 信息
ls -la /proc/$$/ns/
# 输出示例:
# lrwxrwxrwx 1 root root 0 ... cgroup -> 'cgroup:[4026531835]'
# lrwxrwxrwx 1 root root 0 ... ipc -> 'ipc:[4026531839]'
# lrwxrwxrwx 1 root root 0 ... mnt -> 'mnt:[4026531841]'
# lrwxrwxrwx 1 root root 0 ... net -> 'net:[4026531840]'
# lrwxrwxrwx 1 root root 0 ... pid -> 'pid:[4026531836]'
# lrwxrwxrwx 1 root root 0 ... user -> 'user:[4026531837]'
# lrwxrwxrwx 1 root root 0 ... uts -> 'uts:[4026531838]'
#
# 方括号中的数字是 Namespace 的 inode 号
# 相同 inode 号 = 在同一个 Namespace
利用 /proc/<pid>/ns/ 下的文件,可以让其他进程通过 setns() 加入已有容器的 Namespace——这就是 docker exec 和 kubectl exec 的底层原理。
graph LR
subgraph ContainerA["容器 A"]
A1["PID NS"]
A2["Network NS"]
A3["Mount NS"]
end
subgraph ContainerB["容器 B"]
B1["PID NS"]
B2["Network NS"]
B3["Mount NS"]
end
subgraph Kernel["Linux Kernel"]
K1["共享内核"]
end
ContainerA --> Kernel
ContainerB --> Kernel
关键区别:容器 vs 虚拟机
容器只是通过 Namespace 实现了资源视图的隔离,所有容器共享同一个内核。而虚拟机通过 Hypervisor 运行独立的 Guest OS 内核,隔离更彻底但开销更大。容器的优势在于启动快(毫秒级)、资源占用小、密度高。
一句话总结
容器 = 正常的进程 + 被限制的资源视图。容器内运行的进程、占用的资源,在宿主机的进程树和资源树中都只是一个普通节点。但容器内的进程因为 Namespace 的存在,看不见全貌——它"以为"自己独占了一整台机器。
宿主机视角(完整进程树):
systemd(1)
├── sshd(800)
├── dockerd(900)
│ └── containerd(901)
│ ├── nginx(1200) ← 容器 A 的进程
│ └── redis(1300) ← 容器 B 的进程
└── cron(500)
容器 A 视角(PID Namespace 滤镜):
nginx(1) ← 同一个进程,PID 编号重新从 1 开始,看不到其他任何进程
| 要点 | 一句话概括 |
|---|---|
| Namespace 本质 | 内核提供的资源视图隔离机制,让进程看到独立的系统资源 |
| 6 种核心类型 | PID(进程)/ Network(网络)/ Mount(文件系统)/ UTS(主机名)/ IPC(通信)/ User(用户) |
| 三个系统调用 | clone(创建时隔离)/ unshare(当前进程进新NS)/ setns(加入已有NS) |
| 容器的本质 | 容器 ≠ 虚拟机,容器只是戴了滤镜的普通进程,宿主机上一览无余 |
| 安全意义 | User Namespace 实现 Rootless Container,容器内 root ≠ 宿主机 root |
unshare --pid --fork --mount-proc bash 创建 PID Namespace,运行 ps aux 观察效果ip netns 创建两个 Network Namespace,配置 veth pair 并验证 ping 连通unshare --uts bash 修改主机名,退出后确认宿主机不受影响ls -la /proc/<容器PID>/ns/ 查看其 Namespace inode,与宿主机对比docker exec 能"进入"一个已运行的容器?底层调用了哪个系统调用?