pid 进程进程 端口_我的进程成为PID 1,现在信号表现异常

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

在Twitter上关注我@AaronKalair

翻译自: https://hackernoon.com/my-process-became-pid-1-and-now-signals-behave-strangely-b05c52cc551c

pid 进程进程 端口

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值