【docker 底层知识】Linux 内核namespace 原理 - Go语言中文社区

【docker 底层知识】Linux 内核namespace 原理


 

mount namespace

      隔离文件系统挂载点对隔离文件系统提供支持,/proc/{pid}/mounts,/proc/{pid}/mountstats查看文件设备统计信息,包括挂载文件的名字,文件系统类型,挂载位置等。进程在创建mount namespace时,会把当前的文件结构复制给新的namespace,新namespace中的所有mount操作都只影响自身的文件系统

 

network namespace

    网络资源的隔离,包括网络设备,协议栈,IP路由表,防火墙 /proc/net  /sys/class/net  socket,一个物理网络设备最多存在于一个network namespace中

    docker使用veth pair,一端绑定docker0,另一端绑定network namespace进程中,

 

1.  Linux内核namespace机制

Linux Namespaces机制提供一种资源隔离方案。PID,IPC,Network等系统资源不再是全局性的,而是属于某个特定的Namespace。每个namespace下的资源对于其他namespace下的资源都是透明,不可见的。系统中可以同时存在两个进程号为0,1,2的进程,由于属于不同的namespace,所以它们之间并不冲突。而在用户层面上只能看到属于用户自己namespace下的资源,例如使用ps命令只能列出自己namespace下的进程。

 

2 .  Linux内核中namespace结构体

在Linux内核中,一个进程可以属于多个namesapce,既然namespace和进程相关,那么在task_struct结构体中就会包含和namespace相关联的变量

在task_struct 结构中有一个指向namespace结构体的指针nsproxy。

struct task_struct {

……..

/* namespaces */

         struct nsproxy *nsproxy;

…….

}

定义了5个命名空间结构体,多个进程可以使用同一个namespace,所以nsproxy可以共享使用,count字段是该结构的引用计数

  • UTS: 运行内核的名称、版本、底层体系结构类型等信息(UNIX Timesharing System)
  • IPC: 与进程间通信(IPC)有关
  • MNT: 已经装载的文件系统的视图
  • PID:有关进程ID的信息
  • NET:网络相关的命名空间参数
struct nsproxy {

         atomic_t count;

         struct uts_namespace *uts_ns;

         struct ipc_namespace *ipc_ns;

         struct mnt_namespace *mnt_ns;

         struct pid_namespace *pid_ns_for_children;

         struct net             *net_ns;

};

系统中有一个默认的nsproxy,init_nsproxy,该结构在task初始化是也会被初始化。

#define INIT_TASK(tsk)  

{

         .nsproxy   = &init_nsproxy,      

}

static struct kmem_cache *nsproxy_cachep;

struct nsproxy init_nsproxy = {

         .count                         = ATOMIC_INIT(1),

         .uts_ns                       = &init_uts_ns,

#if defined(CONFIG_POSIX_MQUEUE) || defined(CONFIG_SYSVIPC)

         .ipc_ns                        = &init_ipc_ns,

#endif

         .mnt_ns                      = NULL,

         .pid_ns_for_children        = &init_pid_ns,

#ifdef CONFIG_NET

         .net_ns                       = &init_net,

#endif

};

 

3. clone创建Namespace

可以使用系统调用clone()创建命名空间

  • fn:子进程运行的程序主函数 
  • child_stack:子进程使用的栈空间 
  • flags:标志位,与namespace相关的标志位主要包括CLONE_NEWIPC、CLONE_NEWPID、CLONE_NEWNS、CLONE_NEWNET、CLONE_USER、CLONE_UTS
  • arg:用户参数,传给子进程的参数也就是fn指向的函数参数
       int clone(int (*fn)(void *), void *child_stack,
                 int flags, void *arg, ...
                 /* pid_t *ptid, void *newtls, pid_t *ctid */ );

     flags namespace相关的参数如下

  • CLONE_FS:子进程与父进程共享相同的文件系统,包括root、当前目录、umask
  • CLONE_NEWNS:当clone需要自己的命名空间时设置这个标志

 

       Linux内核中看到的实现函数,是经过libc库进行封装过的,在Linux内核中的fork.c文件中,有下面的定义,最终调用的都是do_fork()函数

#ifdef __ARCH_WANT_SYS_CLONE

