namespace命名空间-之使用

文章下载地址    http://download.csdn.net/detail/shichaog/8185583

由于各种原因,用户空间命名空间的实现算是一个里程碑了,首先是历时五年最复杂命名空间之一的user命名空间开发完结,其次是内核绝大多数命名空间已经实现了,当前的命名空间可以说是一个稳定版本了,但这并不意味着命名空间开发工作的完结,新的命名空间可能被添加进内核,也可能需要对当前的命名空间做一些扩展,例如内核日志的隔离。最后,就当前user命名空间的不同使用方法而言,可以说是“游戏规则”的改变:从3.8版本开始,非特权进程可以创建namespace,并且其拥有特权权限,下面将会展示编程中如何使用命名空间的API。

命名空间

目前Linux实现了六种类型的namespace,每一个namespace是包装了一些全局系统资源的抽象集合,这一抽象集合使得在进程的命名空间中可以看到全局系统资源。命名空间的一个总体目标是支持轻量级虚拟化工具container的实现,container机制本身对外提供一组进程,这组进程自己会认为它们就是系统唯一存在的进程。

在下面的讨论中,按命名空间实现的版本先后依次对其介绍,当提到命名空间的API(clone(),ushare(),setns())时括号内的CLONE_NEW*用于标识命名空间的类型。

mount命名空间 (CLONE_NEWS, Linux2.4.19)用于隔离一组进程看到的文件系统挂载点集合,即处于不同mount 命名空间的进程看到的文件系统层次很可能是不一样的。mount()和umount()系统调用的影响不再是全局的而只影响其调用进程指向的命名空间。

mount命名空间的一个应用类似chroot,然而和chroot()系统调用相比,mount 命名空间在安全性和扩展性上更好。其它一些更复杂的应用如:不同的mount命名空间可以建立主从关系,这样可以让一个命名空间的事件自动传递到另一个命名空间。

mount命名空间是Linux内核最早实现的命名空间,于2002年就开始了;这就是CLONE_NEWS的由来,当时没人想到其它不同的命名空间会被添加到内核。

UTS命名空间(CLONE_NEWUTS, Linux2.6.19)隔离了两个系统变量,系统节点名和域名;uname()系统调用返回UTS,名字使用setnodename()和setdomainname()系统调用设置。从容器的上下文看,UTS赋予了每个容器各自的主机名和 网络信息服务名(NIS) (Network Information Service),这使得初始化和配置脚本能够根据不同的名字进行裁剪。UTS源于传递给uname()系统调用的参数:struct utsname。该结构体的名字源于"UNIX Time-sharing System"

IPC namespaces (CLONE_NEWIPC, Linux 2.6.19)隔离进程间通信资源,具体来说就是System V IPC objects and (since Linux2.6.30) POSIX message queues;这些机制的共同特点是由其特点而非文件系统路径名标识。每一个IPC命名空间尤其自己的System V IPC标识符和POSIX 消息队列文件系统。

PID namespaces (CLONE_NEWPID, Linux 2.6.24)隔离进程ID号命名空间,话句话说就是位于不同进程ID命名空间的进程可以有相同的进程ID号,PID命名空间的最大的好处是在主机之间移植container时,可以保留container内的ID号,PID命名空间允许每个container拥有自己的init进程(ID=1),init进程是所有进程的祖先,负责系统启动时的初始化和作为孤儿进程的父进程。

从特殊的角度来看PID命名空间,就是一个进程有两个ID,一个ID号属于PID命名空间,一个ID号属于PID命名空间之外的主机系统,此外,PID命名空间能够被嵌套。

Network namespaces (CLONE_NEWNET, Linux2.6.24开始结束于 Linux 2.6.29)用于隔离和网络有关的资源,这就使得每个网络命名空间有其自己的网络设备、IP地址、IP路由表、/proc/net目录、端口号等等。

从网络命名空间的角度看,每个container拥有其自己的网络设备(虚拟的)和用于绑定自己网络端口号的应用程序。主机上合适的路由规则可以将网络数据包和特定container相关的网络设备关联。例如,可以有多个web 服务器,分别存在不同的container中,这就使得这些web 服务器可以在其命名空间中绑定80端口号。

User namespaces (CLONE_NEWUSER, 起始于 Linux2.6.23 完成于 Linux 3.8) 隔离用户和组ID空间,换句话说,一个进程的用户和组ID在用户命名空间之外可以不同于命名空间之内的ID,最有趣的是一个用户ID在命名空间之外非特权,而在命名空间内却可以是具有特权的。这就意味着在命名空间内拥有全部的特权权限,在命名空间之外则不是这样。

自Linux3.8开始,非特权进程可以创建用户命名空间,由于非特权进程在user命名空间内具有root权限,命名空间内非特权应用程序可以使用以前只有root能够使用的一些功能。

总结

自从第一个命名空间的实现到现在已有十年之久,命名空间的概念也发展为更通用的框架-隔离先前系统级的全局资源。结果使能命名空间能够提供完整的轻量级虚拟化系统,呈现的形式就是container。随着命名空间概念的扩展,与之相关的clone()系统调用和一两个/proc下的文件发展成许多其它的系统调用和/proc下更多的文件。这些扩展后的API成为本文接下来讨论的主题。

系列博文索引

下面的列表给出了后续的系列博文。

Part 2: the namespaces API

demo_uts_s.c:示例了UTS命名空间的使用方法。
ns_exec.c: 使用setns()关联一个命名空间并且执行该命令。

unshare.c: 停止命名空间的共享并执行命令。

 

Part 3: PID namespaces

pidns_init_sleep.c:证明PID命名空间
multi_pidns.c:嵌套的PID命名空间中创建一系列的子进程。

 

Part 4: more on PID namespaces

ns_child_exec.c: 创建一个子进程在新的命名空间中执行命令
simple_init.c: 简单的init类型的程序,用于在PID命名空间中使用。

orphan.c: 证明一个子进程变为孤儿进程后,将被init进程收留

ns_run.c: 使用setns()关联一个或多个命名空间,并且在这些命名空间中执行一个命令,使用场景很可能是在子进程中。

 

Part 5: user namespaces

demo_userns.c: 创建命名空间的简单程序,并且显示了程序的权限和能力
userns_child_exec.c: 创建一个在一个新的命名空间执行shell命令的子进程,类似于ns_child_exec.c,但是提供了用户命名空间的额外选项。

 

Part 6: more on user namespaces

userns_setns_test.c:从连个不同的user命名空间测试setns()参数。

Part 7: network namespaces

命名空间 API

一个命名空间包含一些抽象化的全局系统资源,这些隔离的全局资源在命名空间将呈现给进程。命名空间被用于很多目的,最突出的就是轻量级虚拟化容器(container)了。

命名空间API包括三个系统调用:clone()、unshare()和setns(),此外,还包括/proc目录下的许多文件。本文将讨论上述系统调用以及/proc目录下的一些文件。为了明确使用的命名空间的类型,三个系统调用使用先前提到的CLONE_NEW*常量: 

CLONE_NEWIPC, CLONE_NEWNS, CLONE_NEWNET, CLONE_NEWPID, CLONE_NEWUSER,and CLONE_NEWUTS。

创建一个新的命名空间: clone()

创建一个命名空间的方法是使用clone()系统调用,其会创建一个新的进程。为了说明创建的过程,给出clone()的原型如下:

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

本质上,clone()是一个通用的fork()版本,fork()的功能由flags参数控制。总的来说,约有超过20个不同的CLONE_*标志控制clone()提供不同的功能,包括父子进程是否共享如虚拟内存、打开的文件描述符、子进程等一些资源。如调用clone时设置了一个CLONE_NEW*标志,一个与之对应的新的命名空间将被创建,新的进程属于该命名空间。可以使用多个CLONE_NEW*标志的组合。

我们的例子(demo_uts_namespace.c)调用clone()时设置了CLONE_NEWUTS标志以创建一个UTS命名空间。完整的demo_uts_namespaces.c

 


#define _GNU_SOURCE
#include <sys/wait.h>
#include <sys/utsname.h>
#include <sched.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static int  childFunc(void *arg){

   struct utsname uts;

   if (sethostname(arg, strlen(arg)) == -1)

       exit(0);

   if (uname(&uts) == -1)

       exit(0);

   printf("uts.nodename in child: %s\n", uts.nodename);
 
   sleep(100);

   return 0;           /* Terminateschild */

}


