[Linux] 详解Linux进程托孤 (go语言演示)

2,567 阅读7分钟

前言

我们知道Linux上有三种特殊进程,分别是孤儿进程,僵尸进程,守护进程。

  • 孤儿进程 指的是在其父进程执行完成或被终止后仍继续运行的一类进程。这些孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

  • 僵尸进程 一个子进程在其父进程还没有调用wait()或waitpid()的情况下退出。这个子进程就是僵尸进程。任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。

  • 守护进程 一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。

从上面的进程描述看来,在Linux中僵尸进程是父进程未来得及wait(即告诉内核,这是我儿子,我会替他收尸的。当然也可告诉内核我不会替他收尸,然后由内核完成即可),子进程就执行完毕了。
回到孤儿进程问题上来,可以看到上面描述,不管是孤儿进程,僵尸进程,守护进程最终都会给init进程接收,所以其实不会存在真正意义上的“孤儿进程”。
守护进程其实也是一种孤儿进程的表现,就是在父进程退出后,系统把子进程 过继 给init进程,并且处理输出设备等工作。这个 过继 的过程我们称之为 托孤 ,下面文章将介绍如何实现子进程 托孤 并控制其输出设备,从而达到创建一个类似于守护进程的子进程。

进程托孤实现

  • 演示代码用到gogf 这个脚手架,我个人也已经应用到实际的开发中,很多常用的工具都被其封装得很好。

官方文档: goframe.org/    源码地址: github.com/gogf/gf

Example 1

  • 演示进程 托孤 的表现,及go语言如何控制其输出设备
  • 通过go run t1.go 运行代码,首先使用 gproc会启动一个主进程,通过主进程fork当前进程从而启动子进程。主进程并不会等待子进程就退出了,下面代码也能看到我并没有去调用wait子进程;子进程通过gproc.IsChild发现自己是子进程并进入到子进程代码块,在该代码块中主要是控制输出设备可以做到与终端分离,这里调用了os.Pipe创建了分别创建了两对读写的文件管道,并将w写管道赋值给os.Stdoutw1赋值了给os.Stderr,由此便接过了2个标准输出的结果。后面的操作就是读取rr1这两个管道的数据,为了演示这里又重新new了一个stdout类型的管道文件赋值给os.Stdout,让终端接收print的结果;

t1.go

package main

import (
	"fmt"
	"github.com/gogf/gf/os/glog"
	"github.com/gogf/gf/os/gproc"
	"io"
	"os"
	"syscall"
	"time"
)



func main () {
	if gproc.IsChild() {
		// 建立stdout 的文件管道
		r, w, _ := os.Pipe()
		// 建立stderr 的文件管道
		r1, w1, _ := os.Pipe()
		defer func() {
			_ = w.Close()
			_ = w1.Close()
		}()
		os.Stdout = w
		os.Stderr = w1

		glog.Printf("%d: Hi, I am child, waiting 30 seconds to die", gproc.Pid())
		time.Sleep(time.Second)
		glog.Printf("%d: 1", gproc.Pid())
		time.Sleep(time.Second)
		glog.Printf("%d: 2", gproc.Pid())
		time.Sleep(time.Second)
		glog.Printf("%d: 3", gproc.Pid())
		time.Sleep(time.Second * 30)

		glog.Printf("end")

		_ = w.Close()
		_ = w1.Close()

		var (
			outBuf = make([]byte, 1)
			errBuf = make([]byte, 1)
			output string
		)
		
		// 读取stdout的信息
		for {
			_, err := r.Read(outBuf)
			if err == io.EOF {
				break
			}
			if err != nil {
				panic(err)
			}

			if string(outBuf) != "" {
				output += string(outBuf)
			}
		}
		// 读取stderr的信息
		for {
			_, err := r1.Read(errBuf)
			if err == io.EOF {
				break
			}
			if err != nil {
				panic(err)
			}

			if string(errBuf) != "" {
				output += string(errBuf)
			}
		}
		
		// 重新赋值创建个stdout管道,让数据可以通过stdout 打印到终端上
		os.Stdout = os.NewFile(uintptr(syscall.Stdout), "/dev/stdout")
		fmt.Println(output)


	} else {
		m := gproc.NewManager()
		p := m.NewProcess(os.Args[0], os.Args, os.Environ())
		p.Start()

		//p.Wait()
		glog.Printf("Parent PID: %d", gproc.Pid())
	}
}

  • 可以看到下面当前pid是 806
# go run t1.go
2021-01-10 21:51:24.545 Parent PID: 806
  • 上面的父进程启动后就退出了,并且1号init 接管了其子进程。
# ps l|grep t1
0     0   813     1  20   0 743752  7288 -      Sl   pts/1      0:00 /tmp/go-build311777737/b001/exe/t1
0     0   820   389  20   0 112748  2300 -      S+   pts/3      0:00 grep --color=auto t1
  • 等待30秒后,便在终端上看到输出
