前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >命名空间介绍之二:API

命名空间介绍之二:API

作者头像
谛听
修改2019-11-05 10:34:27
1.5K0
修改2019-11-05 10:34:27
举报
文章被收录于专栏:巫山跬步巫山跬步

命名空间将全局系统资源包装在一个抽象中,使得命名空间中的进程认为它们拥有独立的资源实例。命名空间可用于多种目的,最重要的是实现容器,一种轻量级虚拟化技术。本系列的第二篇文章将看一下命名空间的一些细节和 API。本系列中的第一篇文章对命名空间进行了总览。本文将看一下命名空间的 API 中的一些细节,并在一些例子中展示运行中的 API。

命名空间 API 包含三个系统调用:clone(),unshare() 和 setns(),和一些 /proc 文件。为了明确正在操作哪种命名空间类型,单个系统调用将使用前面文章中所列出的 CLONE_NEW*:CLONE_NEWIPC, CLONE_NEWNS, CLONE_NEWNET, CLONE_NEWPID, CLONE_NEWUSER 和 CLONE_NEWUTS

在新的命名空间中创建一个子空间:clone()

通过 clone() 可创建一个命名空间。clone() 是一个创建新进程的系统调用。clone() 原型如下:

代码语言:txt
复制
int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);

基本上,clone() 是传统 UNIX fork() 系统调用的通用版本,flags 参数影响着 fork() 的功能。clone() 的作用也被 20 多种不同的 CLONE_* 标志影响,包括父子进程是否共享资源,如虚拟内存,打开的文件描述符,和信号量。如果某个 CLONE_NEW* 标志位在系统调用中被设置,那么相应类型的命名空间将被创建,并且新进程会成为该命名空间中的一员;flags 中可设置多个 CLONE_NEW* 标志。

我们的例子(demo_uts_namespace.c)使用带有 CLONE_NEWUTS 标志的 clone() 来创建一个 UTS 命名空间。正如上周所提,UTS 命名空间隔离两个系统标识 --- 主机名和 NIS 域名 --- 使用 sethostname() 和 setdomainname() 设置,并由 uname() 返回。下面,看一下程序的一些关键部分(忽略错误检查代码)。

示例程序中的参数来自于命令行。当程序运行时会创建一个子进程,该子进程在新的 UTS 命名空间中运行。在该命名空间中,子进程会根据命令中的参数修改主机名。

主程序的第一个关键部分就是 clone() 调用,它创建了子进程:

代码语言:txt
复制
child_pid = clone(childFunc, 
                    child_stack + STACK_SIZE,   /* Points to start of 
                                                    downwardly growing stack */ 
                    CLONE_NEWUTS | SIGCHLD, argv[1]);

printf("PID of child created by clone() is %ld\n", (long) child_pid);

新的子进程会运行函数 childFunc();该函数的参数为 clone() 的最后一个参数(argv[1])。因为 CLONE_NEWUTS 是 flags 参数的一部分,所以子进程将会运行在新创建的 UTS 命名空间。

然后让主程序睡眠一会。这是一种(粗略)的方式,让子进程有时间在其 UTS 命名空间内修改主机名。然后使用 uname() 获取父 UTS 命名空间中的主机名,如下:

代码语言:txt
复制
sleep(1);           /* Give child time to change its hostname */

uname(&uts);
printf("uts.nodename in parent: %s\n", uts.nodename);

同时,childFunc() 函数被 clone() 创建出的子进程运行,先根据参数修改了主机名,然后获取并打印修改后的主机名。

