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)) == -)
errExit("sethostname"); // 显示子进程的 hostname
if (uname(&uts) == -)
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 ;
}
// 设置子进程的堆栈大小为 1M
#define STACK_SIZE (1024 * 1024) int main(int argc, char *argv[])
{
char *stack;
char *stackTop;
pid_t pid; if (argc < ) {
fprintf(stderr, "Usage: %s <child-hostname>\n", argv[]);
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[]);
if (pid == -)
errExit("clone"); // 等待子进程退出
if (waitpid(pid, NULL, ) == -)
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 < ) {
fprintf(stderr, "%s /proc/PID/ns/FILE cmd args...\n", argv[]);
exit(EXIT_FAILURE);
} // 打开一个现存的 UTS namespace 文件
fd = open(argv[], O_RDONLY);
if (fd == -)
errExit("open"); // 把当前进程的 UTS namespace 设置为命令行参数传入的 namespace
if (setns(fd, ) == -)
errExit("setns"); // 在新的 UTS namespace 中运行用户指定的程序
execvp(argv[], &argv[]);
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//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 = ; while ((opt = getopt(argc, argv, "imnpuU")) != -) {
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[]);
}
} if (optind >= argc)
usage(argv[]); if (unshare(flags) == -)
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

Linux Namespace : UTS的更多相关文章

  1. Linux Namespace : IPC

    IPC namespace 用来隔离 System V IPC 对象和 POSIX message queues.其中 System V IPC 对象包含共享内存.信号量和消息队列,笔者在<Sy ...

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

    本系列文章将介绍Docker的有关知识: (1)Docker 安装及基本用法 (2)Docker 镜像 (3)Docker 容器的隔离性 - 使用 Linux namespace 隔离容器的运行环境 ...

  3. Docker之Linux Namespace

    Linux Namespace 介绍 我们经常听到说Docker 是一个使用了Linux Namespace 和 Cgroups 的虚拟化工具,但是什么是Linux Namespace 它在Docke ...

  4. Docker基础技术:Linux Namespace(下)

    在 Docker基础技术:Linux Namespace(上篇)中我们了解了,UTD.IPC.PID.Mount 四个namespace,我们模仿Docker做了一个相当相当山寨的镜像.在这一篇中,主 ...

  5. Docker基础技术:Linux Namespace(上)

    时下最热的技术莫过于Docker了,很多人都觉得Docker是个新技术,其实不然,Docker除了其编程语言用go比较新外,其实它还真不是个新东西,也就是个新瓶装旧酒的东西,所谓的The New “O ...

  6. Docker 基础技术:Linux Namespace(下)

    导读 在Docker基础技术:Linux Namespace(上篇)中我们了解了,UTD.IPC.PID.Mount 四个namespace,我们模仿Docker做了一个相当相当山寨的镜像.在这一篇中 ...

  7. Docker 基础技术之 Linux namespace 详解

    Docker 是"新瓶装旧酒"的产物,依赖于 Linux 内核技术 chroot .namespace 和 cgroup.本篇先来看 namespace 技术. Docker 和虚 ...

  8. Docker 基础技术之 Linux namespace 源码分析

    上篇我们从进程 clone 的角度,结合代码简单分析了 Linux 提供的 6 种 namespace,本篇从源码上进一步分析 Linux namespace,让你对 Docker namespace ...

  9. Linux Namespace : User

    User namespace 是 Linux 3.8 新增的一种 namespace,用于隔离安全相关的资源,包括 user IDs and group IDs,keys, 和 capabilitie ...

随机推荐

  1. jQuery 实现文字不停闪烁效果

    使用jQuery实现的小效果:文字不停地闪烁. var flag = true; var text= $('#blink').text(); // blink是需要闪烁的元素id function b ...

  2. linux vbundle插件配置

    1.新建目录,clone源码 mkdir ~/.vim/bundle/ git clone https://github.com/gmarik/vundle.git ~/.vim/bundle/vun ...

  3. 使用Navicat for Mysql连接mysql服务器

    使用Navicat for Mysql连接mysql服务器 在windows上用Navicat for Mysql 连接装在虚拟机Ubuntu上的mysql服务器时出现的问题的解决方案. Navica ...

  4. Turtle绘制带颜色和字体的图形(Python3)

    转载自https://blog.csdn.net/wumenglu1018/article/details/78184930 在Python中有很多编写图形程序的方法,一个简单的启动图形化程序设计的方 ...

  5. Java的基础知识二

    一.方法函数 函数也称为方法,就是定义在类中的具有特定功能的一段独立代码.用于定义功能,提高代码的复用性. 函数的特点1> 定义函数可以将功能代码进行封装,便于对该功能进行复用:2> 函数 ...

  6. kafka_2.11-2.0.0_介绍

    1. JMS是什么 1.1. JMS的基础 JMS是什么:JMS是Java提供的一套技术规范 JMS干什么用:用来异构系统 集成通信,缓解系统瓶颈,提高系统的伸缩性增强系统用户体验,使得系统模块化和组 ...

  7. Alpha阶段 - 博客链接合集

    Alpha阶段 - 博客链接合集 项目Github地址 安卓端(Stardust):https://github.com/StardustProject/Stardust 服务器端(Gravel):h ...

  8. c#基础知识之 Dataset 索引0没有值

    datatable绑定到dataGrieView,在刷新datatable的数据时,常会bug:索引0没有值或索引(int)x没有值 昨天弄了一个下午,发现bug原因: dataGridView中有数 ...

  9. C. Lorenzo Von Matterhorn LCA

    C. Lorenzo Von Matterhorn time limit per test 1 second memory limit per test 256 megabytes input sta ...

  10. Openstack安装Dashboard之后,浏览器无法打开页面 500 Internal Server Error

    在手动部署Openstack时,按照官方文档安装Dashboard,并进行了配置后,发现用浏览器无法打开界面 页面显示: Internal Server Error The server encoun ...