本文演示 CentOS 7.6:Linux testerfans 3.10.0-1160.45.1.el7.x86_64
前言
上一篇Linux Namespaces概述我们对容器基于的Linux底层技术—namespace有了整体了解,本文我们将具体学习一下其中的UTS namespace,UTS是UNIX Time-sharing System的缩写,从Linux 2.6.19加入到内核中,用来隔离系统的主机名和 NIS(Network Information Service) 域名资源。
常用命令
我们可以通过uname和domainname查看主机的hostname和NIS名称,大家可以通过uname --help 和domainname --help来查看具体的使用方法。
查看hostname
使用uname(unix name)可以打印本机的主机名、系统版本等信息,或者使用hostname查看主机名。
[root@testerfans ~]# uname -n
testerfans
[root@testerfans ~]# hostname
testerfans
使用 uname --help可以查看详细信息,关于Linux内核版本命名规则大家可以参考Linux内核版本介绍与查询
命令参数 | 描述 | 示例 | 示例说明 |
---|---|---|---|
-a, --all | 按顺序打印全部信息,如果 -p 和 -i 的信息是未知,那么省略 | ||
-s, --kernel-name | 打印内核名称 | Linux | 当前系统使用的Linux内核,Linux内核属于单内核 |
-n, --nodename | 打印网络节点主机名称 | VM-16-7-centos | 当前主机名 |
-r, --kernel-release | 打印内核release | 3.10.0-1160.45.1.el7.x86_64 | 测试系统使用的是CentOS 7系统 3 主版本号 10 次版本号 0 修订版本号 1160.45.1 发行版本的补丁版本 el7 在Red Hat Linux中用来表示企业版Linux(Enterprise Linux) x86_64 采用的是64位的CPU |
-v, --kernel-version | 打印内核版本 | #1 SMP Wed Oct 13 17:20:51 UTC 2021 | SMP: 对称多处理机,表示内核支持多核、多处理器 Wed Oct 13 17:20:51 UTC 2021 内核的编译时间 2021/10/13 17:20:51 |
-m, --machine | 打印机器名称 | x86_64 | x86_64 : 64位系统 ix86 : 32位系统(x表示3、4、5、6) |
-p, --processor | 打印处理器名称 | x86_64 | 该机器处理器的类型(CPU) |
-i, --hardware-platform | 打印硬件平台名称 | x86_64 | 硬件平台告诉我们构建内核的架构 |
-o, --operating-system | 打印操作系统名称。 | GNU/Linux | 表示当前运行的操作系统为GNU/Linux |
查看NIS domain name
domainname命令作用是显示或设置当前网络信息服务(NIS)域的名称。如果不指定参数domainname命令则显示当前NIS域的名称,一个域通常包含同一管理器下的一组主机。hostname可以查看和设置主机名。
[root@testerfans ~]# hostname -y
testerfans.com
[root@testerfans ~]# domainname
testerfans.com
命令参数 | 描述 | 示例 | 示例说 |
---|---|---|---|
hostname | 显示主机名 | testerfans | 显示主机名为testerfans |
hostname [-b] | 设置主机名(或从文件) | hostname testerfans | 设置主机名为 testerfans |
{yp,nis,}domainname | 显示NIS域名 | testerfans.com | 显示NIS域名为 testerfans.com |
{yp,nis,}domainname | 设置NIS域名(或从文件) | domainname testerfans.com | 设置NIS域名为 testerfans.com |
系统调用
clone() :新建 UTS namespace
我们在Linux系统上通过执行man clone后,可以翻到EXAMPLE位置可以查看样例代码。clone()方法是在创建进程的同时创建UTS namespace,并将进程加入到新建的namespace中。
注意:如果man clone搜索不到内容可能是man手册版本问题或者没有man手册,通过执行
yum install -y man man-pages
之后再用man clone搜索EXAMPLE。如果还找不到可以直接查找man pages online在线man手册。
#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。
- 父进程等待子进程的退出,并最终退出程序。
保存并编译
[root@testerfans ~]# mkdir namespace
[root@testerfans ~]# cd namespace/
[root@testerfans namespace]# vim uts_clone_test.c #将如下代码拷贝进去,保存
[root@testerfans namespace]# gcc uts_clone_test.c -o uts_clone_test #编译文件,输出到uts_clone_test内
执行
执行程序输入child作为子进程的主机名,可以看到如下输出并执行hostname查看主机名。
[root@testerfans namespace]# ./uts_clone_test child #执行程序
uts.nodename in child: child #打印子进程的 主机名
My PID is: 20051 #打印进程ID
My parent PID is: 20050 #打印父进程ID
[root@child namespace]# hostname #查看主机名变成了child
child
查看uts链接文件
保持上面程序运行(保持执行程序的终端不关闭即可),打开一个新的终端,对子进程和父进程的uts进行检查
[root@testerfans ~]# sudo readlink /proc/$$/ns/uts #查看当前进程的uts
uts:[4026531838]
[root@testerfans ~]# sudo readlink /proc/20050/ns/uts #查看父进程的uts
uts:[4026531838]
[root@testerfans ~]# sudo readlink /proc/20051/ns/uts #查看子进程的uts
uts:[4026532291]
- 当前进程$$和父进程因为在系统默认的namespace下,namespace-inode-number相同,为4026531838。
- child进程因为创建分配了不同的uts namespace,namespace-inode-number不同,为4026532291。
退出程序
在执行了./uts_clone_test的终端输入exit退出执行,我们可以看到退出后主机名恢复为testerfans。
[root@child namespace]# exit
exit
child has terminated
[root@testerfans namespace]#
setns() :加入到已存在UTS namespace
我们在Linux系统上通过执行man 2 setns后,可以翻到EXAMPLE位置可以查看样例代码。通过调用setns()方法可以将进程加入到一个已经存在的namespace内。
#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 函数。
- 最后执行用户指定的程序。
保存并编译
[root@child namespace]# vim uts_setns_test.c
[root@child namespace]# gcc uts_setns_test.c -o uts_setns_test
执行
因为我们要将一个进程加入到一个已经存在的UTS namespace中,所以这里面我们传入的参数有两个:
- argv[1] 一个已经存在的 UTS namespace 文件
- argv[2] 指定要运行的程序
我们按照如下1、2、3步骤来进行验证:
- argv[1]参数我们通过uts_clone_test来创建一个新的UTS namespace,得到一个新的namespace—/proc/31686/ns/uts
[root@child namespace]# ./uts_clone_test child
uts.nodename in child: child
My PID is: 31686
My parent PID is: 31685
[root@child namespace]# sudo readlink /proc/31686/ns/uts
uts:[4026532292]
- argv[2] 传入要执行的程序:/bin/bash
- 新打开一个终端,执行uts_setns_test程序并传入argv[1]和argv[2]
[root@testerfans namespace]# ./uts_setns_test /proc/31686/ns/uts /bin/bash
查看uts链接文件
通过hostname和查看uts文件id,我们发现通过hostname命令获取到的名字均为child,查看/proc/1018/ns/uts、readlink /proc/31686/ns/uts 得到的id均为4026532292。说明/bin/bash加入到了我们创建的、已经存在的UTS namespace内。
[root@child namespace]# hostname
child
[root@child namespace]# echo $$
1018
[root@child namespace]# sudo readlink /proc/1018/ns/uts
uts:[4026532292]
退出程序
shell 终端内exit退出演示程序。
unshare() :加入到新建UTS namespace
我们在Linux系统上通过执行man 2 unshare后,可以翻到EXAMPLE位置查看样例代码。unshare方法通过将当前进程加入到一个新建的namespace,当前进程不变。
#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");
}
直接执行程序我们可以看到程序的使用说明。
[root@testerfans namespace]# ./uts_unshare_test
Usage: ./uts_unshare_test [options] program [arg...]
Options can be:
-i unshare IPC namespace
-m unshare mount namespace
-n unshare network namespace
-p unshare PID namespace
-u unshare UTS namespace
-U unshare user namespace
保存并编译
[root@testerfans namespace]# gcc uts_unshare_test.c -o uts_unshare_test
执行
在执行前我们首先查看终端进程的uts namespace id是uts:[4026531838],然后运行程序。这里我们要模拟的是uts namespace,所以输入-u指令,含义是将当前shell进程加入到新建的UTS namespace中。
[root@testerfans namespace]# readlink /proc/$$/ns/uts
uts:[4026531838]
[root@testerfans namespace]# ./uts_unshare_test -u /bin/bash
查看uts链接文件
运行程序后查看ust namespace id uts:[4026532293],与[4026531838]不同,说明当前shell进程加入到了一个新建的UTS namespace中。
[root@testerfans namespace]# readlink /proc/$$/ns/uts
uts:[4026532293]
退出程序
shell 终端内exit退出演示程序。
clone和unshare的区别
clone和unshare的功能都是创建并加入新的namespace, 他们的区别是:
- unshare是使当前进程加入新的namespace
- clone是创建一个新的子进程,然后让子进程加入新的namespace,而当前进程保持不变
总结
本章我们通过实践clone()、setns()、unshare()三个系统调用对uts namespace进行了实际操作,理解了clone()、setns()、unshare()的使用和uts namespace的进程间隔离,接下来我们将继续介绍其他的namespace。
本文参考:
Linux Namespace:UTS
man 2 clone
man 2 setns
man 2 unshare
Network Information Service
Linux内核版本介绍与查询
评论区