Linux Namespace系列(02):UTS namespace (CLONE_NEWUTS)

UTS namespace用來隔離系統的hostname以及NIS domain name。html

這兩個資源能夠經過sethostname(2)和setdomainname(2)函數來設置,以及經過uname(2), gethostname(2)和getdomainname(2)函數來獲取.(這裏括號中的2表示這個函數是system call,具體其餘數字的含義請參看man的幫助文件)node

術語UTS來自於調用函數uname()時用到的結構體: struct utsname. 而這個結構體的名字源自於"UNIX Time-sharing System".linux

因爲UTS namespace最簡單,因此放在最前面介紹,在這篇文章中咱們將會熟悉UTS namespace以及和namespace相關的三個系統調用的使用。git

注意: NIS domain name和DNS沒有關係,關於他的介紹能夠看這裏,因爲本人對它不太瞭解,因此在本文中不作介紹。github

下面的全部例子都在ubuntu-server-x86_64 16.04下執行經過shell

建立新的UTS namespace

多說無益,直接上代碼,我儘可能將註釋寫的足夠詳細,請仔細看代碼和輸出結果ubuntu

注意:bash

  1. 爲了代碼簡單起見,只在clone函數那作了錯誤處理,關於clone函數的詳細介紹請參考man-pagesdom

  2. 爲了描述方便,某些地方會用hostname來區分UTS namespace,如hostname爲container001的namespace,將會被描述成namespace container001。ssh

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

#define NOT_OK_EXIT(code, msg); {if(code == -1){perror(msg); exit(-1);} }

//子進程從這裏開始執行
static int child_func(void *hostname)
{
    //設置主機名
    sethostname(hostname, strlen(hostname));

    //用一個新的bash來替換掉當前子進程,
    //執行完execlp後,子進程沒有退出,也沒有建立新的進程,
    //只是當前子進程再也不運行本身的代碼,而是去執行bash的代碼,
    //詳情請參考"man execlp"
    //bash退出後,子進程執行完畢
    execlp("bash", "bash", (char *) NULL);

    //從這裏開始的代碼將不會被執行到,由於當前子進程已經被上面的bash替換掉了

    return 0;
}

static char child_stack[1024*1024]; //設置子進程的棧空間爲1M

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

    if (argc < 2) {
        printf("Usage: %s <child-hostname>\n", argv[0]);
        return -1;
    }

    //建立並啓動子進程,調用該函數後,父進程將繼續日後執行,也就是執行後面的waitpid
    child_pid = clone(child_func,  //子進程將執行child_func這個函數
                    //棧是從高位向低位增加,因此這裏要指向高位地址
                    child_stack + sizeof(child_stack),
                    //CLONE_NEWUTS表示建立新的UTS namespace,
                    //這裏SIGCHLD是子進程退出後返回給父進程的信號,跟namespace無關
                    CLONE_NEWUTS | SIGCHLD,
                    argv[1]);  //傳給child_func的參數
    NOT_OK_EXIT(child_pid, "clone");

    waitpid(child_pid, NULL, 0); //等待子進程結束

    return 0;    //這行執行完以後,父進程結束
}

在上面的代碼中:

  • 父進程建立新的子進程,而且設置CLONE_NEWUTS,這樣就會建立新的UTS namespace而且讓子進程屬於這個新的namespace,而後父進程一直等待子進程退出

  • 子進程在設置好新的hostname後被bash替換掉

  • 當bash退出後,子進程退出,接着父進程也退出

下面看看輸出效果

#------------------------第一個shell窗口------------------------
#將上面的代碼保存爲namespace_uts_demo.c, 
#而後用gcc將它編譯成可執行文件namespace_uts_demo
dev@ubuntu:~/code$ gcc namespace_uts_demo.c -o namespace_uts_demo   

#啓動程序,傳入參數container001
#建立新的UTS namespace須要root權限,因此用到sudo
dev@ubuntu:~/code$ sudo ./namespace_uts_demo container001

#新的bash被啓動,從shell的提示符能夠看出,hostname已經被改爲了container001
#這裏bash的提示符是‘#’,表示bash有root權限,
#這是由於咱們是用sudo來運行的程序,因而咱們程序建立的子進程有root權限
root@container001:~/code#

#用hostname命令再確認一下
root@container001:~/code# hostname
container001

#pstree是用來查看系統中進程之間父子關係的工具
#下面的輸出過濾掉了跟namespace_uts_demo無關的內容
#本次操做是經過ssh客戶端遠程鏈接到Linux主機進行的,
#因此bash(24429)的父進程是一系列的sshd進程,
#咱們在bash(24429)裏面執行了sudo ./namespace_uts_demo container001
#因此有了sudo(27332)和咱們程序namespace_uts_d(27333)對應的進程,
#咱們的程序本身clone了一個新的子進程,因爲clone的時候指定了參數CLONE_NEWUTS,
#因此新的子進程屬於一個新的UTS namespace,而後這個新進程調用execlp後被bash替換掉了,
#因而有了bash(27334), 這個bash進程擁有全部當前子進程的屬性, 
#因爲咱們的pstree命令是在bash(27334)裏面運行的,
#因此這裏pstree(27345)是bash(27334)的子進程
root@container001:~/code# pstree -pl
systemd(1)───sshd(24351)───sshd(24428)───bash(24429)───sudo(27332)──
─namespace_uts_d(27333)───bash(27334)───pstree(27345)

