5

一个问题后总结孤儿进程和僵尸进程

 3 years ago
source link: https://blog.cugxuan.cn/2020/07/07/Linux/a-bug-to-learn-orphan-and-zombie/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

一个问题后总结孤儿进程和僵尸进程

发表于

2020-07-07 更新于 2021-01-09 分类于 Linux

阅读次数: 190 Valine: 0

操作系统里面很早就讲了孤儿进程和僵尸进程,几年前刚学的时候用 C++ 写过一点 demo
这次在实际写代码过程中遇到,整理再总结一下,写出对应的例子进行演示

在开发的过程中用到了定时任务,定时任务有兴趣可以看看 golang cron v3 定时任务
在每到 cron 的规定时间,对应的任务通过 exec 包执行 rsync 命令,在执行完后退出
在更新代码之后重新部署的时候,先 kill 父进程,然后重新启动,排查问题时发现有很多没有结束的 rsync 进程,都变成了孤儿进程
想到了因为父进程 kill 掉,子进程还没有执行结束,所以被 init 接管,借这次机会整理总结一下

文章大部分概念性内容转自 参考资料 中的 孤儿进程与僵尸进程[总结]

我们知道在 unix/linux 中,正常情况下,子进程是通过父进程创建的,子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。当一个进程完成它的工作终止之后,它的父进程需要调用 wait() 或者 waitpid() 系统调用取得子进程的终止状态。

  • 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。
  • 僵尸进程:一个进程使用 fork 创建子进程,如果子进程退出,而父进程并没有调用 wait 或 waitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。

孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了 init 进程身上,init 进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init,而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。

孤儿进程-子进程内容

子进程我写了一个循环打印的 sleep.go,代码很简单,build 成可执行文件放到 testgo/orphan

package main

import (
"fmt"
"time"
)

func main() {
for i := 1; i <= 50; i++ {
<-time.After(time.Second * 2)
fmt.Printf("now: %d\n", i)
}
}

孤儿进程-主进程内容

testgo/orphanorphan.go 代码如下,执行 $ go build orphan.go

package main

import (
"fmt"
"os/exec"
"time"
)

func main() {
p := "/root/testgo/orphan/sleep"
CreateOrphan(p)
time.Sleep(time.Minute)
}

func CreateOrphan(p string) error {
cmd := exec.Command(p, ">log")
fmt.Println(cmd.String())
if err := cmd.Run(); err != nil {
return err
}
return nil
}

如果父进程比子进程先结束也是可以的,将 main 改为以下代码就不用手动 kill 了

func main() {
p := "/root/testgo/orphan/sleep"
go CreateOrphan(p)
time.Sleep(time.Second)
}

孤儿进程-运行解释

testgo/orphan 下执行 ./orphan,主进程会调用 exec 来执行 sleep 这个程序
此时通过 pid 可以看到,orphan 的进程编号为 5891,sleep 进程的进程编号为 5895,父进程编号为 orphan 的进程编号 5891

orphan-before-kill

手动 kill 父进程,可以看到启动 orphan 的终端中显示已经停止,再次查看 sleep 的进程号,发现进程编号没有变化,但是父进程变成 1 也就是被 init 进程所接管

orphan-after-kill

如果你想要 kill 父进程时一同杀死子进程,可以使用

$ kill -9 `ps -ef |grep PID|awk '{print $2}' `

unix 提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号 the process ID,退出状态 the termination status of the process,运行时间 the amount of CPU time taken by the process 等)。直到父进程通过 wait / waitpid 来取时才释放。但这样就导致了问题,如果进程不调用 wait / waitpid 的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。此即为僵尸进程的危害,应当避免。

任何一个子进程(init 除外)在 exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在 exit()之后,父进程没有来得及处理,这时用 ps 命令就能看到子进程的状态是“Z”。如果父进程能及时处理,可能用 ps 命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。如果父进程在子进程结束之前退出,则子进程将由 init 接管。init 将会以父进程的身份对僵尸状态的子进程进行处理。

