阅读计划-Linux Namespace

Posted by 聪少 on 2019-05-30

Docker和虚拟机技术一样,从操作系统级上实现了资源的隔离,Docker本质上是宿主机上的进程。所以Docker的资源隔离指的是进程资源的隔离。实现资源隔离的核心技术就是Linux Namespace。

演示环境
[root@tyy namespace]# cat /etc/redhat-release
CentOS Linux release 7.4.1708 (Core)

概念

Linux Namespace是Kernel的一个功能,它能隔离PID、UserID、Network等。下表简单介绍列出了各种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

系统调用

1
2
3
clone()		// 创建新进程,根据具体参数创建相应的Namespace,而且它的子进程也包含进去
unshare() // 将进程移出某个Namespace
setns() // 将进程加入某个Namespace

Linux Namespace演示

UTS namespace

c语言演示uts隔离

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
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#define STACK_SIZE (1024 * 1024)

static char container_stack[STACK_SIZE];
char* const container_args[] = {
"/bin/bash",
NULL
};

// 容器进程运行的程序主函数
int container_main(void *args) {
printf("进入容器进程中!\n");
sethostname("hello", 9);
execv(container_args[0], container_args); // 执行/bin/bash return 1;
}

int main(int args, char *argv[]) {
printf("程序开始\n");
// clone 容器进程,并进行uts隔离
int container_pid = clone(container_main, container_stack + STACK_SIZE, SIGCHLD | CLONE_NEWUTS, NULL);
// 等待容器进程结束
waitpid(container_pid, NULL, 0);
printf("程序退出\n");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
# 编译
[root@tyy namespace]# gcc -o uts uts.c
# 执行
[root@tyy namespace]# ./uts
程序开始
进入容器进程中!
[root@hello namespace]# exit
exit
程序退出
[root@tyy namespace]#

# 可以看的出来,主机名从tyy变成了hello,在退出进程的时候,
# 我们发现本身宿主机的hostname是没有被改变的。这里就是uts的隔离演示.

go语言演示uts隔离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 编译
[root@tyy namespace]# go build -o uts uts.go
# 执行
[root@tyy namespace]# ./uts
# 设置hostname
sh-4.2# hostname hello
# 输出hello
sh-4.2# hostname
hello
# 退出进程
sh-4.2# exit
exit
# 输出tyy
[root@tyy namespace]# hostname
tyy
# 和c的代码一致,这里使用了go做了uts隔离

在上面的两个例子中,fork进程的hostname产生了变更,主机不受影响,同样,主机hostname也不会对fork进程产生影响,由此fork进程的hostname
与主机父进程的hostname成功实现了隔离。大家可以自己试试。

IPC Namespace

IPC Namespace实现了进程间通信隔离,常见的如:信号量、消息队列、共享内存。
在代码中使用CLONE_NEWIPC标识符即可。

1
int container_pid = clone(container_main, container_stack + STACK_SIZE, SIGCHLD | CLONE_NEWUTS | CLONE_NEWIPC, NULL);
1
Cloneflags:syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC,
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
# 创建队列
[root@tyy namespace]# ipcmk -Q
消息队列 id:32768

# 查看队列
[root@tyy namespace]# ipcs -q

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

[root@tyy namespace]#
# 编译、执行并查看队列,这个时候发现是隔离的。
[root@tyy namespace]# go build -o uts uts.go
[root@tyy namespace]# ./uts
sh-4.2# ipcs -q

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

sh-4.2# exit
exit
[root@tyy namespace]# ipcs -q

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

[root@tyy namespace]#

附上一些指令的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ipcrm 命令 
移除一个消息对象。或者共享内存段,或者一个信号集,同时会将与ipc对象相关链的数据也一起移除。当然,只有超级管理员,或者ipc对象的创建者才有这项权利啦
ipcrm用法
ipcrm -M shmkey 移除用shmkey创建的共享内存段
ipcrm -m shmid 移除用shmid标识的共享内存段
ipcrm -Q msgkey 移除用msqkey创建的消息队列
ipcrm -q msqid 移除用msqid标识的消息队列
ipcrm -S semkey 移除用semkey创建的信号
ipcrm -s semid 移除用semid标识的信号

ipcs用法
ipcs -a 是默认的输出信息 打印出当前系统中所有的进程间通信方式的信息
ipcs -m 打印出使用共享内存进行进程间通信的信息
ipcs -q 打印出使用消息队列进行进程间通信的信息
ipcs -s 打印出使用信号进行进程间通信的信息

输出格式的控制
ipcs -t 输出信息的详细变化时间
ipcs -p 输出ipc方式的进程ID
ipcs -c 输出ipc方式的创建者/拥有者
ipcs -c 输出ipc各种方式的在该系统下的限制条件信息
ipcs -u 输出当前系统下ipc各种方式的状态信息(共享内存,消息队列,信号)

PID namespace

PID namespace 完成的是进程号的隔离,同样在加入CLONE_NEWPID即可。

1
int container_pid = clone(container_main, container_stack + STACK_SIZE, SIGCHLD | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID, NULL);
1
Cloneflags:syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID,
1
2
3
4
5
6
7
8
9
10
11
# go演示
[root@tyy namespace]# go build -o uts uts.go
[root@tyy namespace]# ./uts
# 查看自身pid,显示为1
sh-4.2# echo $$
1
sh-4.2# exit
exit
[root@tyy namespace]# echo $$
32298
[root@tyy namespace]#

User Namespace

我的内核版本没有验证通过,程序运行起来就报错了。(go版本)

Net Namespace

同理直接加入CLONE_NEWNET。

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
go build -o uts uts.go
[root@tyy namespace]# ./uts
sh-4.2#
sh-4.2#
# 由于网络隔离,所以ifconfig什么信息都没有
sh-4.2# ifconfig
sh-4.2# exit
exit
# 宿主机网络
[root@tyy namespace]# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.16.4.109 netmask 255.255.240.0 broadcast 172.16.15.255
ether 00:16:3f:00:60:8b txqueuelen 1000 (Ethernet)
RX packets 665387 bytes 476888575 (454.7 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 477271 bytes 96222518 (91.7 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1 (Local Loopback)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0