docker是一个是用来linux namespace 和 cgroups 的虚拟化工具。
下面几个小节学习linux namespace技术,以及使用golang实现。
环境:
ps: mac系统的syscall和linux上的有些不同。
UTS namespace(UNIX Time-sharing System namespace)用来隔离系统的hostname以及NIS(Network Information System) domain name 。
hostname的作用和意义
局域网内查找主机
root@godev:~# cat /etc/hosts
127.0.0.1 localhost
127.0.1.1 godev godev # 可以ping godev
# 三部分组成
# IP 地址 主机名[.域名] [主机名(主机名别名)]
百度了一下,找下面这个网址。简单的了解了下。
https://www.freebsd.org/doc/handbook/network-nis.html
Network Information System (NIS) is designed to centralize administration of UNIX®-like systems such as Solaris™, HP-UX, AIX®, Linux, NetBSD, OpenBSD, and FreeBSD. NIS was originally known as Yellow Pages but the name was changed due to trademark issues. This is the reason why NIS commands begin with yp.
NIS is a Remote Procedure Call (RPC)-based client/server system that allows a group of machines within an NIS domain to share a common set of configuration files. This permits a system administrator to set up NIS client systems with only minimal configuration data and to add, remove, or modify configuration data from a single location.
FreeBSD uses version 2 of the NIS protocol.
NIS是C/S模式的系统。用于让一组机器共享公共配置文件。
package main
import (
"log"
"os"
"os/exec"
"syscall"
)
func main() {
// fork出新的进程。并执行sh命令
cmd := exec.Command("sh")
// 设置Cloneflags。 clone新的UTS命名空间
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS, // 注释这一行前后,对比一下UTS的隔离情况
}
// 输入输出
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
验证namespace的隔离
运行程序后,将会进入命令行操作界面。
执行hostname
命令查看主机名, 同时在另一个终端里面也查看主机名。 这时候两个hostname一致。
在程序启动的命令行中执行hostname test
修改主机名为test。 然后在另一个终端中查看主机名。 将会发现主机名没有发生变化。
查看父子进程的UTS id
查看当前golang程序执行的进程,和它的父进程
# 查看当前进程的PID
echo $$ # 8859
# 查看所有进程的PID(层级显示) pstree -pl 也可查看
ps aux -H
获得父子进程的PID后,就可以查看UTS id了。这里借助linux的/proc目录。
/proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口。用户和应用程序可以通过 proc得到系统的信息,并可以改变内核的某些参数。由于系统的信息,如进程,是动态改变的,所以用户或应用程序读取proc文件时,proc文件系统是 动态从系统内核读出所需信息并提交的。
$ readlink /proc/8859/ns/uts
uts:[4026532145]
$ readlink /proc/8855/ns/uts
uts:[4026531838]
父子进程的UTS id并不相同。 成功实现了UTS namespace的隔离。
在linux下的多个进程间的通信机制叫做IPC(Inter-Process Communication)
深刻理解Linux进程通信(IPC)
常用命令
# 查看
ipcs
# 创建
ipcmk
# 删除
ipcrm
package main
import (
"log"
"os"
"os/exec"
"syscall"
)
func main() {
// fork出新的进程。并执行sh命令
cmd := exec.Command("sh")
// 设置Cloneflags。 clone新的IPC命名空间
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.syscall.CLONE_NEWIPC, // 注释这一行前后,对比一下IPC的隔离情况
}
// 输入输出
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
验证IPC的隔离
运行程序后,进入命令行操作界面。 查看ipc资源。
root@godev:~/go/mod/mydocker# go run main.go
# ipcs
------ Message Queues --------
key msqid owner perms used-bytes messages
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
------ Semaphore Arrays --------
key semid owner perms nsems
新建一个消息队列
# ipcmk -Q
Message queue id: 0
# ipcs
------ Message Queues --------
key msqid owner perms used-bytes messages
0x1cccf433 0 root 644 0 0
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
------ Semaphore Arrays --------
key semid owner perms nsems
宿主机上另外打开一个终端,查看IPC资源
root@godev:/proc# ipcs
------ Message Queues --------
key msqid owner perms used-bytes messages
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
------ Semaphore Arrays --------
key semid owner perms nsems
在新创建的namespace中新增消息队列,在老的namespace看不到新增的,说明IPC隔离成功。
PID (Process Identification)
PID namespace是划分那些一个进程可以查看并与之交互的PID的方式。当我们创建一个新的 PID namespace时,第一个进程的PID会被赋值为1。进程退出时,内核会杀死这个namespace内的其他进程
package main
import (
"log"
"os"
"os/exec"
"syscall"
)
func main() {
// fork出新的进程。并执行sh命令
cmd := exec.Command("sh")
// 设置Cloneflags。 clone新的PID命名空间
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.syscall.CLONE_NEWPID, // 注释这一行前后,对比一下PID的隔离情况
}
// 输入输出
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
验证PID的隔离
root@godev:~/go/mod/mydocker# go run main.go
# echo $$
1
打印当前进程PID为1, 证明PID被成功隔离。
这里还不能使用ps、top等命令,这些命令查看的是/proc内容。具体内容在下面的 Mount Namespace。
mount namespace是第一个被添加到linux中的namespace. 因此它的调用参数是CLONE_NEWNS(new namespace的缩写)。当时并没有意识到后续会有其他类型的namespace。
package main
import (
"log"
"os"
"os/exec"
"syscall"
)
func main() {
// fork出新的进程。并执行sh命令
cmd := exec.Command("sh")
// 设置Cloneflags。 clone新的Mount命名空间
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWNS,
}
// 输入输出
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
验证Mount的隔离
打开一个新终端,创建 /tmp/testisolation目录、进入目录、查看文件列表
root@godev:~# mkdir /tmp/testisolation
root@godev:~# cd /tmp/testisolation && ls
root@godev:/tmp/testisolation#
执行程序,挂载/tmp/testisolation目录后, 创建文件b:
root@godev:~/go/mod/mydocker# go run main.go
# mount -t tmpfs tmpfs /tmp/testisolation
# cd /tmp/testisolation
# ls
# touch b
# ls
b
#
切换会第一个终端。查看文件列表.
root@godev:/tmp/testisolation# ls
文件列表为空,说明mount隔离成功。
这个mount namespace其实没有完全隔离。更多细节参照:mount namespace和共享子树
主要用来隔离安全相关的标识符(idebtifier)和属性(attribute),包括用户ID、用户组ID、root目录、key(密钥)以及特殊权限。
User N amespace 主要是隔离用户 的用户组 ID。 也就是说 , 一个进程的 User ID 和 Group ID在UserNamespace内外可以是不同的。 比较常用的是,在宿主机上以一个非root用户运行 创建一个 User Namespace, 然后在 User Namespace 里面却映射成 root 用户。这意味着 , 这个 进程在 User Namespace 里面有 root权限,但是在 User Namespace 外面却没有 root 的权限。从 Linux Kernel 3.8开始, 非root进程也可以创建UserNamespace, 并且此用户在Namespace里 面可以被映射成 root, 且在 Namespace 内有 root权限。
package main
import (
"log"
"os"
"os/exec"
"syscall"
)
func main() {
// fork出新的进程。并执行sh命令
cmd := exec.Command("sh")
// 设置Cloneflags。 clone新的user命名空间
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUSER,
UidMappings: []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: 0,
Size: 1,
},
},
GidMappings: []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: 0,
Size: 1,
},
},
}
// 输入输出
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
验证User的隔离
root@godev:~/go/mod/mydocker# go run main.go
# id
uid=0(root) gid=0(root) groups=0(root)
Network Namespace 是用来隔离网络设备、 IP地址端口 等网络栈的 Namespace。
Network Namespace 可以让每个容器拥有自己独立的(虚拟的)网络设备,而且容器内的应用可以绑定 到自己的端口,每个 Namespace 内的端口都不会互相冲突。在宿主机上搭建网桥后,就能很方 便地实现容器之间的通信,而且不同容器上的应用可以使用相同的端口 。
package main
import (
"log"
"os"
"os/exec"
"syscall"
)
func main() {
// fork出新的进程。并执行sh命令
cmd := exec.Command("sh")
// 设置Cloneflags。 clone新的network命名空间
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWNET,
}
// 输入输出
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
验证User的隔离
root@godev:~/go/mod/mydocker# go run main.go
# ip a
1: lo: mtu 65536 qdisc noop state DOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# ifconfig
#
被隔离的Namespace中只有一个本地回环,其他什么都没有。 在网络上处于隔离状态。
本文只是针对每种namespace做了简单的介绍和实现。熟悉了几种namespace的使用后,又完整的隔离了一个容器。
下一章:【实现简单的容器】- goalng实现namespace隔离的容器