#define STACK_SIZE (1024 * 1024)    /* Stack size for cloned child */
 
static char child_stack[STACK_SIZE];

int main(int argc, char *argv[]){

   pid_t child_pid;
   struct utsname uts;
   if (argc < 2) {

       fprintf(stderr, "Usage: %s <child-hostname>\n",argv[0]);
   }
 
   child_pid = clone(childFunc, child_stack +STACK_SIZE,CLONE_NEWUTS | SIGCHLD,argv[1]);

   if (child_pid == -1){
      
      exit(0);
   }

   printf("PID of child created by clone() is %ld\n", (long)child_pid);

   sleep(1);           

   if (uname(&uts) == -1){
    
       exit(0);

   }

   printf("uts.nodename in parent: %s\n", uts.nodename);

   if (waitpid(child_pid, NULL, 0) == -1){
   
       exit(0);
   }
      
   printf("child has terminated\n");

   exit(0);
}


例程需要一个命令行参数,当运行时,其创建一个在新的UTS命名空间执行的子进程。在新的命名空间中,子进程使用获得的命令行参数更改主机名。

main函数第一个比较重要的部分是创建子进程的clone()调用。

   child_pid = clone(childFunc,

                    child_stack +STACK_SIZE,   /* Points to start of

                                                   downwardly growing stack */

                    CLONE_NEWUTS | SIGCHLD,argv[1]);

   printf("PID of child created by clone() is %ld\n", (long)child_pid);

新的子进程将会执行用户定义的函数childFunc();该函数会接收clone()传递的最后一个参数argv[1],由于创建时使用了CLONE_NEWUTS标识,新的UTS命名空间会被创建。

主程序然后休眠一段时间。这是一个粗暴的方式以让子进程修改UTS命名空间内的主机名。那个程序然后使用uname()获得命名空间内的主机名并且显示主机名。

   sleep(1);           /* Give childtime to change its hostname */

   if (uname(&uts) == -1)

       errExit("uname");

   printf("uts.nodename in child: %s\n", uts.nodename);

与此同时,子进程执行的函数childFunc()首先更改主机名为命令行输入的参数,然后显示修改后的主机名。

   if (uname(&uts) == -1)

       errExit("uname");

   printf("uts.nodename in child: %s\n", uts.nodename);

在结束之前,子进程休眠一会。效果就是让子进程的UTS命名空间存在一段时间,这段时间使得我们能够做一些后面我们给出的实验。

执行如下命令:

   $ su                  # Need privilege to create a UTS namespace

    Password:

    # uname -n

    antero

    # ./demo_uts_namespacesbizarro

    PID of child created byclone() is 27514

    uts.nodename inchild:  bizarro

    uts.nodename in parent:antero

正如其它命名空间(user namespace除外),创建一个UTS命名空间需要特权权限(CAP_SYS_ADMIN)。这可以避免set-user-ID类的应用程序因系统主机问题被误导而执行错误的事。

另外一个可能性是set-user-ID类应用可能使用主机名作为锁文件的一部分。如果一个非特权用户能够在一个具有任何主机名的UTS命名空间运行程序,这可能导致应用受到各种攻击。最简单的,这将使锁无效,在不同的UTS命名空间中引发应用实例被运行。另外,一个恶意用户可以在一个UTS命名空间运行一个set-user-ID应用程序来覆盖一个重要文件的锁。

The /proc/PID/ns 文件

每一个进程有一个/proc/PID/ns目录,该目录下每一个命名空间对应一个文件。从3.8版本起,每一个这类文件都是一个特殊的符号链接。该符号链接提供在其命名空间上执行某个操作的某种方法。

$ ls -l/proc/

/ns         #
is replaced byshell's PID

    total 0

    lrwxrwxrwx. 1 mtk mtk 0Jan  8 04:12 ipc -> ipc:[4026531839]

    lrwxrwxrwx. 1 mtk mtk 0Jan  8 04:12 mnt -> mnt:[4026531840]

    lrwxrwxrwx. 1 mtk mtk 0Jan  8 04:12 net -> net:[4026531956]

    lrwxrwxrwx. 1 mtk mtk 0Jan  8 04:12 pid -> pid:[4026531836]

    lrwxrwxrwx. 1 mtk mtk 0Jan  8 04:12 user -> user:[4026531837]

    lrwxrwxrwx. 1 mtk mtk 0Jan  8 04:12 uts -> uts:[4026531838]

这些符号链接可以用来判断两个命名空间是否在同一个命名空间。如果两个进程在同一个命名空间,内核会保证由/proc/PID/ns导出的inode号将会是一样的。inode号可以通过stat()系统调用获得。

然而,内核同样为每个 /proc/PID/ns构建了符号链接,以使其指向包含标识命名空间类型字符串(字符串以inode号结尾)的名字。我们可以通过ls -l或者readlink命令查看名字。

让我们回到上面 demo_uts_namespaces运行的shell会话,通过查看父子进程的/proc/PID/ns符号链接信息可以知道它们是否位于同一个命名空间。

   ^Z                               # Stop parent and child

    [1]+ Stopped         ./demo_uts_namespaces bizarro

    # jobs-l                        # Show PID of parent process

    [1]+ 27513Stopped         ./demo_uts_namespacesbizarro

    # readlink/proc/27513/ns/uts     # Show parent UTS namespace

    uts:[4026531838]

    # readlink/proc/27514/ns/uts     # Show child UTS namespace

    uts:[4026532338]

正如我们看到的, /proc/PID/ns/uts 符号链接并不一样,表明它们位于不同的命名空间中。

/proc/PID/ns同样服务于其它目的,如果我们随便打开一个文件,那么只要文件描述打开状态,那么命名空间将会保持存在而不论命名空间中的进程是否全部退出,相同的效果可以通过绑定其中一个符号链接到文件系统的其它地方获得。

 # touch~/uts                           # Create mount point

    # mount --bind/proc/27514/ns/uts ~/uts

在3.8之前, /proc/PID/ns 下的文件是硬链接,并且只有ipc、net和uts文件是存在的。

关联一个存在命名空间:setns()

当一个命名空间没有进程时还保持其打开,这么做是为了后续添加进程到该命名空间。而添加这个功能这就是使用setns()系统调用来完成了,这使得调用的进程能够和命名空间关联:

 intsetns(int fd, int nstype);

准确来说,setns()将调用的进程和一个特定的命名空间解除关系并将该进程和一个同类型的命名空间相关联。

fd参数指明了关联的命名空间,其是指向了 /proc/PID/ns 目录下一个符号链接的文件描述符,可以通过打开这些符号链接指向的文件或者打开一个绑定到符号链接的文件来获得文件描述符(所谓的获得指的是引用计数加1)。

nstype参数运行调用者检查fd指向的命名空间的类型,如果这个参数等于零,将不会检查。当调用者已经知道命名空间的类型时这会很有用。我们的示例程序(ns_exec.c)的nstype参数等于零,其适用于任何命名空间。当nstype被赋值为CLONE_NEW*的常量时,内核会检查fd指向的命名空间的类型。 

使用setns()和execve()(或者其它的exec()函数)使得我们能够构建一个简单但是有用的工具,一个和特定命名空间关联的程序并且在命名空间中可以执行一个命令。

/* ns_exec.c

 

  Copyright 2013, Michael Kerrisk

  Licensed under GNU General Public License v2 or later

 

  Join a namespace and execute a command in the namespace

*/

#define _GNU_SOURCE

#include <fcntl.h>

#include <sched.h>

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

 

/* A simple error-handling function:print an error message based

  on the value in 'errno' and terminate the calling process */

 

#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \

                        } while (0)

 

int

main(int argc, char *argv[])

{

   int fd;

 

   if (argc < 3) {

       fprintf(stderr, "%s /proc/PID/ns/FILE cmd [arg...]\n",argv[0]);

       exit(EXIT_FAILURE);

   }

 

   fd = open(argv[1], O_RDONLY);   /*Get descriptor for namespace */

   if (fd == -1)

       errExit("open");

 

   if (setns(fd, 0) == -1)         /*Join that namespace */

       errExit("setns");

 

   execvp(argv[2], &argv[2]);     /* Execute a command in namespace */

   errExit("execvp");

}

