Linux Namespace : UTS

UTS namespace 用来隔离系统的 hostname 以及 NIS domain name。UTS 据称是 UNIX Time-sharing System 的缩写。

hostname 与 NIS domain name

hostname 是用来标识一台主机的,比如登录时的提示,在 Shell 的提示符上,都可以显示出来,这样的话,使用者可以知道自己用的是哪台机器。比如下图中的 [email protected]:

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

原文地址:https://www.cnblogs.com/sparkdev/p/9377072.html

时间: 2024-10-28 18:07:43

Linux Namespace : UTS的相关文章

Docker之Linux Namespace

Linux Namespace 介绍 我们经常听到说Docker 是一个使用了Linux Namespace 和 Cgroups 的虚拟化工具,但是什么是Linux Namespace 它在Docker内是怎么被使用的,说到这里很多人就会迷茫,下面我们就先介绍一下Linux Namespace 以及它们是如何在容器里面使用的. 概念 Linux Namespace 是kernel 的一个功能,它可以隔离一系列系统的资源,比如PID(Process ID),User ID, Network等等.一

Docker基础技术:Linux Namespace(上)

导读 时下最热的技术莫过于Docker了,很多人都觉得Docker是个新技术,其实不然,Docker除了其编程语言用go比较新外,其实它还真不是个新东西,也就是个新瓶装旧酒的东西,所谓的The New “Old Stuff”.Docker和Docker衍生的东西用到了很多很酷的技术,我会用几篇 文章来把这些技术给大家做个介绍,希望通过这些文章大家可以自己打造一个山寨版的docker.先从Linux Namespace开始. 简介 Linux Namespace是Linux提供的一种内核级别环境隔

理解Docker(3):Docker 使用 Linux namespace 隔离容器的运行环境

1. 基础知识:Linux namespace 的概念 Linux 内核从版本 2.4.19 开始陆续引入了 namespace 的概念.其目的是将某个特定的全局系统资源(global system resource)通过抽象方法使得namespace 中的进程看起来拥有它们自己的隔离的全局系统资源实例(The purpose of each namespace is to wrap a particular global system resource in an abstraction th

Docker 基础技术:Linux Namespace(下)

导读 在Docker基础技术:Linux Namespace(上篇)中我们了解了,UTD.IPC.PID.Mount 四个namespace,我们模仿Docker做了一个相当相当山寨的镜像.在这一篇中,主要想向大家介绍Linux的User和Network的Namespace User Namespace User Namespace主要是用了CLONE_NEWUSER的参数,使用了这个参数后,内部看到的UID和GID已经与外部不同了.默认情况下容器没有的UID,系统自动设置上了最大的UID655

《自己动手写Docker》书摘之一: Linux Namespace

Linux Namespace 介绍 我们经常听到说Docker 是一个使用了Linux Namespace 和 Cgroups 的虚拟化工具,但是什么是Linux Namespace 它在Docker内是怎么被使用的,说到这里很多人就会迷茫,下面我们就先介绍一下Linux Namespace 以及它们是如何在容器里面使用的. 概念 Linux Namespace 是kernel 的一个功能,它可以隔离一系列系统的资源,比如PID(Process ID),User ID, Network等等.一

c++ namespace和linux namespace

Namespaces命名空间 wikepedia定义:In general, a namespace is a container for a set of identifiers (also known as symbols, names).[1][2] Namespaces provide a level of direction tospecific identifiers, thus making it possible to distinguish between identifier

理解Docker(3):Docker 容器使用 Linux namespace 进行运行环境隔离

本系列文章将介绍Docker的有关知识: (1)Docker 安装及基本用法 (2)Docker 镜像 (3)Docker 容器的隔离性 - 使用 namespace 进行环境隔离 (4)Docker 容器的隔离性 - 使用 cgroups 进行资源隔离 (4)Docker 容器的网络 (5)Docker 容器的存储 1. 基础知识:Linux namespace 的概念 Linux 内核从版本 2.4.19 开始陆续引入了 namespace 的概念.其目的是将某个特定的全局系统资源(glob

Docker基础技术:Linux Namespace(下)

在 Docker基础技术:Linux Namespace(上篇)中我们了解了,UTD.IPC.PID.Mount 四个namespace,我们模仿Docker做了一个相当相当山寨的镜像.在这一篇中,主要想向大家介绍Linux的User和Network的Namespace. 好,下面我们就介绍一下还剩下的这两个Namespace. User Namespace User Namespace主要是用了CLONE_NEWUSER的参数.使用了这个参数后,内部看到的UID和GID已经与外部不同了,默认显

Docker 基础技术之 Linux namespace 详解

Docker 是"新瓶装旧酒"的产物,依赖于 Linux 内核技术 chroot .namespace 和 cgroup.本篇先来看 namespace 技术. Docker 和虚拟机技术一样,从操作系统级上实现了资源的隔离,它本质上是宿主机上的进程(容器进程),所以资源隔离主要就是指进程资源的隔离.实现资源隔离的核心技术就是 Linux namespace.这技术和很多语言的命名空间的设计思想是一致的(如 C++ 的 namespace). 隔离意味着可以抽象出多个轻量级的内核(容器