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

命名空间介绍之四:PID 命名空间的延伸

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

在本文中,我们将继续上周关于 PID 命名空间的讨论(并扩展我们正在进行的关于命名空间的系列文章)。PID 命名空间的一个用途是实现一个进程包(容器),其行为类似于一个自包含的 Linux系统。init 进程是传统系统和 PID 命名空间容器的关键部分。因此,我们将研究 init 进程的特殊角色,并着重于它与传统 init 进程不同的几个方面。此外,我们还将研究命名空间 API 应用于 PID 命名空间时的一些其他细节。

PID 命名空间的 init 进程

在 PID 命名空间中创建的第一个进程 ID 为 1。该进程的作用与传统 Linux 系统上的 init 进程类似。特别是,init 进程可以执行整个 PID 命名空间所需的初始化(例如,可能启动其它应该为命名空间中标准部分的进程),并成为命名空间中孤儿进程的父进程。

为了解释 PID 命名空间的操作,我们将使用一些专门的实例程序。第一个程序是 ns_child_exec.c,语法如下:

代码语言:txt
复制
ns_child_exec [options] command [arguments]

ns_child_exec 程序使用 clone() 系统调用创建子进程;然后子进程使用可选的 agruments 执行给定的 command。options 用于指定 clone() 所创建的新命名空间。例如,-p 选项会在新的 PID 命名空间中创建子进程,如下所示:

代码语言:txt
复制
$ su                  # Need privilege to create a PID namespace

Password:

# ./ns_child_exec -p sh -c 'echo $$'

1

该命令行会在新 PID 命名空间中创建一个子进程,该子进程会打印 shell 的 PID。因为 PID 为 1,所以该 shell 运行时是 PID 命名空间的 init 进程。

下一个示例是 simple_init.c,运行后成为 PID 命名空间中的 init 进程。这可让我们证明 PID 命名空间和 init 进程的一些特点。

simple_init 程序执行 init 的两个主要功能。其中一个功能是“系统初始化”。大多数 init 系统都是更复杂的程序,采用表驱动的方法进行系统初始化。我们(简单得多)的 simple_init 程序提供了一个简单的 shell 工具,允许用户手动执行初始化命名空间所需的任何 shell 命令;还允许我们自由执行 shell 命令,以便在命名空间中进行实验。simple_init 执行的另一个函数是使用 waitpid() 获取终止的子进程的状态。

因此,例如,我们可以使用 ns_child_exec 程序与 simple_init 一起启动运行于新 PID 命名空间中 init 进程:

代码语言:txt
复制
# ./ns_child_exec -p ./simple_init

init$

init$ 提示符表示 simple_init 程序已准备好读取和执行 shell 命令。

现在,我们将使用目前介绍的两个程序与另一个小程序 orphan.c 一起使用,以演示在 PID 命名空间中孤儿进程是由 PID 命名空间中的 init 进程收养的,而不是系统范围内的 init 进程收养的。

orphan 程序执行 fork() 来创建子进程。当子进程运行时,父进程退出,使得该子进程成为孤儿。子进程执行一个循环,循环将一直持续直到其成为孤儿(即getppid()返回1)。父进程和子进程打印消息,以便我们可以看到这两个进程何时终止,以及子进程何时成为孤儿进程。

为了查看 simple_init 程序从孤儿子进程获取的结果,我们将使用该程序的 -v 选项,让它生成有关其子进程的创建和终止的详细消息:

代码语言:txt
复制
# ./ns_child_exec -p ./simple_init -v

        init: my PID is 1

init$ ./orphan

        init: created child 2

Parent (PID=2) created child with PID 3

Parent (PID=2; PPID=1) terminating

        init: SIGCHLD handler: PID 2 terminated

init$                   # simple\_init prompt interleaved with output from child

Child  (PID=3) now an orphan (parent PID=1)

Child  (PID=3) terminating

        init: SIGCHLD handler: PID 3 terminated