我们的应用程序接收两个命令行参数。第一个参数是/proc/PID/ns/* 符号链接的路径,剩下的参数是在和第一参数关联的命名空间中将要运行的程序名,程序中的关键步骤如下:

   fd = open(argv[1], O_RDONLY);   /* Get descriptor for namespace */

    setns(fd,0);                  /* Join thatnamespace */

   execvp(argv[2], &argv[2]);      /* Execute acommand in namespace */

一个在命名空间中执行的有意思的程序当然是shell程序。我么可以使用先前创建的UTS命名空间以及ns_exec程序来在demo_uts_namespaces.c创建的新的UTS命名空间中执行一个shell。

   # ./ns_exec ~/uts /bin/bash     # ~/uts is bound to/proc/27514/ns/uts

   My PID is: 28788

可以证明新的UTS命名空间创建的shell是emo_uts_namespaces的子进程,可以通过查看主机名或者比较 /proc/PID/ns/uts 的inode节点得到该结论。

   # hostname

   bizarro

   # readlink /proc/27514/ns/uts

   uts:[4026532338]

   # readlink /proc/

/ns/uts      #
is replaced byshell's PID

   uts:[4026532338]

早期的内核版本,使用setns()关联mount、PID和user 命名空间是不可能的,但是从3.8开始支持所有的命名空间类型。

Leaving a namespace: unshare()

最后一种命名空间的系统调用是unshare():

   int unshare(int flags);

unshare()系统调用提供类似clone()的功能,但是作用于调用的进程。其会创建由flags参数中制定的CLONE_NEW*命名空间,并且将调用者作为命名空间的一部分。 unshare()的主要目的是消除命名空间的副作用而不需要创建新的进程或线程。

撇开clone系统调用的影响,调用的形式是:

 clone(...,CLONE_NEWXXX, ....);

就命名空间术语来说,等价于下列顺序:

   if (fork() == 0)

       unshare(CLONE_NEWXXX);      /* Executed in the childprocess */

unshare的系统调用的一个例子是在命令行下使用unshare命令,其允许用户使用shell执行另一个命名空间的命令。该命令的通常形式如下:

   unshare [options] program [arguments]

参数[arguments] 是传递给命令program的,options传递给unshare指向的命名空间。

实现unshare命令的关键步骤很直接:

    /* Code to initialize 'flags' according to command-line options

       omitted */

    unshare(flags);

     /* Now execute 'program' with 'arguments'; 'optind' is the index

       of the next command-line argument after options */

     execvp(argv[optind], &argv[optind]);

一个简单的unshare命令的实现代码如下:

/* unshare.c 

   Copyright 2013, Michael Kerrisk

   Licensed under GNU General Public License v2or later

   A simple implementation of the unshare(1)command: unshare

   namespaces and execute a command.

*/

 

#define_GNU_SOURCE

#include<sched.h>

#include<unistd.h>

#include<stdlib.h>

#include<stdio.h>

 

/* A simpleerror-handling function: print an error message based

   on the value in 'errno' and terminate thecalling process */

 

#defineerrExit(msg)    do { perror(msg);exit(EXIT_FAILURE); \

                        } while (0)

 

static void

usage(char *pname)

{

    fprintf(stderr, "Usage: %s [options]program [arg...]\n", pname);

    fprintf(stderr, "Options canbe:\n");

    fprintf(stderr, "    -i  unshare IPC namespace\n");

    fprintf(stderr, "    -m  unshare mount namespace\n");

    fprintf(stderr, "    -n  unshare network namespace\n");

    fprintf(stderr, "    -p  unshare PID namespace\n");

    fprintf(stderr, "    -u  unshare UTS namespace\n");

    fprintf(stderr, "    -U  unshare user namespace\n");

    exit(EXIT_FAILURE);

}

 

int

main(int argc,char *argv[])

{

    int flags, opt;

    flags = 0;

 

    while ((opt = getopt(argc, argv,"imnpuU")) != -1) {

        switch (opt) {

        case 'i': flags |= CLONE_NEWIPC;        break;

        case 'm': flags |= CLONE_NEWNS;         break;

        case 'n': flags |= CLONE_NEWNET;        break;

        case 'p': flags |= CLONE_NEWPID;        break;

        case 'u': flags |= CLONE_NEWUTS;        break;

        case 'U': flags |= CLONE_NEWUSER;       break;

        default:  usage(argv[0]);

        }

    }

    if (optind >= argc)

        usage(argv[0]);

    if (unshare(flags) == -1)

        errExit("unshare");

    execvp(argv[optind],&argv[optind]); 

    errExit("execvp");

}

在下面的shell会话中,我们使用unshare.c程序在另外一个mount命名空间中执行一个shell。

   # echo$$                            # Show PID of shell

   8490

   # cat /proc/8490/mounts | grep mq     # Show one of themounts in namespace

   mqueue /dev/mqueue mqueue rw,seclabel,relatime 0 0

   # readlink/proc/8490/ns/mnt          # Showmount namespace ID

    mnt:[4026531840]

   # ./unshare -m/bin/bash             # Start new shell in separate mount namespace

   # readlink/proc/$$/ns/mnt           # Show mount namespace ID

    mnt:[4026532325]

从上述readlink输出可以看到两个shell属于不同的命名空间,改变一个命名空间的挂载点,然后查看另外一个命名空间对上述改变是否可见即可分辨它们是否位于同一个命名空间。

   # umount /dev/mqueue                 # Remove a mount point in this shell

   # cat /proc/$$/mounts | grep mq       # Verifythat mount point is gone

   # cat /proc/8490/mounts | grep mq     # Is it still presentin the other namespace?

 mqueue/dev/mqueue mqueuerw,seclabel,relatime 0 0

从输出的最后两个参数看到, /dev/mqueue挂载点一个命名空间可以看到而另一个命名空间看不到。

总结

本文我们查看了命名空间API以及它们的使用。接下来的文章,我们将更深入查看命名空间的,特别会深入查看PID和user 命名空间。

PID命名空间

被PID命名空间隔离的全局资源是进程ID号空间,这就意味着位于不同命名空间的进程的ID号可以相同,PID命名空间被用来实现container。

和传统Linux系统一样,在PID命名空间内的进程ID号是各不相等的,它们被从1开始分配进程ID号。同样的,ID号等于1的init进程是一个特殊进程,它是命名空间中的第一个进程,它也命名空间提供一些管理工作。

初探

一个新的PID命名空间调用clone(...CLONE_NEWPID...)创建,我们将展示一个简单的使用clone创建PID命名空间的例子,并且使用该例子阐释PID命名空间的基本概念,完整的pidns_init_sleep.c的源码如下:

/*pidns_init_sleep.c

 

   Copyright 2013, Michael Kerrisk

   Licensed under GNU General Public License v2or later

 

   A simple demonstration of PID namespaces.

*/

#define_GNU_SOURCE

#include<sched.h>

#include<unistd.h>

#include<stdlib.h>

#include<sys/wait.h>

#include<sys/mount.h>

#include<sys/types.h>

#include<sys/stat.h>

#include<string.h>

#include<signal.h>

#include<stdio.h>

 

/* A simpleerror-handling function: print an error message based

   on the value in 'errno' and terminate thecalling process */

 

#defineerrExit(msg)    do { perror(msg);exit(EXIT_FAILURE); \

                        } while (0)

 

static int              /* Start function for clonedchild */

childFunc(void*arg)

{

    printf("childFunc(): PID  = %ld\n", (long) getpid());

    printf("childFunc(): PPID =%ld\n", (long) getppid());

 

    char *mount_point = arg;

 

    if (mount_point != NULL) {

        mkdir(mount_point, 0555);       /* Create directory for mount point */

        if (mount("proc",mount_point, "proc", 0, NULL) == -1)

            errExit("mount");

        printf("Mounting procfs at %s\n",mount_point);

    }

 

    execlp("sleep","sleep", "600", (char *) NULL);

    errExit("execlp");  /* Only reached if execlp() fails */

}

 

#define STACK_SIZE(1024 * 1024)

 

static charchild_stack[STACK_SIZE];    /* Space forchild's stack */

 

int

main(int argc,char *argv[])

