docker 深入理解之namespace

namespace 名称空间

docker容器主要通过资源隔离来实现的,应该具有的6种资源隔

namespace 的六项隔离

namespace 系统调用参数 隔离的内容
UTS CLONE_NEWUTS 主机名域名
IPC CLONE_NEWIPC 信号量、消息队列与共享内存
PID CLONE_NEWPID 进程编号
Network CLONE_NEWNET 网络设备、网络栈、端口等
MOUNT CLONE_NEWNS 挂载点
USER CLONE_NEWUSER 用户与组

namespace API 操作4种方式包括clone(),setns(),unshare()以及/proc下部分文件

1.通过clone()在创建进程的同时创建namefspace,clone()实际是Linux系统调用fork()的一种更用的实现方式,它可以通过flags开控制使用多少功能。一共有20多种CLON_*的flag(标志位)参数来控制进程的方方面面(如是否与父进程共享虚拟内存等)

*1 child_func 传入子进程运行的程序主函数

*2 child_stack 传入子进程使用的栈空间

*3 args 则用于传入用户参数

查看/proc/pid/ns文件

# ls -l /proc/664/ns/
总用量 0
lrwxrwxrwx. 1 root root 0 3月   1 08:05 ipc -> ipc:[4026531839]
lrwxrwxrwx. 1 root root 0 3月   1 08:05 mnt -> mnt:[4026532432]
lrwxrwxrwx. 1 root root 0 3月   1 08:05 net -> net:[4026531956]
lrwxrwxrwx. 1 root root 0 3月   1 08:05 pid -> pid:[4026531836]
lrwxrwxrwx. 1 root root 0 3月   1 08:05 user -> user:[4026531837]
lrwxrwxrwx. 1 root root 0 3月   1 08:05 uts -> uts:[4026531838]

  如果两个进程执行的namespace编号一样,说明他们在同一个namespace下,否则就会创建不同的namespace里面./proc/pid/ns 里设置这些link的另一个作用,一旦上述link文件被打开,只要打开的文件描述符(fd)存在,那么就是该namesfpace下所有进程都结束,这个namespace也会一直存在,后续进程可以加进来。在docker中,通过文件描述符定位和加入一个已存在的namespace是最基本的方式。

    另外把/proc/[pid]/ns 目录文件使用 --bind 方式挂起来可以起到同样的作用

# touch  ~/cx
# mount --bind /proc/591/ns/uts ~/cx 

通过setns()加入一个已经存在的namespace

  进程在结束的情况下,也可以通过挂载的形式把namesfpace保留下来,保留下来的目的是为了后续进程加入做准备,在docker中 使用docker exec 命令在已经运行着的容器中执行一个新命令,就需要用到该方法。通过setens()系统调用,进程从原来的namesfpace加入某个已存在的namesfpace,使用该方法,通常不影响进程的调用者,也为新加入的pid namespace 生效,会在setns() 函数执行后使用clone()创建子进程执行命令,让原先的进程结束运行

int setns(int fd, int nstype);

 *参数fd表示要加入的namespace的文件描述符,它是指向/proc/[pid]/ns目录的文件描述符,可以通过直接打开该目录连接或者打开一个挂载了该目录下链接的文件得到

   *参数nytype表示让调用者可以检查fd指向的namespace类型是否符合实际要求,该参数为0表示不检查

为了把新加入的namespace利用起来,需要引进execve()系列函数,此函数可以执行用户命令,最常见的就是调用/bin/bash并接受参数,运行起一个shll

fd = open(argv[1], O_RDONLY);      获取namespace的文件描述符
setns(fd, 0);   加入新的namespace
execvp(argv[2], &argv[2]);   执行程序 

  编译后程序为setns-test,加入到namespace中这些shll命令了

# ./setns-test ~/cx  /bin/bash  

  通过unshare()在原先的进程上进行namespace隔离

       注意系统调用是unshare,它与clone很像,不同的是,unshare是运行在原先的进程上,不需要启动新的进程

int unshare(int flage);

  调用unshare()的主要作用就是,不启动新的进程就可以起到隔离效果,相当于跳出原先的namespace进行操作。这样可以在原先的进程进行一些隔离的操作。Linux自带的unshare命令,就是通过unshare()系统调

用实现的。docker目前没有使用

      fork()系统调用

    系统调用函数fork()并不属于namespace的API。当程序调用fork()函数是时,系统会创建新的进程,为其分配资源,像存储数据和代码的空间,然后把原来进程的所有值都复制到新的进程中,只有少量数值与原来的进程值不同,相当于复制了本身。fork()的神奇之处在于它不仅仅被调用一次,却能返回两次(父子进程各一次),通过返回值不同就可以区分父进程与子进程,可能会有3中返回值

*在父进程中,fork()返回新创建子进程的ID

*在子进程中,fork()返回0

*如果出现错误,fork()返回一个负值

[root@mast ~]# vim cx.c
#include <unistd.h>
#include <stdio.h>
int main (){
        pid_t fpid;
        fpid=fork();
        if (fpid < 0)printf("error in fork!");
        else if (fpid==0) {
            printf("I am child. process id is %d\n",getpid());
        }
        else {
            printf("i am parent. process id is %d\n",getpid());
        }
        return 0;
        }
[root@mast ~]# gcc -Wall cx.c && ./a.out
i am parent. process id is 23877
I am child. process id is 23878

  使用fork()后,父进程有义务监控子进程的运行状态,并在子进程退出后自己才能真正退出,否则子进程就会成为孤儿进程

UTS namespace

     UTS namespace提供了主机名和域的隔离,这样docker容器就有独立的主机名和域名了,在网络中视为一个独立节点,而非宿主机上一个进程,docker中,每个镜像本身基本都以自身所提供的服务名称来命名镜像

的hostname 

[root@mast ~]# cat uts.c 
#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 child_stack[STACK_SIZE];
char* const child_args[] = {
     "/bin/bash",
     NULL
};

int child_main(void* args) {
     printf("在子进程中!\n");
     execv(child_args[0], child_args);
     return 1;
}

int main(){
     printf("程序开始:\n");
     int child_pid = clone(child_main, child_stack + STACK_SIZE, SIGCHLD, NULL);
     waitpid(child_pid, NULL, 0);
     printf("已退出\n");
     return 0;
}
[root@mast ~]#  gcc -Wall uts.c -o uts.o && ./uts.o
程序开始:
在子进程中!
[root@mast ~]# exit
exit
已退出

  修改代码,加入UTS隔离,运行代需要root权限,以防止普通用户任意修改主机名导致set-user-ID相关的应用运行出错

[root@mast ~]# cat uts.c 
#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 child_stack[STACK_SIZE];
char* const child_args[] = {
     "/bin/bash",
     NULL
};

int child_main(void* args) {
     printf("在子进程中!\n");
     sethostname("network",12);
     execv(child_args[0], child_args);
     return 1;
}

int main(){
     printf("程序开始:\n");
     int child_pid = clone(child_main, child_stack + STACK_SIZE, CLONE_NEWUTS | SIGCHLD, NULL);
     waitpid(child_pid, NULL, 0);
     printf("已退出\n");
     return 0;
}
[root@mast ~]#  gcc -Wall uts.c -o uts.o && ./uts.o
程序开始:
在子进程中!
[root@network ~]# hostname
network
[root@network ~]# exit
exit
已退出
[root@mast ~]# hostname
mast

  不加CLONE_NEWUTS,运行查看区别

[root@mast ~]# vim uts.c

#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 child_stack[STACK_SIZE];
char* const child_args[] = {
     "/bin/bash",
     NULL
};

int child_main(void* args) {
     printf("在子进程中!\n");
     sethostname("network",12);
     execv(child_args[0], child_args);
     return 1;
}

int main(){
     printf("程序开始:\n");
     int child_pid = clone(child_main, child_stack + STACK_SIZE, SIGCHLD, NULL);
     waitpid(child_pid, NULL, 0);
     printf("已退出\n");
     return 0;
}
[root@mast ~]#  gcc -Wall uts.c -o uts.o && ./uts.o
程序开始:
在子进程中!
[root@network ~]# hostname
network
[root@network ~]# exit
exit
已退出
[root@mast ~]# hostname
network

  似乎没什么区别,实际上不加CLONE_NEWUTS此参数进行隔离时,由于使用sethostname函数,所有宿主机的主机名被修改,而exit退出后看到的主机名还是原来的主机名,是因为bash只在登录的时候读取一次

UTS,不会实时读取最新主机名,当重新登录或者使用uname进行查看时,就会发生变化

    IPC namespace  的实现

    进程间通信涉及的IPC资源包括常见的信号量,消息队列 和共享内存。申请IPC资源就申请了一个全局唯一的32位id,所以IPC namespace中实际包括了系统的IPC标识符以及实现POSIX消息队列的文件系统。在不同一个IPC namespace下实现的进程互相不可见的

#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 child_stack[STACK_SIZE];
char* const child_args[] = {
     "/bin/bash",
     NULL
};

int child_main(void* args) {
     printf("在子进程中!\n");
     sethostname("net",12);
     execv(child_args[0], child_args);
     return 1;
}

int main(){
     printf("程序开始:\n");
     int child_pid = clone(child_main, child_stack + STACK_SIZE, CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD, NULL);
     waitpid(child_pid, NULL, 0);
     printf("已退出\n");
     return 0;
}
[root@mast ~]# ipcmk -Q
消息队列 id:0
[root@mast ~]# ipcs -q

--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息      
0x54ed33bf 0          root       644        0            0       
[root@mast ~]#  gcc -Wall ipc.c -o ipc.o && ./ipc.o
程序开始:
在子进程中!
[root@net ~]# ipcs -q

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

[root@net ~]# exit
exit
已退出
[root@mast ~]# ipcs -q

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

  目前使用IPC namespace机制的系统不多,比较有名的PostgreSQL。docker也使用的IPC namespace实现了容器与宿主机、容器与容器之间的IPC隔离

PID namespace  的实现

     PID namespace 隔离非常实用,它对进程的PID重新标号,即两个不同的namespace下的进程可以有相同的PID。每个PID namespace 都有自己的计算程序。内核为所有的PID namespace 维护了一个树状的结构

,最顶层是系统初始化时创建的,被称为root namespace  ,而它创建的新的PID namespace 被称为child namespace 树的子节点 而原来的PID namespace就是新建的namespace的父节点。通过这种方式,不同的

PID namespace会形成一个层级结构,所属父节点可以看子节点中的进程,可以通过信号等手段对子节点中的进程产生影响,反之子节点无法看到父节点的 PID namespace 中任何内容

猜你喜欢

转载自www.cnblogs.com/rdchenxi/p/10455831.html