上述输出中,以 init: 为前缀的缩进消息由 simple_init 程序打印。所有其他消息(init$ 标识除外)都由 orphan 程序打印。从输出中,我们可以看到子进程(PID 3)在其父进程(PID 2)终止时成为孤儿进程。此时,子进程被 PID 命名空间中的 init 进程(pid 1)收养。

信号和 init 进程

传统的 Linux init 进程是专门响应信号处理的。只有那些已被进程已建立了信号处理程序的信号可以传递到 init;其它信号都将被忽略。这可以防止 init 进程被意外终止。

PID 命名空间为命名空间中的 init 进程实现了一些类似的行为。命名空间中的其它进程(甚至是特权进程)只能发送那些已经被 init 建立处理程序的那些信号。这可防止命名空间成员意外中终止在命名空间中担任重要角色的进程。但是,请注意(对于传统的 init 进程),内核仍然可以在所有常见情况下(例如,硬件异常、终端生成的信号如 SIGTTOU,和计时器过期)为 PID 命名空间 init 进程生成信号。

信号也可以(通过通常的权限检查后)由祖先 PID 命名空间中的进程发送到 PID 命名空间中的 init 进程。同样,只能发送被 init 进程建立处理程序的信号,但有两个例外:SIGKILL 和 SIGSTOP。当祖先 PID 命名空间中的进程将这两个信号发送到 init 进程时,它们将被强制传递(并且无法捕获)。SIGSTOP 信号停止 init 进程;SIGKILL 终结它。由于 init 进程对于 PID 命名空间的运行至关重要,如果 init 进程被 SIGKILL 终止(或者由于任何其他原因终止),内核会向命名间中的所有其他进程发送 SIGKILL 信号。

通常,PID 命名空间也会在其 init 进程终止时被破坏。但是,有一个例外:只要命名空间中某个进程的 /proc/pid/ns/pid 文件被绑定挂载或保持打开,命名空间就不会被破坏。但是,无法在命名空间中创建新进程(通过setns() 和 fork()):在fork() 调用期间检测到缺少 init 进程,会导致该调用失败并出现 ENOMEM 错误(通常表示无法分配 PID)。换句话说,PID 命名空间将继续存在,但不再可用。

挂载一个 procfs 文件系统(重温)

在该系列之前的文章中,PID 命名空间的 /proc 文件系统(procfs)被挂载在别的地方而非传统的 /proc 挂载点。这运行我们使用 shell 命令去查看与每个新 PID 命名空间相关的 /proc/PID 目录中的内容,并使用 ps 命令查看根 PID 命名空间中的可见进程。

然而,类似于 ps 的工具是根据挂载在 /proc 的 procfs 中的内容来获取信息的。因此,如果我们想要 ps 工具运行于正确的 PID 命名空间中,需要为该命名空间挂载一个正确的 procfs。因为 simple_init 程序允许我们执行 shell 命令,所以我们可以通过执行 mount 命令来展示:

代码语言:txt
复制
# ./ns_child_exec -p -m ./simple_init

init$ mount -t proc proc /proc

init$ ps a

    PID TTY      STAT   TIME COMMAND

    1 pts/8    S      0:00 ./simple_init

    3 pts/8    R+     0:00 ps a

ps 命令可列出通过 /proc 访问的所有进程。本例中,我们只看到了两个进程,说明该命名空间中只有两个进程在运行。

当运行上述 ns_child_exec 命令时,我们使用了 -m 选项,会将创建的子进程(运行 simple_init 的进程)放到一个单独的挂载命名空间中。效果是,mount 命令不会影响命名空间之外的 /proc 挂载。

unshare() 和 setns()

在第二篇文章中,我们描述了命名空间 API 中的两个系统调用:unshare() 和 setns()。自 Linux 3.8 以来,这些系统调用可被 PID 命名空间使用,但被其他命名空间使用时有些特殊的地方。

使用 unshare() 时明确 CLONE_NEWPID 标志可创建一个新的 PID 命名空间,但不会将调用者置于新的命名空间中。然而,调用者所创建的子进程会被置于新的命名空间中;第一个子进程会成为命名空间中的 init 进程。

setns() 系统调用现在可支持 PID 命名空间:

代码语言:txt
复制
setns(fd, 0);   /* Second argument can be CLONE_NEWPID to force a

                       check that 'fd' refers to a PID namespace */

fd 参数是一个文件描述符,标识一个被调用者所创建的子 PID 命名空间;该文件描述符可通过打开目标命名空间中的 /proc/PID/ns/pid 查看。同 unshare(),setns() 也不会将调用者移到 PID 命名空间;但调用者所创建的子进程会被放到一个命名空间中。

可使用本系列第二篇文章中的介绍的 ns_exec.c 的加强版来演示一起使用 setns() 和 PID 命名空间的某些方面,在我们弄懂发生了什么之前会很惊讶。新程序 ns_run.c 的语法如下:

代码语言:txt
复制
ns_run [-f] [-n /proc/PID/ns/FILE]... command [arguments]

该程序使用 setns() 来加入一个命名空间,命名空间由 -n 选项中的 /proc/PID/ns 文件指定。然后根据给定的 arguments 执行 command。如果指定 -f 选项,会使用 fork() 创建一个新的子进程来执行命令。

在一个终端窗口,我们在新 PID 命名空间中启动了 simple_init 程序,根据程序的输出可知道什么时候收养子进程:

代码语言:txt
复制
# ./ns_child_exec -p ./simple_init -v

        init: my PID is 1

init$ 

然后切换到运行 ns_run 程序的终端窗口,执行 orphan 程序。这将影响到创建于被 simple_init 所控制的 PID 命名空间中的两个进程:

代码语言:txt
复制
# ps -C sleep -C simple_init

PID TTY          TIME CMD

9147 pts/8    00:00:00 simple_init

# ./ns_run -f -n /proc/9147/ns/pid ./orphan

Parent (PID=2) created child with PID 3

Parent (PID=2; PPID=0) terminating

# 

Child  (PID=3) now an orphan (parent PID=1)

Child  (PID=3) terminating

看一下当 orphan 程序执行时其“父”进程(PID 2)的输出,可见其父进程 ID 为 0。这反映了启动 orphan 进程的进程(ns_run)在不同的命名空间中 --- 其成员对“父”进程不可见。正如前述文章,getppid() 在本例中返回 0。

下图展示了在 orphan 的“父”进程终止前不同进程之间的关系。箭头表示进程之间的父-子关系。

在这里插入图片描述
在这里插入图片描述

回到运行 simple_init 程序的窗口,可看到如下输出:

代码语言:txt
复制
init: SIGCHLD handler: PID 3 terminated

simple_init 获取了 orphan 程序创建的“子”进程(PID 3),但没有获取其“父”进程(PID 2)。因为“父”进程被它的位于另一个命名空间的父进程(ns_run)获取。下图展示了在 orphan 的“父”进程终止后,“子”进程终止前,进程之间的关系。

在这里插入图片描述
在这里插入图片描述

值得强调的是,setns() 和 unshare() 对待 PID 命名空间的方式有点特殊。对于其它类型的命名空间,这些系统调用确实改变了调用者。这些系统调用之所以没有改变 PID 命名空间,是因为成为另一个 PID 命名空间的成员会改变进程对自己 PID 的看法,因为 getpid() 是在进程所在的特定 PID 命名空间返回其 PID 的。许多用户空间的程序和系统调用均依赖于这样的假设:进程的 PID (被 getpid() 返回)是一个常量(事实上,GNU C 库的 getpid() 包装了缓存 PID 函数);如果进程 PID 改变,那些程序会崩溃。换言之,一个进程的 PID 命名空间取决于创建它的进程,并且之后(不像其它类型的命名空间关系)不能被改变。

结束语

本文中,我看了关于 PID 命名空间中的 init 进程的特殊角色,展示了如果挂载一个 PID 命名的 procfs,以便被 ps 之类的工具使用,还看了当使用 PID 命名空间时,一些 unshare() 和 setns() 的特性。关于 PID 命名空间的讨论至此结束;下篇文章中,我们将看一下用户命名空间。


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

公众号:Geek乐园

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

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

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

本文系外文翻译前往查看

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

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