{

    pid_t child_pid;

 

    child_pid = clone(childFunc,

                    child_stack +STACK_SIZE,   /* Points to start of

                                                  downwardly growing stack */

                    CLONE_NEWPID | SIGCHLD,argv[1]);

 

    if (child_pid == -1)

        errExit("clone");

 

    printf("PID returned by clone():%ld\n", (long) child_pid);

 

    if (waitpid(child_pid, NULL, 0) == -1)      /* Wait for child */

        errExit("waitpid");

 

    exit(EXIT_SUCCESS);

}

主程序使用clone()创建一个PID命名空间,并且显示了返回的PID号。

 child_pid =clone(childFunc,

                   child_stack +STACK_SIZE,   /* Points to start of

                                                  downwardly growing stack */

                   CLONE_NEWPID | SIGCHLD, argv[1]);

    printf("PID returned by clone(): %ld\n", (long) child_pid);

新的子进程开始执行childFunc()函数,该函数接收clone()调用的最后一个参数argv[1],该参数的作用后续讲解。 childFunc()函数显示进程和其父进程ID,并且使用sleep函数结束。

   printf("childFunc(): PID = %ld\n", (long) getpid());

   printf("ChildFunc(): PPID = %ld\n", (long) getppid());

    ...

   execlp("sleep", "sleep", "1000", (char *) NULL);

使用sleep的主要价值在于其让我们区分子进程和父进程更简单。当我们允许该程序, 第一行的输出如下:

   $ su         # Need privilege to createa PID namespace

   Password:

    #./pidns_init_sleep /proc2

   PID returned by clone(): 27656

   childFunc(): PID  = 1

   childFunc(): PPID = 0

   Mounting procfs at /proc2

 pidns_init_sleep 前两行输出了从两个不同的PID命名空间查看子进程的ID号:clone()调用所在的PID命名空间和子进程存在的PID命名空间。即子进程有两个PID:父进程命名空间中的为27656,新创建的命名空间中的ID是1。输出的下一行展示了子进程的父进程ID。父进程ID等于0显示了命名空间实现的一点怪异之处。正如下面我们讨论的,命名空间形成一个层次结构:一个进程仅仅能够"看见"在其命名空间内和子命名空间内的进程ID(子进程的子进程...均能看见),子进程看不见父进程命名空间的进程。由于由clone()创建的子进程同父进程在不同的命名空间中,所有子进程“看”不到父进程;因此,getppid()返回的父进程ID为0.

为了解释最后一行,我们需要重新查看childFunc() 中跳过的一些代码段。

/proc/PID和PID命名空间

Linux系统上的每一个进程都有一个/proc/PID目录,该目录包括了描述进程的伪文件。这一机制可以直接得到PID的命名空间。在一个命名空间内部,/proc/PID目录仅包含在该命名空间内和子命名空间。

然而,为了使和一个PID命名空间相关的/proc/PID目录可见,proc文件系统("procfs")需要在PID命名空间内被挂载。在一个命名空间内运行的shell上,我们可以使用mount命令完成:

 #mount -tproc proc/mount_point

也可以使用mount()系统调用完成,这里在childFunc()里调用如下:

   mkdir(mount_point, 0555);       /* Createdirectory for mount point */

   mount("proc", mount_point, "proc", 0, NULL);

   printf("Mounting procfs at %s\n", mount_point);

mount_pioint参数在运行pidns_init_sleep时通过命令行参数给出。

在我们的例子中,shell中运行pidns_init_sleep,我们在/proc2目录下挂着procfs。在现实世界中,procfs通常被挂载在/proc目录下。然而,我们的例子在/proc2目录下挂载procfs,这避免了会给系统上其它进程带来问题:因为他们位于同样的挂载点,更改挂载在/proc目录下的文件系统将使得root PID 命名空间“看”不到/proc/PID目录。

所以,在我们的shell会话中,/proc目录下挂载的procfs将显示从父PID命名空间能够看到的进程的PID子目录,/proc2则用于子进程命名空间。需要提醒的是虽然子进程PID命名空间的进程能够看见由/proc挂载点导出的PID目录,但是这些PID目录是对于子进程PID命名空间的进程而言是无意义的,因为这些进程的系统调用只能看到它们所在命名空间的PID。

如果我们想让像ps这样的工具在一个子进程中能够正确运行,那么在/proc挂载点挂载一个procfs文件系统还是必要的。因为这些工具的信息源于/proc目录。有两种方法在不影响父进程使用的PID命名空间前提下达到这个目标。其一,如果子进程使用CLONE_NEWNS标志创建,那么子进程将和系统的其它部分在不同的mount 命名空间,在这种情况下,在/proc目录下挂载procfs不会产生任何问题。另外,不采用CLONE_NEWNS的方法,子进程可以使用chroot()并在/proc目录下挂载procfs。

让我们回到运行pidns_init_sleep程序的shell上,我们停止该程序并在fu2命名空间中使用ps检查父子进程的一些信息。

   ^Z                         Stopthe program, placing in background

   [1]+ Stopped                ./pidns_init_sleep /proc2

   # ps -C sleep -C pidns_init_sleep -o "pid ppid stat cmd"

     PID  PPID STAT CMD

   27655 27090 T    ./pidns_init_sleep /proc2

   27656 27655 S    sleep 600

PPID的值为27655,最后一行系显示了sleep是在父进程中执行的。

通过使用readlink命令查看父子进程的不同/proc/PID/ns/pid符号链接信息,我们可以看到两个进程在不同的PID命名空间中:

   # readlink /proc/27655/ns/pid

   pid:[4026531836]

   # readlink /proc/27656/ns/pid

   pid:[4026532412]

到此,我们可以使用新挂载的procfs获得新PID命名空间的进程信息。我们可以使用下面的命令获得PID的列表:

   # ls -d /proc2/[1-9]*

   /proc2/1

正如我们看到的,PID命名空间仅仅包括一个进程,它的进程ID号是1。同样也可以使用/proc/PID/status文件作为一个获得一个进程信息的不同方法。

   # cat /proc2/1/status | egrep '^(Name|PP*id)'

   Name:   sleep

   Pid:    1

   PPid:   0

PPid是0,符合前面getppid()系统调用的返回的父进程的ID号。

嵌套的 PID 命名空间

如前面提到的,PID命名空间是以父子关系层次嵌套的。在一个PID命名空间内,可以看同一个命名空间中的所有其它进程以及后裔进程。这里,“看见”意思是能够在特定的进程ID号上使用系统调用,一个子PID命名空间不能看见父PPID的命名空间。

一个进程在PID命名空间的每一层都有一个进程ID,存在的范围是该进程的PID命名空间一直到root命名空间。getpid()总是返回PID命名空间内的进程ID。

我们可以使用multi_pidns.c来展示一个进程在不同的命名空间中拥有不同的进程ID号。为了简洁,我们简单阐述该程序都做了什么。

/* multi_pidns.c

 

   Copyright 2013, Michael Kerrisk

   Licensed under GNU General Public License v2or later

 

   Create a series of child processes in nestedPID namespaces.

*/

#define_GNU_SOURCE

#include<sched.h>

#include<unistd.h>

#include<stdlib.h>

#include<sys/wait.h>

#include<string.h>

#include<signal.h>

#include<stdio.h>

#include<limits.h>

#include<sys/mount.h>

#include<sys/types.h>

#include<sys/stat.h>

 

/* A simpleerror-handling function: print an error message based

   on the value in 'errno' and terminate thecalling process */

 

#defineerrExit(msg)    do { perror(msg);exit(EXIT_FAILURE); \

                        } while (0)

 

#define STACK_SIZE(1024 * 1024)

 

static charchild_stack[STACK_SIZE];    /* Space forchild's stack */

                /* Since each child gets a copyof virtual memory, this

                   buffer can be reused as eachchild creates its child */

 

/* Recursivelycreate a series of child process in nested PID namespaces.

   'arg' is an integer that counts down to 0during the recursion.

   When the counter reaches 0, recursion stopsand the tail child

   executes the sleep(1) program. */

 

static int

childFunc(void*arg)

