linux一共实现了6种不同类型的Namespace:
Namespace 类型 | 系统调用参数 | 内核版本 |
---|---|---|
Mount Namespace | CLONE_NEWNS | 2.4.19 |
UTS Namespace | CLONE_NEWUTS | 2.6.19 |
IPC Namespace | CLONE_NEWIPC | 2.6.19 |
PID Namespace | CLONE_NEWPID | 2.6.24 |
Network Namespace | CLONE_NEWNET | 2.6.29 |
User Namespace | CLONE_NEWUSER | 3.8 |
Namespace的api主要使用3个系统调用:
- clone() 创建新的进程
- unshare() 将进程移除某个Namespace
- sents() 将进程加入到Namespace
UTS Namespace
UTS Namespace 主要用来隔离nodename(hostname) 和domainname 两个系统标识。
在UTS Namespace里面, 每个Namespace 允许有自己的hostname 。
下面将使用Go 来做一个UTS Namespace 的例子。
package main
import (
"os/exec"
"syscall"
"os"
"log"
)
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)
}
}
解释一下代码, exec .Command (“sh” )用来指定被fork 出来的新进程内的初始命令,默认使用s h 来执行。下面就是设置系统调用参数, 像2.1.l 小节中讲到的一样,使用CL ONENE WU TS 这个标识符去创建一个UTS Namespace 。Go 帮我们封装了对clone() 函数的调用,这段代码执行后就会进入到一个s h 运行环境中。
执行
go run main. go 命令,在这个交互式环境里,使用pstree -pl 查看一下系统中进程之间的关系, 如下
sh-4.2# echo $$
23137
sh-4.2# pstree -p| grep 23137
|-sshd(1291)-+-sshd(21695)---bash(21697)---go(23110)-+-utc(23132)-+-sh(23137)-+-grep(23215)
验证一下父进程和子进程是否不在同一个UTS Namespace 中, 看到确实不在同一个uts namespace中
sh-4.2# readlink /proc/23137/ns/uts
uts:[4026532440]
sh-4.2# readlink /proc/23132/ns/uts
uts:[4026531838]
所以在这个环境内修改hostname 应该不影响外部主机, 下面来做一下实验:
sh-4.2# hostname -b test
sh-4.2# hostname
test
开启新的终端查看hostname
[root@DH-PROXY-T01 gofile]# hostname
DH-PROXY-T01
IPC Namespace(消息队列)
IPC Namespace 用来隔离System V IPC 和POSIX message queues 每一个IPC Namespace都有自己的system v IPC 和POSIX message queue
样例:
package main
import (
"os/exec"
"syscall"
"os"
"log"
)
func main() {
cmd := exec.Command("sh")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC,
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
查看队列
[root@DH-PROXY-T01 gofile]# go run ipc.go
sh-4.2#
sh-4.2# ipcmk -Q
消息队列 id:0
sh-4.2# ipcs -q
--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息
0xfd247a01 0 root 644 0 0
切换到另一个shell,查看,没有消息证明ipc隔离
sh-4.2# ipcs -q
--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息
PIO Namespace
PID Namespace 是用来隔离进程ID 的。同样一个进程在不同的PID Namespace 里可以拥有不同的PID 。这样就可以理解, 在docker container 里面, 使用ps -ef 经常会发现, 在容器内, 前台运行的那个进程PID 是1 , 但是在容器外,使用ps -ef 会发现同样的进程却有不同的PID , 这就是PID Namespace 做的事情。
在上一小结代码的基础上, 再修改一下代码, 添加一个syscall.CLONE_ NEWPID ,代表为fork 出来的子进程创建自己的PID Namespace 。
package main
import (
"os/exec"
"syscall"
"os"
"log"
)
func main() {
cmd := exec.Command("sh")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID,
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
要打开两个shell 。首先在宿主机上看一下进程树,找一下进程的真实PID
[root@DH-PROXY-T01 gofile]# go run ipc.go
sh-4.2# echo $$
1
[root@DH-PROXY-T01 gofile]# pstree -p | grep ipc
|-sshd(1291)-+-sshd(13183)-+-bash(13185)---go(13409)-+-ipc(13431)-+-sh(13436)
可以看到,该操作打印了当前Namespace 的PID , 其值为1 。也就是说,这个13431 的PID 被映射到Namespace 里后PID 为1。这里还不能使用ps 来查看, 因为ps 和top 等命令会使用/proc 内容,具体内容在下面的Mount Namespace 部分会进行讲解。
Mount Namespace
Mount Namespace 用来隔离各个进程看到的挂载点视图。在不同Names pace 的进程中, 看到的文件系统层次是不一样的。在Mount Namespace 中调用mount()和umount() 仅仅只会影响当前Namespace 内的文件系统,而对全局的文件系统是没有影响的。看到这里, 也许就会想到chroot()。它也是将某一个子目录变成根节点。但是, MountNames pace 不仅能实现这个功能,而且能以更加灵活和安全的方式实现。Mount Namespace 是Linux 第一个实现的Names pace 类型, 因此,它的系统调用参数是NEWNS ( New Namespace 的缩写)。
增加了NEWNS 标识,如下
package main
import (
"os/exec"
"syscall"
"os"
"log"
)
func main() {
cmd := exec.Command("sh")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
运行,执行命令,此时的/proc 还是宿主机的,执行ps -ef 无法正常运行
sh-4.2# ls /proc/
1 consoles driver iomem key-users mdstat net self sysrq-trigger version
acpi cpuinfo execdomains ioports kmsg meminfo pagetypeinfo slabinfo sysvipc vmallocinfo
buddyinfo crypto fb irq kpagecount misc partitions softirqs timer_list vmstat
bus devices filesystems kallsyms kpageflags modules sched_debug stat timer_stats zoneinfo
cgroups diskstats fs kcore loadavg mounts schedstat swaps tty
cmdline dma interrupts keys locks mtrr scsi sys uptime
sh-4.2# ps -ef
Error, do this: mount -t proc proc /proc
挂在后再执行,发现sh的pid为1,因为ps -ef是获取的proc的内容,所以当前的mount namespace和外部的空间是隔离的,proc 是一个文件系统,提供额外的机制,可以通过内核和内核模块将信息发送给进程。
sh-4.2# mount -t proc proc /proc
sh-4.2# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 09:26 pts/0 00:00:00 sh
root 7 1 0 09:30 pts/0 00:00:00 ps -ef
User Namespace
User N amespace 主要是隔离用户的用户组ID 。也就是说, 一个进程的User ID 和GroupID 在User Namespace 内外可以是不同的。比较常用的是,在宿主机上以一个非root 用户运行创建一个User Namespace , 然后在User Namespace 里面却映射成root 用户。这意味着, 这个进程在User Namespace 里面有root 权限,但是在User Namespace 外面却没有root 的权限。从Linux Kernel 3 . 8 开始, 非root 进程也可以创建User Namespace , 并且此用户在N amespace 里面可以被映射成root , 且在Namespace 内有root 权限。下面,继续以一个例子来描述, 代码如下。
package main
import (
"log"
"os"
"os/exec"
"syscall"
)
func main() {
cmd := exec.Command("sh")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID |
syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER,
}
cmd.SysProcAttr.Credential = &syscall.Credential{
Uid: uint32(1), Gid: uint32(1)}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
os.Exit(-1)
}
本例在原来的基础上增加了syscall.CLONE_NEWUSER 。首先,以root 来运行这个程序,运行前在宿主机上看一下当前的用户和用户组, 显示如下:
root@iZ254rt8xflZ :~/gocode/src/book# id
uid=O(root) gid=O(root ) groups=O(root )
可以看到我们是root 用户,接下来运行一下程序。
root@iZ254rt8xflZ : ~/ gocode/src/book# go run main.go
$ id
uid=65534 (nobody ) gid=65534(nogroup) groups=65534(nogroup)
可以看到, 它们的UID 是不同的,因此说明User Namespace 生效了。
Network Namespace
Network Namespace 是用来隔离网络设备、IP 地址端口等网络械的Namespace 。NetworkNamespace 可以让每个容器拥有自己独立的(虚拟的)网络设备,而且容器内的应用可以绑定到自己的端口,每个Namespace 内的端口都不会互相冲突。在宿主机上搭建网桥后,就能很方便地实现容器之间的通信,而且不同容器上的应用可以使用相同的端口。同样,在2.1.6 小节的代码的基础上增加syscall.CLONE_ NEWNET 标识符,如下:
package main
import (
"log"
"os"
"os/exec"
"syscall"
)
func main() {
cmd := exec.Command("sh")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID |
syscall.CLONE_NEWNS | syscall.CLONE_NEWNET,
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
os.Exit(-1)
}
进入程序查看网络
[root@DH-PROXY-T01 gofile]# go run main.go
sh-4.2# ifconfig
sh-4.2# mount -t proc proc /proc
sh-4.2# ifconfig
sh-4.2#
返回宿主机查看网络
[root@DH-PROXY-T01 gofile]# ifconfig
ens192: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.50.133.98 netmask 255.255.255.0 broadcast 10.50.133.255
ether 00:50:56:b2:47:aa txqueuelen 1000 (Ethernet)
我们发现, 在Namespace 里面什么网络设备都没有。这样就能断定Network Namespace 与宿主机之间的网络是处于隔离状态了。