Linux Namespace : UTS


UTS namespace 用來隔離系統的 hostname 以及 NIS domain name。UTS 據稱是 UNIX Time-sharing System 的縮寫。

hostname 與 NIS domain name

hostname 是用來標識一台主機的,比如登錄時的提示,在 Shell 的提示符上,都可以顯示出來,這樣的話,使用者可以知道自己用的是哪台機器。比如下圖中的 nick@tigger:

nick 是用戶名,而 tigger 就是主機的 hostname。我們可以通過 hostname 命令來查看當前主機的名稱,比如上圖中的輸出:tigger。本質上,hostname 命令是通過執行系統調用 gethostname 來獲得 hostname 的,我們在本文的結尾處會分析 gethostname 的相關實現。

NIS domain name
在一些大型的網絡中,會有很多的 Linux 主機,如果能夠有一部賬號主控服務器來管理網絡中所有主機的賬號, 當其他的主機有用戶登入的需求時,才到這部主控服務器上面請求相關的賬號、密碼等用戶信息, 如此一來,如果想要增加、修改、刪除用戶數據,只要到這部主控服務器上面處理即可(聽起來是不是有點類似 windows 平台上的域控制器的概念)。
在 Linux 平台上,一般通過 Network Information Services(NIS Server) 創建的域(domain)來實現相關的功能。而主機的 NIS domain name 就是加入 NIS domain 的主機顯示的 NIS domain 的名稱(類似 windows 平台上使用域控制器創建的域名)。

簡單起見,本文以 hostname 為例進行 Linux UTS namespace 的介紹。文中的 demo 均在 ubuntu 16.04 中完成。

通過 clone 函數創建 UTS 隔離的子進程

我們在《Linux Namespace 簡介》一文中介紹了 clone 函數用於在創建新進程的同時創建 namespace,下面的 demo 就是通過 clone 函數為新進程創建新的 UTS namespace(該 demo 的主要代碼來自 clone 函數的 man page,為了進行演示,筆者進行了適當的調整和擴展):

#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>

#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); } while (0)

// 調用 clone 時執行的函數
static int childFunc(void *arg)
{
    struct utsname uts;
    char *shellname;
    // 在子進程的 UTS namespace 中設置 hostname
    if (sethostname(arg, strlen(arg)) == -1)
        errExit("sethostname");

    // 顯示子進程的 hostname
    if (uname(&uts) == -1)
        errExit("uname");
    printf("uts.nodename in child:  %s\n", uts.nodename);
    printf("My PID is: %d\n", getpid());
    printf("My parent PID is: %d\n", getppid());
    // 獲取系統的默認 shell
    shellname = getenv("SHELL");
    if(!shellname){
        shellname = (char *)"/bin/sh";
    }
    // 在子進程中執行 shell
    execlp(shellname, shellname, (char *)NULL);

    return 0;
}
// 設置子進程的堆棧大小為 1M
#define STACK_SIZE (1024 * 1024)

int main(int argc, char *argv[])
{
    char *stack;
    char *stackTop;        
    pid_t pid;

    if (argc < 2) {
        fprintf(stderr, "Usage: %s <child-hostname>\n", argv[0]);
        exit(EXIT_SUCCESS);
    }

    // 為子進程分配堆棧空間,大小為 1M
    stack = malloc(STACK_SIZE);
    if (stack == NULL)
        errExit("malloc");
    stackTop = stack + STACK_SIZE;  /* Assume stack grows downward */

    // 通過 clone 函數創建子進程
    // CLONE_NEWUTS 標識指明為新進程創建新的 UTS namespace
    pid = clone(childFunc, stackTop, CLONE_NEWUTS | SIGCHLD, argv[1]);
    if (pid == -1)
        errExit("clone");

    // 等待子進程退出
    if (waitpid(pid, NULL, 0) == -1)
        errExit("waitpid");
    printf("child has terminated\n");

    exit(EXIT_SUCCESS);
}

這段代碼中的 main 函數負責調用 clone 函數創建一個子進程。在調用 clone 函數時通過設置 CLONE_NEWUTS 標識讓子進程擁有自己的 UTS namespace。 子進程執行 childFunc 函數,它先設置新的 hostname,然后分別輸出 hostname,當前進程的 PID 和 父進程的 PID,並在最后執行系統的默認 shell。父進程等待子進程的退出,並最終退出程序。把上面的代碼保存在文件 uts_clone.c 文件中,並執行下面的命令進行編譯:

$ gcc -Wall uts_clone.c -o uts_clone_demo

然后以 myhost 為參數運行 demo 程序:

$ sudo ./uts_clone_demo

注意圖中第二個紅框,hostname 已經成了 myhost。我們在當前的 shell 中執行 hostname 命令,得到的結果也是 myhost。
下面讓我們確認新創建的子進程和父進程分別屬於不同的 UTS namespace。具體的做法是查看 /proc 目錄中相關進程目錄下的 ns/uts 鏈接文件。讓我們新打開一個命令行終端,以管理員權限運行下面 3 個命令(注意,在執行下面命令的同時請不要退出 demo 程序):

