热门标签 | 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路由等操作




推荐阅读
  • 本文详细探讨了Zebra路由软件中的线程机制及其实际应用。通过对Zebra线程模型的深入分析,揭示了其在高效处理网络路由任务中的关键作用。文章还介绍了线程同步与通信机制,以及如何通过优化线程管理提升系统性能。此外,结合具体应用场景,展示了Zebra线程机制在复杂网络环境下的优势和灵活性。 ... [详细]
  • 在过去,我曾使用过自建MySQL服务器中的MyISAM和InnoDB存储引擎(也曾尝试过Memory引擎)。今年初,我开始转向阿里云的关系型数据库服务,并深入研究了其高效的压缩存储引擎TokuDB。TokuDB在数据压缩和处理大规模数据集方面表现出色,显著提升了存储效率和查询性能。通过实际应用,我发现TokuDB不仅能够有效减少存储成本,还能显著提高数据处理速度,特别适用于高并发和大数据量的场景。 ... [详细]
  • 过去查询Mysql的时候,都见3306对所有端口开放着,感觉不安全。netstat -anlp | grep mysqltcp 0&am ... [详细]
  • 本文介绍如何使用OpenCV和线性支持向量机(SVM)模型来开发一个简单的人脸识别系统,特别关注在只有一个用户数据集时的处理方法。 ... [详细]
  • Spring – Bean Life Cycle
    Spring – Bean Life Cycle ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 本文详细解析了 Android 系统启动过程中的核心文件 `init.c`,探讨了其在系统初始化阶段的关键作用。通过对 `init.c` 的源代码进行深入分析,揭示了其如何管理进程、解析配置文件以及执行系统启动脚本。此外,文章还介绍了 `init` 进程的生命周期及其与内核的交互方式,为开发者提供了深入了解 Android 启动机制的宝贵资料。 ... [详细]
  • 深入解析Android 4.4中的Fence机制及其应用
    在Android 4.4中,Fence机制是处理缓冲区交换和同步问题的关键技术。该机制广泛应用于生产者-消费者模式中,确保了不同组件之间高效、安全的数据传输。通过深入解析Fence机制的工作原理和应用场景,本文探讨了其在系统性能优化和资源管理中的重要作用。 ... [详细]
  • MATLAB字典学习工具箱SPAMS:稀疏与字典学习的详细介绍、配置及应用实例
    SPAMS(Sparse Modeling Software)是一个强大的开源优化工具箱,专为解决多种稀疏估计问题而设计。该工具箱基于MATLAB,提供了丰富的算法和函数,适用于字典学习、信号处理和机器学习等领域。本文将详细介绍SPAMS的配置方法、核心功能及其在实际应用中的典型案例,帮助用户更好地理解和使用这一工具箱。 ... [详细]
  • 本文详细介绍了在Linux系统上编译安装MySQL 5.5源码的步骤。首先,通过Yum安装必要的依赖软件包,如GCC、GCC-C++等,确保编译环境的完备。接着,下载并解压MySQL 5.5的源码包,配置编译选项,进行编译和安装。最后,完成安装后,进行基本的配置和启动测试,确保MySQL服务正常运行。 ... [详细]
  • 在 Linux 环境下,多线程编程是实现高效并发处理的重要技术。本文通过具体的实战案例,详细分析了多线程编程的关键技术和常见问题。文章首先介绍了多线程的基本概念和创建方法,然后通过实例代码展示了如何使用 pthreads 库进行线程同步和通信。此外,还探讨了多线程程序中的性能优化技巧和调试方法,为开发者提供了宝贵的实践经验。 ... [详细]
  • 数字图书馆近期展出了一批精选的Linux经典著作,这些书籍虽然部分较为陈旧,但依然具有重要的参考价值。如需转载相关内容,请务必注明来源:小文论坛(http://www.xiaowenbbs.com)。 ... [详细]
  • 本文详细介绍了在Windows XP系统中安装和配置Unix打印服务的方法,以支持远程行式打印机(LPR)功能。对于同时使用Windows 2000 Server打印服务器和Unix打印服务器的网络环境,该指南提供了实用的步骤和配置建议,确保不同平台之间的兼容性和高效打印。 ... [详细]
  • FastDFS Nginx 扩展模块的源代码解析与技术剖析
    FastDFS Nginx 扩展模块的源代码解析与技术剖析 ... [详细]
  • 在PHP的设计中,预定义了9个超级全局变量、8个魔术变量和13个魔术函数,这些变量和函数无需声明即可在脚本的任意位置使用。这些特性在PHP开发中极为常见,能够显著提升开发效率和代码的灵活性。相比之下,Java并没有类似的内置机制,但通过其他方式如上下文对象和反射机制,也可以实现类似的功能。本文将详细探讨这两种语言中这些特殊变量和函数的使用方法及其应用场景。 ... [详细]
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社区 版权所有