Linux Namespace

概念

Linux Namespace是Linux内核提供的一个功能,它可以隔离一系列的系统资源,比如:PID,UserID,NetWork等。

当前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主要使用如下三个系统调用:

  1. clone() 创建新进程。根据系统调用参数来判断哪些Namespace被创建,而且它们的紫禁城也会被包含到这些Namespace中
  2. unshare() 将进程移除某个Namespace
  3. setns() 将进程加入到Namespace中

UTS Namespace

UTS Namespace主要用来隔离 nodename和domainname两个系统标识,在UTS Namespace中,每个Namespace允许有自己的hostname。

golang测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// file : uts.go
package main

import (
"mydocker/tools/logging"
"os"
"os/exec"
"syscall"
)

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 {
logging.Fatal(err)
}
}

在代码目录下执行命令:go run uts.go,然后进入shell交互页面。

然后执行命令echo $$,打印出当前shell的进程id,然后再执行命令 readlink /proc/$pid/ns/uts,打印出的结果与直接在terminal中执行该命令结果是不一样的,说明已实现了uts隔离。

在该shell中修改hostname:hostname -b test,然后同时在该shell中和直接在terminal中打印hostname,发现两者不一致,说明在uts隔离的shell中对uts修改不会影响宿主机。

IPC Namespace

IPC Namespace 用来隔离 信号量、消息队列和共享内存,每一个IPC Namespace的这些资源都是隔离的。
在上一版本的代码中略作修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//file: ipc.go
package main

import (
"mydocker/tools/logging"
"os"
"os/exec"
"syscall"
)

func main() {

cmd := exec.Command("sh")

cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC, // 增加一个flag
}

cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {
logging.Fatal(err)
}
}

IPC隔离测试:

首先在宿主机上打开一个shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 查看现有 ipc message queues
root@master:~# ipcs -q
--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息

# 创建一个message queue
root@master:~# ipcmk -Q
消息队列 id:0

# 然后在查看一下
root@master:~# ipcs -q

--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息
0xfee7b418 0 root 644 0 0

# 此时能看到一个queue了,再使用另一个shell去运行程序: go run ipc.go
# ipcs -q

--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息

由以上实验可以发现,在新建的Namespace里,看不到宿主机上创建的queue,说明IPC Namespace创建成功,IPC已经被隔离。

PID Namespace

PID Namespace是用来隔离进程ID的,同一个进程在不同的PID Namespace里可以有不同的PID,在docker容器里面,使用 ps -ef就会发现,容器内,前台运行的进程PID是1,但在容器外使用ps -ef却发现同一进程有不同的PID,这就是PID Namespace的功劳。

测试代码,在上一节代码中增加一个CloneFlag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// file: pid.go
package main

import (
"mydocker/tools/logging"
"os"
"os/exec"
"syscall"
)

func main() {

cmd := exec.Command("sh")

cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC |
syscall.CLONE_NEWPID, // add
}

cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {
logging.Fatal(err)
}
}

同时打开两个shell,其中一个运行: go run pid.go

在宿主机shell中执行命令: ps -ef|grep pid.go可以查到另一个shell中运行的go程序的pid,然后在另一个运行go程序的shell中执行命令: echo $$可以得到当前sh的pid,可以发现,在运行go程序的shell中其PID为1,而在宿主机看到该程序的PID不为1。说明PID已隔离。

Mount Namespace

Mount Namespace 用来隔离各个进程看到的挂载点视图,在不同的Namespace中,看到的文件层次是不一样的,在Mount Namespace中调用mount() umount()仅影响当前Namespace的文件系统(注:Mount有传播模式,如果是share模式还是会影响到的,测试时设置private模式可避免影响)

测试代码,在上一节基础上再增加一个CloneFlag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// file: ns.go
package main

import (
"mydocker/tools/logging"
"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,
}

cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {
logging.Fatal(err)
}
}

运行程序: go run ns.go

