Linux Namespace 是内核提供的轻量级资源隔离机制,核心是让不同进程组看到独立的系统资源视图,是容器(Docker、K8s)的底层基石。它隔离的是进程对资源的可见性,而非物理资源本身,因此比虚拟机更轻量化
- 本质:为全局系统资源(PID、网络、挂载等)划分独立作用域,进程只能访问所属命名空间内的资源,对其他空间不可见。
- 内核实现:每个进程通过
task_struct中的nsproxy指针,关联一组命名空间实例;内核在系统调用与资源访问时,按当前进程的命名空间做过滤与映射。 - 生命周期:命名空间由引用计数管理,最后一个进程退出时自动销毁;子进程默认继承父进程的所有命名空间。
内核核心数据结构
1. struct nsproxy(命名空间代理)
所有命名空间的统一入口,每个进程的task_struct持有一个nsproxy指针,指向当前所属的命名空间集合:
struct nsproxy { atomic_t count; // 引用计数 struct uts_namespace *uts_ns; // UTS命名空间 struct ipc_namespace *ipc_ns; // IPC命名空间 struct mnt_namespace *mnt_ns; // 挂载命名空间 struct pid_namespace *pid_ns_for_children; // PID命名空间(子进程用) struct net *net_ns; // 网络命名空间 struct cgroup_namespace *cgroup_ns; // Cgroup命名空间 struct user_namespace *user_ns; // 用户命名空间(基础) struct time_namespace *time_ns; // 时间命名空间 };2. 各类型命名空间结构体
每种隔离资源对应独立内核结构,维护该空间的资源状态与映射:
- PID:
struct pid_namespace(维护 PID 分配、层级、init 进程) - Mount:
struct mnt_namespace(维护独立挂载树) - Net:
struct net(独立网络栈、设备、路由) - User:
struct user_namespace(UID/GID 映射表) - IPC:
struct ipc_namespace(独立 System V IPC 对象) - UTS:
struct uts_namespace(独立 hostname/domainname)
关键机制详解
1. PID 命名空间(分层架构)
- 每个空间有独立 PID 号段,首个进程为PID 1(init),负责回收僵尸进程。
- 嵌套:子空间进程在父空间有全局 PID,如子空间 PID=1 → 父空间 PID=1234 → 根空间 PID=5678(
/proc/[pid]/status中NSpid字段可见)。 - 销毁:init 进程退出时,内核终止该空间所有进程。
让一组进程看到独立的进程号空间:
- 容器里看到 PID 从 1 开始
- 宿主机看到容器进程是正常全局 PID
- 容器内部看不见宿主机其他进程
一句话:给进程套一层 PID 映射表。
内核基本结构
每个 PID namespace 在内核里是一个独立结构:
struct pid_namespace { struct kref kref; // 引用计数 struct pidmap pidmap[PIDMAP_ENTRIES];// PID 分配位图 int last_pid; // 上次分配的 PID struct task_struct *child_reaper; // 本空间的 init 进程(PID 1) struct pid_namespace *parent; // 父 namespace // ... };关键点:
- 每个 namespace 有自己的 PID 分配器
- 有层级关系:子 → 父 → 根 namespace
- 有固定的init 进程(PID 1)
PID分配与存储
内核并不只存一个 PID,而是给每个进程在所有祖先 namespace 中各分配一个 PID。
结构struct pid保存一个数字,而struct upid保存每个层级的编号:
struct pid { rwlock_t lock; unsigned int level; // 命名空间深度 struct upid numbers[]; // 每个 ns 对应的 PID };例如:
- 根 ns:PID = 1234
- 父 ns:PID = 100
- 子 ns:PID = 1
内核会完整保存整条链。
可见性规则(最重要)
- 子 namespace 完全看不见父 / 兄弟
- 父 namespace 可以看见子
- 看到的 PID 是当前 ns 对应的编号
这就是为什么容器里 ps 只能看到自己,宿主机 ps 能看到容器。
嵌套层级原理
PID namespace 是树状结构:
根 NS (level 0) ├─ 容器A NS (level 1) │ └─ 容器A内嵌套容器 (level 2) └─ 容器B NS (level 1)- 一个进程在每一层都有一个 PID
/proc/[pid]/status里的NSpid字段会列出所有层级 PID
示例:
NSpid: 14528 42 1表示:
- 根 ns:14528
- 父 ns:42
- 本 ns:1
内核关键逻辑
内核在遍历进程、分配 PID、发送信号时,都会:
- 获取当前进程的
task_struct->nsproxy->pid_ns - 用这个 ns 去查找 / 分配 / 展示 PID
本质就是:所有涉及 PID 的内核行为,都以当前进程所在 PID namespace 为基准。
2. User 命名空间(权限隔离核心)
User Namespace(CLONE_NEWUSER)是所有容器隔离的权限基石,是 Linux 安全容器的核心:实现UID/GID 隔离、权限映射、Capability 隔离、安全边界隔离,让容器内root ≠ 宿主机 root。
- 是其他命名空间的基础,允许非特权用户创建命名空间。
- 通过
/proc/[pid]/uid_map/gid_map做内外 UID 映射,如:0 1000 1表示容器内 root (0) 映射到宿主机 uid=1000。 - 权限:进程在空间内的能力(capabilities)受限于映射后的真实权限。
核心作用
- 独立的UID / GID 编号空间
- 内外 ID 映射:容器内
0(root)→ 宿主机普通非特权用户 - Capability 能力隔离:容器内即使是 root,也只拥有受限权限
- 允许普通非特权用户创建其他所有命名空间(mnt/pid/net/ipc 等)
- 隔离安全 KEY、LSM、资源限额、用户组体系
关键结论:没有 User NS,容器就是弱隔离,容器 root 极易提权击穿宿主机。
进程关联
task_struct └→ nsproxy └→ user_ns // struct user_namespace每个进程都绑定一个user_namespace,是权限检查的最高上下文。
关键结构体
struct user_namespace { struct uid_gid_map uid_map; // UID 映射表 struct uid_gid_map gid_map; // GID 映射表 struct user_namespace *parent;// 父 user ns,树状层级 struct cap_set inheritable; // 权限能力集 // 安全、LSM、proc、cgroup 关联 bool ns_capable_setid; // ... };映射单元
struct uid_gid_map { u32 nr_extents; struct uid_gid_extent extent[UID_GID_MAP_MAX_EXTENTS]; }; // 一段映射规则 struct uid_gid_extent { u32 first; // 容器内起始ID u32 lower; // 宿主机起始ID u32 count; // 映射数量 };UID/GID 映射机制(核心)
1. 双向映射
- From 容器 → 宿主机:内核做
ns uid → 全局 uid转换,读写文件、权限检查、磁盘持久化用宿主机真实 ID - From 宿主机 → 容器:全局 ID 反向翻译为容器内 ID,
ps、ls -n展示容器内编号
2. 映射文件(/proc 接口)
/proc/[pid]/uid_map /proc/[pid]/gid_map标准容器映射示例:
# uid_map 0 1000 65536含义:
- 容器内 UID
0~65535 - 映射到宿主机 UID
1000~1000+65535
👉 容器里的 root (0),在宿主机上就是普通用户 1000。
3. 无映射 = nobody
如果不配置 map,容器内所有未映射 ID 都会被内核强制映射为65534(nobody),天然防越权。
Capability 权限隔离
Linux 不再靠传统 root/sudo,而是拆分细粒度 Capability。User NS 会裁剪、隔离 Cap:
- 新 user ns 内的进程
- 默认不继承父命名空间的高权限 Cap
- 只被授予一组受限安全 Cap(如 CAP_CHOWN、CAP_SETUID 等)
- 容器内 root
- 在当前 user ns 内拥有完整 Cap
- 但对父 user ns / 宿主机资源无任何特权
举例:容器内执行mount只能在自己的 mnt ns 里生效;无法挂载宿主机真实磁盘、无法修改宿主机文件权限。
层级模型(树状)
User NS 天然层级化:
根 User NS(宿主机顶层,真实权限) ├─ 容器A User NS(子级,权限受限) │ └─ 嵌套容器 User NS(孙子级) └─ 容器B User NS权限规则:
- 子 ns 不能修改父 ns 资源
- 父 ns 可以管控、限制子 ns 权限
- 权限检查逐级向上校验
3. Network 命名空间
隔离完整网络协议栈,是容器网络、虚拟化网络、多租户网络隔离的底座:
- 每个 NetNS 拥有独立:网卡、IP、路由、ARP、iptables、TCP/UDP 栈、端口、socket
- 默认完全隔离,互不监听、互不路由、互不冲突
- 底层内核协议栈代码全局共用,只是网络资源实例隔离
- 新空间默认仅含lo 回环接口(DOWN 状态),需手动启用与配置。
- 跨空间通信:用veth pair(虚拟以太网对)连接两个空间,一端在宿主机、一端在容器空间。
隔离范围(全栈隔离)
- 网络设备lo、物理网卡、虚拟网卡(veth、tap、macvlan、ipvlan)
- 三层网络IP 地址、路由表、网关、ARP、邻居表
- 四层传输TCP/UDP/ICMP 独立端口空间,不同 netns 可重复占用同一端口
- 防火墙 / 转发独立 iptables、nftables、netfilter 规则
- 网络配置
/proc/sys/net内核参数隔离(tcp_tw_reuse、ip_forward 等) - Socket & 连接每个 ns 的 socket 完全隔离,不能直接跨 ns 通信
默认行为
- 新建
net ns只自动创建独立 lo 设备(默认 DOWN) - 无任何物理网卡、无路由、无防火墙规则
- 必须手动:
- 启用 lo:
ip link set lo up - 挂载虚拟网卡、配置 IP / 路由
- 启用 lo:
- 端口隔离:宿主机 80 端口 和 容器 80 端口 互不冲突
跨 NetNS 通信核心方案
1. veth pair(容器默认)
一对虚拟以太网卡,像一根网线两端:
- 一端留在宿主机 root netns
- 一端移入容器 netns实现:容器 ↔ 宿主机 二层互通
2. macvlan / ipvlan
让容器直接复用物理网卡,独立 MAC/IP,直连局域网
3. 网桥 bridge
宿主机创建虚拟网桥,多个容器 veth 接入网桥,实现容器之间互通
4. SDN/Overlay
VXLAN、Geneve,跨主机容器网络
5. 本地 UNIX 域套接字
unix socket不受 netns 隔离,可跨 ns 通信
4. Mount 命名空间
- 新空间复制父空间挂载树,后续 mount/umount 仅影响当前空间。
- 挂载传播:支持shared/slave/private/unbindable,控制挂载事件在父子空间的传播。
隔离文件系统挂载视图,核心:
- 每个 ns 拥有独立挂载树;
- 新的
mount/umount仅作用于当前 ns,不影响宿主机 / 其他容器; - 是容器隔离根目录、OverlayFS、只读挂载、临时文件系统隔离的底层基础;
- 内核标识:
CLONE_NEWNS,最早、最基础的命名空间。
进程维度
task_struct→nsproxy→mnt_ns
struct nsproxy { struct mnt_namespace *mnt_ns; // 当前进程所属挂载命名空间 // ...其他ns };挂载命名空间本体
struct mnt_namespace { struct mount *root; // 该ns的根挂载节点 struct list_head list; // 所有挂载项链表 struct mount_event event; unsigned int mount_seq; // 挂载序列,每次mount+1 enum mnt_propagation_type propagation; // 挂载传播默认属性 // 引用计数、用户ns关联、生命周期 };关键挂载对象
struct vfsmount:文件系统挂载实例struct mount:挂载点实体,维护父子挂载层级、挂载传播标记- 全局不再只有一棵统一挂载树,每个 mnt_ns 一棵独立挂载树
挂载树复制机制(Copy-on-Write 思想)
通过clone(CLONE_NEWNS)创建新挂载 ns 时:
- 复制父进程完整挂载树(初始视图完全一致);
- 复制是浅拷贝:底层文件系统、超级块、inode 全局共享;
- 后续当前 ns 内执行
mount/umount/bind mount只会修改自己这棵树,完全隔离。
区别:物理资源(磁盘、块设备)全局共享;挂载视图、挂载点列表、目录覆盖关系完全隔离。
视图隔离效果
- 容器内挂载
/tmp、/dev/shm、临时磁盘,宿主机看不到; - 宿主机挂载硬盘,默认不会自动穿透到容器;
- 容器内
umount /sys不会影响宿主机系统。、
挂载传播(Mount Propagation)—— 重中之重
Mount ns 不是完全隔绝,内核提供挂载传播属性,控制挂载事件跨 ns 传递,是容器、systemd 关键机制。
四种传播类型
| 类型 | 宏 | 作用 |
|---|---|---|
| MS_PRIVATE | 私有 | 完全隔离,挂载变更不互通(容器默认) |
| MS_SHARED | 共享 | 挂载事件双向同步,父子 ns 互相传递 |
| MS_SLAVE | 从属 | 只能接收父 ns 挂载事件,自身变更不向外扩散 |
| MS_UNBINDABLE | 不可绑定 | 不允许 bind mount,禁止跨目录挂载 |
典型场景
- 容器默认:private容器内部挂载、卸载完全隔离,是安全隔离基础。
- 宿主机 /run/media 等:shared/slave,U 盘插入、自动挂载,能被所有需要的 ns 感知。
- bind 挂载、容器目录映射依赖传播属性控制是否泄露宿主机目录。
修改传播属性命令
# 设置目录为私有(容器标准做法) mount --make-private /挂载层级 & 根目录隔离原理
- 新 mnt ns 继承父的
/根挂载; - 容器通过
pivot_root/chroot结合独立挂载树:- 先在私有挂载 ns 内挂载 OverlayFS(读写层 + 镜像只读层);
- 用
pivot_root切换根目录,替换为容器文件系统; - 再挂载独立
/proc /sys /dev,实现完整环境隔离。
关键区别
chroot:只是目录视图限制,无法隔离挂载、无法隐藏宿主机挂载点;- Mount NS + pivot_root:真正隔离完整文件系统挂载拓扑。
生命周期
mnt_namespace基于引用计数管理;- 当所有绑定该 ns 的进程全部退出、无文件句柄引用时,内核自动销毁该挂载树;
- 销毁不会卸载全局真实文件系统,仅销毁当前 ns 的挂载拓扑。
Mount NS 与其他容器组件配合
- + User NS非特权用户也能创建私有挂载、绑定挂载,提升容器安全性。
- + PID NS独立进程视图 + 独立 /proc 挂载,彻底隔离进程信息。
- + Cgroup隔离视图 + 资源限制,构成完整容器底座。
- OverlayFS + Mount NS每层镜像只读挂载 + 容器私有读写层,实现容器镜像分层。
全局挂载树 与 NS 挂载树 关系
1. 初始状态:根命名空间(init_ns)
系统启动后,只有根 Mount NS:
- 全局只有一棵唯一挂载树
- 所有进程默认共享这棵树
- 所有
/ /home /tmp /sys /proc都在这棵树上
2. 创建新 Mount NS:CLONE_NEWNS
clone(CLONE_NEWNS) / unshare(CLONE_NEWNS)内核关键动作:
- 浅拷贝根 NS / 父 NS 的整棵挂载树
- 复制所有
mount节点的拓扑关系 - 底层
super_block、dentry、inode完全共享,不复制数据
- 复制所有
- 生成一个全新
mnt_namespace - 新进程 / 当前进程绑定到新 mnt_ns
3. 隔离本质
- ✅ 底层资源:全局共享(磁盘、inode、sb)
- ✅ 挂载对象:多个 NS 可以引用同一个
mount - ❌ 挂载操作隔离:新 NS 内执行
mount / umount / bind mount只修改当前 NS 自己的挂载树,不影响父 NS、其他容器
挂载树的「覆盖」机制(关键)
Linux 挂载是覆盖式:
- 原目录存在原有 dentry 与内容
- 在该目录执行 mount → 新文件系统覆盖该目录
- 上层原目录内容被隐藏,仅在卸载后恢复
结合 NS:
- 容器内单独挂载
/etc覆盖 - 宿主机同目录完全不受影响
- 因为二者挂载树拓扑独立
挂载传播:多 NS 挂载树的联动规则
如果完全隔离,U 盘自动挂载、容器挂载宿主机目录会失效;内核引入挂载传播,控制挂载事件是否跨 NS 同步。
四种类型:
- MS_PRIVATE 私有(容器默认)挂载树完全隔离,双向不通,容器安全基础。
- MS_SHARED 共享父子 NS 挂载树双向同步,一方 mount,另一方自动看到。
- MS_SLAVE 从属只能继承父 NS 挂载,自身修改不会往外扩散。
- MS_UNBINDABLE禁止 bind 挂载,防止目录穿透泄露。
典型流程
宿主机/media设为shared
→ U 盘插入自动挂载到/media/usb
→ 所有 shared/slave 的子 NS 自动看到该挂载
→ 私有容器看不到,隔离生效
多 Mount NS 整体架构图示
[根Mount NS 挂载树] 全局原始挂载拓扑 │ ├─ 复制生成 → [容器A mnt_ns 挂载树](私有) └─ 复制生成 → [容器B mnt_ns 挂载树](私有) 底层:super_block / inode / dentry 全局共用 上层:每棵挂载树 挂载点、覆盖关系、卸载、新增挂载 完全独立内核关键执行逻辑(用户访问文件)
当进程 open ("/tmp/file"):
- 取当前进程 →
nsproxy->mnt_ns - 以当前 NS 的挂载树为基准做路径解析
- 逐级匹配挂载点,应用挂载覆盖规则
- 最终找到对应 super_block + inode
核心结论:路径解析、挂载可见性,全部绑定在当前进程的 Mount 命名空间上
创建与管理的三大系统调用
1. clone ()(创建新进程 + 新命名空间)
创建子进程并指定新命名空间,是容器启动的核心调用:
// 创建新UTS+PID命名空间的子进程 pid = clone(child_func, stack, CLONE_NEWUTS | CLONE_NEWPID | SIGCHLD, arg);2. unshare ()(当前进程脱离旧空间,创建新空间)
不创建新进程,直接为当前进程切换到新命名空间(unshare命令底层):
unshare(CLONE_NEWNET | CLONE_NEWNS); // 脱离当前网络与挂载空间3. setns ()(加入已存在的命名空间)
通过命名空间文件描述符(/proc/[pid]/ns/xxx),将进程加入指定空间(nsenter命令底层):
int fd = open("/proc/1234/ns/net", O_RDONLY); setns(fd, CLONE_NEWNET); // 加入PID=1234的网络空间