# 2021-01-10 21:51:24.548 813: Hi, I am child, waiting 30 seconds to die
2021-01-10 21:51:25.548 813: 1
2021-01-10 21:51:26.549 813: 2
2021-01-10 21:51:27.549 813: 3
2021-01-10 21:51:57.549 end

Example 2 (subreaper)

  • 这里其实想介绍另一个知识点 subreaper, 自linux3.4内核起有的一个系统调用,subreaper 由名字可得是一个子进程的收割者, 意思就是通过PR_SET_CHILD_SUBREAPER这个系统调用便能把一个进程设置为祖先进程与init进程一样可以收养孤儿进程(设置时arg2需大于1,后面代码上会体现出来)。而子进程被收养的方式是先会被自己最近的祖先先收养。

参考: man7.org/linux/man-p…

  • 下面ancestor_process.go, 将自己设置为祖先进程,然后启动sub_process_1.go 第一个子进程,并且进行了60秒等待和调用了wait函数;sub_process_1继续启动 sub_process_2但是 sub_process_1不会调用wait函数便退出了,让sub_process_2进入了孤儿状态。在通常没有设置祖先进程的情况下sub_process_2会被init收养,但我们这里设置了祖先进程,所以sub_process_2应该被最近的ancestor_process收养。

ancestor_process.go

package main

import (
	"github.com/gogf/gf/os/glog"
	"github.com/gogf/gf/os/gproc"
	"golang.org/x/sys/unix"
	"os"
	"time"
)

//
func SetSubreaper(i int) error {
	return unix.Prctl(unix.PR_SET_CHILD_SUBREAPER, uintptr(i), 0, 0, 0)
}

func main () {
	// 当设置参数需大于1才能设置为祖先进程
	if err := SetSubreaper(1) ; err != nil {
		panic(err)
	}
	glog.Printf("subreaper started....")
	m := gproc.NewManager()
	// 调用第一个子进程
	p := m.NewProcess("/usr/local/go/bin/go", []string{"run", "sub_process_1.go"}, os.Environ())

	_ , err := p.Start()
	if err != nil {
		panic(err)
	}
	glog.Printf("ancestor: %d", gproc.Pid())
	time.Sleep(60 * time.Second)
	if err := p.Wait(); err != nil {
		panic(err)
	}

}

sub_process_1.go

package main

import (
	"github.com/gogf/gf/os/glog"
	"github.com/gogf/gf/os/gproc"
	"os"
)


func main () {
	glog.Printf("sub process 1....")
	m := gproc.NewManager()

	p := m.NewProcess("/usr/local/go/bin/go", []string{"run", "sub_process_2.go"}, os.Environ())

	_ , err := p.Start()
	if err != nil {
		panic(err)
	}
	glog.Printf("sub process 1: %d", gproc.Pid())

}

sub_process_2.go

package main

import (
	"github.com/gogf/gf/os/glog"
	"github.com/gogf/gf/os/gproc"
	"os"
	"time"
)

func main() {
	glog.Printf("sub process 2.... started")
	glog.Printf("sub process 2: %d", gproc.Pid())
	glog.Printf("sub process 2: ppid %d", os.Getppid())
	time.Sleep(30 * time.Second)
	glog.Printf("sub process 2.... finished")
}

  • 输出结果,这里很奇怪 ancestor 的pid 竟然和 sub_porecess_2的ppid 不一致,其实是因为我们这里是用go run启动,所以又多fork了一个进程来执行。
# go run ancestor_process.go
2021-01-10 23:27:44.109 subreaper started....
2021-01-10 23:27:44.114 ancestor: 1571
2021-01-10 23:27:44.621 sub process 1....
2021-01-10 23:27:44.625 sub process 1: 1608
2021-01-10 23:27:45.120 sub process 2.... started
2021-01-10 23:27:45.120 sub process 2: 1648
2021-01-10 23:27:45.120 sub process 2: ppid 1613
2021-01-10 23:28:15.120 sub process 2.... finished
  • 可以通过之前的办法看看谁在执行 sub_process_2.go,这里就很清晰了pid 1571 是ancestor的,而它的子进程则是1613 也就是go run这个进程的,而1613的子进程则是1648,
    最终关系是 1571(acestor) -> 1613(go run) -> 1648(sub_process_2);
# ps l |grep sub_process_2
0     0  1613  1571  20   0 836060 30040 -      Sl+  pts/1      0:00 /usr/local/go/bin/go run sub_process_2.go
0     0  1648  1613  20   0 745092  9420 -      Sl+  pts/1      0:00 /tmp/go-build141590160/b001/exe/sub_process_2
0     0  1662  1512  20   0 112748  2360 -      S+   pts/2      0:00 grep --color=auto sub_process_2

写在最后

都到这里了,随手 点个 再走呗。
自从接触go语言后,恶补了不少系统层面的知识及网络知识,以前被一笔带过的知识点现在都得翻出来细细琢磨。
后面会更新 Containerd 的内容,近期DockerGoogle(k8s)的“爱恨情仇”也摆到桌面上来了,say goodbye 是迟早的事了,有兴趣的可以关注下。