13 分钟
容器核心技术(六) PID Namespace
手册页面:pid namespaces。
背景知识
信号
信号是类 Unix 操作系统一种进程间通知的机制(参见:手册 signal(7))。本部分涉及的为:
- 用来协调多个进程的执行,如监听子孙进程的状态变更
SIGCHLD
,默认忽略。需要注意的是,如果一个进程退出后,其父进程进程没有处理SIGCHLD
信号,则该进程占用 PCB 将不会释放,此时该进程被称为僵尸进程。 - 无法覆盖的特权信号(包括 1 号进程同样无法覆盖),
SIGKILL
(终止) 和SIGSTOP
(挂起,需通过SIGCONT
信号唤醒) SIGTERM
,可以覆盖默认的行为,一般用于优雅退出
1 进程
1 号进程是内核创建的第 1 个用户态进程,内核对该进程的有特殊处理。
1 进程和进程树
在 Unix 类系统中,进程会组成一颗进程树,其根节点是 0 号进程。每个进程都有一个父进程,有 0 个或多个子进程。
0 号进程是内核进程,内核进程会创建 1 号进程,1 号进程是第一个用户态进程。
一个进程通过 fork/clone 系统调用创建一个子进程,一个进程的父进程为 fork 该进程的进程。
当一个进程的父进程退出了,为了维持进程树的关系,该进程的父进程将会被设置为 1 号进程。这种父进程变化为 1 号进程的进程被称为孤儿进程。这个过程可以叫做:1 号进程(如果该进程的祖先进程使用 prctl(2) 系统调用
和 PR_SET_CHILD_SUBREAPER
进行标记,则该进程会进行收养)收养了该孤儿进程。
1 号进程和信号
- 1 号进程只能收到一种信号,即 1 号进程注册了信号处理器的信号。参见:kill(2)。因此,默认情况下
kill -15 1
是收不到信号的。 - 由于
SIGKILL
和SIGSTOP
两个特权信号是无法覆盖的,所以任何情况下,其子进程向 1 号进程发送这两个信号都是无效的(父 PID Namespace 的进程发送是可以的)。 - 通过 reboot(2) (
LINUX_REBOOT_CMD_CAD_OFF
)关闭 CAD (Ctrl-Alt-Del) 快捷键时,CAD 将会向 1 号进程发送SIGINT
信号
/proc
文件系统
手册:proc(5)
进程文件系统,通过 mount -t proc proc /proc
调用。top、ps 等命令都是通过该文件系统实现的。
Unix domain socket
手册:unix(7)
遵循 Socket API 的 进程通讯方式,相比于网络层面的 Socket API 接口,性能是更好。
描述
- 通过
CLONE_NEWPID
可以创建一个 PID Namespaceclone(2)
系统调用产生的进程就是该 PID Namespace 的第一个进程setns(2)
系统调用后,再调用fork/clone
系统调用(不需要指定CLONE_NEWPID
),产生的进程就是该 PID Namespace 的进程。注意:调用setns(2)
的进程的 PID Namespace 不会发生变化。unshare(2)
系统调用后,再调用fork/clone
系统调用(不需要指定CLONE_NEWPID
),产生的进程就是该 PID Namespace 的进程,第一次调用时产生的进程,就是该 PID Namespace 的第一个进程。注意:调用unshare(2)
的进程的 PID Namespace 不会发生变化。
- PID Namespace 支持嵌套
- 最大 32 层
- 当前 PID Namespace 可见所有子孙 Namespace 的进程,可见意味着可以 kill、设置优先级
- 当前 PID Namespace 无法看到祖先 Namespace 下的进程
- 一个进程在每一层 PID Namespace 都有一个 PID,进程自身调用
getpid
看到的是当前 PID Namespace 的 PID - 如果当前 PID Namespace 的进程的父进程也是当前 PID Namespace 内的进程,则
getppid(2)
返回该父进程在该 PID Namespace 的 PID - 如果当前 PID Namespace 的进程的父进程不是当前 PID Namespace 内的进程,则
getppid(2)
返回该父进程返回 0 (setns(2)
和unshare(2)
语义造成的)
setns(2)
和unshare(2)
语义,由于一个进程的 PID Namespace 从创建的那一刻就固定了,所以setns(2)
和unshare(2)
,并不会影响当前进程的 PID Namespace( 仅仅修改/proc/[pid]/ns/pid_for_children
文件)。(试想一下,如果 PID Namespace 发生了变化,那么他们的进程号就变了,而很多程序假设自身的进程号不会发生变化的,这样就破坏了兼容性)- 新的 PID Namespace 的第一个进程的进程号为
1
,即在该 PID Namespace 中,该进程就是受内核特殊处理的 1 号进程(参见上文:1 号进程和信号,1 号进程和进程树),此外还需要注意:- 该 PID Namespace 内的进程永远无法通过
kill -9
杀死 1 号进程。 - 祖先 PID Namespace 的进程可以向该 1 号进程通过
kill -9
发送信号,此时该进程的行为和普通进程一致。但是有一点需要注意的是(手册也没有阐述5.10.0-11-amd64
稳定复现):- 如果该 PID Namespace 存在一个
进程 a
,其父进程不在该 PID Namespace 中 (即:通过setns(2)
创建到该 Namespace 中),且这个父进程没有处理SIGCHLD
信号。此时kill -9
1 号进程 进程 a
将变成僵尸进程,该名字空间下的所有进程都将无响应- 该 PID Namespace 处于可不加入状态(即:通过
setns(2)
创建将报错ENOMEM
) - 只有
进程 a
真正退出,该 PID Namespace 的其他进程才能退出
- 如果该 PID Namespace 存在一个
- 如果某 PID Namespace 的 1 号进程退出了,则整个 PID Namespace 所有进程将被杀死,也就是说这个 PID Namespace 已经消失了。
- 内核会向该 PID Namespace 下的所有进程发送
SIGKILL
(9) 信号 - 无法再在该 Namespace 中
fork
进程,比如:之前通过调用了setns(2)
和unshare(2)
将该进程/proc/[pid]/ns/pid_for_children
设置为一个 1 号进程现在已经退出的 PID Namespace,然后执行fork
,此时会报ENOMEM
错误。
- 内核会向该 PID Namespace 下的所有进程发送
- 在非 Init PID Namespace 调用
reboot(2)
行为不同,调用后,1 号进程将直接被终止,该进程的父进程wait(2)
收到子进程的退出信号为SIGHUP
或SIGINT
(由参数决定) 信号(通过WTERMSIG(wstatus)
获得) - PID Namespace 1 号进程收养孤儿机制
getppid(2)
不为 0 的会被当前 PID Namespace 的 1 号进程收养getppid(2)
为 0 的不会被当前 PID Namespace 的 1 号进程收养,而是被之前父 PID Namespace 所在的 PID Namespace 的 1 号进程收养 (产生这种进程的原因还是setns(2)
和unshare(2)
语义造成的),在当前 PID Namespace 看来,该进程的getppid(2)
仍为 0
- 该 PID Namespace 内的进程永远无法通过
/proc
- 显示的是在执行 mount 时刻进程所属的 PID Namespace 下的可见的进程(包含子孙进程)。
- 一个常见做法是,PID Namespace 配合 Mount Namespace 使用,执行
mount -t proc proc /proc
,将当前 PID Namespace 的进程信息挂载进去。(如果不这么做,/proc/self
看到的还是该进程在父 PID Namespace 中的信息) /proc/sys/kernel/ns_last_pid
是当前 PID Namespace 的 last pid,可以通过更改该文件的值,来配置即将创建的进程的 ID(从ns_last_pid + 1
开始查找)
- 杂项
SCM_CREDENTIALS
unix(7)
会翻译成对应的 PID Namespace 的 PID
实验
实验设计
为了验证 PID Namespace 的能力,按照时序进行如下操作:
- (0s)
主进程
启动一个具有新 PID Namespace 和 Mount Namespace 的进程(a)
,主进程 sleep 1s - (0s)
进程(a)
会先挂载/proc
, sleep 2s - (1s)
主进程
构造一个孤儿进程,进程(b)
,该进程在新 PID Namespace 中,其 ppid 为 0,在父 PID Namespace 中其 ppid 为 1 - (2s)
主进程
构造进程(c)
,该进程在新 PID Namespace 中,其 ppid 为 0,,在父 PID Namespace 中其 ppid 为主进程
- (3s)
子进程(a)
执行命令:- 构造一个
孤儿进程(d)
,在该 PID Namespace,其 ppid 为 1 - 观察
/proc
目录 - 观察该 PID Namespace 的所有进程
- 尝试
kill -9 1
- 再次观察该 PID Namespace 的所有进程
exec sleep infinity
- 构造一个
- (4s) 然后 fork
子进程(e)
,该进程在主进程初始状态的 PID Namespace 中,执行命令:- 观察
/proc
目录 - 观察所有 sleep 进程
- 尝试
kill -9 进程(a)
- 再次观察观察所有 sleep 进程
- 观察
- (5s) 主进程退出
该实验在第 3s 末进程状态应该为:
注意,上图描述的是 C 语言版本可以达到的效果:
- 在 Go 语言 和 Shell 描述的实验代码中,
进程 c
的父进程应该是nsenter
。这个nsenter
的 PID Namespace 为 main,这个nsenter
的父进程为 main。
此外,阅读下列实验代码,可以关注注释中的如下内容:
seq: xxs
标明时序过程。进程 x
表示相关语句来生成实验设计中的进程
源码
C 语言描述
注意事项
- 父进程需要处理
SIGCHLD
信号,否则主进程kill -9
新 PID Namespace 的 1 号进程(即进程 a
)时,上文实验设计进程 c
变成僵尸进程,导致新 PID Namespace 的所有进程无响应 - 使用
sleep(3) 库函数
会被SIGCHLD
信号中断,导致时序不符合预期。因此需要使用nanosleep(2) 系统调用
手动实现一个专门的 sleep。
实验代码如下
// gcc src/c/01-namespace/04-pid/main.c && sudo ./a.out
#define _GNU_SOURCE // Required for enabling clone(2)
#include <sys/wait.h> // For waitpid(2)
#include <sys/mount.h> // For mount(2)
#include <sys/mman.h> // For mmap(2)
#include <sys/syscall.h> // For SYS_pidfd_open
#include <time.h> // For nanosleep(2)
#include <sched.h> // For clone(2)
#include <signal.h> // For SIGCHLD
#include <stdio.h> // For perror(3), printf(3), perror(3)
#include <unistd.h> // For execv(3), sleep(3)
#include <stdlib.h> // For exit(3), system(3)
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
} while (0)
#define STACK_SIZE (1024 * 1024)
void my_sleep(int sec)
{
struct timespec t = {
.tv_sec = sec,
.tv_nsec = 0};
// sleep 会被信号打断,因此通过 nanosleep 重新实现一下
// https: // man7.org/linux/man-pages/man2/nanosleep.2.html
while (nanosleep(&t, &t) != 0)
;
}
// 进程 a:当前 bash,最终为 sleep infinity
// 进程 d:nohup sleep infinity 孤儿进程在该 PID Namespace 中,其 ppid 为 1
char *const proccess_a_args[] = {
"/bin/bash",
"-xc",
"bash -c 'nohup sleep infinity >/dev/null 2>&1 &' \
&& echo $$ \
&& ls /proc \
&& ps -o pid,ppid,cmd \
&& kill -9 1\
&& ps -o pid,ppid,cmd \
&& exec sleep infinity \
",
NULL};
// 进程 b: 在该 PID Namespace 中,构造一个孤儿进程,其 ppid 为 0,在父 PID Namespace 中 为 1
char *proccess_b_args[] = {
"/bin/bash",
"-c",
"",
NULL};
// 进程 c: sleep infinity 进程在该 PID Namespace 中,其 ppid 为 0,在父 PID Namespace 中 ppid 为 主进程
char *const proccess_c_args[] = {
"/bin/bash",
"-c",
"exec sleep infinity",
NULL};
// 进程 e:
char *const proccess_e_args[] = {
"/bin/bash",
"-xc",
"ls /proc \
&& ps -eo pid,ppid,cmd | grep sleep | grep -v grep \
&& kill -9 $(ps -eo pid,ppid | grep $PPID | awk '{print $1}' | sed -n '2p') \
&& ps -eo pid,ppid,cmd | grep sleep | grep -v grep \
",
NULL};
int new_namespace_func(void *args)
{
// seq: 0s
// 首先,需要阻止挂载事件传播到其他 Mount Namespace,参见:https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#NOTES
// 如果不执行这个语句, cat /proc/self/mountinfo 所有行将会包含 shared,这样在这个子进程中执行 mount 其他进程也会受影响
// 关于 Shared subtrees 更多参见:
// https://segmentfault.com/a/1190000006899213
// https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#SHARED_SUBTREES
// 下面语句的含义是:重新递归挂(MS_REC)载 / ,并设置为不共享(MS_SLAVE 或 MS_PRIVATE)
// 说明:
// MS_SLAVE 换成 MS_PRIVATE 也能达到同样的效果
// 等价于执行:mount --make-rslave / 命令
if (mount(NULL, "/", NULL , MS_SLAVE | MS_REC, NULL) == -1)
errExit("mount-MS_SLAVE");
// 挂载当前 PID Namespace 的 proc
// 因为在新的 Mount Namespace 中执行,所有其他进程的目录树不受影响
// 等价命令为:mount -t proc proc /proc
// mount 函数声明为:
// int mount(const char *source, const char *target,
// const char *filesystemtype, unsigned long mountflags,
// const void *data);
// 更多参见:https://man7.org/linux/man-pages/man2/mount.2.html
if (mount("proc", "/proc", "proc", 0, NULL) == -1)
errExit("mount-proc");
my_sleep(3);
// seq: 3s
printf("=== new pid namespace process ===\n");
execv(proccess_a_args[0], proccess_a_args);
perror("exec");
exit(EXIT_FAILURE);
}
pid_t fork_proccess(char *const *argv)
{
pid_t p = fork();
if (p == 0)
{
execv(argv[0], argv);
perror("exec");
exit(EXIT_FAILURE);
}
return p;
}
void set_pid_namespace(pid_t pid) {
int fd = syscall(SYS_pidfd_open, pid, 0);
if (fd == -1)
errExit("pidfd_open");
if (setns(fd, CLONE_NEWPID) == -1)
errExit("setns");
close(fd);
}
void print_child_handler(int sig) {
int wstatus;
pid_t pid;
// https://man7.org/linux/man-pages/man2/waitpid.2.html
// 获取子进程退出情况
while ((pid=waitpid(-1, &wstatus, WNOHANG)) > 0) {
printf("*** pid %d exit by %d signal\n", pid, WTERMSIG(wstatus));
}
}
void register_signal_handler() {
// 处理 SIGCHLD 信号,解决僵尸进程阻塞 Namespace 进程退出的情况。
signal(SIGCHLD, print_child_handler);
}
int main(int argc, char *argv[])
{
// seq: 0s
printf("=== main: %d\n", getpid());
// 注册 SIGCHLD 处理程序,会产生僵尸进程,而导致 PID Namespace 无法退出
register_signal_handler();
// 为子进程提供申请函数栈
void *child_stack = mmap(NULL, STACK_SIZE,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK,
-1, 0);
if (child_stack == MAP_FAILED)
errExit("mmap");
// 创建新进程,并为该进程创建一个 PID Namespace(CLONE_NEWPID),并执行 new_namespace_func 函数
// clone 库函数声明为:
// int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
// /* pid_t *parent_tid, void *tls, pid_t *child_tid */);
// 更多参见:https://man7.org/linux/man-pages/man2/clone.2.html
pid_t pa = clone(new_namespace_func, child_stack + STACK_SIZE, SIGCHLD | CLONE_NEWNS | CLONE_NEWPID, NULL); // 进程 a
if (pa == -1)
errExit("clone-PA");
printf("=== PA: %d\n", pa);
my_sleep(1);
// seq: 1s
// 构造 进程 b
char buf[256];
// 通过 nsenter 进入进程 a 的 PID Namespace
sprintf(buf, "exec nsenter -p -t %d bash -c 'echo === PB: \"$$ in new pid namespace\" && exec sleep infinity'", pa);
proccess_b_args[2] = buf;
pid_t pbp = fork_proccess(proccess_b_args);
if (pbp == -1)
errExit("clone-PB");
my_sleep(1);
// seq: 2s
// 此时 kill 掉 nsenter 进程,sleep infinity 就能称为满足条件的进程 b
kill(pbp, SIGKILL);
// 主进程 setns PID Namespace 为 进程 a
set_pid_namespace(pa);
// fork 进程 c
pid_t pc = fork_proccess(proccess_c_args);
if (pc == -1)
errExit("clone-PC");
printf("=== PC: %d\n", pc);
my_sleep(2);
// seq: 4s
// 恢复主进程 PID Namespace
set_pid_namespace(1);
printf("=== old pid namespace process ===\n");
pid_t pe = fork_proccess(proccess_e_args);
my_sleep(1);
// seq: 5s
return 0;
}
Go 语言描述
注意事项:
Go 不能直接使用 setns 系统调用(因为 setns 不支持多线程调用,而 go runtime 是多线程),因此还是通过 nsenter 命令实现
进程 c
//go:build linux // sudo go run src/go/01-namespace/04-pid/main.go package main import ( "fmt" "log" "os" "os/exec" "os/signal" "syscall" "time" ) const ( sub = "sub" ) var proccess_a_args = []string{ "/bin/bash", "-xc", "bash -c 'nohup sleep infinity >/dev/null 2>&1 &' " + "&& echo $$ " + "&& ls /proc " + "&& ps -o pid,ppid,cmd " + "&& kill -9 1 " + "&& ps -o pid,ppid,cmd " + "&& exec sleep infinity ", } var proccess_e_args = []string{ "/bin/bash", "-xc", "ls /proc " + "&& ps -eo pid,ppid,cmd | grep sleep | grep -v grep " + "&& kill -9 $(ps -eo pid,ppid | grep $PPID | awk '{print $1}' | sed -n '2p') " + "&& ps -eo pid,ppid,cmd | grep sleep | grep -v grep ", } func asyncExec(name string, arg ...string) int { cmd := exec.Command(name, arg...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Start() return cmd.Process.Pid } func newNamespaceProccess() int { cmd := exec.Command(os.Args[0], "sub") // 创建新进程,并为该进程创建一个 PID Namespace(syscall.CLONE_NEWPID // 更多参见:https://man7.org/linux/man-pages/man2/clone.2.html cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWPID, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Start() return cmd.Process.Pid } func newNamespaceProccessFunc() { // seq: 0s // 首先,需要阻止挂载事件传播到其他 Mount Namespace,参见:https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#NOTES // 如果不执行这个语句, cat /proc/self/mountinfo 所有行将会包含 shared,这样在这个子进程中执行 mount 其他进程也会受影响 // 关于 Shared subtrees 更多参见: // https://segmentfault.com/a/1190000006899213 // https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#SHARED_SUBTREES // 下面语句的含义是:重新递归挂(MS_REC)载 / ,并设置为不共享(MS_SLAVE 或 MS_PRIVATE) // 说明: // MS_SLAVE 换成 MS_PRIVATE 也能达到同样的效果 // 等价于执行:mount --make-rslave / 命令 if err := syscall.Mount("", "/", "", syscall.MS_SLAVE|syscall.MS_REC, ""); err != nil { panic(err) } // 挂载当前 PID Namespace 的 proc // 因为在新的 Mount Namespace 中执行,所有其他进程的目录树不受影响 // 等价命令为:mount -t proc proc /proc // 更多参见:https://man7.org/linux/man-pages/man8/mount.8.html if err := syscall.Mount("proc", "/proc", "proc", 0, ""); err != nil { panic(err) } time.Sleep(3 * time.Second) // seq: 3s fmt.Println("=== new pid namespace process ===") if err := syscall.Exec(proccess_a_args[0], proccess_a_args, nil); err != nil { panic(err) } } func registerSignalhandler() { // 处理 SIGCHLD 信号,解决僵尸进程阻塞 Namespace 进程退出的情况。 sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGCHLD) go func() { for { <-sigs for { var wstatus syscall.WaitStatus pid, err := syscall.Wait4(-1, &wstatus, syscall.WNOHANG, nil) if err != nil || pid == -1 || pid == 0 { break } fmt.Printf("*** pid %d exit by %d signal\n", pid, wstatus.Signal()) } } }() } func mainProccess() { // seq: 0s fmt.Printf("=== main: %d\n", os.Getpid()) // 注册 SIGCHLD 处理程序,会产生僵尸进程,而导致 PID Namespace 无法退出 registerSignalhandler() // 1. 执行 newNamespaceExec,启动一个具有新的 PID Namespace 的进程 pa := newNamespaceProccess() fmt.Printf("=== PA: %d\n", pa) time.Sleep(1 * time.Second) // seq: 1s // 构造 进程 b // 通过 nsenter 进入进程 a 的 PID Namespace pbp := asyncExec("/bin/bash", "-c", fmt.Sprintf("exec nsenter -p -t %d bash -c 'echo === PB: \"$$ in new pid namespace\" && exec sleep infinity'", pa)) time.Sleep(1 * time.Second) // 此时 kill 掉 nsenter 进程,sleep infinity 就能称为满足条件的进程 b syscall.Kill(pbp, syscall.SIGKILL) // seq: 2s // 构造进程 c // Go 不能直接使用 setns 系统调用(因为 setns 不支持多线程调用,而 go runtime 是多线程),因此还是通过 nsenter 命令实现 _ = asyncExec("/bin/bash", "-c", fmt.Sprintf("exec nsenter -p -t %d bash -c 'echo === PC: \"$$ in new pid namespace\" && exec sleep infinity'", pa)) time.Sleep(2 * time.Second) // seq: 4s fmt.Println("=== old pid namespace process ===") _ = asyncExec(proccess_e_args[0], proccess_e_args[1:]...) time.Sleep(1 * time.Second) // seq: 5s return } func main() { switch len(os.Args) { case 1: mainProccess() return case 2: if os.Args[1] == sub { newNamespaceProccessFunc() return } } log.Fatalf("usage: %s [sub]", os.Args[0]) }
Shell 描述
src/shell/01-namespace/04-pid/main.sh
#!/usr/bin/env bash
# sudo ./src/shell/01-namespace/04-pid/main.sh
# 注意:该脚本运行于进程为 main
# 设置 main 进程的 /proc/[pid]/ns/pid_for_children
# mount namespace 不能在此设置,因为 mount namespace 会立即生效
exec unshare -p bash $(cd $(dirname "$0"); pwd)/seq00.sh
src/shell/01-namespace/04-pid/seq00.sh
#!/usr/bin/env bash
# 注意:该脚本运行于进程为 main
# seq: 0s
echo "=== main: $$"
# bash 默认处理了 SIGCHLD 信号,因此不需要处理信号
### 构造进程 a
# 创建一个新的 mount namespace
unshare -m bash -c 'mount -t proc proc /proc \
&& sleep 3 \
&& echo "=== new pid namespace process ===" \
&& set -x \
&& bash -c "nohup sleep infinity >/dev/null 2>&1 &" \
&& echo $$ \
&& ls /proc \
&& ps -o pid,ppid,cmd \
&& kill -9 1 \
&& ps -o pid,ppid,cmd \
&& exec sleep infinity \
' &
pa=$!
echo "=== PA: $pa"
sleep 1
# seq: 1s
# 恢复 main 进程 /proc/[pid]/ns/pid_for_children 为初始状态
exec nsenter -p -t 1 bash $(cd $(dirname "$0"); pwd)/seq01.sh $pa
src/shell/01-namespace/04-pid/seq01.sh
#!/usr/bin/env bash
# 注意:该脚本运行于 main 进程的子进程,其 PID Namespace 和 main 进程相同
pa=$1
# seq: 1s
### 构造进程 b
nsenter -p -t $pa bash -c 'echo "=== PB: $$ in new pid namespace" && exec sleep infinity' &
pbp=$! # 进程 b 的父进程
sleep 1
# seq: 2s
kill -9 $pbp # kill 进程 b 的父进程,进程 b 构造完成
### 构造进程 c
nsenter -p -t $pa bash -c 'echo "=== PC: $$ in new pid namespace" && exec sleep infinity' &
sleep 2
# seq: 4s
echo "=== old pid namespace process ==="
set -x
ls /proc
ps -eo pid,ppid,cmd | grep sleep | grep -v grep
kill -9 $pa
ps -eo pid,ppid,cmd | grep sleep | grep -v grep
输出及分析
=== main: 4683
=== PA: 4684
=== PB: 2 in new pid namespace
=== PC: 4708
*** pid 4697 exit by 9 signal
=== new pid namespace process ===
+ bash -c 'nohup sleep infinity >/dev/null 2>&1 &'
+ echo 1
1
+ ls /proc
1 6 bus cpuinfo dma fb iomem kcore kpagecgroup locks mounts partitions self swaps thread-self version
2 acpi cgroups crypto driver filesystems ioports keys kpagecount meminfo mtrr pressure slabinfo sys timer_list vmallocinfo
3 asound cmdline devices dynamic_debug fs irq key-users kpageflags misc net sched_debug softirqs sysrq-trigger tty vmstat
5 buddyinfo consoles diskstats execdomains interrupts kallsyms kmsg loadavg modules pagetypeinfo schedstat stat sysvipc uptime zoneinfo
+ ps -o pid,ppid,cmd
PID PPID CMD
1 0 /bin/bash -xc bash -c 'nohup sleep infinity >/dev/null 2>&1 &' ?&& echo $$ ?&& ls /proc ?&& ps -o pid,ppid,cmd ?&& kill -9 1?&& ps -o pid,ppid,cmd ?&& exec sleep infini
2 0 sleep infinity
3 0 sleep infinity
5 1 sleep infinity
7 1 ps -o pid,ppid,cmd
+ kill -9 1
+ ps -o pid,ppid,cmd
PID PPID CMD
1 0 /bin/bash -xc bash -c 'nohup sleep infinity >/dev/null 2>&1 &' ?&& echo $$ ?&& ls /proc ?&& ps -o pid,ppid,cmd ?&& kill -9 1?&& ps -o pid,ppid,cmd ?&& exec sleep infini
2 0 sleep infinity
3 0 sleep infinity
5 1 sleep infinity
8 1 ps -o pid,ppid,cmd
+ exec sleep infinity
=== old pid namespace process ===
+ ls /proc
1 11 15 204 24 278 3 3853 4426 4683 48 585 691 cmdline dynamic_debug irq kpageflags net softirqs tty
10 110 1558 2072 2445 280 302 3854 4458 4684 489 6 717 consoles execdomains kallsyms loadavg pagetypeinfo stat uptime
104 1183 17 2078 25 283 306 4 45 4698 49 62 9 cpuinfo fb kcore locks partitions swaps version
105 12 18 2079 253 285 311 4193 4586 47 490 621 acpi crypto filesystems keys meminfo pressure sys vmallocinfo
106 13 183 21 270 287 318 429 46 4708 50 641 asound devices fs key-users misc sched_debug sysrq-trigger vmstat
107 14 19 22 272 290 3762 43 462 4710 51 65 buddyinfo diskstats interrupts kmsg modules schedstat sysvipc zoneinfo
108 147 2 229 274 291 3764 44 463 4714 52 652 bus dma iomem kpagecgroup mounts self thread-self
109 148 20 23 277 294 3852 4413 4682 4715 575 66 cgroups driver ioports kpagecount mtrr slabinfo timer_list
+ grep -v grep
+ grep sleep
+ ps -eo pid,ppid,cmd
4684 4683 sleep infinity
4698 1 sleep infinity
4708 4683 sleep infinity
4710 4684 sleep infinity
++ sed -n 2p
++ awk '{print $1}'
++ grep 4683
++ ps -eo pid,ppid
+ kill -9 4684
*** pid 4708 exit by 9 signal
*** pid 4684 exit by 9 signal
+ grep -v grep
+ grep sleep
+ ps -eo pid,ppid,cmd
*** pid 4714 exit by 0 signal
分析上面输出日志可以看出,第 3s 末进程关系和实验描述一致:
注意,以上输出是 C 语言版本的 Go 和 Shell 版本略有不同:
- 在 Go 语言场景,进程 B 的 PID 为
5
而不是2
,因为 Go 会启动多个线程,这些线程会占用一些 PID。 - 在 Shell 语言场景,进程 B 的 PID 也不为
2
,因为在seq00.sh
脚本里面的一部分外部命令也占用了部分进程号。