僵尸进程危害场景:
例如有个进程,它定期的产生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵尸进程,倘若用 ps 命令查看的话,就会看到很多状态为 Z 的进程。严格地来说,僵尸进程并不是问题的根源,罪魁祸首是产生出大量僵尸进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵尸进程时,答案就是把产生大量僵尸进程的那个元凶枪毙掉(也就是通过 kill 发送 SIGTERM 或者 SIGKILL 信号啦)。枪毙了元凶进程之后,它产生的僵尸进程就变成了孤儿进程,这些孤儿进程会被 init 进程接管,init 进程会 wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵尸的孤儿进程就能瞑目而去了。

僵尸进程-子进程内容

子进程跟孤儿进程中的一样

package main

import (
"fmt"
"time"
)

func main() {
for i := 1; i <= 50; i++ {
<-time.After(time.Second * 2)
fmt.Printf("now: %d\n", i)
}
}

僵尸进程-主进程内容

主进程写了三个函数,分别使用 cmd.Start()cmd.Run() 这两种方式的区别就是主进程是否调用了 Wait() 来处理子进程的信号,该文件在 /root/testgo/zombie/zombie.go

package main

import (
"flag"
"fmt"
"os/exec"
"time"
)

func main() {
choice := flag.String("c", "1", "选择运行函数,1: CreateChild, 2: WaitChild, 3: CreateZombie, 4: go CreateZombie")
flag.Parse()
fmt.Println("you choose:", *choice)
p := "/root/testgo/zombie/sleep"
if *choice == "1" {
CreateChild(p)
} else if *choice == "2" {
CreateWait(p)
} else if *choice == "3" {
CreateZombie(p)
} else if *choice == "4" {
go CreateZombie(p)
}
fmt.Println("before sleep")
time.Sleep(time.Minute)
}

func CreateChild(p string) error {
cmd := exec.Command(p, ">log")
fmt.Println(cmd.String())
if err := cmd.Run(); err != nil {
return err
}
return nil
}

func CreateWait(p string) error {
cmd := exec.Command(p, ">log")
fmt.Println(cmd.String())
if err := cmd.Start(); err != nil {
return err
}
fmt.Println("after Start before wait")
if err := cmd.Wait(); err != nil {
return err
}
return nil
}

func CreateZombie(p string) error {
cmd := exec.Command(p, ">log")
fmt.Println(cmd.String())
if err := cmd.Start(); err != nil {
return err
}
// 这里的 kill 是为了结束子进程
// 如果使用的子进程完成后就退出(比如只输出一个 hello 就结束),那么不 kill 也可以
if err := cmd.Process.Kill(); err != nil {
return err
}
return nil
}

僵尸进程-运行解释

首先解释一下 cmd.Start()cmd.Run() 的区别,从 cmd.Run() 的代码中可以看到,cmd.Run() 就是执行了 cmd.Start() 然后 cmd.Wait(),所以会等待这个子进程执行完成之后退出。运行表现就是在 cmd.Run() 这一行阻塞,等待执行完成之后才执行下面的内容
cmd.Start() 这一行执行之后就不会有阻塞的行为,会继续执行下面的内容

// Run starts the specified command and waits for it to complete.
//
// The returned error is nil if the command runs, has no problems
// copying stdin, stdout, and stderr, and exits with a zero exit
// status.
//
// If the command starts but does not complete successfully, the error is of
// type *ExitError. Other error types may be returned for other situations.
//
// If the calling goroutine has locked the operating system thread
// with runtime.LockOSThread and modified any inheritable OS-level
// thread state (for example, Linux or Plan 9 name spaces), the new
// process will inherit the caller's thread state.
func (c *Cmd) Run() error {
if err := c.Start(); err != nil {
return err
}
return c.Wait()
}

choice 1

第一个 choice 运行之后,主进程等待子进程执行完之后才执行下面内容
由于 cmd.Run() 中有 cmd.Wait() 处理了子进程结束时发出的信号,所以不会有僵尸进程
zombie-choice-1

choice 2

可以看到在启动之后,在 cmd.Start() 执行后开始子进程,在 cmd.Wait() 处阻塞等待
在子进程结束后 cmd.Wait() 处理了信号也不会出现僵尸进程
zombie-choice-2

