热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

【docker底层知识】Linux内核namespace原理,Go语言社区,Golang程序员人脉社

 
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
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
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
#include
#include
#include
#include
#include
#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
#include
#include
#include
#include
#include
#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
#include
#include
#include
#include
#include
#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
#include
#include
#include
#include
#include
#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路由等操作




推荐阅读
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • http:my.oschina.netleejun2005blog136820刚看到群里又有同学在说HTTP协议下的Get请求参数长度是有大小限制的,最大不能超过XX ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • 本文讨论了在openwrt-17.01版本中,mt7628设备上初始化启动时eth0的mac地址总是随机生成的问题。每次随机生成的eth0的mac地址都会写到/sys/class/net/eth0/address目录下,而openwrt-17.01原版的SDK会根据随机生成的eth0的mac地址再生成eth0.1、eth0.2等,生成后的mac地址会保存在/etc/config/network下。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • 本文介绍了django中视图函数的使用方法,包括如何接收Web请求并返回Web响应,以及如何处理GET请求和POST请求。同时还介绍了urls.py和views.py文件的配置方式。 ... [详细]
  • Imtryingtofigureoutawaytogeneratetorrentfilesfromabucket,usingtheAWSSDKforGo.我正 ... [详细]
  • 本文介绍了在CentOS上安装Python2.7.2的详细步骤,包括下载、解压、编译和安装等操作。同时提供了一些注意事项,以及测试安装是否成功的方法。 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • SpringBoot整合SpringSecurity+JWT实现单点登录
    SpringBoot整合SpringSecurity+JWT实现单点登录,Go语言社区,Golang程序员人脉社 ... [详细]
  • 本文探讨了容器技术在安全方面面临的挑战,并提出了相应的解决方案。多租户保护、用户访问控制、中毒的镜像、验证和加密、容器守护以及容器监控都是容器技术中需要关注的安全问题。通过在虚拟机中运行容器、限制特权升级、使用受信任的镜像库、进行验证和加密、限制容器守护进程的访问以及监控容器栈,可以提高容器技术的安全性。未来,随着容器技术的发展,还需解决诸如硬件支持、软件定义基础设施集成等挑战。 ... [详细]
  • RabbitMq之发布确认高级部分1.为什么会需要发布确认高级部分?在生产环境中由于一些不明原因,导致rabbitmq重启,在RabbitMQ重启期间生产者消息投递失败,导致消息丢 ... [详细]
  • 本文介绍了如何清除Eclipse中SVN用户的设置。首先需要查看使用的SVN接口,然后根据接口类型找到相应的目录并删除相关文件。最后使用SVN更新或提交来应用更改。 ... [详细]
author-avatar
1994-MMMs
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有