一文彻底搞懂Docker中的namespace

Posted 神技圈子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文彻底搞懂Docker中的namespace相关的知识,希望对你有一定的参考价值。

什么是namespace

namespace是对全局系统资源的一种封装隔离。这样可以让不同namespace的进程拥有独立的全局系统资源。这样改变一个namespace的系统资源只会影响当前namespace中的进程,对其它namespace中的资源没有影响。以前Linux也有一个。之前有一个系统调用chroot和namespace类似。

namespace分类

chroot内部不能访问外部的内容,namespce在此基础上提供了UTS、IPC、mount、PID、network、User等隔离机制。

分类参数隔离内容
Mount namespacesCLONE_NEWNSMount points (since Linux 2.4.19)
UTS namespacesCLONE_NEWUTSHostname and NIS domain name (since Linux 2.6.19)
IPC namespacesCLONE_NEWIPCSystem V IPC, POSIX message queues (since Linux 2.6.19)
PID namespacesCLONE_NEWPIDProcess IDs (since Linux 2.6.24)
Network namespacesCLONE_NEWNETNetwork devices, stacks, ports, etc. (since Linux 2.6.24)
User namespacesCLONE_NEWUSERUser and group IDs (started in Linux 2.6.23 and completed in Linux 3.8)

查看进程所属的namespace

Linux中每个进程都有一个/proc/[pid]/ns这样一个目录,这里面包含了该进程所属的namespace信息。如下图所示

从结果可以看到,对于每种类型的namespace,进程都会同一个namespace ID 相关联。
就ipc这个namespace类型来说,ipc -> ipc:[4026532560]中,ipc是namespace类型,4026532560是inode number。假设两个进程的ipc namespace下的inode number一样,说明这两个进程属于同一个namespace。对于其它的namespace类型也一样。

相关系统调用

clone

clone调用主要创建一个新进程并放入到namespace中。

int clone(int (*child_func)(void *), void *child_stack
            , int flags, void *arg);

flags: 
    指定一个或者多个上面的CLONE_NEW*(当然也可以包含跟namespace无关的flags), 
    这样就会创建一个或多个新的不同类型的namespace, 
    并把新创建的子进程加入新创建的这些namespace中。

setns

setns调用将当前进程加入到已有的namespace中

int setns(int fd, int nstype);

fd: 
    指向/proc/[pid]/ns/目录里相应namespace对应的文件,
    表示要加入哪个namespace

nstype:
    指定namespace的类型(上面的任意一个CLONE_NEW*):
    1. 如果当前进程不能根据fd得到它的类型,如fd由其他进程创建,
    并通过UNIX domain socket传给当前进程,
    那么就需要通过nstype来指定fd指向的namespace的类型
    2. 如果进程能根据fd得到namespace类型,比如这个fd是由当前进程打开的,
    那么nstype设置为0即可

unshare

使当前进程退出指定的namespace,并加入到新创建的namespace中。

int unshare(int flags);

flags:
    指定一个或者多个上面的CLONE_NEW*,
    这样当前进程就退出了当前指定类型的namespace并加入到新创建的namespace

unshare和clone都是创建并加入到namespace,它们的区别是clone是将本进程写入到namespace,而unshare是创建子进程让子进程。

UTS namespace

UTS namespace 是用来隔离系统的hostname以及NIS domain name。UTS的由来就是struct utsname结构体。hostname和domainname可以通过sethostname和setdomainname系统函数来设置,以及gethostname和getdomainname函数来获取。

#include <iostream>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

using namespace std;

#define STACK_SIZE (1024 * 1024)

static char child_stack[STACK_SIZE];

char *const child_args[] = "/bin/bash", NULL;

int ChildFunc(void *arg)

   cout << "Container - inside the container, it's pid is "  <<  getpid() << endl;
   ::sethostname("container_001", 13);
   execv(child_args[0],child_args);
   return 1;


#define NO_EXIT(code, msg); if(code==-1)perror(msg); exit(-1);

int main()

   cout << "parent pid is: " << getpid() << endl;
   pid_t child_pid = ::clone(ChildFunc, child_stack+STACK_SIZE, CLONE_NEWUTS|SIGCHLD, NULL);

  NO_EXIT(child_pid, "clone");

  ::waitpid(child_pid, nullptr, 0);
  return 0;   

编译后运行结果如下

可以看到父进程和子进程不在同一个UTS namespace。

再来开一个将当前进程加入到namespace的例子

#include <iostream>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>

using namespace std;


#define NO_EXIT(code, msg); if(code==-1)perror(msg); exit(-1);

int main(int argc, char **argv)

   //获取namespace对应的fd   
   int fd = open(argv[1], O_RDONLY);

  NO_EXIT(fd, "open");

  int ret =setns(fd, 0);

  NO_EXIT(ret, "setns");
  
  execv("/bin/bash", NULL);
  return 0;   

重新打开个shell窗口看下hostname

在上一个程序的shell窗口查看namespace的inode number(应用程序和其所属的namespace在同一namespace)。

继续回到第二个窗口运行程序,可以看到bash提示符里面的hostname以及UTS namespace的inode number和第一个shell窗口的都一样

IPC namespace

IPC全称 Inter-Process Communication,是Unix/Linux下进程间通信的一种方式,IPC有共享内存、信号量、消息队列等方法。所以,为了隔离,我们也需要把IPC给隔离开来,这样,只有在同一个Namespace下的进程才能相互通信。要启动IPC隔离,我们只需要在调用clone时加上CLONE_NEWIPC参数就可以了。
下面来看一个例子
首先创建一个Queue,执行ipcmk创建队列,并用ipcs查看结果。正常情况下子进程也能看到该队列。
这个两个命令解释如下

  • ipcmk :创建shared memory segments, message queues, 和semaphore arrays
  • ipcs : 查看shared memory segments, message queues, 和semaphore arrays的相关信息

    对创建UTS程序进行修改,添加CLONE_NEWIPC参数

    运行结果如下:

运行程序后可以看到IPC已经被隔离

PID namespace

PID namespaces用来隔离进程的ID空间,使得不同pid namespace里的进程ID可以重复且相互之间不影响。
PID namespace可以嵌套,也就是说有父子关系,当前创建的所有新namespace都是当前namespace的子namespace,父namespace可以看到所有孙子的namespace中的进程信息。
继续来看一个例子吧:

#include <iostream>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

using namespace std;

#define STACK_SIZE (1024 * 1024)

static char child_stack[STACK_SIZE];

char *const child_args[] = "/bin/bash", NULL;

int ChildFunc(void *arg)

   cout << "Container - inside the container, it's pid is "  <<  getpid() << endl;
   ::sethostname("container_001", 13);
   execv(child_args[0],child_args);
   return 1;


#define NO_EXIT(code, msg); if(code==-1)perror(msg); exit(-1);

int main()

//添加了CLONE_NEWPID参数
   pid_t child_pid = ::clone(ChildFunc, child_stack+STACK_SIZE, CLONE_NEWUTS|CLONE_NEWPID|SIGCHLD, NULL);

  NO_EXIT(child_pid, "clone");

  ::waitpid(child_pid, nullptr, 0);
  return 0;   

运行结果如下

结果打印出来看到PID为1,1就是init进程,它是所有进程的父进程。如果子进程脱离了父进程,那么init就会负责回收资源并结束这个子进程。所以,要做到进程隔离,首先就得创建PID为1的进程(类似chroot)。但是这并没有完全隔离,比如/proc 文件系统父子进程还是一样的。

mount namespace

Mount namespace用来隔离文件系统的挂载点, 使得不同的mount namespace拥有自己独立的挂载点信息,不同的namespace之间不会相互影响,这对于构建用户或者容器自己的文件系统目录非常有用。
Mount namespace是第一个被加入Linux的namespace,由于当时没想到还会引入其它的namespace,所以取名为CLONE_NEWNS,而没有叫CLONE_NEWMOUNT。
下面例子继续对示例代码进行修改

#include <iostream>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

using namespace std;

#define STACK_SIZE (1024 * 1024)

static char child_stack[STACK_SIZE];

char *const child_args[] = "/bin/bash", NULL;

int ChildFunc(void *arg)

   cout << "Container - inside the container, it's pid is "  <<  getpid() << endl;
   ::sethostname("container_001", 13);
   system("mount -t proc proc /proc");
   execv(child_args[0],child_args);
   return 1;


#define NO_EXIT(code, msg); if(code==-1)perror(msg); exit(-1);

int main()

   cout << "parent pid is: " << getpid() << endl;
   pid_t child_pid = ::clone(ChildFunc, child_stack+STACK_SIZE, CLONE_NEWUTS|CLONE_NEWPID|CLONE_NEWNS|SIGCHLD, NULL);

  NO_EXIT(child_pid, "clone");

  ::waitpid(child_pid, nullptr, 0);
  return 0;   



可以看到程序运行后用ps查看只有两个进程了,而进程1就是/bin/bash进程。子进程中新namespace的所有mount操作都只影响自身的文件系统,做到了真正意义上的隔离。

以上是关于一文彻底搞懂Docker中的namespace的主要内容,如果未能解决你的问题,请参考以下文章

一文彻底搞懂Docker中的namespace

一文带你彻底搞懂Docker中的cgroup

一文带你彻底搞懂Docker中的cgroup

一文带你彻底搞懂Docker中的cgroup

一文彻底搞懂前端沙箱

一文彻底搞懂SLAM技术