Docker容器与容器云读书笔记(一)–Namespace

Linux提供了一系列隔离的API

  • Mount: 隔离文件系统挂载点,CLONE_NEWNS
  • UTS: 隔离主机名和域名信息,CLONE_NEWUTS
  • IPC: 隔离进程间通信,CLONE_NEWIPC
  • PID: 隔离进程的ID,CLONE_NEWPID
  • Network: 隔离网络资源,CLONE_NEWNET
  • User: 隔离用户和用户组的ID,CLONE_NEWUSER

UTC Namespace

创建utc.c文件如下:

#define _GNU_SOURCE
#include <sys/wait.h>
#include <sys/utsname.h>
#include <sched.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define errExit(msg)        \
    do                      \
    {                       \
        perror(msg);        \
        exit(EXIT_FAILURE); \
    } while (0)

// 调用 clone 时执行的函数
static int childFunc(void *arg)
{
    struct utsname uts;
    char *shellname;
    // 在子进程的 UTS namespace 中设置 hostname
    if (sethostname(arg, strlen(arg)) == -1)
        errExit("sethostname");

    // 显示子进程的 hostname
    if (uname(&uts) == -1)
        errExit("uname");
    printf("uts.nodename in child:  %s\n", uts.nodename);
    printf("My PID is: %d\n", getpid());
    printf("My parent PID is: %d\n", getppid());
    // 获取系统的默认 shell
    shellname = getenv("SHELL");
    if (!shellname)
    {
        shellname = (char *)"/bin/sh";
    }
    // 在子进程中执行 shell
    execlp(shellname, shellname, (char *)NULL);

    return 0;
}
// 设置子进程的堆栈大小为 1M
#define STACK_SIZE (1024 * 1024)

int main(int argc, char *argv[])
{
    char *stack;
    char *stackTop;
    pid_t pid;

    if (argc < 2)
    {
        fprintf(stderr, "Usage: %s <child-hostname>\n", argv[0]);
        exit(EXIT_SUCCESS);
    }

    // 为子进程分配堆栈空间,大小为 1M
    stack = malloc(STACK_SIZE);
    if (stack == NULL)
        errExit("malloc");
    stackTop = stack + STACK_SIZE; /* Assume stack grows downward */

    // 通过 clone 函数创建子进程
    // CLONE_NEWUTS 标识指明为新进程创建新的 UTS namespace
    pid = clone(childFunc, stackTop, CLONE_NEWUTS | SIGCHLD, argv[1]);
    if (pid == -1)
        errExit("clone");

    // 等待子进程退出
    if (waitpid(pid, NULL, 0) == -1)
        errExit("waitpid");
    printf("child has terminated\n");

    exit(EXIT_SUCCESS);
}

编译

## gcc version

gcc -Wall  utc.c -o utc

执行后即可进入myhostns里面,exit即可退出;

[root@docker-ns pgy]# ./utc myhost
[root@myhost pgy]# hostname
myhost
[root@myhost pgy]# exit
exit
child has terminated
[root@docker-ns pgy]#

IPC Namespace

## 修改clone参数,添加CLONE_NEWIPC即可
 pid = clone(childFunc, stackTop, CLONE_NEWIPC |CLONE_NEWUTS | SIGCHLD, argv[1]);

编译

gcc -Wall  ipc.c -o ipc

测试

## 在原host里面查看并创建ipc
[root@docker-ns pgy]# ipcmk -Q
Message queue id: 0
[root@docker-ns pgy]# ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages
0xc4bdb217 0          root       644        0            0

[root@docker-ns pgy]# ipcmk -Q
Message queue id: 32769


### 添加ipc隔离
[root@docker-ns pgy]# ./ipc myhost
uts.nodename in child:  myhost
My PID is: 125325
My parent PID is: 125324
### 可以看到没有输出对应的ipc
[root@myhost pgy]# ipcmk -Q
Message queue id: 0

PID Namespace

修改代码,在clone的时候,添加CLONE_NEWPID参数

pid = clone(childFunc, stackTop, CLONE_NEWPID | CLONE_NEWIPC |CLONE_NEWUTS | SIGCHLD, argv[1]);

编译执行

[root@docker-ns pgy]# gcc -Wall  pid.c -o pid
### 查看当前shell的PID
[root@docker-ns pgy]# echo $$
122947
### 执行pid隔离,并查看PID,可以看到PID为1
[root@docker-ns pgy]# ./pid myhost
uts.nodename in child:  myhost
My PID is: 1
My parent PID is: 0
[root@myhost pgy]# echo $$
1

但是执行ps你还会发现其他的进程

[root@myhost pgy]# ps
   PID TTY          TIME CMD
122947 pts/0    00:00:00 bash
127135 pts/0    00:00:00 pid
127136 pts/0    00:00:00 bash
127663 pts/0    00:00:00 ps

因为ps读取的是原host proc下的文件,所以还需要做mount隔离

Mount Namespace

## 修改代码CLONE_NEWNS,由于mount是第一个namespace,所以名称为CLOUD_NEWNS
pid = clone(childFunc, stackTop, CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD, argv[1]);

编译测试

[root@docker-ns pgy]# gcc -Wall mount.c -o mount
[root@docker-ns pgy]# ./mount myhost
uts.nodename in child:  myhost
My PID is: 1
My parent PID is: 0
## 将proc目录挂载为proc类型的file system
[root@myhost pgy]# mount -t proc proc /proc
[root@myhost pgy]# ps
   PID TTY          TIME CMD
     1 pts/0    00:00:00 bash
    17 pts/0    00:00:00 ps
[root@myhost pgy]#

User Namespace

## 修改代码,添加CLONE_NEWUSER
pid = clone(childFunc, stackTop,  CLONE_NEWUSER | SIGCHLD, argv[1]);

编译测试

[root@docker-ns pgy]# ./utc myhost
uts.nodename in child:  myhost
My PID is: 129716
My parent PID is: 129715
[root@myhost pgy]# id
uid=0(root) gid=0(root) groups=0(root)
[root@myhost pgy]# id -u
0
[root@myhost pgy]# id -g
0
[root@myhost pgy]#

映射

docker里面的用户和用户组是与宿主机进行映射;

[root@docker-ns ~]# docker ps | awk '{print $1}'
CONTAINER
c884b7311190

可以看到有一个 docker的进程,查一下它的PID

[root@docker-ns ~]# ps -aux | grep c88 | grep -v pts/0 | awk '{print $2}'
41986

查看它的 usergroup的映射

[root@docker-ns ~]# cat /proc/41986/gid_map
         0          0 4294967295
[root@docker-ns ~]# cat /proc/41986/uid_map
         0          0 4294967295

这三列分别表示:

  • ID-inside-ns:docker里面的ID
  • ID-outside-ns:docker映射宿主机的ID
  • length:长度范围

上面的结果的意思即为:dockerrootdocker-ns主机的root进行了映射;

Network Namespace

单NS的情况

宿主机创建一个网络namespace

ip netns add nstest

添加一对“网线”

ip link add veth-a type veth peer name veth-b

veth-a添加IP并启动

ip addr add  10.0.0.1/24 dev veth-a
ip link set dev veth-a up

veth-b放入nstest当中,然后设置IP并启动

ip link set veth-b netns nstest
ip netns exec nstest ip addr add  10.0.0.2/24 dev veth-b
ip netns exec nstest ip link set dev veth-b up

添加完成后,测试ping

# veth-a
ping 10.0.0.2
# veth-b
ip netns exec nstest ping 10.0.0.1

veth-b的操作前面都需要添加ip netns exec这样的命令,我们也可以进入一个bashnetns当中

# 前提是已经将veth-b添加至nstest当中
ip link set veth-b netns nstest
# 进入netns里面
ip netns exec nstest  bash

此时进入了一个netns环境当中,输入exit即可退出,我们可以在这里面对veth-b进行同样的操作;

ip addr add  10.0.0.2/24 dev veth-b

查看一下link状态,测试是否与veth-a想通;

[root@CloudBoot-dev-2-103 ~]# ip link list
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
16: veth-b@if17: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether ca:31:5f:1d:00:10 brd ff:ff:ff:ff:ff:ff link-netnsid 0
[root@CloudBoot-dev-2-103 ~]# ping 10.0.0.1
PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.071 ms

清空ns

# 删除netns,同时会删除veth-b
ip netns del nstest
# 删除veth-a
ip link del veth-a

两个NS怎么玩呢?

创建两个ns

[root@CloudBoot-dev-2-103 ~]# ip netns add ns1
[root@CloudBoot-dev-2-103 ~]# ip netns add ns2

创建一根“网线”, veth-pairs

[root@CloudBoot-dev-2-103 ~]# ip link add veth-a type veth  peer name veth-b

ns1当中创建veth-a,在ns2当中创建veth-b

[root@CloudBoot-dev-2-103 ~]# ip link set veth-a netns ns1
[root@CloudBoot-dev-2-103 ~]# ip link set veth-b netns ns2

启动

[root@CloudBoot-dev-2-103 ~]# ip netns exec ns1 ip link set dev veth-a up
[root@CloudBoot-dev-2-103 ~]# ip netns exec ns2 ip link set dev veth-b up

配置IP

[root@CloudBoot-dev-2-103 ~]# ip netns exec ns1 ip addr add 10.0.0.1/24 dev veth-a
[root@CloudBoot-dev-2-103 ~]# ip netns exec ns2 ip addr add 10.0.0.2/24 dev veth-b

测试

[root@CloudBoot-dev-2-103 ~]# ip netns exec ns1 ping 10.0.0.2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.073 ms
......
[root@CloudBoot-dev-2-103 ~]# ip netns exec ns2 ping 10.0.0.1
PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.061 ms
64 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.077 ms