mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3826 字
11 分钟
Linux Namespace 深入
2021-09-05

当你在一个容器里执行 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,允许进程拥有独立的文件系统挂载视图。此后十余年间,内核逐步添加了其余 7 种 Namespace:

  1. Mount Namespace

    2.4.19,文件系统挂载视图隔离,Linux 第一个 Namespace

  2. UTS Namespace

    2.6.19,主机名与域名隔离

  3. IPC Namespace

    2.6.19,System V IPC 隔离(POSIX 消息队列隔离至 2.6.30 加入)

  4. PID Namespace

    2.6.24,进程号隔离

  5. Network Namespace

    2.6.29,网络栈隔离(网卡、路由、端口);CLONE_NEWNET 于 2.6.24 引入,至 2.6.29 完整可用

  6. User Namespace

    3.8,用户与组 ID 隔离;框架引入于 2.6.23,3.8 才支持非特权创建

  7. Cgroup Namespace

    4.6,Cgroup 视图隔离

  8. Time Namespace

    5.6,系统时钟与引导时钟隔离

2013 年 Docker 诞生后,Namespace 从”内核开发者的小众特性”一跃成为容器技术的基石,容器的”视图隔离”全部由 Namespace 实现。这些差异根植于它们各自的历史背景和设计目标。

前置知识#

Important
  • Linux 系统编程基础:clone()unshare()setns() 三个系统调用是操作 Namespace 的主要接口

  • Ch01 容器全景:从 chroot 到 OCI:建立容器技术的全景认知,理解 Namespace 在容器架构中的位置

  • Linux /proc 文件系统:Namespace 的信息通过 /proc/[pid]/ns/ 目录暴露

Note

Namespace 决定进程”能看到什么”,Cgroup 决定进程”能用多少”,两者经常被混淆。

Namespace 有 8 种,行为差异很大,User Namespace 允许非特权创建,Network Namespace 却需要 CAP_NET_ADMIN。为什么?答案藏在各自的历史背景和设计目标里。下面逐一拆解。

一、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 三个系统调用#

操作 Namespace 只有三个系统调用,区别在于作用对象不同:clone() 给新进程一个新 Namespace,unshare() 把当前进程自己搬进新 Namespace,setns() 让当前进程钻进一个已经存在的 Namespace。

系统调用作用对象关键参数
clone()新建的子进程CLONE_NEWPID, CLONE_NEWNET, …
unshare()当前进程自己同上
setns()已存在的 Namespacefd(指向 /proc/PID/ns/ 的文件描述符)

cloneunshare 都靠同一组 CLONE_NEW* 标志指定要隔离哪些资源;setns 要加入的 Namespace 已经存在,参数换成指向 /proc/PID/ns/ 下某个文件的 fd。

日常用得最多的是命令行封装:

# unshare + fork 语义:先 unshare 把当前进程搬进新 Namespace,再 fork 出子进程(子进程继承新 Namespace)
sudo unshare --mount --pid --fork --mount-proc /bin/bash
# unshare 语义:不 fork,把当前 shell 自己关进新 Namespace
sudo unshare --mount /bin/bash
# setns 语义:钻进已有容器的 Namespace
sudo nsenter -t $(docker inspect -f '{{.State.Pid}}' mycontainer) -n ip addr

Namespace 的继承与嵌套关系,见 1.3。

1.3 Namespace 的继承与嵌套#

Namespace 支持嵌套,一个 Namespace 可以是另一个 Namespace 的子 Namespace。子 Namespace 中的资源对父 Namespace 可见(反之不一定),这取决于具体的 Namespace 类型。

