pid 进程进程 端口
或者让我们编写自己的初始化过程
当您的进程在Docker容器中作为PID 1运行时,信号处理的行为与您预期的有所不同。
首先,让我们检查在“正常”系统上某个过程不是PID 1时会发生什么情况。
一个简单的Python进程
Aarons-iMac:bin aaronkalair$ cat mypy.py
import subprocess
subprocess.call(["sleep", "100"])
如果我们运行它并发送SIGTERM
Aarons-iMac:init-proc aaronkalair$ ps -ef | grep python
501 14013 6588 0 2:08pm ttys004 0:00.02 python mypy.py
Aarons-iMac:bin aaronkalair$ kill 14013
Terminated: 15
它终止了,这不足为奇
现在让我们在Docker容器中将其作为PID 1运行
Aarons-iMac:bin aaronkalair$ cat Dockerfile
from ubuntu:16.04
RUN apt-get update
RUN apt-get install -y python
COPY mypy.py /srv/
CMD ["python", "/srv/mypy.py"]
运行此容器,执行,然后发送相同的信号
Aarons-iMac:init-proc aaronkalair$ docker exec -it 0229aa205b48 bash
root@0229aa205b48:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 14:15 ? 00:00:00 python /srv/mypy.py
root 7 1 0 14:15 ? 00:00:00 sleep 100
root@0229aa205b48:/# kill 1
root@0229aa205b48:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 14:15 ? 00:00:00 python /srv/mypy.py
root 7 1 0 14:15 ? 00:00:00 sleep 100
现在什么也没有发生!
让我们通过执行类似操作的Go流程尝试一下
package main
import (
"time"
)
func main() {
time.Sleep(time.Duration(100000) * time.Millisecond)
}
将其弹出到Docker容器中,运行它,执行并发送给SIGTERM
Aarons-iMac:init-proc aaronkalair$ docker exec -it e6ccf11be060 bash
root@e6ccf11be060:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 14:28 ? 00:00:00 ./srv/sleep-spawner
root@e6ccf11be060:/# kill 1
root@e6ccf11be060:/# Aarons-iMac:init-proc aaronkalair$
它被杀死,就像它不是以PID 1运行时一样
那么,这是怎么回事?
PID 1在Linux中是特殊的,除其他事项外,它会忽略任何信号,除非明确声明了该信号的处理程序。 从泊坞窗文档- https://docs.docker.com/engine/reference/run/#foreground
注意 :Linux会特别处理在容器内以PID 1运行的进程:它会忽略具有默认操作的任何信号。 因此, 除非经过编码,否则 该过程不会在SIGINT
或SIGTERM
上终止 。
我们只需要在要在Docker容器中运行的每个进程中为这些信号定义处理程序,但这是很多工作,并且我们可能没有这样做的源代码。 此外,PID 1还有其他职责,我们将在后面探讨。
因此,我们可以运行与PID 1不同的进程,并让它代理我们要运行的实际进程的信号,并执行标准初始化进程的其他职责
例如,有许多解决方案可以做到这一点
Yelps dumb-init
- https://github.com/Yelp/dumb-init
Docker随附的Tini
- https: //docs.docker.com/engine/reference/run/#specify-an-init-process
以及更多您可以通过搜索找到的内容。
但是我要写我自己的…
因此,让我们从基础开始,我需要一个程序,该程序需要另一个进程的名称来执行并执行它
func main() {
cmd := exec.Command(os.Args[1], os.Args[2:]...)
err := cmd.Start()
if err != nil {
panic(err)
}
err = cmd.Wait()
if err != nil {
panic(err)
}
}
关于我们如何执行此操作的一些重要注意事项,因为稍后将变得很重要。
在Start()
之后,我们将新过程称为Wait()
这一点很重要,它将阻塞直到命令退出,并且一旦它清除了所有与之相关的资源。
未能wait
您生成的进程会导致僵尸进程在执行完消耗某些资源后立即挂起。
在手册页中-http://man7.org/linux/man-pages/man2/waitpid.2.html#NOTES
一个终止但尚未等待的孩子成为“僵尸”。 内核维护有关僵尸进程的最少信息集(PID,终止状态,资源使用信息),以便允许父级稍后执行等待以获取有关子级信息的等待。 只要没有通过等待从系统中删除僵尸,僵尸就会消耗内核进程表中的一个插槽,如果该表已满,将无法创建更多进程。
因此,如果我们在容器中运行它,让我们尝试一下新的信号代理...
CMD ["./srv/init-proc", "/srv/sleep-spawner", "1"]
我们可以看到我们的代理进程现在是PID 1,并且已经产生了sleep-spawner
root@36c4892039db:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 17:45 ? 00:00:00 ./srv/init-proc /srv/sleep-spawner 1
root 11 1 0 17:45 ? 00:00:00 /srv/sleep-spawner 1
好吧,下一步是将自己注册为对所有可能的信号感兴趣的人
func main() {
signalChannel := make(chan os.Signal, 2)
signal.Notify(signalChannel)
pid := -1
go sigHandler(&pid, signalChannel)
cmd := exec.Command(os.Args[1], os.Args[2:]...)
err := cmd.Start()
pid = cmd.Process.Pid
if err != nil {
panic(err)
}
err = cmd.Wait()
if err != nil {
panic(err)
}
}
sigHandler
定义为:
func sigHandler(pid *int, signalChannel chan os.Signal) {
var sigToSend syscall.Signal = syscall.SIGHUP
for {
sig := <-signalChannel
switch sig {
// #1 - Sent went the controlling terminal is closed, typically used by daemonised processes to reload config
case syscall.SIGHUP:
sigToSend = syscall.SIGHUP
// #2 - Like pressing CTRL+C
case syscall.SIGINT:
sigToSend = syscall.SIGINT
.....
repeat for all signals
}
syscall.Kill(*pid, sigToSend)
}
}
它只是打开Go支持的所有信号-https: //golang.org/pkg/syscall/#pkg-constants
然后使用kill
系统调用将信号发送到正在运行的进程。
现在,让我们用它来运行我们的Python程序,看看它是否正确处理了SIGTERM。
Aarons-iMac:init-proc aaronkalair$ docker exec -it 579ef1d3ce77 bash
root@579ef1d3ce77:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 18:33 ? 00:00:00 ./srv/init-proc python /srv/mypy.py
root 13 1 0 18:33 ? 00:00:00 python /srv/mypy.py
root 14 13 0 18:33 ? 00:00:00 sleep 100
root@579ef1d3ce77:/# kill 1
root@579ef1d3ce77:/# Aarons-iMac:init-proc aaronkalair$
而且有效!
现在让我们来处理PID 1负责的另一件事,清理Zombie进程。
想象一下这种情况
A-生成-> B-生成-> C
现在,如果B在C之前死亡或退出,那么C成为孤立进程,那么C的父母现在是谁?
好了,操作系统负责将孤立进程重新关联到PID 1,所以现在看起来像
A--> C的父项
现在,当C退出时,A将收到SIGCHILD
信号,并负责在C上调用wait
来清理此Zombie进程。
因此,让此逻辑添加到SIGCHILD案例中:
case syscall.SIGCHLD:
var status syscall.WaitStatus
var rusage syscall.Rusage
syscall.Wait4(-1, &status, syscall.WNOHANG, &rusage)
sigToSend = syscall.SIGCHLD
-1
等待任何子进程更改状态,而不是特定的状态,因为当我们收到信号时我们不知道已退出的进程的ID
WNOHANG
表示如果没有更改状态的子进程不阻塞等待,请立即返回
对终止的孩子执行wait
清理其资源,以防止其成为僵尸进程
从wait
页中— http://man7.org/linux/man-pages/man2/waitpid.2.html
如果子项已终止,则执行等待可以使系统释放与子项关联的资源。 如果不执行等待,则终止的孩子保持“僵尸”状态
现在只需要处理一种情况:
A-生成-> B-生成-> C
现在C退出了,但是B不再等待
A--> B的父代-> C的父代(已取消僵尸进程)
wait
仅适用于子进程,因此无论我们的init进程A调用了wait
多少次,它都不会清理C正在使用的资源。 (请注意, SIGCHILD
只会发送给B,因此A甚至都不知道C退出了)
现在B退出,A收到SIGCHILD
调用, wait
并且B被很好地清理了。
C现在是孤儿,可以成为A的孤儿,所以我们有
A--> C的父级(已取消僵尸进程)
我们可以看到上述内容在运行中进行了一些修改,对睡眠程序进行了一些修改,以产生父母在有孩子之前退出而不叫wait
func main() {
MAX_LEVEL := 4
level, err := strconv.Atoi(os.Args[1])
if err != nil {
panic(err)
}
// We'll have a bunch of processes that immediately exit at the max level
if level == MAX_LEVEL {
return
}
// Need the top level to outlive the others, otherwise the container would exit and you wouldn't be able to inspect the process tree
sleepTime := 0
if level == 1 {
sleepTime = 20000000
} else {
// Generate proceses where children sleep for longer than there parents so parents exit first without waiting on the children showing what happens to orphan / zombie processes
sleepTime = level * 1000
}
level += 1
for i := 0; i < 2; i++ {
// Spawn a command and intentionally dont wait on it
err := exec.Command("/srv/sleep-spawner", strconv.Itoa(level)).Start()
if err != nil {
panic(err)
}
}
time.Sleep(time.Duration(sleepTime) * time.Millisecond)
}
它可以在Github上获得-https: //github.com/AaronKalair/sleep-spawner
如果我们运行它,我们可以看到流程树是什么样的:
Aarons-iMac:init-proc aaronkalair$ docker exec -it 854a232d4b89 bash
root@854a232d4b89:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 22:13 ? 00:00:00 ./srv/init-proc /srv/sleep-spawner 1
root 12 1 0 22:13 ? 00:00:00 /srv/sleep-spawner 1
root 17 12 0 22:13 ? 00:00:00 [sleep-spawner] <defunct>
root 22 12 0 22:13 ? 00:00:00 [sleep-spawner] <defunct>
root 32 1 0 22:13 ? 00:00:00 [sleep-spawner] <defunct>
在我们当前的实现中,这种情况将永远存在,因此我们需要对其进行一些修改以处理如下情况:
case syscall.SIGCHLD:
var status syscall.WaitStatus
var rusage syscall.Rusage
for {
retValue, err := syscall.Wait4(-1, &status, syscall.WNOHANG, &rusage)
if err != nil {
panic(err)
}
if retValue <= 0 {
break
}
}
sigToSend = syscall.SIGCHLD
当与WNOHANG
结合使用时,我们利用wait4
的返回值来在每次收到SIGCHILD
信号时在循环中调用它。
再次从手册页开始(wait4的返回值符合waitpid — http://man7.org/linux/man-pages/man2/waitpid.2.html )
成功时,返回状态已更改的子进程的ID。 如果指定了WNOHANG ,并且存在由pid指定的一个或多个孩子,但尚未更改状态,则返回0。 出错时,返回-1。
因此,我们可以坐着Wait4
直到我们知道返回的值正在清理已退出的进程,然后得到小于或等于0的返回值。
现在,如果我们在容器中运行此命令并执行,并检查ps
Aarons-iMac:init-proc aaronkalair$ docker exec -it 30f13d4e53bd bash
root@30f13d4e53bd:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 22:05 ? 00:00:00 ./srv/init-proc /srv/sleep-spawner 1
root 12 1 0 22:05 ? 00:00:00 /srv/sleep-spawner 1
root 17 12 0 22:05 ? 00:00:00 [sleep-spawner] <defunct>
root 18 12 0 22:05 ? 00:00:00 [sleep-spawner] <defunct>
我们可以看到以PID 1为父级的僵尸已被清理!
至此,我们已经完成了一个基本的初始化流程,该流程可让我们向运行在Docker容器中的进程发送信号,并使其行为与在容器外部的行为相同,并具有清理僵尸进程的能力!
在此处查看完整的源代码— https://github.com/AaronKalair/init-proc
翻译自: https://hackernoon.com/my-process-became-pid-1-and-now-signals-behave-strangely-b05c52cc551c
pid 进程进程 端口