在程序shell中执行命令mount --make-private /proc,先修改挂载模式为private

修改 mount 之前,查看 /proc (ls /proc)目录发现里边文件有很多,然后执行命令:mount -t proc proc /proc,在查看 /proc目录,发现里边文件少了很多。此时使用 ps -ef查看系统进程,可以看到只有 sh进程和 ps -ef进程。说明Mount隔离成功。(注:/proc目录下有很多数字的目录,这些每个目录都代表一个进程,数字为进程的PID,里边存储着跟进程相关数据,Mount隔离和PID隔离配合,使得ps -ef命令查看到当前Namespace下的进程,且PID是从1开始编号的)

User Namespace

User Namespace主要是隔离用户的用户组ID,可以在宿主机上以一个非root用户运行创建一个User Namespace,并且在这个Namespace下创建root用户,且在namespace下有root权限。

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// file: user.go
package main

import (
"mydocker/tools/logging"
"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,
UidMappings: []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: syscall.Getuid(),
Size: 1,
},
},
GidMappings: []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: syscall.Getgid(),
Size: 1,
},
},
}

cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {
logging.Fatal(err)
}
}

以普通用户执行:go run user.go,在shell中输入命令id,打印出:uid=0(root) gid=0(root) 组=0(root),65534(nogroup),但当尝试列出 /root目录时,会提示权限不够。

Network Namespace

Network Namespace是用来隔离网络设备,IP地址端口等网络栈的Namespace。

测试代码:在上一节代码基础上增加CloneFlag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// file: network.go
package main

import (
"mydocker/tools/logging"
"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 | syscall.CLONE_NEWNET,
UidMappings: []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: syscall.Getuid(),
Size: 1,
},
},
GidMappings: []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: syscall.Getgid(),
Size: 1,
},
},
}

cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {
logging.Fatal(err)
}
}

在宿主机上查看网络设备,ifconfig,可以看到很多个网络设备。

执行命令:go run network.go后,在查看网络设备,发现只有 lo 一个网络设备了,说明Network Namespace隔离成功。

Linux Cgroup

Linux Cgroup提供了对一组进程及将来子进程的资源限制、控制和统计的能力。

Cgroup的三个组件:

  1. cgroup是对进程分组管理的一种机制,一个cgroup包含一组进程,并可以在这个cgroup上增加subsystem的各种参数配置,将一组进程和一组subsystem的系统参数关联起来
  2. subsystem是一组资源控制的模块,一般包含以下几项:
    1. blkio:设置对块设备输入输出的访问控制
    2. cpu:设置cgroup中进程的cpu被调度的策略
    3. cpuacct:可以统计cgroup中进程的cpu占用
    4. cpuset:在多核机器上设置cgroup中进程可以使用的cpu和内存
    5. devices:控制cgroup中进程对设备的访问
    6. freezer:用于挂起和恢复cgroup中的进程
    7. memory:用于控制cgroup中进程的内存占用
    8. net_cls:用于将cgroup中进程产生的网络包分类,以便Linux的tc(traffic controller)可以根据分类区分出来自某个cgroup的包并做限流或监控
    9. net_prio:设置cgroup中进程产生的网络流量的优先级
    10. ns:使cgroup中的进程在新的Namespace中fork新进程时,创建一个新的cgroup,这个cgroup包含新的Namespace中的进程
  3. hierarchy的功能是把一组cgroup串成一个树状结构,通过树状结构Cgroup可以做到集成

三个组件相互关系

  1. 系统在创建了新的hierarchy后,系统中所有进程都会加入这个hierarchy的cgroup根节点,这个cgroup根节点是hierarchy默认创建的
  2. 一个subsystem只能附加到一个hierarchy上
  3. 一个hierarchy能附加多个subsystem
  4. 一个进程可以作为多个cgroup成员,但这些cgroup必须在不同的hierarchy中
  5. 一个进程fork出子进程时,子进程和父进程是在同一个cgroup中,也可移到其他cgroup中