graph TB subgraph 宿主Namespace["宿主 Namespace (init_ns)"] INIT["PID 1 (systemd)"] A["PID 100 (dockerd)"] B["PID 200 (containerd)"] end subgraph 容器NS1["容器 Namespace 1"] C1["PID 1 (nginx master)"] C2["PID 10 (nginx worker)"] end subgraph 容器NS2["容器 Namespace 2"] D1["PID 1 (redis-server)"] D2["PID 15 (redis-cli)"] end subgraph 嵌套NS["嵌套 Namespace (DinD)"] E1["PID 1 (dockerd)"] E2["PID 50 (containerd)"] end A -->|"clone(CLONE_NEWPID)"| C1 B -->|"clone(CLONE_NEWPID)"| D1 C1 -->|"clone(CLONE_NEWPID)"| E1 style 宿主Namespace fill:#e8eaf6,stroke:#283593 style 容器NS1 fill:#e0f2f1,stroke:#00695c style 容器NS2 fill:#fff3e0,stroke:#e65100 style 嵌套NS fill:#fce4ec,stroke:#c62828

二、PID Namespace#

2.1 原理#

PID Namespace 隔离进程 ID 号空间。在新的 PID Namespace 中,第一个进程的 PID 为 1,后续进程依次递增。宿主机上,这些进程仍然有全局唯一的 PID。

# 在容器中看到的 PID
docker 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 process

2.2 PID 1 的特殊性#

PID 1 在 Linux 中有特殊地位:

特性说明
信号处理PID 1 默认忽略 SIGINT 和 SIGTERM,除非显式注册处理函数
僵尸回收PID 1 负责回收所有子进程的退出状态(wait)
优雅退出容器停止时,PID 1 收到 SIGTERM,应优雅关闭子进程

PID 1 默认忽略 SIGTERM/SIGINT 不是约定俗成,而是内核有意为之:内核担心 init 进程被误杀会导致整个命名空间不可用,所以在信号投递路径上对 PID 1 做了特殊处理,没有显式注册 handler 的终止类信号会被丢弃。这也是为什么容器里若直接用 shell 当 PID 1,docker stop 发的 SIGTERM 会被忽略,超时后只能靠 SIGKILL 强杀。

Warning

很多容器因为 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 Namespace
sudo unshare --mount /bin/bash
# 在新 Namespace 中挂载 tmpfs
mount -t tmpfs tmpfs /mnt
mount | 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 /
# 将挂载点设为 slave
mount --make-slave /sys

Linux 内核默认将根文件系统的传播类型设为 shared。这个默认值对桌面和服务器场景很合理:插入 U 盘后,宿主挂载了 /run/media/user/usb1,所有 Mount Namespace 都能看到这个新挂载点,不用手动同步。但在容器场景下,shared 传播会破坏隔离。

假设容器继承了宿主的 shared 传播类型,容器内执行 mount -t tmpfs tmpfs /mnt,这个挂载事件会反向传播到宿主的 Mount Namespace,宿主机上也会出现 /mnt 的 tmpfs 挂载。反过来,宿主插入 U 盘触发的新挂载也会传播进容器,容器内凭空多出一个 /run/media/user/usb1,这既破坏了容器的文件系统视图隔离,也可能带来安全问题:容器内的恶意进程可以通过挂载事件窥探宿主的设备变动。

runc 在创建容器时,第一步就是将根挂载点设为 private

mount --make-rprivate /

--make-rprivater 表示递归,将根目录下所有挂载点的传播类型都设为 private。这样一来,容器内的挂载/卸载操作不会泄漏到宿主,宿主的挂载变动也不会渗入容器,每个 Mount Namespace 的挂载视图完全独立。

Tip

如果确实需要宿主和容器共享某些挂载点(比如 Kubernetes 的 HostPath 卷),可以在 private 的基础上,对特定挂载点单独设为 slave,只允许单向接收宿主的传播,不允许反向泄漏。

3.3 容器中的 Mount 操作#

runc 在创建容器时,不会继承宿主的 /proc/sys/dev 等关键文件系统,而是在新的 Mount Namespace 中重新挂载。原因很简单:宿主的这些文件系统暴露的是宿主内核的状态,直接继承等于放弃隔离。