代码语言:txt
复制
sethostname(arg, strlen(arg);

uname(&uts);
printf("uts.nodename in child:  %s\n", uts.nodename);

在结束之前,让子进程睡眠一会。这会让子 UTS 命名空间持续存在,从而让我们有机会展示稍后要进行的实验。

运行程序,证明父进程和子进程有各自的 UTS 命名空间:

代码语言:txt
复制
$ su                   # Need privilege to create a UTS namespace
Password: 
# uname -n
antero
# ./demo_uts_namespaces bizarro
PID of child created by clone() is 27514
uts.nodename in child:  bizarro
uts.nodename in parent: antero

正如其它的命名空间(用户命名空间除外),需使用特权(尤其是 CAP_SYS_ADMIN)来创建 UTS 命名空间。这可避免 set-user-ID 应用的误操作,因为系统有一个意料之外的主机名。

另一种可能是 set-user-ID 应用也许会将主机名作为锁文件名的一部分。如果允许一个非特权用户使用任意主机名在一个 UTS 命名空间中跑应用,会使得应用遭受各种攻击。最简单的,锁文件失效,会导致运行在不同 UTS 命名空间中的应用出错。或者,一个恶意用户可使用任意主机名在一个 UTS 命名空间中运行 set-user-ID 应用,将导致锁文件的创建覆盖重要文件。(主机名可包含任意字符,包括斜线)。

/proc/PID/ns 文件

每个进程都有一个 /proc/PID/ns 目录,该目录包含了对应于每种命名空间的一份文件。从 Linux 3.8 开始,每份文件都是一个特殊的符号链接,提供了对进程关联的命名空间进行操作的一种句柄。

代码语言:txt
复制
$ ls -l /proc/$$/ns         # $$ is replaced by shell's PID
total 0
lrwxrwxrwx. 1 mtk mtk 0 Jan  8 04:12 ipc -> ipc:[4026531839]
lrwxrwxrwx. 1 mtk mtk 0 Jan  8 04:12 mnt -> mnt:[4026531840]
lrwxrwxrwx. 1 mtk mtk 0 Jan  8 04:12 net -> net:[4026531956]
lrwxrwxrwx. 1 mtk mtk 0 Jan  8 04:12 pid -> pid:[4026531836]
lrwxrwxrwx. 1 mtk mtk 0 Jan  8 04:12 user -> user:[4026531837]
lrwxrwxrwx. 1 mtk mtk 0 Jan  8 04:12 uts -> uts:[4026531838]

这些符号链接的用途之一是判断两个进程是否位于同一命名空间。内核如果确认了两个进程在同一命名空间,那么 /proc/pid/ns 中相应符号链接的 inode 号将相同。inode 号可通过 stat() 获取。

内核还构造了每个 /proc/pid/ns 的符号链接,它指向一个由标识命名空间类型的字符串组成的名称,后跟 inode 号。我们可以使用 ls -l 或 readlink 命令查看该名称。

返回上述运行 demo_uts_namespaces 程序的 shell。父进程和子进程的 /proc/PID/ns 符号链接提供了另一种检查两个进程是否在同一 UTS 命名空间的方式。

代码语言:txt
复制
^Z                                # Stop parent and child
[1]+  Stopped          ./demo_uts_namespaces bizarro
# jobs -l                         # Show PID of parent process
[1]+ 27513 Stopped         ./demo_uts_namespaces bizarro
# readlink /proc/27513/ns/uts     # Show parent UTS namespace
uts:[4026531838]
# readlink /proc/27514/ns/uts     # Show child UTS namespace
uts:[4026532338]

正如所见,/proc/PID/ns/uts 符号链接不同,说明两个进程在不同的 UTS 命名空间。

/proc/PID/ns 符号链接还有其他用途。如果我们打开其中一个文件,只要文件描述符保持打开的状态,即便命名空间中的所有进程都结束了,命名空间也将持续存在。将一个符号链接挂载到文件系统中的另外一个本地位置也可达到相同效果。

代码语言:txt
复制
# touch ~/uts                            # Create mount point
# mount --bind /proc/27514/ns/uts ~/uts

在 Linux 3.8 以前,/proc/PID/ns 中的文件是硬链接,而不是如上所述的特殊符号链接。而且,仅能看到 ipc,net 和 uts 文件。

加入一个已存在的命名空间:setns()

当一个命名空间中没有进程时,只有当打算添加进程时,使其保持打开的状态才有用。这就是 setns() 系统调用要做的,可让调用它的进程加入一个已存在的命名空间:

代码语言:txt
复制
int setns(int fd, int nstype);

更准确来说,setns() 将调用进程与特定命名空间类型的一个实例解除关联,并与同一命名空间类型的另一个实例重新关联。

fd 参数为要加入的命名空间;它是一个文件描述符,引用自某个 /proc/PID/ns 目录中的一个符号链接。可通过直接打开符号链接或打开挂载到链接的文件来获取该文件描述符。

nstype 参数允许调用者检查 fd 所引用的命名空间类型。如果该参数为 0,则无需检查。如果调用者早就知道了命名空间的类型,或不在乎命名空间的类型,那么该参数就有用。我们稍后讨论的示例程序(ns_exec.c)属于后者:它可在任意类型的命名空间中运行。明确 nstype,而不是采用某个 CLONE_NEW* 会使得内核去验证 fd 是否为相应命名空间类型的一个文件描述符。这比较有用,例如,调用者通过 unix 域套接字传递了文件描述符,需要验证该文件描述符所引用的命名空间类型。

使用 setns() 和 execve()(或其它的 exec() 函数)可构建一个简单但有用的工具:一个程序加入一个特定的命名空间,然后在该命名空间中执行一个命令。

我们的程序(ns_exec.c) 的参数来自于命令行。第一个参数是 /proc/PID/ns/* 符号链接(或挂载到其中一个符号链接的文件)。剩余参数为程序将所处的命名空间的名称,和可选的命令行参数。程序中的关键步骤如下:

代码语言:txt
复制
fd = open(argv[1], O_RDONLY);   /* Get descriptor for namespace */

setns(fd, 0);                   /* Join that namespace */

execvp(argv[2], &argv[2]);      /* Execute a command in namespace */

要在命名空间中运行一个程序自然是 shell。可使用之前 UTS 命名空间的绑定挂载(~uts)和 ns_exec 程序在 demo_uts_namespaces 所创建的新 UTS 命名空间中执行一个新 shell:

代码语言:txt
复制
# ./ns_exec ~/uts /bin/bash     # ~/uts is bound to /proc/27514/ns/uts
My PID is: 28788

可验证 shell 和 demo_uts_namespaces 中的子进程位于同一 UTS 命名空间,主机名和 /proc/PID/ns/uts 文件的 inode 号均相同:

代码语言:txt
复制
# hostname
bizarro
# readlink /proc/27514/ns/uts
uts:[4026532338]
# readlink /proc/$$/ns/uts      # $$ is replaced by shell's PID
uts:[4026532338]

在早期的内核版本中,不能通过 setns() 加入一个挂载、PID 或用户命名空间,但是,从 Linux 3.8 开始,setns() 支持所有类型的命名空间。

离开一个命名空间:unshare()

最后一个命名空间中的系统调用是 unshare():

代码语言:txt
复制
int unshare(int flags);

unshare() 类似于 clone(),但作用施加于调用者:它通过 flags 参数中特定的 CLONE_NEW* 位创建来新的命名空间,并使得调用者成为命名空间中的一员。(我们将忽略 clone(),unshare() 提供命名空间之外的功能)。unshare() 的主要目的是隔离命名空间,但无需创建一个新进程或线程(clone() 所做的)。

撇开 clone() 系统调用的其他影响不谈,调用的形式如下:

代码语言:txt
复制
clone(..., CLONE_NEWXXX, ....);

在命名空间中,大致等同于:

代码语言:txt
复制
if (fork() == 0)
    unshare(CLONE_NEWXXX);      /* Executed in the child process */

unshare() 系统调用的用途之一是实现 unshare 命令,允许用户在隔离于 shell 的命名空间中执行命令。该命令的通用形式如下:

代码语言:txt
复制
unshare [options] program [arguments]

options 为命令行标志,在使用特定的 arguments 执行程序前离开命名空间。

unshare 命令的关键部分实现如下:

代码语言:txt
复制
/* Code to initialize 'flags' according to command-line options
omitted */

unshare(flags);

/* Now execute 'program' with 'arguments'; 'optind' is the index
of the next command-line argument after options */

execvp(argv[optind], &argv[optind]);

一个 unshare 命令的简单实现(unshare.c)在这儿

接下来的 shell 中,我们使用 unshare.c 程序在一个隔离的挂载命名空间中执行一个 shell。正如上周的文章所提,挂载命名空间隔离一组进程看到的文件系统挂载点集,使得不同挂载命名空间中的进程具有不同的文件系统结构视图。

代码语言:txt
复制
# echo $$                             # Show PID of shell
8490
# cat /proc/8490/mounts | grep mq     # Show one of the mounts in namespace
mqueue /dev/mqueue mqueue rw,seclabel,relatime 0 0
# readlink /proc/8490/ns/mnt          # Show mount namespace ID 
mnt:[4026531840]
# ./unshare -m /bin/bash              # Start new shell in separate mount namespace
# readlink /proc/$$/ns/mnt            # Show mount namespace ID 
mnt:[4026532325]

通过比较两个 readlink 命令的输出,可看到两个 shell 位于不同的挂载命名空间。修改其中一个命名空间的挂载点集,然后观察该修改在另外一个命名空间中是否可见,可证明两个程序位于两个隔离的命名空间中:

代码语言:txt
复制
# umount /dev/mqueue                  # Remove a mount point in this shell
# cat /proc/$$/mounts | grep mq       # Verify that mount point is gone
# cat /proc/8490/mounts | grep mq     # Is it still present in the other namespace?
mqueue /dev/mqueue mqueue rw,seclabel,relatime 0 0

从上面最后两行命令的输出可见,/dev/mqueue 挂载点从其中一个挂载空间中消失了,但在另外一个中可见。

结束语

本文中查看了一些命名空间 API 中的基本部分,以及它们是如何一起使用的。接下来的文章中将看一些命名空间中的更深部分,尤其是 PID 和用户命名空间;用户命名空间为应用程序提供了一系列新的可能性,可以使用以前仅限于特权应用程序的内核接口。


原文:https://lwn.net/Articles/531381/

公众号:Geek乐园

博客:https://blog.csdn.net/u012319493/article/details/102793652

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系外文翻译前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 在新的命名空间中创建一个子空间:clone()
  • /proc/PID/ns 文件
  • 加入一个已存在的命名空间:setns()
  • 离开一个命名空间:unshare()
  • 结束语
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档