什么是僵尸进程, 孤儿进程

僵尸进程: 当一个进程终止,但其父进程还没有通过wait()waitpid()系统调用来获取其终止状态时,该进程就变成了僵尸进程。僵尸进程是指已经完成执行但仍然在进程表中保留条目的进程。这种情况通常发生在父进程没有充分回收子进程资源的情况下。僵尸进程会占用系统资源,因此父进程应该及时处理子进程的终止状态,以避免僵尸进程的产生。

孤儿进程: 孤儿进程是指其父进程提前终止或者意外终止,而子进程仍然在继续执行。当父进程退出时,孤儿进程会被init进程(在现代系统中通常是systemd或者initd等)接管。init进程会成为孤儿进程的新父进程,并负责回收其资源,确保其能够正常终止,避免它变成僵尸进程。通过将孤儿进程的父进程设置为init进程,系统确保了即使原始父进程退出,子进程仍然能够得到适当的处理,防止了孤儿进程的出现。

创建僵尸进程

下面的代码可以创建僵尸进程, 前10秒子进程是正常进程, 之后20秒子进程变成僵尸进程, 再之后程序进入循环等待状态, 此时子进程一直是僵尸进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main
import (
"fmt"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
)

func main(){
cmd := exec.Command("/bin/bash","-c","sleep 10","echo \"hello zombie\"")
err := cmd.Start()
if err != nil {
fmt.Println("启动子进程失败")
}
fmt.Println("父进程 PID: ",os.Getpid())
fmt.Println("子进程 PID: ",cmd.Process.Pid)
// 用于等待子进程执行完成, 使得可以观察到僵尸进程
time.Sleep(30 * time.Second)
// 使用信号监听器,以便在收到信号时杀死子进程
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
cmd.Process.Kill()
cmd.Process.Wait() // 等待进程结束,避免僵尸进程
os.Exit(0)
}()

// 主程序进入等待状态
select {}
}

通过下面的命令编译上面的代码, 之后执行三遍生成的二进制文件, 且挂起到后台, 此时便产生了三个父进程以及对应三个僵尸子进程

1
2
3
4
5
6
7
mkdir zombie && cd zombie
go mod init zombie
go mod tidy
go build
./zombie &
./zombie &
./zombie &

这里还有一份C语言的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
pid_t child_pid = fork(); // 创建子进程

if (child_pid == 0) {
// 子进程代码
printf("子进程:%d\n", getpid());
exit(0); // 子进程立即退出
} else if (child_pid > 0) {
// 父进程代码
printf("父进程:%d\n", getpid());
sleep(360); // 父进程等待一段时间,这段时间内子进程是僵尸进程
wait(NULL); // 父进程收回子进程的资源
printf("父进程退出\n");
} else {
// 创建子进程失败
perror("fork failed");
return 1;
}

return 0;
}

1
2
3
gcc zombie.c -o zombie
./zombie &
./zombie &

批量杀死僵尸进程

下面的shell脚本, 获取系统所有的僵尸进程的PID, 然后用这个PID获取其父进程的PID, 最后杀死所有的父进程, 这样僵尸进程也就都变成了孤儿进程, 然后被init进程回收资源, 从而达到批量杀死僵尸进程的目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/bash
# 获取系统中所有僵尸进程的PID
zombie_pids=$(ps -aux | grep -v "grep" | grep "Z" | awk 'NR>1{print $2}' | tr '\n' ' ')
father_pids=""
# 没有僵尸进程时退出脚本
if [ "$zombie_pids" == "" ];then
exit 0
fi
# 获取每一个僵尸进程父进程的PID
for child_pid in $zombie_pids
do
father_pid=$(ps -o ppid= -p $child_pid | awk '{print $1}')
father_pids="$father_pids $father_pid"
done

# 使用tr命令将空格替换为换行符,并用sort和uniq命令获取唯一的父进程PID
unique_father_pids=$(echo $father_pids | tr ' ' '\n' | sort -u)

# 使用kill命令一次性杀掉所有的父进程
if kill -9 $unique_father_pids ;then
echo "kill father_pids done"
fi

log_path="$(pwd)"

echo "$(date +"%F %T") father_pids: $(echo $unique_father_pids | tr '\n' ' ')" | tee -a $log_path/father_pids_killed.log
echo "$(date +"%F %T") zombie_pids: $zombie_pids" | tee -a $log_path/zombie_pids_get.log
1
2
chmod +x kill-zombie.sh
./kill-zombie.sh

截图

截图