#ifdef CONFIG_CLONE_BACKWARDS

SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,

                    int __user *, parent_tidptr,

                    int, tls_val,

                    int __user *, child_tidptr)

#elif defined(CONFIG_CLONE_BACKWARDS2)

SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,

                    int __user *, parent_tidptr,

                    int __user *, child_tidptr,

                    int, tls_val)

#elif defined(CONFIG_CLONE_BACKWARDS3)

SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,

                   int, stack_size,

                   int __user *, parent_tidptr,

                   int __user *, child_tidptr,

                   int, tls_val)

#else

SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,

                    int __user *, parent_tidptr,

                    int __user *, child_tidptr,

                    int, tls_val)

#endif

{

         return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);

}

#endif

  3.1  do_fork函数

         clone()函数中调用do_forkdo_fork函数中调用copy_process

long do_fork(unsigned long clone_flags,

               unsigned long stack_start,

               unsigned long stack_size,

               int __user *parent_tidptr,

               int __user *child_tidptr)

{

         struct task_struct *p;

         int trace = 0;

         long nr;



         /*

           * Determine whether and which event to report to ptracer.  When

           * called from kernel_thread or CLONE_UNTRACED is explicitly

           * requested, no event is reported; otherwise, report if the event

          * for the type of forking is enabled.

          */

         if (!(clone_flags & CLONE_UNTRACED)) {

                   if (clone_flags & CLONE_VFORK)

                           trace = PTRACE_EVENT_VFORK;

                  else if ((clone_flags & CSIGNAL) != SIGCHLD)

                            trace = PTRACE_EVENT_CLONE;

                   else

                            trace = PTRACE_EVENT_FORK;



                   if (likely(!ptrace_event_enabled(current, trace)))

                           trace = 0;

         }



          p = copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace);

         /*

          * Do this prior waking up the new thread - the thread pointer

           * might get invalid after that point, if the thread exits quickly.

           */

         if (!IS_ERR(p)) {

                   struct completion vfork;

                   struct pid *pid;



                   trace_sched_process_fork(current, p);



                   pid = get_task_pid(p, PIDTYPE_PID);

                  nr = pid_vnr(pid);



                   if (clone_flags & CLONE_PARENT_SETTID)

                            put_user(nr, parent_tidptr);



                   if (clone_flags & CLONE_VFORK) {

                            p->vfork_done = &vfork;

                            init_completion(&vfork);

                            get_task_struct(p);

                   }



                   wake_up_new_task(p);



                   /* forking complete and child started to run, tell ptracer */

                   if (unlikely(trace))

                            ptrace_event_pid(trace, pid);



                   if (clone_flags & CLONE_VFORK) {

                            if (!wait_for_vfork_done(p, &vfork))

                                     ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);

                   }



                  put_pid(pid);

         } else {

                   nr = PTR_ERR(p);

         }

         return nr;

}

  3.2  copy_process函数

在copy_process函数中调用copy_namespaces函数

static struct task_struct *copy_process(unsigned long clone_flags,

                                               unsigned long stack_start,

                                               unsigned long stack_size,

                                               int __user *child_tidptr,

                                               struct pid *pid,

                                               int trace)

{

          int retval;

          struct task_struct *p;

clone_flag标志进行检查,有部分表示是互斥的,例如CLONE_NEWNSCLONENEW_FS     

     if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))

                   return ERR_PTR(-EINVAL);



          if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))

                   return ERR_PTR(-EINVAL);



          if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))

                   return ERR_PTR(-EINVAL);



          if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))

                   return ERR_PTR(-EINVAL);



          if ((clone_flags & CLONE_PARENT) &&

                                      current->signal->flags & SIGNAL_UNKILLABLE)

                   return ERR_PTR(-EINVAL);


retval = copy_namespaces(clone_flags, p);

          if (retval)

                   goto bad_fork_cleanup_mm;

          retval = copy_io(clone_flags, p);

          if (retval)

                   goto bad_fork_cleanup_namespaces;

          retval = copy_thread(clone_flags, stack_start, stack_size, p);

          if (retval)

                   goto bad_fork_cleanup_io;

 