{

    static int first_call = 1;

    long level = (long) arg;

 

    if (!first_call) {

 

        /* Unless this is the first recursivecall to childFunc()

           (i.e., we were invoked from main()),mount a procfs

           for the current PID namespace */

 

        char mount_point[PATH_MAX];

 

        snprintf(mount_point, PATH_MAX,"/proc%c", (char) ('0' + level));

 

        mkdir(mount_point, 0555);       /* Create directory for mount point */

        if (mount("proc",mount_point, "proc", 0, NULL) == -1)

            errExit("mount");

        printf("Mounting procfs at %s\n",mount_point);

    }

 

    first_call = 0;

 

    if (level > 0) {

 

        /* Recursively invoke childFunc() tocreate another child in a

           nested PID namespace */

 

        level--;

        pid_t child_pid;

 

        child_pid = clone(childFunc,

                    child_stack +STACK_SIZE,   /* Points to start of

                                                  downwardly growing stack */

                    CLONE_NEWPID | SIGCHLD,(void *) level);

 

        if (child_pid == -1)

            errExit("clone");

 

        if (waitpid(child_pid, NULL, 0) ==-1)  /* Wait for child */

            errExit("waitpid");

 

    } else {

 

        /* Tail end of recursion: executesleep(1) */

 

        printf("Final childsleeping\n");

        execlp("sleep","sleep", "1000", (char *) NULL);

        errExit("execlp");

    }

 

    return 0;

}

 

int

main(int argc,char *argv[])

{

    long levels;

 

    levels = (argc > 1) ? atoi(argv[1]) : 5;

    childFunc((void *) levels);

 

    exit(EXIT_SUCCESS);

}

该程序递归创建一系列的子进程命名空间,命令行参数指明了子进程和PID命名空间的创建次数:

#./multi_pidns5

除了递归创建子进程,每一个递归步骤还会在一个独一无二的挂载点挂载procfs文件系统。递归最后的子进程执行sleep()系统调用。上面的命令行产生如下的输出:

   Mounting procfs at /proc4

   Mounting procfs at /proc3

   Mounting procfs at /proc2

   Mounting procfs at /proc1

   Mounting procfs at /proc0

   Final child sleeping

在每一个procfs下查看PID,我们看到越是后创建的procfs包含的PID越少,表明每一个PID命名空间只显示其命名空间自身以及其后创建的命名空间的信息。

   ^Z                          Stop the program, placing in background

   [1]+ Stopped            ./multi_pidns5

   # ls -d /proc4/[1-9]*        Topmost PIDnamespace created by program

   /proc4/1  /proc4/2  /proc4/3  /proc4/4  /proc4/5

   # ls -d /proc3/[1-9]*

   /proc3/1  /proc3/2  /proc3/3  /proc3/4

   # ls -d /proc2/[1-9]*

   /proc2/1  /proc2/2  /proc2/3

   # ls -d /proc1/[1-9]*

   /proc1/1  /proc1/2

   # ls -d /proc0/[1-9]*        Bottommost PIDnamespace

   /proc0/1

一个grep命令使得我们能够看见递归最后的PID。

   # grep -H 'Name:.*sleep' /proc?/[1-9]*/status

   /proc0/1/status:Name:       sleep

   /proc1/2/status:Name:       sleep

   /proc2/3/status:Name:       sleep

   /proc3/4/status:Name:       sleep

   /proc4/5/status:Name:       sleep

换句话说,嵌套最深的PID命名空间(/proc0),该进程执行sleep并且进程ID是1,在最上面创建的PID命名空间是/proc4,进程的PID是5。

如果你运行本文的例子,需要说明的是它们将残留下挂载点和挂载目录。在结束程序时,如下的shell命令行完成做够的清理工作:

   # umount /proc?

   # rmdir /proc?

总结

在本文,我们了解了一些PID命名空间的操作。在下文中,我们将讨论PID命名空间的init进程和一些其它的PID命名空间的API。

 

深入PID 命名空间

本文是对PID命名空间的更深入探讨。PID命名空间的一个应用是打包一组进程(container),使打包的进程组自身就像一个操作系统。传统操作系统和这里的container一样的一个关键点是init进程。所以我们来看看init进程以及两种情况下都有哪些不同。按惯例,我们将看看适用于PID命名空间的其它一些细节。

PID命名空间的init进程

在PID命名空间中创建的第一个进程的进程ID是1,该进程的角色和传统操作系统的init进程一样;特别地,init进程能够完成PID命名空间需要的初始化工作(这些工作很可能包括启动其它进程),其同样会是孤儿进程的父进程。

为了解释PID命名空间,我们将使用一些服务于目的的例程。第一个例程是ns_child_exec.c,命令行运行的语法如下:

ns_child_exec[options]command [arguments]

/* ns_child_exec.c

 

   Copyright 2013, Michael Kerrisk

   Licensed under GNU General Public License v2or later

 

   Create a child process that executes a shellcommand in new namespace(s).

*/

#define_GNU_SOURCE

#include <sched.h>

#include<unistd.h>

#include<stdlib.h>

#include<sys/wait.h>

#include<signal.h>

#include<stdio.h>

 

/* A simpleerror-handling function: print an error message based

   on the value in 'errno' and terminate thecalling process */

 

#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \

                        } while (0)

 

static void

usage(char *pname)

{

    fprintf(stderr, "Usage: %s [options]cmd [arg...]\n", pname);

    fprintf(stderr, "Options canbe:\n");

    fprintf(stderr, "    -i  new IPC namespace\n");

    fprintf(stderr, "    -m  new mount namespace\n");

    fprintf(stderr, "    -n  new network namespace\n");

    fprintf(stderr, "    -p  new PID namespace\n");

    fprintf(stderr, "    -u  new UTS namespace\n");

    fprintf(stderr, "    -U  new user namespace\n");

    fprintf(stderr, "    -v  Display verbose messages\n");

    exit(EXIT_FAILURE);

}

 

static int              /* Start function for clonedchild */

childFunc(void*arg)

{

    char **argv = arg;

 

    execvp(argv[0], &argv[0]);

    errExit("execvp");

}

 

#define STACK_SIZE(1024 * 1024)

 

static charchild_stack[STACK_SIZE];    /* Space forchild's stack */

 

int

main(int argc,char *argv[])

{

    int flags, opt, verbose;

    pid_t child_pid;

 

    flags = 0;

    verbose = 0;

 

    /* Parse command-line options. The initial'+' character in

       the final getopt() argument preventsGNU-style permutation

       of command-line options. That's useful,since sometimes

       the 'command' to be executed by thisprogram itself

       has command-line options. We don't wantgetopt() to treat

       those as options to this program. */

 

    while ((opt = getopt(argc, argv,"+imnpuUv")) != -1) {

        switch (opt) {

        case 'i': flags |= CLONE_NEWIPC;        break;

        case 'm': flags |= CLONE_NEWNS;         break;

        case 'n': flags |= CLONE_NEWNET;        break;

        case 'p': flags |= CLONE_NEWPID;        break;

        case 'u': flags |= CLONE_NEWUTS;        break;

        case 'U': flags |= CLONE_NEWUSER;       break;

        case 'v': verbose = 1;                  break;

        default:  usage(argv[0]);

        }

    }

 

    child_pid = clone(childFunc,

                    child_stack + STACK_SIZE,

                    flags | SIGCHLD,&argv[optind]);

    if (child_pid == -1)

        errExit("clone");

 

    if (verbose)

        printf("%s: PID of child createdby clone() is %ld\n",

                argv[0], (long) child_pid);

 

    /* Parent falls through to here */

 

    if (waitpid(child_pid, NULL, 0) == -1)      /* Wait for child */

        errExit("waitpid");

 

    if (verbose)

        printf("%s: terminating\n",argv[0]);

    exit(EXIT_SUCCESS);

}

ns_child_exec程序使用clone()系统调用来创建子进程;子进程然后执行命令行中的command命令,命令的参数是命令行中的argument参数。命令行中的option参数用于指定要创建的命名空间类型,该参数将传递给clone()系统调用。例如 -p选项将指导子进程创建新的PID命名空间,如下所示:

   $ su                 # Need privilege to create aPID namespace

   Password:

   # ./ns_child_exec -p sh -c 'echo $$'

   1

上面的命令行在新的PID命名空间中创建了一个子进程,该子进程执行一个显示shell进程ID的echo命令。该shell进程ID是1,当shell运行时其将是PID命名空间的init进程。

我们的下一个例程,simple_init.c是一个我们要将其作为一个PID命名空间的init进程的程序。该程序用于证实PID命名空间的init进程的一些特性。

/* simple_init.c

 

   Copyright 2013, Michael Kerrisk

   Licensed under GNU General Public License v2or later

 

   A simple init(1)-style program to be used asthe init program in

   a PID namespace. The program reaps thestatus of its children and

   provides a simple shell facility forexecuting commands.

*/

