Linux Namespace

Namespace 是 Linux 内核提供的资源隔离机制,让进程"看到"的系统资源是独立的,从而实现容器间的隔离

阶段一 · 02 · 返回阶段大纲

核心思想

Namespace 的作用是隔离视图——不同 Namespace 中的进程看到不同的系统资源(进程列表、网络设备、文件系统等),但它们实际上运行在同一个内核上。这就是容器"轻量级隔离"的根基。

Namespace 类型总览

Namespace 隔离内容 系统调用标志 内核版本
PID进程 IDCLONE_NEWPID2.6.24
Network网络设备、IP、端口、路由表CLONE_NEWNET2.6.29
Mount文件系统挂载点CLONE_NEWNS2.4.19
UTS主机名和域名CLONE_NEWUTS2.6.19
IPC进程间通信(信号量、消息队列、共享内存)CLONE_NEWIPC2.6.19
User用户和用户组 IDCLONE_NEWUSER3.8
CgroupCgroup 根目录视图CLONE_NEWCGROUP4.6
Time系统时钟CLONE_NEWTIME5.6

其中前六种是 Docker 容器使用的核心 Namespace,后两种(Cgroup / Time)较新,容器场景中使用较少。

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 — 网络隔离

每个 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 — 文件系统隔离

# 创建新的 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 — 主机名隔离

# 创建新的 UTS Namespace 并修改主机名
sudo unshare --uts bash
hostname my-container
hostname  # 输出: my-container

# 退出后宿主机的 hostname 不受影响

IPC Namespace — 进程间通信隔离

# 查看当前 IPC 资源
ipcs

# 创建新的 IPC Namespace
sudo unshare --ipc bash
ipcs  # 在新 Namespace 中看到的 IPC 资源是空的

User Namespace — 用户隔离

# 创建新的 User Namespace(无需 sudo!)
unshare --user --map-root-user bash
id  # 输出: uid=0(root) gid=0(root)
# 看起来是 root,但实际上在宿主机上仍是普通用户

安全意义

User Namespace 是容器安全的重要防线。即使攻击者在容器内获取了 root 权限,由于 UID 映射,在宿主机上他只是一个普通用户,无法造成真正的破坏。这就是 Rootless Container 的核心原理。

Namespace 操作的三个系统调用

系统调用作用场景
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 execkubectl exec 的底层原理。

小结:Namespace 与容器的关系

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

动手练习

  1. unshare --pid --fork --mount-proc bash 创建 PID Namespace,运行 ps aux 观察效果
  2. ip netns 创建两个 Network Namespace,配置 veth pair 并验证 ping 连通
  3. unshare --uts bash 修改主机名,退出后确认宿主机不受影响
  4. 运行一个 Docker 容器,用 ls -la /proc/<容器PID>/ns/ 查看其 Namespace inode,与宿主机对比
  5. 思考题:为什么 docker exec 能"进入"一个已运行的容器?底层调用了哪个系统调用?