02-Namespace隔离机制
学习目标
- 深入理解 7 种 Linux Namespace 的作用和原理
- 掌握
clone()
和unshare()
系统调用的使用 - 能够通过命令行演示各种隔离效果
- 理解 Namespace 在内核中的实现机制
前置知识
- Linux 进程和线程基础
- 系统调用概念
- 文件系统基础
- 网络基础概念
一、Namespace 概述
1.1 什么是 Namespace?
Namespace 是 Linux 内核提供的一种进程隔离机制,它让不同的进程组拥有独立的系统资源视图。
graph TD
A[Linux 内核] --> B[全局资源表]
B --> C[UTS Namespace]
B --> D[PID Namespace]
B --> E[MNT Namespace]
B --> F[NET Namespace]
B --> G[IPC Namespace]
B --> H[USER Namespace]
B --> I[CGROUP Namespace]
C --> C1[主机名/域名]
D --> D1[进程ID空间]
E --> E1[挂载点表]
F --> F1[网络协议栈]
G --> G1[IPC对象]
H --> H1[用户/组ID]
I --> I1[控制组]
1.2 7 种 Namespace 详解
Namespace | 隔离内容 | 常用命令 | 典型应用 |
---|---|---|---|
UTS | 主机名和域名 | unshare -u | 容器独立主机名 |
PID | 进程ID空间 | unshare -p | 容器独立进程树 |
MNT | 文件系统挂载点 | unshare -m | 容器独立文件系统 |
NET | 网络协议栈 | unshare -n | 容器独立网络 |
IPC | 进程间通信 | unshare -i | 容器间通信隔离 |
USER | 用户和组ID | unshare -U | 用户权限隔离 |
CGROUP | 控制组 | unshare -C | 资源控制隔离 |
二、UTS Namespace - 主机名隔离
2.1 原理说明
UTS (Unix Timesharing System) Namespace 隔离了系统的主机名和域名。
graph LR
A[宿主机] --> A1[hostname: ubuntu]
B[容器1] --> B1[hostname: web-server]
C[容器2] --> C2[hostname: db-server]
A1 -.-> D[内核 UTS 表1]
B1 -.-> E[内核 UTS 表2]
C2 -.-> F[内核 UTS 表3]
2.2 实战演示
# 1. 查看当前主机名
hostname
# 输出: ubuntu
# 2. 创建新的 UTS namespace
unshare -u bash
# 3. 在新 namespace 中修改主机名
hostname container-1
hostname
# 输出: container-1
# 4. 在另一个终端查看宿主机主机名
hostname
# 输出: ubuntu (未改变)
2.3 内核实现原理
// 内核中的 UTS namespace 结构
struct uts_namespace {
struct kref kref;
struct new_utsname name; // 存储主机名和域名
struct user_namespace *user_ns;
struct ucounts *ucounts;
struct ns_common ns;
};
三、PID Namespace - 进程隔离
3.1 原理说明
PID Namespace 为进程提供了独立的进程ID空间,每个容器都有自己的 PID 1。
graph TD
A[宿主机进程树] --> A1[init (PID 1)]
A1 --> A2[systemd (PID 2)]
A2 --> A3[其他进程...]
B[容器1进程树] --> B1[容器init (PID 1)]
B1 --> B2[容器进程 (PID 2)]
B2 --> B3[容器进程 (PID 3)]
C[容器2进程树] --> C1[容器init (PID 1)]
C1 --> C2[容器进程 (PID 2)]
3.2 实战演示
# 1. 查看当前进程树
ps aux | head -10
# 2. 创建新的 PID namespace
unshare -p -f bash
# 3. 在新 namespace 中查看进程
ps aux
# 注意:可能显示错误,因为 /proc 没有重新挂载
# 4. 重新挂载 /proc
mount -t proc proc /proc
# 5. 再次查看进程
ps aux
# 现在可以看到独立的进程树,PID 从 1 开始
3.3 重要特性
- PID 1 特权:容器中的 PID 1 进程有特殊地位
- 僵尸进程处理:PID 1 需要处理子进程的 SIGCHLD 信号
- 进程可见性:只能看到同一 namespace 内的进程
四、MNT Namespace - 文件系统隔离
4.1 原理说明
MNT (Mount) Namespace 隔离了文件系统挂载点,每个容器可以有独立的文件系统视图。
graph TD
A[宿主机挂载点] --> A1[/]
A1 --> A2[/proc]
A1 --> A3[/sys]
A1 --> A4[/dev]
B[容器1挂载点] --> B1[/]
B1 --> B2[/proc - 独立]
B1 --> B3[/sys - 独立]
B1 --> B4[/dev - 独立]
C[容器2挂载点] --> C1[/]
C1 --> C2[/proc - 独立]
C1 --> C3[/sys - 独立]
C1 --> C4[/dev - 独立]
4.2 实战演示
# 1. 查看当前挂载点
mount | grep proc
# 输出: proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
# 2. 创建新的 MNT namespace
unshare -m bash
# 3. 在新 namespace 中查看挂载点
mount | grep proc
# 输出: (可能为空,因为 /proc 还没有挂载)
# 4. 挂载新的 /proc
mount -t proc proc /proc
# 5. 验证隔离效果
mount | grep proc
# 输出: proc on /proc type proc (rw,relatime)
4.3 关键概念
- 挂载传播:子 namespace 可以继承父 namespace 的挂载点
- 私有挂载:使用
MS_PRIVATE
标志创建私有挂载 - 共享挂载:使用
MS_SHARED
标志创建共享挂载
五、NET Namespace - 网络隔离
5.1 原理说明
NET Namespace 隔离了网络协议栈,每个容器有独立的网络接口、IP地址、路由表等。
graph LR
A[宿主机网络] --> A1[eth0: 192.168.1.100]
A1 --> A2[lo: 127.0.0.1]
B[容器1网络] --> B1[veth0: 10.0.0.2]
B1 --> B2[lo: 127.0.0.1]
C[容器2网络] --> C1[veth1: 10.0.0.3]
C1 --> C2[lo: 127.0.0.1]
5.2 实战演示
# 1. 查看当前网络接口
ip addr show
# 2. 创建新的 NET namespace
unshare -n bash
# 3. 在新 namespace 中查看网络接口
ip addr show
# 输出: 只有 lo 接口
# 4. 启动 lo 接口
ip link set lo up
# 5. 创建 veth pair (需要 root 权限)
ip link add veth0 type veth peer name veth1
# 6. 将 veth1 移到新 namespace
ip link set veth1 netns $BASHPID
# 7. 配置 IP 地址
ip addr add 10.0.0.2/24 dev veth0
ip link set veth0 up
# 8. 在 namespace 内配置 veth1
ip addr add 10.0.0.3/24 dev veth1
ip link set veth1 up
六、IPC Namespace - 进程间通信隔离
6.1 原理说明
IPC Namespace 隔离了进程间通信对象,包括共享内存、信号量、消息队列等。
graph TD
A[宿主机IPC] --> A1[共享内存段]
A --> A2[信号量]
A --> A3[消息队列]
B[容器1IPC] --> B1[独立共享内存]
B --> B2[独立信号量]
B --> B3[独立消息队列]
C[容器2IPC] --> C1[独立共享内存]
C --> C2[独立信号量]
C --> C3[独立消息队列]
6.2 实战演示
# 1. 创建新的 IPC namespace
unshare -i bash
# 2. 查看当前 IPC 对象
ipcs -m # 共享内存
ipcs -s # 信号量
ipcs -q # 消息队列
# 3. 创建测试共享内存
ipcmk -M 1024
# 4. 查看创建的共享内存
ipcs -m
七、USER Namespace - 用户隔离
7.1 原理说明
USER Namespace 隔离了用户和组ID空间,允许在容器内以 root 身份运行,但在宿主机上以普通用户身份运行。
graph LR
A[宿主机用户空间] --> A1[uid=0: root]
A1 --> A2[uid=1000: user]
B[容器用户空间] --> B1[uid=0: root (映射到宿主机 1000)]
B1 --> B2[uid=1: daemon (映射到宿主机 1001)]
7.2 实战演示
# 1. 创建新的 USER namespace
unshare -U bash
# 2. 查看当前用户ID
id
# 输出: uid=65534(nobody) gid=65534(nogroup)
# 3. 映射用户ID (需要 root 权限)
echo '0 1000 1' > /proc/$$/uid_map
echo '0 1000 1' > /proc/$$/gid_map
# 4. 再次查看用户ID
id
# 输出: uid=0(root) gid=0(root)
️ 八、CGROUP Namespace - 控制组隔离
8.1 原理说明
CGROUP Namespace 隔离了控制组视图,让容器只能看到自己的 cgroup 层次结构。
graph TD
A[宿主机CGROUP] --> A1[/sys/fs/cgroup]
A1 --> A2[system.slice]
A1 --> A3[user.slice]
A1 --> A4[docker]
B[容器CGROUP] --> B1[/sys/fs/cgroup]
B1 --> B2[container-1]
B2 --> B3[只能看到自己的cgroup]
8.2 实战演示
# 1. 创建新的 CGROUP namespace
unshare -C bash
# 2. 查看 cgroup 信息
cat /proc/self/cgroup
# 3. 查看 cgroup 文件系统
ls /sys/fs/cgroup/
️ 九、系统调用详解
9.1 clone() 系统调用
clone()
是创建新进程的核心系统调用,通过 flags 参数指定要创建的 namespace。
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...);
常用 flags:
CLONE_NEWUTS
- 创建新的 UTS namespaceCLONE_NEWPID
- 创建新的 PID namespaceCLONE_NEWNS
- 创建新的 MNT namespaceCLONE_NEWNET
- 创建新的 NET namespaceCLONE_NEWIPC
- 创建新的 IPC namespaceCLONE_NEWUSER
- 创建新的 USER namespace
9.2 unshare() 系统调用
unshare()
将当前进程移到新的 namespace 中。
#include <sched.h>
int unshare(int flags);
9.3 setns() 系统调用
setns()
将当前进程加入到已存在的 namespace 中。
#include <sched.h>
int setns(int fd, int nstype);
十、综合实战:创建完整隔离环境
10.1 命令行方式
#!/bin/bash
# 创建完整隔离的容器环境
echo "=== 创建完整隔离环境 ==="
# 1. 创建所有 namespace
unshare -U -p -m -n -i -C -f bash << 'EOF'
echo "=== 进入隔离环境 ==="
# 2. 设置主机名
hostname container-demo
# 3. 重新挂载 /proc
mount -t proc proc /proc
# 4. 启动网络接口
ip link set lo up
# 5. 查看隔离效果
echo "=== 主机名 ==="
hostname
echo "=== 进程树 ==="
ps aux | head -5
echo "=== 网络接口 ==="
ip addr show
echo "=== 挂载点 ==="
mount | grep -E "(proc|sys|dev)"
echo "=== 用户信息 ==="
id
echo "=== 隔离环境创建完成 ==="
echo "输入 'exit' 退出隔离环境"
EOF
10.2 Go 代码实现
package main
import (
"fmt"
"os"
"os/exec"
"syscall"
)
func main() {
// 创建所有 namespace 的 flags
flags := syscall.CLONE_NEWUTS |
syscall.CLONE_NEWPID |
syscall.CLONE_NEWNS |
syscall.CLONE_NEWNET |
syscall.CLONE_NEWIPC |
syscall.CLONE_NEWUSER |
syscall.CLONE_NEWCGROUP
// 准备子进程命令
cmd := exec.Command("/bin/bash")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: flags,
}
// 设置标准输入输出
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// 启动子进程
if err := cmd.Run(); err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
}
十一、调试技巧
11.1 查看 Namespace 信息
# 查看当前进程的 namespace
ls -la /proc/self/ns/
# 查看特定进程的 namespace
ls -la /proc/<PID>/ns/
# 使用 nsenter 进入特定 namespace
nsenter -t <PID> -n bash # 进入网络 namespace
nsenter -t <PID> -p bash # 进入进程 namespace
11.2 常见问题排查
- 权限问题:某些 namespace 需要 root 权限
- 挂载问题:MNT namespace 需要重新挂载 /proc
- 网络问题:NET namespace 需要手动配置网络接口
- 用户映射问题:USER namespace 需要正确配置 ID 映射
十二、验证检查清单
基础理解
- [ ] 理解 7 种 Namespace 的作用和区别
- [ ] 掌握 clone() 和 unshare() 系统调用
- [ ] 能够通过命令行创建各种隔离环境
- [ ] 理解 Namespace 在内核中的实现原理
实践能力
- [ ] 能够创建 UTS namespace 并修改主机名
- [ ] 能够创建 PID namespace 并查看独立进程树
- [ ] 能够创建 MNT namespace 并重新挂载文件系统
- [ ] 能够创建 NET namespace 并配置网络接口
- [ ] 能够创建完整的隔离环境
调试技能
- [ ] 掌握 namespace 调试工具的使用
- [ ] 能够排查常见的 namespace 问题
- [ ] 理解 namespace 的生命周期管理
相关链接
- 01-容器本质与基础概念 - 容器技术基础
- 03-CGroup资源控制 - 资源限制技术
- 10-命令行手撸容器 - 综合实践应用
下一步:让我们学习 CGroup 资源控制技术,这是容器资源管理的核心!