#define_GNU_SOURCE

#include<unistd.h>

#include<stdio.h>

#include<stdlib.h>

#include<string.h>

#include<signal.h>

#include<wordexp.h>

#include<errno.h>

#include<sys/wait.h>

 

#defineerrExit(msg)    do { perror(msg);exit(EXIT_FAILURE); \

                        } while (0)

 

static int verbose= 0;

 

/* Display waitstatus (from waitpid() or similar) given in 'status' */

 

/* SIGCHLDhandler: reap child processes as they change state */

 

static void

child_handler(intsig)

{

    pid_t pid;

    int status;

 

    /* WUNTRACED and WCONTINUED allow waitpid()to catch stopped and

       continued children (in addition toterminated children) */

 

    while ((pid = waitpid(-1, &status,

                          WNOHANG | WUNTRACED |WCONTINUED)) != 0) {

        if (pid == -1) {

            if (errno == ECHILD)        /* No more children */

                break;

            else

               perror("waitpid");     /* Unexpected error */

        }

 

        if (verbose)

            printf("\tinit: SIGCHLDhandler: PID %ld terminated\n",

                    (long) pid);

    }

}

 

/* Perform wordexpansion on string in 'cmd', allocating and

   returning a vector of words on success orNULL on failure */

 

static char **

expand_words(char*cmd)

{

    char **arg_vec;

    int s;

    wordexp_t pwordexp;

 

    s = wordexp(cmd, &pwordexp, 0);

    if (s != 0) {

        fprintf(stderr, "Word expansionfailed\n");

        return NULL;

    }

 

    arg_vec = calloc(pwordexp.we_wordc + 1,sizeof(char *));

    if (arg_vec == NULL)

        errExit("calloc");

 

    for (s = 0; s < pwordexp.we_wordc; s++)

        arg_vec[s] = pwordexp.we_wordv[s];

 

    arg_vec[pwordexp.we_wordc] = NULL;

 

    return arg_vec;

}

 

static void

usage(char *pname)

{

    fprintf(stderr, "Usage: %s[-q]\n", pname);

    fprintf(stderr, "\t-v\tProvide verboselogging\n");

 

    exit(EXIT_FAILURE);

}

 

int

main(int argc,char *argv[])

{

    struct sigaction sa;

#define CMD_SIZE10000

    char cmd[CMD_SIZE];

    pid_t pid;

    int opt;

 

    while ((opt = getopt(argc, argv,"v")) != -1) {

        switch (opt) {

        case 'v': verbose = 1;          break;

        default:  usage(argv[0]);

        }

    }

 

    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;

    sigemptyset(&sa.sa_mask);

    sa.sa_handler = child_handler;

    if (sigaction(SIGCHLD, &sa, NULL) ==-1)

        errExit("sigaction");

 

    if (verbose)

        printf("\tinit: my PID is%ld\n", (long) getpid());

 

    /* Performing terminal operations while notbeing the foreground

       process group for the terminal generatesa SIGTTOU that stops the

       process. However our init "shell" needs to be able to perform

       such operations (just like a normalshell), so we ignore that

       signal, which allows the operations toproceed successfully. */

 

    signal(SIGTTOU, SIG_IGN);

 

    /* Become leader of a new process group andmake that process

       group the foreground process group forthe terminal */

 

    if (setpgid(0, 0) == -1)

        errExit("setpgid");;

    if (tcsetpgrp(STDIN_FILENO, getpgrp()) ==-1)

        errExit("tcsetpgrp-child");

 

    while (1) {

 

        /* Read a shell command; exit on end offile */

 

        printf("init$ ");

        if (fgets(cmd, CMD_SIZE, stdin) ==NULL) {

            if (verbose)

                printf("\tinit:exiting");

            printf("\n");

            exit(EXIT_SUCCESS);

        }

 

        if (cmd[strlen(cmd) - 1] == '\n')

            cmd[strlen(cmd) - 1] = '\0';        /* Strip trailing '\n' */

 

        if (strlen(cmd) == 0)

            continue;           /* Ignore empty commands */

 

        pid = fork();           /* Create child process */

        if (pid == -1)

            errExit("fork");

 

        if (pid == 0) {         /* Child */

            char **arg_vec;

 

            arg_vec = expand_words(cmd);

            if (arg_vec == NULL)        /* Word expansion failed */

                continue;

 

            /* Make child the leader of a newprocess group and

               make that process group theforeground process

               group for the terminal */

 

            if (setpgid(0, 0) == -1)

                errExit("setpgid");;

            if (tcsetpgrp(STDIN_FILENO,getpgrp()) == -1)

               errExit("tcsetpgrp-child");

 

            /* Child executes shell command andterminates */

 

            execvp(arg_vec[0], arg_vec);

            errExit("execvp");          /* Only reached if execvp() fails */

        }

 

        /* Parent falls through to here */

 

        if (verbose)

            printf("\tinit: created child%ld\n", (long) pid);

 

        pause();                /* Will be interrupted by signalhandler */

 

        /* After child changes state, ensurethat the 'init' program

           is the foreground process group forthe terminal */

 

        if (tcsetpgrp(STDIN_FILENO, getpgrp())== -1)

            errExit("tcsetpgrp-parent");

    }

}

simple_init程序实现init进程的两个主要功能,一个是系统初始化,大多数init程序是复杂的且多半是基于表的方法来初始化系统。我们的simple_init程序提供一个简单的shell工具,该工具使我们能够手动执行需要初始化命名空间的任何命令;该方法使我们能够随意执行shell命令以对命名空间做一些测试。另一个simple_init实现的功能是使用waitpid()获得其子进程的退出状态。

所以可以联合使用ns_child_exec和simple_init来在一个新的PID命名空间中启动init进程:

   # ./ns_child_exec -p ./simple_init

   init$

init$提示符表明simple_init程序能够读取和执行shell命令。

我们将使用上面的两个以及下面的orphan.c示例来证实在PID命名空间中变为孤儿进程将被PID命名空间中的init进程收留,而不是系统的init进程收留。

/* orphan.c

 

   Copyright 2013, Michael Kerrisk

   Licensed under GNU General Public License v2or later

 

   Demonstrate that a child becomes orphaned(and is adopted by init(1),

   whose PID is 1) when its parent exits.

*/

#include<stdio.h>

#include<stdlib.h>

#include<unistd.h>

 

int

main(int argc,char *argv[])

{

    pid_t pid;

 

    pid = fork();

    if (pid == -1) {

        perror("fork");

        exit(EXIT_FAILURE);

    }

 

    if (pid != 0) {             /* Parent */

        printf("Parent (PID=%ld) createdchild with PID %ld\n",

                (long) getpid(), (long) pid);

        printf("Parent (PID=%ld; PPID=%ld)terminating\n",

                (long) getpid(), (long)getppid());

        exit(EXIT_SUCCESS);

    }

 

    /* Child falls through to here */

 

    do {

        usleep(100000);

    }while (getppid() != 1);           /* Am Ian orphan yet? */

 

    printf("\nChild  (PID=%ld) now an orphan (parentPID=%ld)\n",

            (long) getpid(), (long) getppid());

 

    sleep(1);

 

    printf("Child  (PID=%ld) terminating\n", (long) getpid());

    _exit(EXIT_SUCCESS);

}

orphan程序执行一个fork()命令创建子进程。在进程继续执行时父进程会退出;当父进程退出时子进程变成孤儿进程。子进程执行一个循环直到其变为一个孤儿进程(getppid()的返回值是1);一旦子进程变成孤儿进程,它将退出。父子进程打印的信息使我们能够看见当两个子进程退出时刻以及何时子进程变成孤儿进程。

为了更清楚simple_init程序接受孤儿进程的哪些信息,我们使用-v选项,该选项将产生子进程自己产生的各种信息。

    # ./ns_child_exec-p ./simple_init -v

           init: my PID is 1

   init$ ./orphan

           init: created child 2

   Parent (PID=2) created child with PID 3

   Parent (PID=2; PPID=1) terminating

           init: SIGCHLD handler: PID 2terminated

   init$                   #simple_init promptinterleaved with output from child

   Child (PID=3) now an orphan (parent PID=1)

   Child (PID=3) terminating

           init: SIGCHLD handler: PID 3terminated

