Linux Namespace 是 Linux 提供的一种内核级别环境隔离方法,以前的 Unix 有一个叫 chroot 的系统调用,针对正在运作的软件行程和它的子进程,改变它外显的根目录,使它不能访问给它指定的目录以外路径。Linux 的 Namespace 在此基础上,提供了对 UTS、IPC、Mount、PID、Network、User 等的隔离机制。
UTS Namespace
UTS(UNIX Timesharing System) Namespace 可以用来隔离 nodename 和 domainname 两个系统标识。在 UTS Namespace 中,每个 Namespace 可以有自己的 hostname。
package main
import (
"os"
"log"
"syscall"
"os/exec"
)
func main(){
cmd := exec.Command("sh")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS,
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
执行 go run namespace.go
使用命令 pstree -pl | grep 'go('
查看进程之间的关系:
|-zsh(1501)---sudo(1668)---go(1669)-+-namespace(1780)-+-sh(1785)
查看这几个父子进程是否属于同一个 UTS namespace
:
- 首次查看
go
进程所属 uts:
sudo readlink /proc/1669/ns/uts
uts:[4026531838]
- 再查看
namespace
进程:
sudo readlink /proc/1780/ns/uts
uts:[4026531838]
- 上面可以看出:
go
进程和namespace
进程是属于同一 uts namespace 的,在查看一下fork的进程的uts:
sudo readlink /proc/1780/ns/uts
uts:[4026533208]
IPC Namespace
IPC(Inter-Process Communication) Namespace 用来隔离 System V IPC 和 POSIX message queues。每一个 IPC Namespace 都有自己的 System V IPC 和 POSIX message queue。
在上面的代码的基础上修改为:
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC,
}
然后查看 fork 出来的进程是否共享 主进程的 IPC 消息队列:
# 先查看 当前的消息队列:
ipcs -q
--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息
# 然后创建一个 ipc 消息队列:
(base) ➜ ~ ipcmk -Q
消息队列 id:65536
(base) ➜ ~ ipcs -q
--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息
0x69147b74 65536 hongyu 644 0 0
# 查看 fork 出的进程的 IPC 队列:
# ipcs -q
--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息
上面的实验可以看出,在创建的 IPC namespace 中是看不到宿主机的 IPC 队列的,它们已被隔离。
PID Namespace
PID(Process ID) Namespace 可以用来隔离进程 ID。同一个进程在不同的 PID Namespace 中可以拥有不同的 PID。在 Docker Container 中,使用 ps -ef 可以看到启动容器的进程 PID 为 1,但是在宿主机上,该进程却又有不同的 PID。
再在上面代码的基础上修改:
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID,
}
然后执行,在 fork 出的进程外查看 该进程 的pid:
(base) ➜ ~ pstree -pl | grep 'go('
|-zsh(1501)---sudo(7914)---go(7915)-+-namespace(8023)-+-sh(8028)
可以看到 fork 出的进程的id 为: 8028, 然后在该进程中查看进程id:
(base) ➜ go-workspace sudo /usr/local/go/bin/go run my-docker/namespace/namespace.go
# echo $$
1
Mount Namespace
Mount Namespace 用来隔离各个进程看到的挂载点视图。在不同的 Namespace 中,看到的挂载点文件系统层次是不一样的。在 Mount Namespace 中调用 mount 和 unmount 仅仅会影响当前 Namespace 内的文件系统,而对全局文件系统是没有影响的。
在上面代码的基础上修改:
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID |
syscall.CLONE_NEWNS,
}
执行程序后对比:(关于 /proc
文件夹介绍, /proc
存在于内存中,是挂载在根目录的,所以可以用来对比)
# fork 的进程外:
(base) ➜ ~ ls /proc
1 12998 2322 275 3765 41 66 8423 fs
10 13 23505 276 3767 4136 661 8502 interrupts
100 1301 2379 277 377 42 6787 8564 iomem
1001 1302 2382 278 3771 4201 68 865 ioports
1002 1308 2385 28 3773 4204 6811 8676 irq
101 1311 2387 287 3775 4221 6812 8750 kallsyms
1011 1313 24 288 3786 428 688 879 kcore
1013 1318 2417 289 3790 4351 69 88 keys
1018 1320 2432 29 3798 44 6921 89 key-users
102 1329 2434 290 38 45 70 890 kmsg
1020 1337 24342 3 380 4532 71 891 kpagecgroup
1023 1338 2435 30 3814 4533 72 9 kpagecount
1025 1341 2436 301 3831 46 728 90 kpageflags
104 13665 2438 302 3835 465 729 907 loadavg
106 13668 2439 312 3839 466 730 908 locks
107 13700 2441 3165 3844 467 74 91 mdstat
1076 1371 2442 318 3848 47 741 912 meminfo
108 14 2446 32 3852 48 75 92 misc
1092 14121 2447 322 3854 50 76 93 modules
1098 14228 2449 3223 3855 51 77 94 mounts
11 14259 2452 325 3856 5147 775 944 mtrr
11047 14260 2454 326 3857 52 776 95 net
......
......
# 查看 fork 出的进程的 `/proc` 目录:
# 首先给此进程挂载 `proc` 若没有挂载,它显示的是外面环境的:
# mount -t proc proc /proc
# 这里可以看出 `/proc` 文件夹下的东西少了好多
# ls /proc
1 devices irq mdstat scsi tty
4 diskstats kallsyms meminfo self uptime
acpi dma kcore misc slabinfo version
asound driver keys modules softirqs version_signature
buddyinfo execdomains key-users mounts stat vmallocinfo
bus fb kmsg mtrr swaps vmstat
cgroups filesystems kpagecgroup net sys zoneinfo
cmdline fs kpagecount pagetypeinfo sysrq-trigger
consoles interrupts kpageflags partitions sysvipc
cpuinfo iomem loadavg sched_debug thread-self
crypto ioports locks schedstat timer_list
# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 13:49 pts/0 00:00:00 sh
root 5 1 0 13:58 pts/0 00:00:00 ps -ef
User Namespace
User Namespace 主要是隔离用户的用户组 ID。也就是说,一个进程的 User ID 和 Group ID 在 User Namespace 内外可以是不同的。比较常用的是,在宿主机上以一个非 root 用户运行创建一个 User Namespace,然后在 User Namespace 中被映射为了 root 用户。这意味着这个进程在 User Namespace 中有 root 权限,但是在宿主机上却没有 root 权限。
在上面的代码基础上修改:
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID |
syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER,
}
查看id(这里使用了sudo为了和在执行 go 代码时一样):
(base) ➜ ~ sudo id
uid=0(root) gid=0(root) 组=0(root)
# 在 fork 的进程中:
$ id
uid=65534(nobody) gid=65534(nogroup) 组=65534(nogroup)
可以看到它们的id不同,说明 User Namespace 隔离生效了。
Network Namespace
Network Namespace 用来隔离网络设置、IP 地址和端口号等网络栈的 Namespace。Network Namespace 可以让每个容器拥有自己独立的网络设备,而且容器内的应用可以绑定到自己的端口,每个 Namespace 的端口都不会有冲突。在宿主机搭建网桥后,就能很方便地实现容器之间的通信。
在上面的代码上修改:
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID |
syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER | syscall.CLONE_NEWNET,
}
查看网络设备信息:
在 fork 的进程中只有回环接口:
$ ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
在外部环境:
(base) ➜ ~ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: enp9s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether b4:2e:99:19:83:ed brd ff:ff:ff:ff:ff:ff
inet 192.168.1.5/24 brd 192.168.1.255 scope global dynamic noprefixroute enp9s0
valid_lft 84007sec preferred_lft 84007sec
inet6 fe80::1e33:7ee:c607:631c/64 scope link noprefixroute
valid_lft forever preferred_lft forever
3: wlp7s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether c8:21:58:2c:21:71 brd ff:ff:ff:ff:ff:ff
inet 192.168.1.6/24 brd 192.168.1.255 scope global dynamic noprefixroute wlp7s0
valid_lft 84004sec preferred_lft 84004sec
inet6 fe80::d46:c27f:8e60:c0f8/64 scope link noprefixroute
valid_lft forever preferred_lft forever
可以看出 fork 出来的进程和外部环境已经处于网络隔离的状态了。