挂载操作隔离原因
mount -t proc proc /proc宿主的 /proc 暴露宿主所有进程的 PID、状态、命令行,容器必须挂载自己的 procfs,只显示容器 PID Namespace 内的进程
mount -t sysfs sysfs /sys宿主的 /sys 暴露所有网络设备、内核模块、电源状态,容器重新挂载 sysfs 后只能看到自己的网络设备和内核参数
mount -t devtmpfs devtmpfs /dev宿主的 /dev 包含所有物理设备节点(磁盘、USB、GPU),容器重新挂载 devtmpfs 后只看到通用设备节点,再通过白名单(device cgroup 或 device whitelist)进一步限制可访问的设备
mount -t tmpfs tmpfs /dev/shm/dev/shm 是 POSIX 共享内存的挂载点,必须用 tmpfs 提供独立的内存文件系统。如果共享宿主的 /dev/shm,不同容器可以通过共享内存文件互相读写,破坏 IPC 隔离
mount -t tmpfs tmpfs /run/run 存放运行时临时文件(PID 文件、socket 文件),每个容器需要独立的 /run 避免与宿主或其他容器的运行时状态冲突
mount -t cgroup2 cgroup2 /sys/fs/cgroup容器内需要看到自己的 Cgroup 视图,配合 Cgroup Namespace 让容器以为自己在 Cgroup 根目录,而不是宿主 Cgroup 子树的某个子节点
# runc 的典型 mount 操作(简化)
mount --make-rprivate / # 先设为 private,阻断传播
mount -t proc proc /proc # 挂载 procfs
mount -t sysfs sysfs /sys # 挂载 sysfs
mount -t devtmpfs devtmpfs /dev # 挂载 devtmpfs
mount -t tmpfs tmpfs /dev/shm # 挂载共享内存
mount -t tmpfs tmpfs /run # 挂载运行时目录
mount -t cgroup2 cgroup2 /sys/fs/cgroup # 挂载 Cgroup v2

其中 /dev/shm 的隔离容易被忽视。POSIX 共享内存通过 shm_open()/dev/shm 下创建文件,多个进程可以 mmap 同一文件实现内存共享。如果两个容器共享宿主的 /dev/shm,容器 A 创建的共享内存文件,容器 B 可以直接打开并读写,IPC Namespace 的隔离就被绕过了。所以 runc 为每个容器挂载独立的 tmpfs 到 /dev/shm,确保共享内存的作用域限制在容器内部。

四、Network Namespace#

4.1 原理#

Network Namespace 隔离网络栈,包括网络设备、IP 地址、路由表、端口号、iptables 规则等。

# 创建 Network Namespace
sudo ip netns add ns1
sudo 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 连接两个 Namespace
sudo ip link add veth1 type veth peer name veth2
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 ns1 ip link set lo 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 ns2 ip link set lo up
# 测试连通性
sudo ip netns exec ns1 ping -c 3 10.0.0.2

4.2 容器网络模式#

Docker 支持多种网络模式,每种模式对 Network Namespace 的使用不同:

网络模式Namespace 策略特点
bridge独立 Network NS + veth pair默认模式,容器有独立 IP
host共享宿主 Network NS性能最好,无网络隔离
none独立 Network NS,仅 loopback无网络,用于安全隔离
container共享另一容器的 Network NSPod 内容器共享网络栈

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"}]
}
}
Note

Network Namespace 是容器网络的基础,但 CNI 插件负责了 veth pair 创建、bridge 连接、IP 分配、路由配置等复杂工作。详见 Ch11 容器网络

五、User Namespace#

5.1 原理#

User Namespace 隔离用户和组 ID。最强大的特性是UID 映射:容器内的 root(UID 0)可以映射到宿主机上的普通用户(UID 100000),实现 rootless 容器。

# 创建 User Namespace,映射 UID
sudo 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_map
echo "0 100000 65536" > /proc/$PID/gid_map

5.3 User Namespace 与 Capability#

User Namespace 的一个关键特性:在新的 User Namespace 中,进程拥有全部 Capability,但这些 Capability 只在该 Namespace 内有效。

graph LR subgraph 宿主UserNS["宿主 User Namespace"] ROOT["UID 0 (root)<br/>全部 Capability"] USER["UID 1000<br/>无 Capability"] end subgraph 容器UserNS["容器 User Namespace"] CROOT["UID 0 (容器内 root)<br/>容器内全部 Capability<br/>映射到宿主 UID 100000"] CUSER["UID 1000 (容器内)<br/>映射到宿主 UID 101000"] end ROOT -->|"创建 User NS"| CROOT CROOT -.->|"UID 映射"| USER style 宿主UserNS fill:#e8eaf6,stroke:#283593 style 容器UserNS fill:#e0f2f1,stroke:#00695c