上面的输出中,以init:开始的信息由simple_init在启动verbose选项时打印出的。其它所有的信息(除了init$提示符)由orphan程序打印。从输出来看,子进程(PID是3)在父进程(PID是2)退出时变成孤儿进程。变成孤儿进程的子进程(PID是3)被命名空间的init进程(PID是1)收留。

信号和init进程

传统的init进程对信号的处理有些特殊。能够发送给init进程的信号是那些已经有信号处理函数的信号。其它所有信号将被忽略。这阻止了init进程被任何用户意外的kill掉,init进程的存在对于系统的稳定是至关重要的。

PID命名空间使命名空间内的init进程实现了和传统init类似的一些行为。命名空间内的其它进程(即使特权进程)仅能够发送init进程建立了处理函数的信号。这防止了命名空间内的其它进程不经意地杀死了命名空间中具有特殊作用的init进程。然而,如同传统的init进程一样,在通常的环境中内核仍然能够为PID命名空间内的init进程产生信号(例如,硬件中断,终端产生的SIGTOU等信号,定时器超时)。

PID命名空间内的祖先进程仍然能够发送信号给子PID命名空间内的Init进程。同样的,只有init设置了对应处理函数的信号才可以被发送给它,除了SIGKILL和SIGSTOP这两个特例。当一个位于祖先PID命名空间的进程发送上述两个特殊信号给init进程时,它们被强行发送。SIGSTOP停止init进程;SIGKILL终结init进程。由于init进程对于PID命名空间如此重要,以至于如果init进程被SIGKILL信号终结掉,内核将对该PID命名空间内的其它所有进程发送SIGKILL信号来终结所有进程。

通常,PID命名空间在init进程终结时将被摧毁,然而,特例是:只要该命名空间内对应的/proc/PID/ns/pid文件被打开或者绑定挂载点,命名空间将不会被摧毁。然而,不太可能使用setns()和fork()在新的命名空间中创建进程:在fork()调用时会检测到缺少init进程,这将会返回ENOMEM错误码(就是PID不能够被分配)。换句话说,PID命名空间仍然存在,但是变得不可用。

挂载一个procfs文件系统

在先前这个系列的文章中,PID命名空间的/proc文件系统procfs被挂载在不同的挂载点(而不是在/proc挂载点),这使得我们可以使用shell命令行载对应的/proc/PID目录下查看每一个新的PID命名空间,同时也可以使用ps命令查看在rootPID命名空间可以看见的进程。

然而像ps之类内容依赖于挂载在/proc目录下的procfs文件以获取它们需要的信息。因此,如果我们想让ps在命名空间中也运行正确,我们需要为命名空间挂载一个procfs。由于simple_init程序允许我们执行shell命令行,我们可以再命令行下使用如下mount命令:

   # ./ns_child_exec -p -m ./simple_init

   init$ mount -t proc proc /proc

   init$ ps a

     PID TTY      STAT  TIME COMMAND

       1 pts/8    S     0:00 ./simple_init

       3 pts/8    R+    0:00 ps a

ps命令列出了通过/proc命令能够存取的所有进程。在这种情况下,我们仅看见两个进程,表明命名空间只有两个进程在运行。

当运行上面的ns_child_exec命令,我们使用了了-m参数,该参数用于将创建的子进程放在一个独立的mount命名空间。这使得mount命令并不会影响命名空间之外的进程看到的/proc挂载点。

unshare()和setns()

自从3.8版本,上面两个系统调用能够被PID命名空间使用,但是有些特殊的地方。

指定CLONE_NEWPID标志使用unshare()创建新的PID命名空间,但是并没有将unshare的调用者放入命名空间中。而调用者创建的任何子进程将被放在新的命名空间中;第一个子进程将是该命名空间的init进程。

setns()系统调用现在支持PID命名空间:

   setns(fd, 0);   /* Second argument can be CLONE_NEWPIDtoforce a

                      check that 'fd' refersto a PID namespace */

fd参数是一个指明PID命名空间的文件描述符,该描述符是PID命名空间调用者的后裔;通过打开目标命名空间中一个进程的/proc/PID/ns/pid文件来获得该文件描述符。unshare()、setns()不将调用者放在创建的命名空间中,而调用者后续创建的子进程将被放入命名空间中。

我们可以使用一个增强型ns_exec.c版本来验证setns()的一些特性。

/* ns_exec.c

 

  Copyright 2013, Michael Kerrisk

  Licensed under GNU General Public License v2 or later

 

  Join a namespace and execute a command in the namespace

*/

#define _GNU_SOURCE

#include <fcntl.h>

#include <sched.h>

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

 

/* A simple error-handling function: printan error message based

  on the value in 'errno' and terminate the calling process */

 

#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \

                        } while (0)

 

int

main(int argc, char *argv[])

{

   int fd;

 

   if (argc < 3) {

       fprintf(stderr, "%s /proc/PID/ns/FILE cmd [arg...]\n",argv[0]);

       exit(EXIT_FAILURE);

   }

 

   fd = open(argv[1], O_RDONLY);   /*Get descriptor for namespace */

   if (fd == -1)

       errExit("open");

 

   if (setns(fd, 0) == -1)         /*Join that namespace */

       errExit("setns");

 

   execvp(argv[2], &argv[2]);     /* Execute a command in namespace */

   errExit("execvp");

}

新的ns_run.c程序如下:

/* ns_run.c

 

  Copyright 2013, Michael Kerrisk

  Licensed under GNU General Public License v2 or later

 

  Join one or more namespaces using setns() and execute a command in

  those namespaces, possibly inside a child process.

 

  This program is similar in concept to nsenter(1), but has a

  different command-line interface.

*/

#define _GNU_SOURCE

#include <fcntl.h>

#include <sched.h>

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <sys/wait.h>

 

/* A simple error-handling function: printan error message based

  on the value in 'errno' and terminate the calling process */

 

#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \

                        } while (0)

 

static void

usage(char *pname)

{

   fprintf(stderr, "Usage: %s [-f] [-n /proc/PID/ns/FILE] cmd[arg...]\n",

           pname);

   fprintf(stderr, "\t-f    Execute command in child process\n");

   fprintf(stderr, "\t-n    Join specified namespace\n");

 

   exit(EXIT_FAILURE);

}

 

int

main(int argc, char *argv[])

{

   int fd, opt, do_fork;

   pid_t pid;

 

   /* Parse command-line options. The initial '+' character in

      the final getopt() argument prevents GNU-style permutation

      of command-line options. That's useful, since sometimes

      the 'command' to be executed by this program itself

      has command-line options. We don't want getopt() to treat

      those as options to this program. */

 

   do_fork = 0;

   while ((opt = getopt(argc, argv, "+fn:")) != -1) {

       switch (opt) {

 

       case 'n':       /* Join anamespace */

           fd = open(optarg, O_RDONLY); /* Get descriptor for namespace */

           if (fd == -1)

                errExit("open");

 

           if (setns(fd, 0) == -1)      /* Join that namespace */

                errExit("setns");

           break;

 

       case 'f':

           do_fork = 1;

           break;

 

       default:

           usage(argv[0]);

       }

   }

 

   if (argc <= optind)

       usage(argv[0]);

 

   /* If the "-f" option was specified, execute the suppliedcommand

      in a child process. This is mainly useful when working with PID

      namespaces, since setns() to a PID namespace only places

      (subsequently created) child processes in the names, and

      does not affect the PID namespace membership of the caller. */

 

   if (do_fork) {

       pid = fork();

       if (pid == -1)

           errExit("fork");

 

       if (pid != 0) {                 /*Parent */

           if (waitpid(-1, NULL, 0) == -1)    /* Wait for child */

                errExit("waitpid");

           exit(EXIT_SUCCESS);

       }

 

       /* Child falls through to code below */

   }

 

   execvp(argv[optind], &argv[optind]);

    errExit("execvp");

}

ns_run [-f][-n/proc/PID/ns/FILE]... command [arguments]

该程序使用setns()关联由/proc/PID/ns(-n选项指定)文件指定的命名空间。然后其执行给定的command命令并将参数arguments传递给它,如果指定了-f参数,它使用fork()创建的子进程执行上面的命令。

