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. 《高性能JavaScript》--读书笔记

    第一章 加载和运行 延迟脚本 defer 该属性表明脚本在执行期间不会影响到页面的构造,脚本会先下载但被延迟到整个页面都解析完毕后再运行.只适用于外部脚本 <script src="j ...

  2. [转] Scala 中的异步事件处理

    在任何并发性应用程序中,异步事件处理都至关重要.无论事件的来源是什么(不同的计算任务.I/O 操作或与外部系统的交互),您的代码都必须跟踪事件,协调为响应它们而执行的操作.应用程序可以采用两种基本方法 ...

  3. 语句调优基础知识-set statistics profile on

    set statistics profile on 获取语句真实的执行计划信息 set statistics profile on go select distinct Productid,unitp ...

  4. Android Studio插件GsonFormat

    GsonFormat插件用于在androidStudio 根据json自动生成class的字段和方法,极大提高了开发效率 一.安装GsonFormat插件 二.重启Android Studio,新建一 ...

  5. java 一个实例

     this 代替

  6. [Hive_10] Hive 的分析函数

    0. 说明 Hive 的分析函数 窗口函数  | 排名函数 | 最大值 | 分层次 | lead && lag 统计活跃用户 | cume_dist 1. 窗口函数(开窗函数) ove ...

  7. June.19 2018, Week 25th Tuesday

    True love is visible not to the eyes but to the heart. 真爱不靠眼睛看,要用心感受. True love is visible not to th ...

  8. MySQL 数据库初识

    一.数据库概述 (详情参考:https://www.cnblogs.com/clschao/articles/9907529.html) 1.概念:存储数据,共享数据 数据库,简而言之可视为电子化的文 ...

  9. Nginx使用教程(六):使用Nginx缓存之FastCGI缓存

    启用FastCGI缓存 <br\>编辑必须启用缓存的虚拟主机配置文件. nano /etc/nginx/sites-enabled/vhost 将以下行添加到server{}指令之外的文件 ...

  10. 在Intellij IDEA中使用Maven的方式将项目导出为jar包

    前言:由于项目使用maven管理方式,所以在未发布版本的时候,就需要将项目打成jar包,供本地调试使用.注意在使用本地jar包的时候,需要将pom文件中相关jar包的依赖屏蔽,再将jar包加入项目中. ...