do_fork中调用copy_process函数,该函数中pid参数为NULL,所以这里的if判断是成立的。为进程所在的namespace分配pid,在3.0的内核之前还有一个关键函数,就是namespace创建后和cgroup的关系,

if (current->nsproxy != p->nsproxy) {

retval = ns_cgroup_clone(p, pid);

if (retval)

goto bad_fork_free_pid;

但在3.0内核以后给删掉了,具体请参考remove the ns_cgroup*/

          if (pid != &init_struct_pid) {

                   retval = -ENOMEM;

                   pid = alloc_pid(p->nsproxy->pid_ns_for_children);

                   if (!pid)

                            goto bad_fork_cleanup_io;

          }…..

}

  3.3  copy_namespaces 函数

       在kernel/nsproxy.c文件中定义了copy_namespaces函数。

        int copy_namespaces(unsigned long flags, struct task_struct *tsk)

       {

                struct nsproxy *old_ns = tsk->nsproxy;

                struct user_namespace *user_ns = task_cred_xxx(tsk, user_ns);

                struct nsproxy *new_ns;

               /*首先检查flag,如果flag标志不是下面的五种之一,就会调用get_nsproxyold_ns递减引用计数,然后直接返回0*/

               if (likely(!(flags & (CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC |

                                  CLONE_NEWPID | CLONE_NEWNET)))) {

                           get_nsproxy(old_ns);

                           return 0;

                }

               /*当前进程是否有超级用户的权限*/

               if (!ns_capable(user_ns, CAP_SYS_ADMIN))

                         return -EPERM;

 

                /*

                * CLONE_NEWIPC must detach from the undolist: after switching

                * to a new ipc namespace, the semaphore arrays from the old

                * namespace are unreachable.  In clone parlance, CLONE_SYSVSEM

                * means share undolist with parent, so we must forbid using

                * it along with CLONE_NEWIPC.

                CLONE_NEWIPC进行特殊的判断,*/

                if ((flags & (CLONE_NEWIPC | CLONE_SYSVSEM)) ==

                        (CLONE_NEWIPC | CLONE_SYSVSEM))

                        return -EINVAL;

                /*为进程创建新的namespace*/

                new_ns =create_new_namespaces(flags, tsk, user_ns, tsk->fs);

                if (IS_ERR(new_ns))

                         return  PTR_ERR(new_ns);

 

                tsk->nsproxy = new_ns;

               return 0;

        }

  3.4  create_new_namespaces函数

      create_nsproxy函数为新的nsproxy分配内存空间,并对其引用计数设置为初始1

      static struct nsproxy *create_new_namespaces(unsigned long flags,

                struct task_struct *tsk, struct user_namespace *user_ns,

                struct fs_struct *new_fs)

       {

                struct nsproxy *new_nsp;

                int err;

                new_nsp = create_nsproxy();

                if (!new_nsp)

                          return ERR_PTR(-ENOMEM);

               new_nsp->mnt_ns = copy_mnt_ns(flags, tsk->nsproxy->mnt_ns, user_ns, new_fs);

               if (IS_ERR(new_nsp->mnt_ns)) {

                         err = PTR_ERR(new_nsp->mnt_ns);

                        goto out_ns;

               }



               new_nsp->uts_ns = copy_utsname(flags, user_ns, tsk->nsproxy->uts_ns);

               if (IS_ERR(new_nsp->uts_ns)) {

                        err = PTR_ERR(new_nsp->uts_ns);

                        goto out_uts;

               }



         new_nsp->ipc_ns = copy_ipcs(flags, user_ns, tsk->nsproxy->ipc_ns);

         if (IS_ERR(new_nsp->ipc_ns)) {

                   err = PTR_ERR(new_nsp->ipc_ns);

                   goto out_ipc;

         }



         new_nsp->pid_ns_for_children =

                   copy_pid_ns(flags, user_ns, tsk->nsproxy->pid_ns_for_children);

         if (IS_ERR(new_nsp->pid_ns_for_children)) {

                   err = PTR_ERR(new_nsp->pid_ns_for_children);

                   goto out_pid;

         }

 

  3.4.1 create_nsproxy函数

static inline struct nsproxy *create_nsproxy(void)

{

         struct nsproxy *nsproxy;



         nsproxy = kmem_cache_alloc(nsproxy_cachep, GFP_KERNEL);

         if (nsproxy)

                   atomic_set(&nsproxy->count, 1);

         return nsproxy;

}

 

用户态namespace API

    创建容器(进程)主要用到三个系统调用:

  • clone() – 实现线程的系统调用,用来创建一个新的进程,并可以通过上述参数达到隔离
  • unshare() – 使某进程脱离某个namespace
  • setns() – 把某进程加入到某个namespace

  1. clone()

    功能是创建一个新的进程,创建一个新的namespace的进程(Docker使用namespace的方法)

  • fn:子进程运行的程序主函数 
  • child_stack:子进程使用的栈空间 
  • flags:标志位,与namespace相关的标志位主要包括CLONE_NEWIPC、CLONE_NEWPID、CLONE_NEWNS、CLONE_NEWNET、CLONE_USER、CLONE_UTS
  • arg:用户参数
NAME
       clone, __clone2 - create a child process

SYNOPSIS
       /* Prototype for the glibc wrapper function */

       #define _GNU_SOURCE
       #include <sched.h>

       int clone(int (*fn)(void *), void *child_stack,
                 int flags, void *arg, ...
                 /* pid_t *ptid, void *newtls, pid_t *ctid */ );

       /* For the prototype of the raw system call, see NOTES */

 

  2. setns()

    将进程加入到一个已经存在的namespace中,在一个Docker容器中用exec运行一个新命令,就是将该命令在该容器的namespace中运行

  • fd:加入的namespace的文件描述符。一个指向/proc/[pid]/ns目录的文件描述符,可以通过直接打开该目录下的链接得到 
  • nstype:让调用者可以检查fd指向的namespace类型是否符合实际的要求。参数为0表示不检查
NAME
       setns - reassociate thread with a namespace

SYNOPSIS
       #define _GNU_SOURCE             /* See feature_test_macros(7) */
       #include <sched.h>

       int setns(int fd, int nstype);

 

  3. unshare()

    unshare()是在原进程上作隔离,参数flags是标志位,选择需要隔离的资源。与clone()的falgs参数基本相同

NAME
       unshare - run program with some namespaces unshared from parent

SYNOPSIS
       unshare [options] program [arguments]

 

  PID namespace示例

    pid namespace的flag为CLONE_NEWPID

#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("子进程begin...n");
        execv(child_args[0],child_args);
        return 1;
}

