【实现简单的容器】- docker基础技术之namespace

简介

docker是一个是用来linux namespace 和 cgroups 的虚拟化工具。
下面几个小节学习linux namespace技术,以及使用golang实现。

环境:

  • ubuntu 16.04 LTS
  • golang 1.12.5

ps: mac系统的syscall和linux上的有些不同。

一、UTS Namespace 隔离主机名和域名

UTS namespace(UNIX Time-sharing System namespace)用来隔离系统的hostname以及NIS(Network Information System) domain name 。

1. hostname 主机名

hostname的作用和意义

局域网内查找主机

root@godev:~# cat /etc/hosts
127.0.0.1       localhost
127.0.1.1    godev    godev          # 可以ping godev

# 三部分组成
# IP 地址     主机名[.域名]     [主机名(主机名别名)]
2. NIS(Network Information System)

百度了一下,找下面这个网址。简单的了解了下。
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模式的系统。用于让一组机器共享公共配置文件。

Golang实现
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                      

在这里插入图片描述
从图片中可以看出, 8859进程的父进程的PID是8855

获得父子进程的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的隔离。

二、IPC Namespace 隔离进程间通信

在linux下的多个进程间的通信机制叫做IPC(Inter-Process Communication)
深刻理解Linux进程通信(IPC)

常用命令

# 查看
ipcs   

# 创建
ipcmk 

# 删除
ipcrm
Golang实现
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 Namespace 隔离进程ID

PID (Process Identification)
PID namespace是划分那些一个进程可以查看并与之交互的PID的方式。当我们创建一个新的 PID namespace时,第一个进程的PID会被赋值为1。进程退出时,内核会杀死这个namespace内的其他进程

Golang实现
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 隔离挂载点和文件系统

mount namespace是第一个被添加到linux中的namespace. 因此它的调用参数是CLONE_NEWNS(new namespace的缩写)。当时并没有意识到后续会有其他类型的namespace。

Golang实现
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和共享子树

五、User Namespace 隔离用户的用户组ID

主要用来隔离安全相关的标识符(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权限。

Golang实现
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 隔离网络设备

Network Namespace 是用来隔离网络设备、 IP地址端口 等网络栈的 Namespace。
Network Namespace 可以让每个容器拥有自己独立的(虚拟的)网络设备,而且容器内的应用可以绑定 到自己的端口,每个 Namespace 内的端口都不会互相冲突。在宿主机上搭建网桥后,就能很方 便地实现容器之间的通信,而且不同容器上的应用可以使用相同的端口 。

Golang实现
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隔离的容器

你可能感兴趣的:(linux,docker)