Linux namespace之:pid namespace
目录:
- Linux namespace概述
- Linux namespace之:uts namespace
- Linux namespace之:mount namespace
- Linux namespace之:pid namespace
- Linux namespace之:network namespace
- Linux namespace之:user namespace
理解pid namespace
PID namespace表示隔离一个具有独立PID的运行环境。在每一个pid namespace中,进程的pid都从1开始,且和其他pid namespace中的PID互不影响。这意味着,不同pid namespace中可以有相同的PID值。
因为PID namespace中的PID是独立的,每一个PID namespace都允许一些特殊的操作:允许pid namespace挂起、迁移以及恢复,就像虚拟机一样。
在介绍PID namespace之前,有必要回顾一下创建其他类型namespace时的进程关系。
1 | # 在root namespace中 |
其中sudo在root namespace中,其子进程bash(14931)在新创建的uts namespace ns1中。从上面输出的结果可知,创建其他类型namespace时,unshare进程会在创建新的namespace后被该namespace中的第一个进程给替换掉。如果忘记了,请回到前文uts namespace复习复习。
了解了创建其他类型namespace进程关系的基础后,再来对比介绍pid namespace。
创建新的pid namespace的方式:
1 | # unshare --pid --fork [--mount-proc] <CMD> |
--fork
以及--mount-proc
选项稍后再解释。先看看创建pid namespace后的进程关系:
1 | $ sudo unshare -p -f -m -u --mount-proc /bin/bash |
注意上面输出的进程关系,和之前创建普通的namespace的进程关系不同,创建PID namespace时,sudo的子进程unshare进程保留了,这就是命令行中使用--fork
的效果:在unshare中创建出pid namespace后,它将fork出它的子进程加入到新的pid namespace中,并在该子进程中exec加载指定的/bin/bash进程作为该pid namespace中的第一个进程。
所以使用--fork
后导致的结果是:unshare进程被保留,且保留在原来的pid namespace中,而不是加入新的pid namespace中(在man pid_namespaces
中明确指出了创建pid namespace的unshare或setns进程不会也不能进入新的pid namespace)。
1 | # unshare进程和bash(12314)、sudo(15070)都在同一个pid namespace |
在新创建的pid namespace中使用ps命令查看进程信息:
1 | root@ns1:~# ps j |
关注输出结果中的两点:
1.pid namespace中的第一个进程bash,其PID=1
2.PID=1的进程的PPID=0,这和root namespace中pid=1的init进程的PPID=0是一样的
1 | $ ps -elf |
在pid namespace中,PID=1的进程作为该pid namespace环境的【init】进程,当该pid namespace中的某个进程A的父进程退出了,进程A将称为孤儿进程,孤儿进程将被该pid namespace中PID=1的进程收养。
另外,观察旧namespace以及新pid namespace中的进程:
1 | # 原来的namespace |
其实unshare的子进程bash(15072)和pid namespace中的bash(1)是同一个进程。查看它们的pid namespace的inode可知:
1 | # 原来的namespace中执行 |
所以可得知一个结论:当在namespace ns1中创建一个pid namespace ns2,祖先ns1中可查看ns2中的所有进程,但ns2中无法查看祖先ns1中的进程。
另外需要注意的是,--fork
并非一定是结合--pid
创建pid namespace使用的,它也可以直接使用或结合其他类型的namespace选项使用。例如:
1 | $ sudo unshare --fork sleep 3 |
这仅仅表示在一个子进程中执行sleep程序,这里的unshare并没有创建出namespace,所以上面的命令和在子shell中执行sleep命令没什么区别,例如(sleep 3)
。
嵌套pid namespace
其实,对于pid namespace来说,是存在嵌套关系的。所有的子孙pid namespace中的进程信息都会保存在父级以及祖先级namespace中,只不过在不同嵌套层级中,同一个进程对应的PID不同。
例如,创建嵌套了2层的pid namespace:
1 | # 当前namespace为ns0 |
现在ns0有3个子孙级的pid namespace:ns1、ns2和ns3。
在ns0中查看进程关系:
1 | $ pstree -lp | grep -oE 'sudo.*' |
其中每一对unshare---bash
代表一个pid namespace(但注意,unshare不在其子进程bash所在的pid namespace中)。
1 | unshare(16511)---bash(16513) |
这三个bash进程在各自的pid namespace中的PID=1。但从输出结果可知,在ns0中,这三个pid namespace中的PID分别为:
1 | bash(16513) |
PID=16513对应ns1中的bash,PID=16532对应ns2中的bash,PID=16540对应ns3中的bash。
在/proc/<PID>/status
中的NSPID字段记录了当前进程<PID>
在各父级pid namespace中对应的PID值。
例如,在ns0中查看ns3中的bash(pid=16540)进程:
1 | $ grep 'NSpid' /proc/16540/status |
这表示pid=16540这个进程对应ns1中的pid=25,对应ns2中的pid=9,对应ns3中的pid=1。
同样地,可以进入到ns1中(bash pid=16513)去查看进程pid=25的进程对应关系:
1 | # nsenter命令可进入一个已存在的namespace, |
pid namespace和procfs(/proc)
/proc
目录是内核对外暴露的可供用户查看或修改的内核中所记录的信息,包括内核自身的部分信息以及每个进程的信息。比如对于pid=N的进程来说,它的信息保存在/proc/<N>
目录下。
/proc
是一个挂载点,是伪文件系统procfs的挂载点,文件系统类型是proc:
1 | $ mount | grep proc |
在操作系统启动的过程中,会挂载procfs到/proc目录,它存在于root namespace中。
但是,创建新的pid namespace时不会自动重新挂载procfs,而是直接拷贝父级namespace的挂载点信息。这使得在新的pid namespace中仍然保留了父级namespace的/proc
目录,也就是在新创建的这个pid namespace中仍然保留了父级的进程信息。
1 | # 在root namespace中查看pid namespace的inode |
之所以有上述问题,其原因是在pid namespace中保留了root namespace中的/proc目录,而不是属于pid namespace自己的/proc。
但用户创建pid namespace时希望的是有完全独立的进程运行环境。这时,需要在pid namespace中重新挂载procfs,或者在创建pid namespace时指定--mount-proc
选项。
1 | # 重新挂载procfs |
pid namespace的信号问题
pid=1的进程是每一个pid namespace(无论是root namespace还是用户自己创建的pid namespace)的核心进程,它不仅负责收养其所在pid namespace中的孤儿进程,还影响整个pid namespace。
当pid namespace中pid=1的进程退出或终止,内核默认会发送SIGKILL信号给该pid namespace中的所有进程以便杀掉它们(如果该pid namespace中有子孙namespace,也会直接被杀)。
在创建pid namespace时可以通过--kill-child
选项指定pid=1的进程终止后内核要发送给pid namespace中进程的信号,其默认信号便是SIGKILL。
1 | # 例如指定发送SIGHUP信号 |
在pid namespace内部,只能向pid=1的进程发送那些在pid=1的进程中注册了信号处理程序的信号。
例如,如果pid namespace中/bin/bash作为pid=1的进程,那么在此环境中,只能向该bash进程发送设置了trap的信号,其他信号都被直接忽略,这样可以避免无法信号导致pid namespace被自己内部的进程终止。
但对于pid=1的/bin/bash做测试,发现SIGHUP信号总是有效。
1 | $ sudo unshare -p -f -m -u --mount-proc -w'/' /bin/bash |
在pid namespace外部,只有其祖先级namespace可发送信号给该pid namespace中的进程,因为其他namespace中看不到该pid namespace中的进程信息。但是发送信号时要找准pid namespace中的进程在祖先namespace中的PID值。
例如:
1 | # 在root namespace中,创建pid namespace ns1,并睡眠30秒 |