Go Exec 僵尸与孤儿进程
原文地址:Go Exec 僵尸与孤儿进程
最近,使用 golang 去管理本地应用的生命周期,期间有几个有趣的点,今天就一起看下。
场景一
我们来看看下面两个脚本会产生什么问题:
创建两个 shell 脚本
- start.sh
#!/bin/sh
sh sub.sh
- sub.sh
#!/bin/sh
n=0
while [ $n -le 100 ]
do
echo $n
let n++
sleep 1
done
执行脚本
输出结果
$ ./start.sh
0
1
2
...
进程关系
查看进程信息
ps -j
USER PID PPID PGID SESS JOBC STAT TT TIME COMMAND
root 31758 31346 31758 0 1 S+ s000 0:00.00 /bin/sh ./start.sh
root 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 COMMAND
root 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 main
import (
"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 -j
USER PID PPID PGID SESS JOBC STAT TT TIME COMMAND
root 45458 45457 45457 0 0 Ss+ s004 0:00.03 ...___1go_build_go_code_cmd
root 45462 45458 45457 0 0 S+ s004 0:00.01 /bin/sh ./start.sh
root 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 -j
USER PID PPID PGID SESS JOBC STAT TT TIME COMMAND
root 45458 45457 45457 0 0 Ss+ s004 0:00.03 ...___1go_build_go_code_cmd
root 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 main
import (
"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://cs.opensource.google/go/go/+/refs/tags/go1.17.7:src/syscall/syscall_linux.go;l=279
Go Exec 僵尸与孤儿进程的更多相关文章
- day34 并行并发、进程开启、僵尸及孤儿进程
day34 并行并发.进程开启.僵尸及孤儿进程 1.并行与并发 什么是并行? 并行指的是多个进程同时被执行,是真正意义上的同时 什么是并发? 并发指的是多个程序看上去被同时执行,这是因为cpu在多个程 ...
- 进程,多进程,进程与程序的区别,程序运行的三种状态,multiprocessing模块中的Process功能,和join函数,和其他属性,僵尸与孤儿进程
1.进程 什么是进程: 一个正在被运行的程序就称之为进程,是程序具体执行的过程,是一种抽象概念,进程来自操作系统 2.多进程 多个正在运行的程序 在python中实现多线程的方法 from mult ...
- linux系统编程之进程(三):进程复制fork,孤儿进程,僵尸进程
本节目标: 复制进程映像 fork系统调用 孤儿进程.僵尸进程 写时复制 一,进程复制(或产生) 使用fork函数得到的子进程从父进程的继承了整个进程的地址空间,包括:进程上下文.进程堆栈. ...
- Linux进程理解与实践(二)僵尸&孤儿进程 和文件共享
孤儿进程与僵尸进程 孤儿进程: 如果父进程先退出,子进程还没退出那么子进程的父进程将变为init进程.(注:任何一个进程都必须有父进程) [cpp] view plaincopy #include & ...
- [Linux] 孤儿进程与僵尸进程[总结]
转载: http://www.cnblogs.com/Anker/p/3271773.html 1.前言 之前在看<unix环境高级编程>第八章进程时候,提到孤儿进程和僵尸进程,一直对这两 ...
- wait、waitpid 僵尸进程 孤儿进程
man wait: NAME wait, waitpid, waitid - wait for process to change state SYNOPSIS #include <sys/ty ...
- wait函数返回值总结,孤儿进程与僵尸进程[总结]
http://blog.csdn.net/astrotycoon/article/details/41172389 wait函数返回值总结 http://www.cnblogs.com/Anker/p ...
- 进程——wait与waitpid、僵尸进程与孤儿进程
僵尸进程:子进程终止了,但是父进程没有回收子进程的资源PCB.使其成为僵尸进程 孤儿进程:父进程先与子进程结束了,使得子进程失去了父进程,这个时候子进程会被1号进程init进程领养,成为孤儿进程 为了 ...
- Linux-进程描述(3)之进程状态僵尸进程与孤儿进程
进程状态 进程状态反映进程执行过程的变化.这些状态随着进程的执行和外界条件的变化而转换.为了弄明正正在运行的进程是什么意思,我们需要知道进程的不同状态.一个进程可以有多个状态(在Linux内核中,进程 ...
随机推荐
- 单篇长文TestNG从入门到精通
简介 TestNG是Test Next Generation的缩写,它的灵感来自于JUnit和NUnit,在它们基础上增加了很多很牛的功能,比如说: 注解. 多线程,比如所有方法都在各自线程中,一个测 ...
- 《剑指offer》面试题19. 正则表达式匹配
问题描述 请实现一个函数用来匹配包含'. '和'*'的正则表达式.模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(含0次).在本题中,匹配是指字符串的所有字符匹配整个模式. ...
- echarts的通用属性的介绍
通常做数据可视化时,会用到统计图,这里我使用的是Echarts,对于第一次用的人来说,还是有点难度的,主要是里面的属性太多,看的头痛,这里我自己做个笔记 这里的配置项手册里面就是查找各种属性了,在Ec ...
- GitHub镜像
GitHub 官网镜像(可以用来clone push等,但是不能登录) https://github.com.cnpmjs.org https://git.sdut.me https://hub.fa ...
- 你可能不知道的Animation动画技巧与细节
引言 在web应用中,前端同学在实现动画效果时往往常用的几种方案: css3 transition / animation - 实现过渡动画 setInterval / setTimeout - 通过 ...
- Nginx搭建游戏
目录 一:Nginx搭建<小游戏> 1.上传<象棋游戏>代码 2.编辑配置文件(尾部必须要加 .conf<文件>) 3.测试配置文件是否正常 4.重启Nginx 5 ...
- CSS之常见布局|常用单位|水平垂直居中
常见布局: 1. 流式布局:百分比布局,宽高.margin.pinding都是百分比 2. 固定布局:盒子的宽高固定,如:margin.padding等 3. 浮动布局:float 4. 弹性布局:f ...
- JDBC 连接DRUID 连接池!
一.1.创建一个floder目录,[名称lib] 2. 导入mysql.jar包和 druid.jar 包.---------->bulid path 二.创建 sourcefolder 目录 ...
- PyTorch 介绍 | AUTOMATIC DIFFERENTIATION WITH TORCH.AUTOGRAD
训练神经网络时,最常用的算法就是反向传播.在该算法中,参数(模型权重)会根据损失函数关于对应参数的梯度进行调整. 为了计算这些梯度,PyTorch内置了名为 torch.autograd 的微分引擎. ...
- AT2651 [ARC077D] SS
定义 \(nxt_i\) 表示在字符串 \(S\) 中以 \(i\) 结尾的最长 \(border\). 引理一:若 \(n - nxt_n \mid n\) 则 \(S_{1 \sim n - nx ...