六、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/bash
ipcs -q # 空的

6.2 UTS Namespace#

UTS Namespace 隔离主机名和域名(源自 UNIX Time-Sharing System):

# 创建新 UTS Namespace 并设置主机名
sudo unshare --uts /bin/bash
hostname mycontainer
hostname
# mycontainer
# 宿主机的主机名不受影响

6.3 Cgroup Namespace#

Cgroup Namespace 隔离 Cgroup 根目录视图。在容器内,进程只能看到自己的 Cgroup 子树:

# 不使用 Cgroup Namespace
cat /proc/self/cgroup
# 0::/system.slice/docker-abc123.scope → 看到完整路径
# 使用 Cgroup Namespace
sudo unshare --cgroup /bin/bash
cat /proc/self/cgroup
# 0::/ → 看到的是根目录,不知道自己在子树中

6.4 Time Namespace#

Time Namespace(Linux 5.6+)隔离 CLOCK_BOOTTIMECLOCK_MONOTONIC 时钟,主要用于容器迁移场景(如 checkpoint/restore):

# 创建 Time Namespace
sudo 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 NSPod 内容器Kubernetes Pod
共享 IPC NS进程间通信System V 共享内存
共享 PID NS进程可见调试 sidecar
User NS + 其他 NSRootless 容器Podman rootless
flowchart LR subgraph Pod["Kubernetes Pod"] PAUSE["pause 容器<br/>持有 Network NS"] APP1["应用容器 A<br/>共享 Network NS"] APP2["应用容器 B<br/>共享 Network NS"] end PAUSE -->|"创建 Network NS"| NET_NS["Network Namespace<br/>共享 IP / 端口 / 路由"] APP1 -->|"join Network NS"| NET_NS APP2 -->|"join Network NS"| NET_NS APP1 -.->|"localhost 通信"| APP2 style Pod fill:#e8eaf6,stroke:#283593 style NET_NS fill:#e0f2f1,stroke:#00695c
Tip

Kubernetes Pod 中多个容器共享 Network Namespace 时,应用之间可以通过 localhost 直接通信,但端口不能冲突。设计 Pod 时要为每个容器规划好监听端口,避免端口抢占导致启动失败。

7.3 Namespace 与安全#

Namespace 做的是视图隔离,不是安全边界。它决定进程能看到什么,却不决定进程能对内核做什么。把 Namespace 当成安全防线,是容器事故里最常见的认知错位。下面的图把 Namespace 放进整个安全层级,并标出几条已知的逃逸路径,重点不在”哪层能挡住哪条路”,而在”每层都只是缓解、而非阻断”。

flowchart TB subgraph 安全层级["容器安全层级"] NS["Namespace<br/>视图隔离"] CG["Cgroup<br/>资源限制"] CAP["Capabilities<br/>权限控制"] SEC["seccomp<br/>系统调用过滤"] AA["AppArmor<br/>文件访问控制"] end NS -->|"隔离不等于安全"| CAP CAP -->|"限制特权操作"| SEC SEC -->|"限制系统调用"| AA subgraph 逃逸风险["已知逃逸路径"] E1["/proc/sysrq-trigger"] E2["内核漏洞(CVE)"] E3["特权容器 + 挂载"] E4["共享 PID Namespace"] end NS -.->|"无法阻止"| E1 CAP -.->|"可缓解(drop CAP_SYS_ADMIN)"| E1 SEC -.->|"可缓解(覆盖部分调用)"| E2 AA -.->|"可缓解(限制文件访问)"| E3 style 安全层级 fill:#e8f5e9,stroke:#2e7d32 style 逃逸风险 fill:#ffcdd2,stroke:#c62828

