原文地址:Go Exec 僵尸与孤儿过程
最近,应用 golang
去治理本地利用的生命周期,期间有几个乏味的点,明天就一起看下。
场景一
咱们来看看上面两个脚本会产生什么问题:
创立两个 shell 脚本
- start.sh
#!/bin/shsh sub.sh
- sub.sh
#!/bin/shn=0while [ $n -le 100 ]do echo $n let n++ sleep 1done
执行脚本
输入后果
$ ./start.sh 012...
过程关系
查看过程信息
ps -jUSER PID PPID PGID SESS JOBC STAT TT TIME COMMANDroot 31758 31346 31758 0 1 S+ s000 0:00.00 /bin/sh ./start.shroot 31759 31758 31758 0 1 S+ s000 0:00.01 sh sub.sh
sub.sh
的 父过程(PPID)为start.sh
的过程id(PID)sub.sh
和start.sh
两个过程的PGID
是同一个,( 属一个过程组)。
删除 start.sh
的过程
kill -9 31758# 再查看过程组ps -j## 返回USER PID PPID PGID SESS JOBC STAT TT TIME COMMANDroot 31759 1 31758 0 0 S s000 0:00.03 sh sub.sh
start.sh
过程不在了sub.sh
过程还在执行sub.sh
过程的PID
变成了 1
问题1:
那sub.sh
这个过程当初属于什么?
场景二
假如sub.sh
是理论的利用, start.sh
是利用的启动脚本。
那么,golang
是如何治理他们的呢? 咱们持续看看上面 对于golang
的场景。
在下面两个脚本的根底上,咱们用golang
的 os/exec
库去调用 start.sh
脚本
package mainimport ( "context" "log" "os" "os/exec" "time")func main() { cmd := exec.CommandContext(context.Background(), "./start.sh") // 将 start.sh 和 sub.sh 移到当前目录下 cmd.Dir = "/Go/src/go-code/cmd/" cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { log.Printf("cmd.Start error %+v \n", err) } for { select { default: log.Println(cmd.Process.Pid) time.Sleep(2 * time.Second) } }}
执行程序
go run ./main.go
查看过程
ps -jUSER PID PPID PGID SESS JOBC STAT TT TIME COMMANDroot 45458 45457 45457 0 0 Ss+ s004 0:00.03 ...___1go_build_go_code_cmdroot 45462 45458 45457 0 0 S+ s004 0:00.01 /bin/sh ./start.shroot 45463 45462 45457 0 0 S+ s004 0:00.03 sh sub.sh
发现 go
、 start.sh
、sub.sh
三个过程为同一个过程组(同一个 PGID)
父子关系为: main.go
-> start.sh
-> sub.sh
删除 start.sh
的过程
理论场景,有可能启动程序挂了,导致咱们无奈监听到执行程序的状况,删除start.sh
过程,模仿下场景 :
kill -9 45462
再查看过程
ps -jUSER PID PPID PGID SESS JOBC STAT TT TIME COMMANDroot 45458 45457 45457 0 0 Ss+ s004 0:00.03 ...___1go_build_go_code_cmdroot 45462 1 45457 0 0 S+ s004 0:00.01 (bash)root 45463 45462 45457 0 0 S+ s004 0:00.03 sh sub.sh
- 发现没,
start.sh
的PPID
为1 - 即便
start.sh
的PPID
变成了1 ,log.Println(cmd.Process.Pid)
还继续的输入 .
问题2:
那如果 PPID
为1 ,golang
程序不就无奈治理了吗? 即便 sub.sh 退出也不晓得了,那要如何解决?
问题剖析
- 两个场景中, 都有一个独特的点,就是
PPID
为1,这妥妥的成为没人要的娃了——孤儿过程
- 场景二中,如果
cmd
的没有过程没有被回收,go
程序也无奈治理,那么start.sh
就成为了占着茅坑不拉屎的子过程——僵尸过程
那到底什么是孤儿过程
和 僵尸过程
?
孤儿过程
在类 UNIX
操作系统中,孤儿过程(Orphan Process)指:是在其父过程执行实现或被终止后仍持续运行的一类过程。
为防止孤儿过程退出时无奈开释所占用的资源而僵死,任何孤儿过程产生时都会立刻为零碎过程 init
或 systemd
主动接管为子过程,这一过程也被称为收养
。在此需注意,尽管事实上该过程已有init
作为其父过程,但因为创立该过程的过程已不存在,所以仍应称之为孤儿过程
。孤儿过程会节约服务器的资源,甚至有耗尽资源的潜在危险。
解决&预防
- 终止机制:强制杀死孤儿过程(最罕用的伎俩);
- 再生机制:服务器在指定工夫内查找调用的客户端,若找不到则间接杀死孤儿过程;
- 超时机制:给每个过程指定一个确定的运行工夫,若超时仍未实现则强制终止之。若有须要,亦可让过程在指定工夫耗尽之前申请延时。
- 过程组:因为父过程终止或解体都会导致对应子过程成为孤儿过程,所以也无奈意料一个子过程执行期间是否会被“遗弃”。有鉴于此,少数类UNIX零碎都引入了过程组以避免产生孤儿过程。
僵尸过程
在类 UNIX
操作系统中,僵尸过程(zombie process)指:实现执行(通过exit零碎调用,或运行时产生致命谬误或收到终止信号所致),但在操作系统的过程表中依然存在其过程管制块,处于"终止状态"的过程。
失常状况下,过程间接被其父过程 wait
并由零碎回收。而僵尸过程与失常过程不同,kill
命令对僵尸过程有效,并且无奈回收,从而导致资源透露。
解决&预防
收割僵尸过程的办法是通过 kill
命令手工向其父过程发送SIGCHLD信号。如果其父过程依然回绝收割僵尸过程,则终止父过程,使得 init
过程收养僵尸过程。init
过程周期执行 wait
零碎调用收割其收养的所有僵尸过程。
查看过程详情
# 列出过程ps -l
- USER:过程的所属用户
- PID:过程的过程ID号
- RSS:过程占用的固定的内存量 (Kbytes)
- S:查看过程状态
- CMD:过程对应的理论程序
过程状态(S)
- R:运行 Runnable (on run queue) 正在运行或在运行队列中期待
- S:睡眠 Sleeping 休眠中,碰壁,在期待某个条件的造成或承受到信号
- I:闲暇 Idle
- Z:僵死 Zombie(a defunct process) 过程已终止,但过程描述符存在, 直到父过程调用wait4()零碎调用后开释
- D:不可中断 Uninterruptible sleep (ususally IO) 收到信号不唤醒和不可运行, 过程必须期待直到有中断产生
- T:终止 Terminate 过程收到SIGSTOP、SIGSTP、 SIGTIN、SIGTOU信号后进行运行运行
- P:期待替换页
- W:无驻留页 has no resident pages 没有足够的记忆体分页可调配
- X:死掉的过程
Go解决方案
采纳 杀掉过程组(kill process group,而不是只 kill 父过程,在 Linux 外面应用的是 kill -- -PID
) 与 过程wait计划,后果如下:
package mainimport ( "context" "log" "os" "os/exec" "syscall" "time")func main() { ctx := context.Background() cmd := exec.CommandContext(ctx, "./start.sh") // 设置过程组 cmd.SysProcAttr = &syscall.SysProcAttr{ Setpgid: true, } cmd.Dir = "/Users/Wilbur/Project/Go/src/go-code/cmd/" cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { log.Printf("cmd.Start error %+v \n", err) } // 监听过程wait errCmdCh := make(chan error, 1) go func() { errCmdCh <- cmd.Wait() }() for { select { case <-ctx.Done(): log.Println("ctx.done") pid := cmd.Process.Pid if err := syscall.Kill(-1*pid, syscall.SIGKILL); err != nil { return } case err := <-errCmdCh: log.Printf("errCmdCh error %+v \n", err) return default: log.Println(cmd.Process.Pid) time.Sleep(2 * time.Second) } }}
分析 cmd.Wait()
源码
在 os/exec_unix
下:
var ( status syscall.WaitStatus rusage syscall.Rusage pid1 int e error )for { pid1, e = syscall.Wait4(p.Pid, &status, 0, &rusage) if e != syscall.EINTR { break }}
进行了 syscall.Wait4
对系统监听,正如"僵死 Zombie(a defunct process) 过程已终止,但过程描述符存在, 直到父过程调用wait4()零碎调用后开释",所说统一。
总结
严格地来说,僵尸过程并不是问题的本源,罪魁祸首是产生出大量僵尸过程的那个父过程。
因而,当咱们寻求如何毁灭零碎中大量的僵尸过程时,更应该是在理论的开发过程中,思考如何防止僵尸过程的产生。
参考:
https://pkg.go.dev/syscall
https://cs.opensource.google/...;l=279
https://pkg.go.dev/os/exec