#驗證一下咱們運行的bash進程是否是bash(27334)
#下面這個命令能夠輸出當前bash的PID
root@container001:~/code# echo $$
27334

#驗證一下咱們的父進程和子進程是否不在同一個UTS namespace
root@container001:~/code# readlink /proc/27333/ns/uts
uts:[4026531838]
root@container001:~/code# readlink /proc/27334/ns/uts
uts:[4026532445]
#果真不屬於同一個UTS namespace,說明新的uts namespace建立成功

#默認狀況下,子進程應該繼承父進程的namespace
#systemd(1)是咱們程序父進程namespace_uts_d(27333)的祖先進程,
#他們應該屬於同一個namespace
root@container001:~/code# readlink /proc/1/ns/uts
uts:[4026531838]

#全部bash(27334)裏面執行的進程應該和bash(27334)屬於一樣的namespace
#self指向當前運行的進程,在這裏即readlink進程
root@container001:~/code# readlink /proc/self/ns/uts
uts:[4026532445]

#------------------------第二個shell窗口------------------------
#從新打開一個新的shell窗口,確認這個shell和上面的namespace_uts_d(27333)屬於同一個namespace
dev@ubuntu:~/code$ readlink /proc/$$/ns/uts
uts:[4026531838]

#老的namespace中的hostname仍是原來的,不受新的namespace影響
dev@ubuntu:~/code$ hostname     
ubuntu
#有興趣的同窗能夠在兩個shell窗口裏面分別用命令hostname設置hostname試試,
#會發現他們兩個之間相互不受影響,這裏就不演示了


#------------------------第一個shell窗口------------------------
#繼續回到原來的shell,試試在container001裏面再運行一下那個程序會怎樣
root@container001:~/code# ./namespace_uts_demo container002

#建立了一個新的UTS namespace,hostname被改爲了container002
root@container002:~/code#
root@container002:~/code# hostname
container002

#新的UTS namespace
root@container002:~/code# readlink /proc/$$/ns/uts
uts:[4026532455]

#進程間的關係和上面的差很少,在後面又生成了namespace_uts_d(27354)和bash(27355)
root@container002:~/code# pstree -pl
systemd(1)───sshd(24351)───sshd(24428)───bash(24429)───sudo(27332)──
─namespace_uts_d(27333)───bash(27334)───namespace_uts_d(27354)──
─bash(27355)───pstree(27367)

#退出bash(27355)後,它的父進程namespace_uts_d(27354)也接着退出,
#因而又回到了進程bash(27334)中,hostname因而也回到了container001
#注意: 在bash(27355)退出的過程當中,並無任何進程的namespace發生變化,
#只是全部屬於namespace container002的進程都執行完退出了
root@container002:~/code# exit
exit
root@container001:~/code#
root@container001:~/code# hostname
container001

將當前進程加入指定的namespace

仍是直接上代碼,有了前面的鋪墊,這裏的代碼就很是簡單了,請仔細看代碼和輸出結果

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

#define NOT_OK_EXIT(code, msg); {if(code == -1){perror(msg); exit(-1);} }

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

    if (argc < 2) {
        printf("%s /proc/PID/ns/FILE\n", argv[0]);
        return -1;
    }

    //獲取namespace對應文件的描述符
    fd = open(argv[1], O_RDONLY);
    NOT_OK_EXIT(fd, "open");

    //執行完setns後,當前進程將加入指定的namespace
    //這裏第二個參數爲0,表示由系統本身檢測fd對應的是哪一種類型的namespace
    ret = setns(fd, 0);
    NOT_OK_EXIT(ret, "open");

    //用一個新的bash來替換掉當前子進程
    execlp("bash", "bash", (char *) NULL);

    return 0;
}

在上面的代碼中,程序經過setns調用讓本身加入到參數指定的namespace中,而後用bash替換掉本身,開始執行bash。

再來看結果

#--------------------------第一個shell窗口----------------------
#重用上面建立的namespace container001
#先確認一下hostname是否正確,
root@container001:~/code# hostname
container001

#獲取bash的PID
root@container001:~/code# echo $$
27334

#獲得bash所屬的UTS namespace
root@container001:~/code# readlink /proc/27334/ns/uts
uts:[4026532445]



#--------------------------第二個shell窗口----------------------
#從新打開一個shell窗口,將上面的代碼保存爲文件namespace_join.c並編譯
dev@ubuntu:~/code$ gcc namespace_join.c -o namespace_join

#運行程序前,確認下當前bash不屬於namespace container001
dev@ubuntu:~/code$ hostname
ubuntu
dev@ubuntu:~/code$ readlink /proc/$$/ns/uts
uts:[4026531838]

#執行程序,使其加入第一個shell窗口中的bash所在的namespace
#27334是第一個shell窗口中bash的pid
dev@ubuntu:~/code$ sudo ./namespace_join /proc/27334/ns/uts
root@container001:~/code#

#加入成功,bash提示符裏面的hostname以及UTS namespace的inode number和第一個shell窗口的都同樣
root@container001:~/code# hostname
container001
root@container001:~/code# readlink /proc/$$/ns/uts
uts:[4026532445]

退出當前namespace並加入新建立的namespace

繼續看代碼

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

#define NOT_OK_EXIT(code, msg); {if(code == -1){perror(msg); exit(-1);} }

static void usage(const char *pname)
{
    char usage[] = "Usage: %s [optins]\n"
                   "Options are:\n"
                   "    -i   unshare IPC namespace\n"
                   "    -m   unshare mount namespace\n"
                   "    -n   unshare network namespace\n"
                   "    -p   unshare PID namespace\n"
                   "    -u   unshare UTS namespace\n"
                   "    -U   unshare user namespace\n";
    printf(usage, pname);
    exit(0);
}

int main(int argc, char *argv[])
{
    int flags = 0, opt, ret;

    //解析命令行參數,用來決定退出哪一個類型的namespace
    while ((opt = getopt(argc, argv, "imnpuUh")) != -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 'h': usage(argv[0]);               break;
            default:  usage(argv[0]);
        }
    }

    if (flags == 0) {
        usage(argv[0]);
    }

    //執行完unshare函數後,當前進程就會退出當前的一個或多個類型的namespace,
    //而後進入到一個或多個新建立的不一樣類型的namespace
    ret = unshare(flags);
    NOT_OK_EXIT(ret, "unshare");

    //用一個新的bash來替換掉當前子進程
    execlp("bash", "bash", (char *) NULL);

    return 0;
}

看運行效果:

#將上面的代碼保存爲文件namespace_leave.c並編譯
dev@ubuntu:~/code$ gcc namespace_leave.c -o namespace_leave

#查看當前bash所屬的UTS namespace
dev@ubuntu:~/code$ readlink /proc/$$/ns/uts
uts:[4026531838]

#執行程序, -u表示退出並加入新的UTS namespace
dev@ubuntu:~/code$ sudo ./namespace_leave -u
root@ubuntu:~/code#

#再次查看UTS namespace,已經變了,說明已經離開原來的namespace並加入了新的namespace
#細心的同窗可能已經發現這裏的inode number恰好和上面namespace container002的相同,
#這說明在container002被銷燬後,inode number被回收再利用了
root@ubuntu:~/code# readlink /proc/$$/ns/uts
uts:[4026532455]

#反覆執行幾回,獲得相似的結果
root@ubuntu:~/code# ./namespace_leave -u
root@ubuntu:~/code# readlink /proc/$$/ns/uts
uts:[4026532456]
root@ubuntu:~/code# ./namespace_leave -u
root@ubuntu:~/code# readlink /proc/$$/ns/uts
uts:[4026532457]
root@ubuntu:~/code# ./namespace_leave -u
root@ubuntu:~/code# readlink /proc/$$/ns/uts
uts:[4026532458]

內核中的實現

上面演示了這三個函數的功能,那麼UTS namespace在內核中又是怎麼實現的呢?

在老版本中,UTS相關的信息保存在一個全局變量中,全部進程都共享這個全局變量,gethostname()的實現大概以下

asmlinkage long sys_gethostname(char __user *name, int len)
{
  ...
  if (copy_to_user(name, system_utsname.nodename, i))
    errno = -EFAULT;
  ...
}

在新的Linux內核中,在每一個進程對應的task結構體struct task_struct中,增長了一個叫nsproxy的字段,類型是struct nsproxy

struct task_struct {
  ...
  /* namespaces */
  struct nsproxy *nsproxy;
  ...
}

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;
  struct cgroup_namespace *cgroup_ns;
};

因而新的gethostname()的實現大概就是這樣

static inline struct new_utsname *utsname(void)
{
  //current指向當前進程的task結構體
  return &current->nsproxy->uts_ns->name;
}

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

處於不一樣UTS namespace中的進程,它task結構體裏面的nsproxy->uts_ns所指向的結構體是不同的,因而達到了隔離UTS的目的。

其餘類型的namespace基本上也是差很少的原理。

總結

  • namespace的本質就是把原來全部進程全局共享的資源拆分紅了不少個一組一組進程共享的資源

  • 當一個namespace裏面的全部進程都退出時,namespace也會被銷燬,因此拋開進程談namespace沒有意義

  • UTS namespace就是進程的一個屬性,屬性值相同的一組進程就屬於同一個namespace,跟這組進程之間有沒有親戚關係無關

  • clone和unshare都有建立並加入新的namespace的功能,他們的主要區別是:

    • unshare是使當前進程加入新建立的namespace

    • clone是建立一個新的子進程,而後讓子進程加入新的namespace

  • UTS namespace沒有嵌套關係,即不存在說一個namespace是另外一個namespace的父namespace

參考

相關文章
相關標籤/搜索