int main() {
        printf("主程序begin...:n");
        int child_pid = clone(child_main, child_stack + STACK_SIZE,  CLONE_NEWPID|SIGCHLD, NULL);
        waitpid(child_pid, NULL, 0);
        printf("主程序退出...n");
        return 0;
}

    编译并运行如下,可以看到使用clone()创建了一个进程并进行隔离,当前进程的pid为1,但是ps可以看到一大堆进程,理论上是不应该看到的,因为ps命令或者top命令都是从Linux系统中的/proc目录下取值的

 

  IPC namespace示例

    pid namespace的flag为CLONE_NEWIPC

#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,  CLONE_NEWIPC|SIGCHLD, NULL);
        waitpid(child_pid, NULL, 0);
        printf("主程序已退出...n");
        return 0;
}

     使用ipcmk -Q创建了一个消息队列,clone()创建了一个新进程。使用ipcs -q命令查看该namespace下的消息队列,创建的消息队列未在该namespace。说明了IPC namespace将进程间通信消息队列隔离了

 

  UTS namespace示例

    pid namespace的flag为CLONE_NEWUTS

#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("IamZhu", 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;
}

    编译并运行如下,主机名改了,说明新的UTS namespace下,主机名被隔离了。每个容器拥有自己独立的主机名和域名

 

NET namespace示例

    pid namespace的flag为CLONE_NEWNET

#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,  CLONE_NEWNET|SIGCHLD, NULL);
        waitpid(child_pid, NULL, 0);
        printf("主程序已退出...n");
        return 0;
}

    编译运行如下,可以看到该namespace下只有loopback网卡,可以在该namespace添加网络设备,建立veth pair,设置ip路由等操作

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/zhonglinzhang/article/details/64441263
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2021-06-13 17:57:00
  • 阅读 ( 1048 )
  • 分类:Linux

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