1. 程式人生 > >namespace名稱空間-之使用

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成為本文接下來討論的主題。

系列博文索引

下面的列表給出了後續的系列博文。

demo_uts_s.c:示例了UTS名稱空間的使用方法。
ns_exec.c: 使用setns()關聯一個名稱空間並且執行該命令。

unshare.c: 停止名稱空間的共享並執行命令。

ns_child_exec.c: 建立一個子程序在新的名稱空間中執行命令
simple_init.c: 簡單的init型別的程式,用於在PID名稱空間中使用。

orphan.c: 證明一個子程序變為孤兒程序後,將被init程序收留

ns_run.c: 使用setns()關聯一個或多個名稱空間,並且在這些名稱空間中執行一個命令,使用場景很可能是在子程序中。

demo_userns.c: 建立名稱空間的簡單程式,並且顯示了程式的許可權和能力
userns_child_exec.c: 建立一個在一個新的名稱空間執行shell命令的子程序,類似於ns_child_exec.c,但是提供了使用者名稱空間的額外選項。

名稱空間 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