图里的”可缓解”是刻意没用”可以阻止”。以 /proc/sysrq-trigger 为例,写入它需要 CAP_SYS_ADMIN,只有 drop 掉这个 capability 才算堵住;特权容器默认带着它,Capability 层就形同虚设。内核漏洞(E2)更彻底,seccomp 能拦截掉被滥用的具体系统调用,但挡不住漏洞本身存在于内核里。每层防护都依赖前置条件是否配齐,不存在哪一层能单挑哪条逃逸路径。

八、动手实践#

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 手工创建隔离环境#

Note

本节用 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-NMOAUL8E

UTS 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: 1
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 2800 1664 ? S 01:32 0:00 sh -c echo "容器内 PID: $$" && ps aux
root 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
Note

--mount-proc 会重新挂载 /proc,这是 ps 命令能正确显示新 PID Namespace 进程的前提。如果不加 --mount-procps 仍会读取宿主的 /proc,显示所有进程。

附.4 Network Namespace:独立网络栈#

# 创建新的 Network Namespace
unshare --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 插件配置的原因(详见 Ch11 容器网络)。

附.5 用 setns 加入已有 Namespace#

nsenter 命令可以进入一个正在运行的容器的 Namespace:

# 找到容器的 PID
CONTAINER_PID=$(docker inspect -f '{{.State.Pid}}' mycontainer)
# 进入容器的 Namespace
nsenter -t $CONTAINER_PID -m -p -u -i -n -- /bin/sh

这等价于 docker exec,但 nsenter 更底层,它直接调用 setns() 系统调用,不经过 Docker API。

九、本章小结#

上一章从全景视角介绍了容器全景与三大内核基石。本章逐一拆解了 8 种 Namespace 的隔离范围与行为差异,从 PID 1 的特殊性到 Mount Namespace 的共享子树传播,从 User Namespace 的 UID 映射到 Network Namespace 的 veth pair 组网。这些差异根植于各自的历史背景和设计目标,理解它们才能在容器配置中做出合理选择。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Linux Namespace 深入
https://blog.souloss.cn/posts/container-runtime/namespace-deep-dive/
作者
Souloss
发布于
2021-09-05
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
Cgroup v2 深入
容器运行时 Cgroup 是容器资源限制的核心机制。逐层拆解 Cgroup v2 的统一层级设计、CPU/内存/IO 三大控制器的实现原理、eBPF 扩展机制,以及容器运行时如何通过 Cgroup 实现资源隔离,从「docker run --memory=512m」到内核的 cgroup 文件系统,理解每一步的资源控制逻辑。
2
容器运行时深入系列导读
容器运行时 从 Linux 内核的 Namespace、Cgroup、OverlayFS 出发,深入 OCI 规范、runc 源码、containerd 架构与 shim 机制,再覆盖容器安全、网络、镜像构建三个运行时核心维度,建立从内核机制到工业实现的完整认知链。
3
runc 源码分析
容器运行时 runc 是 OCI Runtime Spec 的参考实现,也是 Docker/Kubernetes 默认的低层容器运行时。从零讲透 runc 的源码架构,libcontainer 核心库、容器创建/启动的代码路径、Namespace/Cgroup/OverlayFS 的内核交互、安全配置(seccomp/AppArmor/Capabilities),让你从「知道 runc 是什么」到「理解 runc 的每一行关键代码」。
4
容器安全:seccomp/AppArmor/Capabilities
容器运行时 容器的安全边界在哪里?Namespace 提供视图隔离,但不阻止特权操作。从零讲透 Linux 安全模块在容器中的应用,Capabilities(权限细分)、seccomp(系统调用过滤)、AppArmor(文件访问控制),以及 rootless 容器的实现,让你理解容器的安全边界和加固方法。
5
OverlayFS:容器文件系统
容器运行时 OverlayFS 是容器分层文件系统的核心。全面剖析 OverlayFS 的 lowerdir/upperdir/workdir 三层结构、whiteout 标记文件机制、copy-up 写时复制语义、多层叠加规则,以及容器运行时如何用 OverlayFS 实现镜像层复用和容器可写层,理解「为什么容器启动这么快」和「镜像层是怎么共享的」。