第2天:UTS namespace详解
2019-06-29T13:28:20    692    0    0
#### 在之前的文章中,我们已经了解了什么是namespace,接下来,在本文中我们将会首先学习第一个namespace类型:UTS namespace。 ## 什么是UTS namespace? UTS namespace(UNIX Time Sharing)用来隔离系统的hostname以及NIS domain name。 这两个资源可以通过`sethostname`和`setdomainname`函数来设置,以及通过`uname`, `gethostname`和`getdomainname`函数来获取。 由于UTS namespace最简单,所以放在最前面介绍,在这篇文章中我们将会熟悉UTS namespace以及和namespace相关的三个系统调用的使用。 ## 创建一个新的UTS namespace 接下来,我们将会直接通过实例来进行UTS namespace的演示和学习。 说明: 1. 为了代码简单起见,只在clone函数那做了错误处理,关于clone函数的详细介绍请参考man-pages。 2. 为了描述方便,某些地方会用hostname来区分UTS namespace,如hostname为container001的namespace,将会被描述成namespace container001。 创建`namespace_uts_demo.c`文件如下: ``` #define _GNU_SOURCE #include #include #include #include #include #include #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 \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; //这行执行完之后,父进程结束 } ``` 在上面的代码中,实现的功能如下: 1. 父进程创建新的子进程,并且设置`CLONE_NEWUTS`,这样就会创建新的UTS namespace并且让子进程属于这个新的namespace,然后父进程一直等待子进程退出。 2. 子进程在设置好新的hostname后被bash替换掉。 3. 当bash退出后,子进程退出,接着父进程也退出。 下面,我们用gcc将它编译成可执行文件namespace_uts_demo: ``` gcc namespace_uts_demo.c -o namespace_uts_demo ``` 接下来,我们启动程序,并传入参数container001: ``` ./namespace_uts_demo container001 ``` 此时,新的bash被启动,从shell的提示符可以看出,hostname已经被改成了container001。 ![uts_namespace](/static/files/591/5989cee6e519f50ef7000031/91/images/e6b242936332aaf414427d6dc5fdf129.png) 此外,我们可以执行`hostname`查询进行确认: ![hostname](/static/files/591/5989cee6e519f50ef7000031/63/images/6e577d5b9fa4e707106a718e8d3e2ef9.png) `pstree`是用来查看系统中进程之间父子关系的工具。我们可以使用pstree查询进程之间的关系。 ``` pstree -pl ``` 如下是与本次namespace_uts_demo有关的进程信息。 ``` init(1)---sshd(15290)---sshd(3439)---bash(3486)---bash(3562)---namespace_uts_d(9052)---bash(9053)---pstree(2729) ``` 回滚我们操作的完整流程: 1. 首先是通过ssh客户端远程连接到Linux主机进行的。 2. bash(3486)的父进程是一系列的sshd进程。 3. 我们在bash(24429)里面执行了./namespace_uts_demo container001。所以有了bash(3562)和我们程序namespace_uts_d(9052)对应的进程。 4. 我们的程序自己clone了一个新的子进程,由于clone的时候指定了参数CLONE_NEWUTS,所以新的子进程属于一个新的UTS namespace,然后这个新进程调用execlp后被bash替换掉了,于是有了bash(9053)。 5. 这个bash进程拥有所有当前子进程的属性, 由于我们的pstree命令是在bash(9053)里面运行的,所以这里pstree(2729)是bash(9053)的子进程。 我们可以验证下我们当前的bash进程号: ``` echo $$ ``` ![bash](/static/files/591/5989cee6e519f50ef7000031/26/images/86a933d920b9bc266db06fc7abcb0a24.png) 可以看到,当前的bash进程号与刚才pstree的父进程是一致的。 接下来,我们可以验证一下当前的uts namespace与其父进程的uts namespace是否不同。 ``` readlink /proc/9053/ns/uts readlink /proc/3562/ns/uts ``` ![namespace](/static/files/591/5989cee6e519f50ef7000031/9/images/91a809669e5d0c48fa4c779017aab48f.png) 从上图可以看出,二者的确不属于同一个UTS namespace,说明新的uts namespace创建成功。 接下来,我们可以退出该namespace: ``` exit ``` ![title](/static/files/591/5989cee6e519f50ef7000031/35/images/0ed456002adba0faac55ee7944743866.png) 可以看到,执行`exit`命令后,我们退出了之间创建的namespace,HostName也回到了原始名称。 ## 将当前进程加入指定的namespace ```c #define _GNU_SOURCE #include #include #include #include #include #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] ``` Ps:CentOS中由于glibc的版本导致setns的函数无法使用。 ## 退出当前namespace并加入新创建的namespace 我们继续通过代码来进行学习,创建`namespace_leave.c`文件: ```c #define _GNU_SOURCE #include #include #include #include #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; } ``` 首先对文件进行编译: ``` gcc namespace_leave.c -o namespace_leave ``` 接下来,我们首先可以查看当前的uts namespace: ``` readlink /proc/$$/ns/uts # uts:[4026531838] ``` 接下来,我们执行编译后的文件,-u表示退出并加入新的UTS namespace: ``` ./namespace_leave -u ``` 现在,我们可以查看当前的uts namespace: 可以发现,当前的uts namespace已经发生的变化。接下来,我们还需要验证`unshare`是否退出了之前的进程。 同样,我们可是使用`pstree`来查询进程间的关系: ``` pstree -pl ``` 找出其中相关的内容如下: ``` init(1)---sshd(2274)---sshd(16290)---bash(16331)---bash(16406)---bash(28370)---pstree(48008) ``` 对比之前的clone进程树,可以发现在unshare的进程树中,并没有执行可执行文件`namespace_leave`的进程信息,因此,可以判断出在执行`unshare`时退出了之前的进程。 ## uts namespace在内核中的实现 上面演示了这三个函数的功能,那么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 ¤t->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的功能,他们的主要区别是: 1. unshare是使当前进程加入新创建的namespace。 2. clone是创建一个新的子进程,然后让子进程加入新的namespace。 - UTS namespace没有嵌套关系,即不存在说一个namespace是另一个namespace的父namespace。

上一篇: 第1天:Linux namespace概述

下一篇: 第3天:IPC namespace详解

0条评论