考虑一个场景,在一个终端窗口,我们在一个新的PID命名空间中启动simple_init程序,启动verbose选项记录日志以方便我们知道何时子进程被收留:

   # ./ns_child_exec -p ./simple_init -v

           init: my PID is 1

   init$ 

然后我们切到第二个终端,这里我们使用ns_run程序来执行我们的orphan程序。这个效果就是在simple_init管理的PID命名空间中创建两个子进程。

   # ps -C sleep -C simple_init

     PID TTY          TIME CMD

    9147 pts/8    00:00:00 simple_init

    # ./ns_run -f -n /proc/9147/ns/pid./orphan

    Parent (PID=2) created child with PID 3

    Parent (PID=2; PPID=0) terminating

    #

    Child (PID=3) now an orphan (parent PID=1)

    Child (PID=3) terminating

来看“父”进程(PID=2)的输出,可以知道其父进程ID是0。这揭示了和启动orphan进程位于不同命名空间的事实。

下图揭示了再Oprah的"父"进程终结之前的进程关系。箭头表明了进程间的父子关系。


回到创建simple_init 程序的窗口,可以看到如下的输出:

  init: SIGCHLD handler: PID 3 terminated

由orphan创建的子进程(PID等于3)由simple_init 收留,但是其“父”进程(PID=2)并不是这样,这是因为“父”进程由在另一个命名空间中它自己的父进程ns_run收留。下图这展示了orphan“父进程”结束但是“子”进程还未结束时的进程关系:


值得强调的是setns()和unshare()处理PID命名空间的方式不同。对其它类型的命名空间,这些系统调用确实改变了调用这些函数的进程的命名空间。不改变这些调用者的PID命名空间的原因是:成为其它PID命名空间的进程将导致进程自己的PID改变,这是由于getpid()返回的是进程所在PID命名空间的进程PID。许多用户空间的程序和库依赖一个进程的PID是确定的这一假设;如果进程的PID改变可能导致这些程序失败。换句话说,进程的PID命名空间在进程创建时确定,且后续不会改变。

总结

本文,我们查看了PID命名空间init进程,展示了为了让ps之类的工具可用而如何为一个PID命名空间挂载一个procfs。此外,还查看了unshare和setns对PID命名空间特别的地方。

网络 namespaces

网络命名空间,正如从名字可以联想到的,网络命名空间分割了网络资源-设备、地址、端口、路由、防火墙规则等。网络命名空间出现在2.6.24内核版本,那几乎是五年前的事了;在一年前开发好了从那时起也被很多开发这忽略了。

网络命名空间的基本管理

正如其它命名空间,网络命名空间通过传递CLONE_NEWNET标志给clone()系统调用来创建。通过命令行工作ip很容易对网络命名空间进行相关的操作,例如:

   # ip netns add netns1

上面的命令创建一个叫netns1的网络命名空间。当ip工具创建一个网络命名空间,其将在/var/run/netns目录下创建一个绑定的mount点;这样即使命名空间中没有进程运行命名空间仍然存在,并且也简化的命名空间自身的操作。由于网络命名空间在可用前需要进行相当数量的配置工作,系统管理这真该感谢该特性。

“ip netns exec”命令能被用于在一个命名空间中执行网络管理命令:

   # ip netns exec netns1 ip link list

   1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT

        link/loopback00:00:00:00:00:00 brd 00:00:00:00:00:00

这个命令列出了命名空间可以看见的接口。一个网络命名空间的移除使用如下命令:

   # ip netns delete netns1

该命令删除命名空间对应的挂载点。然而,只要命名空间内有进程在运行命名空间自身将持续存在。

网络命名空间配置

新的网络命名空间将仅有一个回环设备。除了回环设备,每一个网络设备(物理和虚拟接口,桥等)仅仅只能存在于一个网络命名空间。此外,除了root不能够将物理设备(没有连接到真实硬件)分配到命名空间。相反,虚拟网络设备(虚拟以太网或者veth)能够被创建并分配到命名空间中。这些虚拟设备使得命名空间内的进程能够通过网络进行通信;如何通信则依赖于配置、路由等。

当第一次被创建时,在命名空间中的lo回环设备是down的状态,所以即使一个ping命令也会失败:

   # ip netns exec netns1 ping 127.0.0.1

   connect: Network is unreachable

启动该接口将允许ping回环设备的地址:

   # ip netns exec netns1 ip link set dev lo up

   # ip netns exec netns1 ping 127.0.0.1

   PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.

   64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.051 ms

   ...

但这仍然不能是netns1命名空间和root命名空间通信。为了实现它们间通信的目的,虚拟网络设备需要被创建和配置:

   # ip link add veth0 type veth peer name veth1

   # ip link set veth1 netns netns1

第一个命令设置了连接的一对虚拟网络设备。发送给veth0数据包将会被veth1收到,反之亦然。第二个命令将veth1的网络命名空间设为netns1。

   # ip netns exec netns1 ifconfig veth1 10.1.1.1/24 up

   # ifconfig veth0 10.1.1.2/24 up

然后,这两个命令设置两个设备的IP地址。

   # ping 10.1.1.1

   PING 10.1.1.1 (10.1.1.1) 56(84) bytes of data.

   64 bytes from 10.1.1.1: icmp_seq=1 ttl=64 time=0.087 ms

   ...

   

    #ip netns exec netns1 ping 10.1.1.2

   PING 10.1.1.2 (10.1.1.2) 56(84) bytes of data.

   64 bytes from 10.1.1.2: icmp_seq=1 ttl=64 time=0.054 ms

   ...

两个方向的通信现在变得可行了,上述的ping命令已经验证了。

正如先前提到的,命名空间并不共享路由表或者防火墙规则,如在netns1中运行route和iptables命令。

    # ipnetns exec netns1 route

   # ip netns exec netns1 iptables -L

第一个命令显示了数据包路由到10.1.1子网的路由,第二个命令则表明没有防火墙配置。所有的那些意味着从netns1送到网络上的数据包过大将会产生“网络不可达”错误。如果需要还有几个方法将命名空间连接上网。可以再在root命名空间和netns1命名空间为veth设备创建网桥。另外,IP转发和网络地址转换(NAT)可以再root命名空间中被配置。这两种方法将允许从网络接收数据包,也运行netns1发送数据包到网络。

被分配到命名空间的非root进程(通过clone、unshare、setns)仅能访问命名空间内已经设置的网络设备。当然,root能够添加并配置新的网络设备。使用 ip netns 前缀的命令,有两种方法来寻址一个命名空间:通过名字,如netns1,或者通过进程ID。因为init通常存在于root命名空间,可以使用如下的命令:

   # ip link set vethX netns 1

这将在root命名空间中创建一个新的veth设备,并且该设备可以为任何其他命名空间root所用。在并不希望root拥有操作某个命名空间网络设备的权限时,PID和mount命名空间可以用来完成此目的。

网络命名空间使用

正如我们看到的,一个命名空间的网络可以从不能进行网络通信到使用所有的的网络带宽进行通信。这导致了网络命名空间的许多不同的使用案例。

通过关闭一个命名空间的网络,管理员可以确保命名空间中运行的进程不能和命名空间之外的程序进行通信。即使一个进程牺牲一些安全性,其也不能够执行一些像关联僵尸网络或者发送垃圾邮件之类的动作。

即使处理网络流量的进程(web服务器工作者进程或者web浏览渲染进程等)可以被放置于一个特定的命名空间。一旦和远端的连接建立,连接的文件描述符可以被由clone()创建的新的网络命名空间内的子进程处理。子进程会继承父进程的文件描述符,这就可以存取连接的描述符了。另外一个可能的情况是父进程通过UNIX套接字将连接的文件描述符发送到一个给定的命名空间。不论哪种方式,命名空间中缺少合适的网络设备将使子进程或者工作进程的其它网络连接失败。

命名空间同样可以被用来在所有的网络命名空间中测试复杂的网络配置。在锁定的,限定防火墙的命名空间中运行敏感的服务同样也是一个使用案例。很明显,container依赖网络命名空间为每一个container提供各自的网络视图。

命名空间提供了一个分割系统资源以及将进程组的进程与它们的资源隔离的方法。网络命名空间也是这样,但是由于网络是安全敏感的一个领域,提供各种类型的隔离具有很到的价值。当然,使用多种类型的命名空间可以实现超越安全和其它应用需要的隔离。

原文网址

http://lwn.net/Articles/531114/

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值