一、Linux命名空间概述
Linux命名空间是一种对系统资源隔离的解决方案,这些系统资源包括进程ID、主机名、网络、进程间通讯和文件系统等。 名称空间的目的是通过一种抽象的形式包装特定的全局系统资源,以使在相应名称空间中的进程中看起来像是拥有隔离的全局资源实例。因此这些命名空间为容器的虚拟化技术提供了很好的基础。目前存在以下6种不同的命名空间:
| 名称 | 宏定义 | 隔离内容 | 内核版本 |
|---|---|---|---|
| Mount | CLONE_NEWNS | 文件系统挂载点 | 2.4.19 |
| UTS | CLONE_NEWUTS | 主机名和NIS域名 | 2.6.19 |
| IPC | CLONE_NEWIPC | 进程间通信资源 | 2.6.19 |
| PID | CLONE_NEWPID | 进程编号 | 2.6.24 |
| Network | CLONE_NEWNET | 与网络关联的系统资源 | 始于2.6.24完成2.6.29 |
| USER | CLONE_NEWUSER | 用户和用户组 | 始于2.6.23完成3.8 |
不同命令空间的作用:
- Mount:对不同mount命令空间的来说可以具有不同的文件系统层次结构视图。并且进程在对应的mount命名空间执行mount()或者umonut()的系统调用仅会影响该进程所在的mount命令空间,对于其他进程的mount命名空间无影响。
- UTS:允许容器拥有自己的主机名和NIS域名,这对于需要根据主机名或者NIS域名来执行初始化或者配置脚本的环境十分有用。
- IPC:同一个IPC命名空间中的进程允许相互交互,不同空间的则无法交互。
- PID:用于隔离进程的PID空间,因此不同PID空间的进程可以具有相同的PID号。PID名称空间还允许每个容器具有自己的init(PID=1),用于管理系统初始化任务或者回收孤儿进程。PID命名空间是一个父子结构,子空间对于父空间是可见的。
- Network:Network命名空间有自己的网络设备、IP地址、IP转发表以及端口等。每个容器可以拥有自己的(虚拟)网络设备和绑定到每个命名空间端口号空间的应用程序; 主机系统中合适的路由规则可以将网络数据包定向到与特定容器关联的网络设备。
- User:值得注意的是,进程可以在用户名称空间之外具有正常的非特权用户ID,而同时在名称空间内部具有0的用户ID。 这意味着该进程对用户名称空间内的操作具有完全的root特权,但对于该名称空间外的操作则没有特权。
二、命名空间常用函数说明:
1.clone函数
创建进程
#define _GNU_SOURCE
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
参数:
1) fn:函数指针,此指针指向一个函数体,即想要创建进程的静态程序
2) child_stack:给子进程分配系统堆栈的指针
3) arg:传给子进程的参数,一般为(0)
4) flags为要复制资源的标志,描述你需要从父进程继承那些资源
2.unshare函数
unshare函数用于修改当前的进程的namespace的信息。比如更换当前进程的namespace等等
#define _GNU_SOURCE
#include <sched.h>int unshare(int flags);
参数:
1) flags:指定一个或者多个上面的CLONE_NEW*, 这样当前进程就退出了当前指定类型的namespace并加入到新创建的namespace
3.setns函数
将当前进程的namespace设置为另一进程的namespace
#define _GNU_SOURCE
#include <sched.h>int setns(int fd, int nstype);
参数:
1) fd:表示要加入 namespace 的文件描述符。它是一个指向 /proc/[pid]/ns 目录中文件的文件描述符,可以通过直接打开该目录下的链接文件或者打开一个挂载了该目录下链接文件的文件得到。
2) nstype:参数 nstype 让调用者可以检查 fd 指向的 namespace 类型是否符合实际要求。若把该参数设置为 0 表示不检查。
4.execve函数
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
参数:
1) filename:必须是一个二进制的可执行文件,或者是一个脚本以#!格式开头的解释器参数参数。如果是后者,这个解释器必须是一个可执行的有效的路径名,但是不是脚本本身,它将调用解释器作为文件名。
2) argv:是要调用的程序执行的参数序列,也就是我们要调用的程序需要传入的参数。
3) envp:同样也是参数序列,一般来说他是一种键值对的形式 key=value. 作为我们是新程序的环境。
5.挂载与卸载函数
mount挂上文件系统,umount执行相反的操作。
#include <sys/mount.h>
int mount(const char *source, const char *target, const char *filesystemtype, unsigned long mountflags, const void *data);
int umount(const char *target);
参数:
1) source:将要挂上的文件系统,通常是一个设备名。
2) target:文件系统所要挂在的目标目录。
3) filesystemtype:文件系统的类型,可以是"ext2","msdos","proc","nfs","iso9660"
4) mountflags:指定文件系统的读写访问标志
6.uname函数
获取当前内核名称和其它信息。
#include <sys/utsname.h>
int uname(struct utsname *buf);
7.gethostname函数/sethostname函数
获取/设置主机名
#include <unistd.h>
int gethostname(char *name, size_t len);
int sethostname(const char *name, size_t len);
8.getdomainname函数/setdomainname函数
获取/设置域名
#include <unistd.h>
int getdomainname(char *name, size_t len);
int setdomainname(const char *name, size_t len);
9.kill函数
用于向任何进程组或进程发送信号。
#include <sys/types.h>
include <signal.h>
int kill(pid_t pid, int sig);
参数:
1) pid:可能选择有以下四种
pid > 0,pid是信号欲送往的进程的标识。
pid = 0,信号将送往所有与调用kill()的那个进程属同一个使用组的进程。
pid = -1时,信号将送往所有调用进程有权给其发送信号的进程,除了进程1(init)。
pid < -1时,信号将送往以-pid为组标识的进程。
2) sig:准备发送的信号代码,假如其值为零则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为零来检验某个进程是否仍在执行。
10.stat函数
返回一个文件的详细信息
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *buf);
参数:
1) pathname:文件路径
2)buf:返回的文件信息
11.waitpid()函数
如果在调用waitpid()函数时,当指定等待的子进程已经停止运行或结束了,则waitpid()会立即返回;但是如果子进程还没有停止运行或结束,则调用waitpid()函数的父进程则会被阻塞,暂停运行。
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);
参数:
1) pid:欲等待的子进程识别码,其具体含义如下:
pid<-1 等待进程组号为pid绝对值的任何子进程。
pid=-1 等待任何子进程,此时的waitpid()函数就退化成了普通的wait()函数。
pid=0 等待进程组号与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程。
pid>0 等待进程号为pid的子进程。
2)status:保存子进程的状态信息,有了这个信息父进程就可以了解子进程为什么会推出,是正常推出还是出了什么错误。如果status不是空指针,则状态信息将被写入器指向的位置。当然,如果不关心子进程为什么推出的话,也可以传入空指针。
3)options:参数options提供了一些另外的选项来控制waitpid()函数的行为。
三、命名空间API介绍
3.1 在新的命令空间创建子进程:clone()
样例程序一展示了clone函数使用标记CLONE_NEWUTS标记创建一个UTS命名空间。在该UTS命名空间内,子进程能够修改主机名。
主程序的第一个重要部分是调用clone()创建子进程:
child_pid = clone(childFunc, child_stack + STACK_SIZE, CLONE_NEWUTS | SIGCHLD, argv[1]);
printf(“PID of child created by clone() is %ld\n”, (long) child_pid);
这个子进程将会执行用户定义的函数childFunc(),该函数的参数即为clone第四个参数(argv[1]),CLONE_NEWUTS是一个标记位表明子进程将在一个新的UTS命名空间中执行。
主程序然后等待1s,以便子进程有时间去修改所在UTS命名空间的主机名。然后程序使用uname()去获取父类的UTS命名空间的主机名。
sleep(1);
uname(&uts);
printf(“uts.nodename in parent: %s\n”, uts.nodename);
同时,childFunc()将会修改主机名,并获取和显示修改的主机名。
sethostname(arg, strlen(arg);
uname(&uts);
printf(“uts.nodename in child: %s\n”, uts.nodename);
在子程序退出之前,子程序需要休眠一会,以保留该命名空间使我们可以进行后续的一些实验。接下来我们运行程序发现会发现父进程和子进程会有独立的命名空间。(注意这里运行需要root权限)
root@yupf# sudo ./demo_uts_namespaces newns
PID of child created by clone() is 16502
uts.nodename in child: newns.
uts.nodename in parent: yupf
在往下之前,先介绍一下 /proc/PID/ns/文件
每一个进程都有一个/proc/PID/ns目录,这里面包含了上述提到的几种命名空间。这些文件中的每个文件都是一个特殊的符号链接,提供了一种用于在进程的关联名称空间上执行某些操作的句柄。如下所示($$表示shell的PID):
yupf@yupf:~/namespace/uts$ ls -l /proc/$$/ns
总用量 0
lrwxrwxrwx 1 yupf yupf 0 6月 21 12:14 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 yupf yupf 0 6月 21 12:14 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 yupf yupf 0 6月 21 12:14 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 yupf yupf 0 6月 21 12:14 net -> 'net:[4026531993]'
lrwxrwxrwx 1 yupf yupf 0 6月 21 12:14 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 yupf yupf 0 6月 21 12:14 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 yupf yupf 0 6月 21 12:14 user -> 'user:[4026531837]'
lrwxrwxrwx 1 yupf yupf 0 6月 21 12:14 uts -> 'uts:[4026531838]'
这些符号链接的使用是为了发现两个进程是否在同一名称空间中。 内核做了一些处理,以确保如果两个进程在同一个名称空间中,则为/ proc / PID / ns中的相应符号链接报告中的inode编号将相同。这里可以使用stat() 系统调用获得inode编号。从下面的执行结果可以看出,父进程和子进程两者的符号链接是不同的,表明了两个进程在不同的UTS命名空间中。
# sudo ./demo_uts_namespaces newns #执行命令
PID of child created by clone() is 16527
uts.nodename in child: newns.
uts.nodename in parent: yupf
^Z #停止父进程和子进程
[1]+ 已停止 sudo ./demo_uts_namespaces newns
# jobs -l #获取父进程的PID
[1]+ 16525 停止 sudo ./demo_uts_namespaces newns
# readlink /proc/16525/ns/uts #查看父进程的uts命名空间
uts:[4026531838]
# readlink /proc/16527/ns/uts #查看子进程的uts命名空间
uts:[4026532446]
/proc/PID/ns符号连接也被用于其他的目的。如果我们打开这些文件中的一个(这里对打开的理解也不是很清楚),那么即使该命名空间中的所有进程全部退出,只要文件描述符保持打开,命名空间就会一直存在。这和将一个符号链接挂在到另一个位置的文件系统具有相同的影响,如下所示(这一步是必要的,对于3.2节来说)。
# touch ~/uts
# mount --bind /proc/16527/ns/uts ~/uts
3.2 加入已存在的命名空间:setns()
setns() 将调用过程与特定名称空间类型的一个实例解除关联,并将该过程与相同名称空间类型的另一实例重新关联。
样例程序二展示的是使用setns() 和 execve() 构造一个简单但有用的工具:将指定的名称空间加入程序,然后在该名称空间中执行命令的程序。该程序带有两个或多个命令行参数。第一个参数是/ proc / PID / ns / *符号链接(或绑定安装到这些符号链接之一的文件)的路径名。其余参数是要在与该符号链接相对应的名称空间中执行的程序的名称,以及要提供给该程序的可选命令行参数。该程序中的关键步骤如下:
fd = open(argv [1],O_RDONLY); / *获取名称空间的描述符* /
setns(fd,0); / *加入该名称空间* /
execvp(argv [2],&argv [2]); / *在命名空间中执行命令* /
这里,我们使用与先前创建的UTS名称空间绑定挂载的目录,并与ns_exec程序结合使用,以在通过调用demo_uts_namespaces创建的新UTS名称空间中执行shell:
# ./ns_exec ~/uts /bin/bash
# hostname
newns
# readlink /proc/16527/ns/uts
uts:[4026532446]
# readlink /proc/$$/ns/uts
uts:[4026532446]
由上可见,当前的shell的UTS命名空间与demo_uts_namespaces创建的UTS命名空间是相同的。
3.3 离开命名空间:unshare()
unshare() 系统调用提供了与clone() 类似的功能,但在调用过程中进行操作:它在其flags参数中创建由CLONE_NEW *位指定的新名称空间,并使调用者成为该名称空间的成员。 unshare() 的主要目的是隔离名称空间(和其他)副作用,而不必创建新的进程或线程 。
撇开clone() 系统调用的其他影响,该函数与下面所示的执行过程完全一致:
if(0 == fork())
{
unshare(CLONE_NEWXXXX);
}
样例程序三显示了unshare函数的简单使用,其实现了在命名空间执行shell的功能。mount的命名空间已经发生了变化。
# echo $$ #显示当前shell的PID
17060
# cat /proc/17060/mounts | grep mq #显示命名空间的一个挂载点
mqueue /dev/mqueue mqueue rw,relatime 0 0
# readlink /proc/17060/ns/mnt #显示mount命名空间
mnt:[4026531840]
# ./unshare -m /bin/bash #执行命令,开始一个新的shell在一个独立的mount命名空间
# readlink /proc/$$/ns/mnt #显示mount命名空间
mnt:[4026532447]
本文深入解析Linux命名空间,涵盖进程ID、主机名、网络、进程间通讯和文件系统的隔离技术。探讨命名空间如何支撑容器虚拟化,详述clone、unshare、setns等API的使用,及其实现独立命名空间的具体示例。
1567

被折叠的 条评论
为什么被折叠?



