乐趣区

关于golang: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.shstart.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 的场景。

在下面 两个脚本 的根底上,咱们用 golangos/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

发现 gostart.shsub.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.shPPID 为 1
  • 即便 start.shPPID变成了 1,log.Println(cmd.Process.Pid) 还继续的输入 .

问题 2:

那如果 PPID为 1,golang程序不就无奈治理了吗?即便 sub.sh 退出也不晓得了,那要如何解决?

问题剖析

  • 两个场景中,都有一个独特的点,就是 PPID 为 1,这妥妥的成为没人要的娃了——孤儿过程
  • 场景二中,如果 cmd的没有过程没有被回收,go程序也无奈治理,那么 start.sh 就成为了占着茅坑不拉屎的子过程——僵尸过程

那到底什么是 孤儿过程 僵尸过程

孤儿过程

在类 UNIX 操作系统中,孤儿过程(Orphan Process)指:是在其父过程执行实现或被终止后仍持续运行的一类过程。

为防止孤儿过程退出时无奈开释所占用的资源而僵死,任何孤儿过程产生时都会立刻为零碎过程 initsystemd 主动接管为子过程,这一过程也被称为 收养 。在此需注意,尽管事实上该过程已有init 作为其父过程,但因为创立该过程的过程已不存在,所以仍应称之为 孤儿过程 。孤儿过程 会节约服务器的资源,甚至有耗尽资源的潜在危险

解决 & 预防

  1. 终止机制:强制杀死孤儿过程(最罕用的伎俩);
  2. 再生机制:服务器在指定工夫内查找调用的客户端,若找不到则间接杀死孤儿过程;
  3. 超时机制:给每个过程指定一个确定的运行工夫,若超时仍未实现则强制终止之。若有须要,亦可让过程在指定工夫耗尽之前申请延时。
  4. 过程组:因为父过程终止或解体都会导致对应子过程成为孤儿过程,所以也无奈意料一个子过程执行期间是否会被“遗弃”。有鉴于此,少数类 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://pkg.go.dev/syscall

https://cs.opensource.google/…;l=279

https://pkg.go.dev/os/exec

退出移动版