choice 3

使用 Start 启动子进程之后,直接 Kill,子进程的信号没有得到处理
可以看到进程中 sleep 后面出现了 <defunct>,在 top 命令中也可以看到有一个 zombie 进程
zombie-choice-3

待主进程执行完 time.sleep() 后父进程主 goroutine 也退出,对应的僵尸进程也就消失了

zombie-choice-3-done

注意,当父进程还没有退出的时候,子进程已经被 kill 了,这个时候子进程是僵尸进程,你对子进程发送 kill 或者 kill -9 都是没有效果的

choice 4

由于这次是通过一个 goroutine 来启动的,和 choice 3 相比较还是同一个进程启动,所以表现和 choice3 一样

僵尸进程-处理和避免

要注意僵尸进程一定是在父进程没有结束的时候存在的,如果父进程结束或者变成孤儿进程,那么僵尸进程的问题就不存在了
僵尸进程的处理方式很简单,从产生原因入手即可

  • 在运行时直接 kill 掉僵尸进程的父进程,那么僵尸进程也就不复存在
  • 因为父进程没有处理子进程的信号,在代码中使用 cmd.Run() 或者在 cmd.Start() 之后调用 cmd.Wait()
  • fork 两次,《Unix 环境高级编程》8.6 节说的非常详细。原理是将子进程成为孤儿进程,从而其的父进程变为 init 进程,通过 init 进程可以处理僵尸进程,这种方式有点套娃

Recommend

  • 44
    • monkeysayhi.github.io 5 years ago
    • Cache

    浅谈Linux僵尸进程与孤儿进程

    在Linux中,进程退出后,分配的绝大部分资源将被回收,除了 task_struct 结构及少数资源外。 此时的进程已经 “死亡” ,但 task_struct 结构还保存在进程列表中,

  • 5
    • blog.lilydjwg.me 2 years ago
    • Cache

    让我们收养孤儿进程吧

    让我们收养孤儿进程吧 本文来自依云's Blog,转载请注明。 稍微...

  • 5
    • studygolang.com 2 years ago
    • Cache

    exec.Command僵尸进程问题

    exec.Command僵尸进程问题 uuid · 大约5小时之前 · 23 次点击 · 预计阅读时间 1 分钟 · 大约8小...

  • 5

    如何在 Linux 上杀死一个僵尸进程 | Linux 中国要杀死一个僵尸进程,你必须从进程列表中删除其名称。来源:https://linux.c...

  • 4

    每多一个父母进“厂”,就多一个996“孤儿”领英LinkedIn·2022-04-25 12:43“我在北上广996,我的孩子在老家成了留守儿童”随着更多的80后...

  • 9

    PHP多进程(2)孤儿进程与僵尸进程上一节:PHP多进程(1)PHP多进程初探,简单了解了一下关于PHP多进程和简单的通过代码了解其中的一些问题。 那这一节,来学习一下关于 孤儿...

  • 4
    • mingzhi198.github.io 2 years ago
    • Cache

    System: 僵尸进程和孤儿进程

    Statement This article is my study notes about distributed systems. Please refer to the original work for more details and indicate the source for reprinting. 进程就是运行起来的一个程序,但是进程并不局限于...

  • 4

    容器中Java进程产生大量僵尸进程的问题 最近遇到一个问题,某个Java进程使用Runtime().exec()执行脚本文件,创建了大量僵尸进程,而这个Java进程是运行在容器中的。 当时看到 Host 机器上是这么一个情况,可以...

  • 3

    孤儿进程与终端的关系 在本篇文章当中主要给大家介绍一下有关孤儿进程和终端之间的关系。 首先我们的需要知道什么是孤儿进程,简单的来说就是当一个程序还在执行,但是他的父进程已经退出了,这种进程叫做孤儿进程,因为...

  • 3

    1、面试题介绍 以前面试,面试官问了一个问题,大意是: 我们在终端中,通过执行 python main.py 命令,会启动一台前台进程直到程序结束。现在我还是想通过执行 python main.py ,启动一个...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK