Namespaces in operation, part 4: more on PID namespaces

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

PID 命名空间的 init 进程

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

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

ns_child_exec [options] command [arguments]

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

$ 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 进程:

# ./ns_child_exec -p ./simple_init
init$

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

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

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

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

# ./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 命令来展示:

# ./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 命名空间:

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 的语法如下:

ns_run [-f] [-n /proc/PID/ns/FILE]... command [arguments]

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

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

# ./ns_child_exec -p ./simple_init -v
        init: my PID is 1
init$ 

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

# 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 程序的窗口,可看到如下输出:

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://cloud.tencent.com/developer/column/4124

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值