第一條命令查看當前 shell 進程的 uts namespace。
第二條命令查看 demo 程序中父進程的 uts namespace(父進程 PID 來自 demo 程序的輸出)。
第三條命令查看 demo 程序中子進程的 uts namespace(子進程 PID 來自 demo 程序的輸出)。
前兩條命令的輸出是相同的,它們都使用了系統默認的 uts namespace。而第三條命令的輸出則說明 demo 中的子進程使用了和父進程不同的 uts namespace。

把當前進程加入到已存在的 UTS namespace

和 clone 函數一樣,我們在前文中也介紹了通過 setns 函數可以將當前進程加入到已有的 namespace 中,下面的 demo 把當前進程加入到已有 UTS namespace(該 demo 的主要代碼來自 setns 函數的 man page):

#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

#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 args...\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    // 打開一個現存的 UTS namespace 文件
    fd = open(argv[1], O_RDONLY);
    if (fd == -1)
        errExit("open");

    // 把當前進程的 UTS namespace 設置為命令行參數傳入的 namespace
    if (setns(fd, 0) == -1)        
        errExit("setns");

    // 在新的 UTS namespace 中運行用戶指定的程序
    execvp(argv[2], &argv[2]);
    errExit("execvp");
}

代碼的邏輯很簡單,通過 open 函數打開用戶傳入的 UTS namespace 文件,然后把得到的文件描述符傳遞給 setns 函數。最后執行用戶指定的程序。
所以執行上面的程序需要我們傳入兩個參數,第一個參數是一個已經存在的 UTS namespace 文件,第二個參數是指定要運行的程序。下面我們將結合前面的 uts_clone_demo 進行演示。把上面的代碼保存到文件 uts_setns.c 文件中,並執行下面的命令進行編譯:

$ gcc -Wall uts_setns.c -o uts_setns_demo

接下來的思路是:運行 uts_clone_demo 程序創建一個新的 UTS namespace,然后把運行 uts_setns_demo 程序的進程加入到這個新的 UTS namespace 中,並運行 shell 命令。

$ sudo ./uts_clone_demo myhost

需要記住進程的 PID,這里是 96074,需要為 uts_setns_demo 指定 UTS namespace 文件的路徑:

$ sudo ./uts_setns_demo /proc/96074/ns/uts ${SHELL}

執行上的命令會把運行 uts_setns_demo 程序的 UTS namespace 設置為 uts:[4026532540],並運行 shell 程序:

上圖中的 hostname 已經變成了 myhost,並且執行 readlink /proc/$$/ns/uts 命令的結果也和我們預期的相同。

把當前進程加入到一個新建的 UTS namespace

我們要介紹的最后一個函數是 unshare 。它可以創建新的 namespace,並把當前進程加入到這個 namespace 中。下面我們依然通過 demo 程序演示 unshare 對 UTS namespace 的操作(該 demo 的主要代碼來自 unshare 函數的 man page):

#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

#define errExit(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 can be:\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");
}

其實上面的代碼可以根據參數實現幾乎所有 namespace 的隔離,這里我們僅用它來演示對 UTS namespace 的隔離。把代碼保存到文件 uts_unshare.c 文件中,並執行下面的命令進行編譯:

$ gcc -Wall uts_unshare.c -o uts_unshare_demo

接下來運行新創建的程序 uts_unshare_demo:

我們為 uts_unshare_demo 指定了參數 -u,它會把當前的進程加入到一個新的 UTS namespace 中,並讓它運行一個 shell 程序。如上圖中的紅框所示,通過對比 readlink /proc/$$/ns/uts 命令的輸出,我們可以確定運行 uts_unshare_demo 的進程加入了新的 UTS namespace。

UTS namespace 的實現方式

在新版(區別2.6)的 linux 內核中(比如筆者查看的 v4.13),定義進程的結構體 task_struct 包含一個名為 nsproxy 的字段。該字段用來保存於 namespace 相關的信息(/include/linux/sched.h):

而 nsproxy 結構體的定義如下(/include/linux/nsproxy.h):

至於其中的 uts_namespace 結構體這里就不展開了,有興趣的朋友可以自己去代碼中查看。下面我們看看 gethostname 系統調用的大概實現(/kernel/sys.c):

SYSCALL_DEFINE2(gethostname, char __user *, name, int, len)
{
    struct new_utsname *u;
    …
    u = utsname();
    …
    if (copy_to_user(name, u->nodename, i))
        errno = -EFAULT;
    …
}

而 utsname 方法的實現如下(/include/linux/utsname.h):

其實,不管是 gethostname 系統調用還是 uname 系統調用,只要是返回了 hostname 的函數,最后總要落到 utsname 函數的調用上。

總結

對於 linux namespace 的學習總算是邁出了第一步,雖然參考了很多的資料和文章,但一路下來還是感覺很不輕松。學習 linux namespace 的目的主要是想更好的理解和掌握容器技術,並希望能夠通過進一步的學習和分享加深對 Linux 系統的了解。文中如有不當之處,還請朋友們多多指教!

參考:
Linux Namespace系列(02):UTS namespace (CLONE_NEWUTS)
man 2 clone
man 2 setns
man 2 unshare


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM