Linux Namespace 隔离
bash ls -l /proc/$$/ns/
输出示例:
lrwxrwxrwx 1 user user 0 Apr 18 10:00 cgroup -> 'cgroup:[4026531835]' lrwxrwxrwx 1 user user 0 Apr 18 10:00 ipc -> 'ipc:[4026531839]' lrwxrwxrwx 1 user user 0 Apr 18 10:00 mnt -> 'mnt:[4026531841]' lrwxrwxrwx 1 user user 0 Apr 18 10:00 net -> 'net:[4026531840]' lrwxrwxrwx 1 user user 0 Apr 18 10:00 pid -> 'pid:[4026531836]' lrwxrwxrwx 1 user user 0 Apr 18 10:00 pid_for_children -> 'pid:[4026531836]' lrwxrwxrwx 1 user user 0 Apr 18 10:00 time -> 'time:[4026531834]' lrwxrwxrwx 1 user user 0 Apr 18 10:00 time_for_children -> 'time:[4026531834]' lrwxrwxrwx 1 user user 0 Apr 18 10:00 user -> 'user:[4026531837]' lrwxrwxrwx 1 user user 0 Apr 18 10:00 uts -> 'uts:[4026531838]'
方括号中的数字是 Namespace 的唯一标识符。如果两个进程的某个 Namespace 链接指向同一个 inode 号,说明它们处于同一个 Namespace 下。
可以通过 `readlink` 查看具体指向:
```bash
readlink /proc/$$/ns/uts
# 输出:uts:[4026531838]
实战:用 unshare 创建隔离环境
unshare 命令能让一个新进程脱离父进程的部分 Namespace,并可选地创建新的 Namespace 来运行程序。这是体验 Namespace 隔离最直接的方式。
基本语法:
unshare [options] <program> [arguments]
常用选项:
--mount/-m:新的 Mount namespace--uts/-u:新的 UTS namespace--ipc/-i:新的 IPC namespace--net/-n:新的 Network namespace--pid/-p:新的 PID namespace(通常需配合--fork)--user/-U:新的 User namespace--cgroup/-C:新的 Cgroup namespace--fork/-f:执行程序前先 fork 出一个子进程(创建 PID namespace 时必须)
示例 1:隔离主机名 —— UTS Namespace
# 当前主机名
hostname
# 在新的 UTS namespace 中修改主机名
sudo unshare --uts /bin/bash
hostname my-container
hostname
# 输出 my-container
# 退出 bash
exit
# 回到宿主机,主机名未改变
hostname
在新的 UTS namespace 内,主机名被修改为 my-container,但退出后宿主机的名字纹丝不动。这就是 UTS namespace 的隔离效果。
示例 2:隔离进程视图 —— PID Namespace
创建新的 PID namespace 时,必须使用 --fork 才能让新进程变为新 namespace 的 init 进程(PID 1),否则程序会直接在当前 PID namespace 中运行。
sudo unshare --pid --fork --mount-proc /bin/bash
--mount-proc 会重新挂载 /proc 文件系统,这样 ps 等工具才能正确显示新 namespace 中的进程视图。
执行后,在新 bash 中运行:
ps aux
输出只有当前 bash 和 ps 两个进程,且 bash 的 PID 为 1。而宿主机上这个 bash 的真实 PID 可能是 3456,但在新 PID namespace 内部看到的是 1。PID 号完全被隔离。
尝试 kill -9 1,你会发现它不会使系统崩溃,因为在 PID namespace 内,1 号进程就是当前 bash,你只能杀死它,对其他进程无影响。
注意:PID 隔离只影响进程的可见性和 PID 编号,不限制进程间的信号发送(受 User namespace 和权限控制)。
示例 3:隔离网络 —— Network Namespace
当你创建新的 net namespace 时,里面只有一个回环接口(lo),没有任何物理网卡或 IP 配置。
sudo unshare --net bash
# 查看网卡
ip addr
# 只会显示 lo 接口,且状态为 DOWN
# 你可以把 lo 启动
ip link set lo up
ping 127.0.0.1
# 成功
# 尝试 ping 宿主机以外的地址会失败,因为没有外部接口
ping 8.8.8.8
# connect: Network is unreachable
若要为新 namespace 配置网络与外部通信,通常要用 veth pair 把它和宿主机的 namespace 连接起来,或者使用 ip netns 命令进行管理(下面会讲)。
示例 4:组合多个 namespace 模拟“微型容器”
我们来创建一个同时隔离主机名、进程、网络、挂载点的环境:
sudo unshare --uts --pid --net --mount --fork --mount-proc /bin/bash
现在你拥有一个与宿主机完全不同的:
- 主机名(可改)
- 进程列表(只看到自身)
- 网络栈(只有 lo,无外部连接)
- 挂载点(在 mount namespace 内可独立挂载 tmpfs 等)
这不正是一个手工打造的容器雏形吗?Docker 底层正是组合这些 namespace,并加上 cgroup 资源限制和联合文件系统来实现的。
使用 nsenter 进入已有的 Namespace
如果某个进程已经处于某个 Namespace 中,我们可以用 nsenter 进入该进程所在的特定 Namespace。最典型的场景:你想调试运行中的 Docker 容器内部环境。
基本语法:
nsenter --target <PID> --mount --uts --ipc --net --pid -- <command>
比如,Docker 容器的 PID 是 12345,我们可以这样进入它的所有 Namespace:
sudo nsenter --target 12345 --mount --uts --ipc --net --pid -- /bin/bash
现在你就像“钻入”了容器内部,共享其所有隔离视图。nsenter 常用于排查网络或挂载问题。
常用管理工具与命令
lsns —— 列出系统中的 Namespace
lsns
输出会按类型分组,显示每个 namespace 的 inode 号、所属进程等。
# 只看 net namespace
lsns -t net
# 查看特定进程的 namespace
lsns -p <PID>
ip netns —— 管理 Network Namespace
ip netns 提供了一种便捷的方法来操作命名网络栈。你可以给它起一个有意义的名称。
# 创建一个名为 ns1 的 net namespace
sudo ip netns add ns1
# 在 ns1 里运行命令
sudo ip netns exec ns1 ip addr
# 创建一对 veth,一端放在 ns1,一端放在宿主机
sudo ip link add veth0 type veth peer name veth1
sudo ip link set veth1 netns ns1
# 在宿主机配置 veth0
sudo ip addr add 10.0.0.1/24 dev veth0
sudo ip link set veth0 up
# 在 ns1 内配置 veth1
sudo ip netns exec ns1 ip addr add 10.0.0.2/24 dev veth1
sudo ip netns exec ns1 ip link set veth1 up
sudo ip netns exec ns1 ip link set lo up
# 现在宿主机和 ns1 可以通过 10.0.0.x 互通
ping 10.0.0.2
删除 namespace:
sudo ip netns delete ns1
ip netns 依赖于将 namespace 的引用挂载到 /var/run/netns/ 目录下,所以它管理的 netns 是“有名字的”,方便复用。
Namespace 与 Cgroup 的区别与组合
初学者常把 Namespace 和 Cgroup 混淆,这里明确区分:
- Namespace 解决的是 “可见性” 问题(能看见什么),实现资源隔离。
- Cgroup 解决的是 “限制” 问题(能用多少),实现资源控制(CPU、内存、IO 等)。
二者相辅相成:Namespace 让你以为自己在独立的世界里,而 Cgroup 则为这个世界设定了物理限制。Docker 启动容器时,会同时创建对应的 namespace 和 cgroup 组。
举个例子:
# 创建一个 CPU 限制的 cgroup
sudo cgcreate -g cpu:/mycontainer
echo 50000 > /sys/fs/cgroup/cpu/mycontainer/cpu.cfs_quota_us
echo 100000 > /sys/fs/cgroup/cpu/mycontainer/cpu.cfs_period_us
# 在隔离的 namespace 中将进程放入该 cgroup
sudo cgexec -g cpu:/mycontainer unshare --uts --pid --fork --mount-proc /bin/bash