前言
我们知道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.Stdout
、w1
赋值了给os.Stderr
,由此便接过了2个标准输出的结果。后面的操作就是读取r
和r1
这两个管道的数据,为了演示这里又重新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,后面代码上会体现出来)。而子进程被收养的方式是先会被自己最近的祖先先收养。
- 下面
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
的内容,近期Docker
与Google(k8s)
的“爱恨情仇”也摆到桌面上来了,say goodbye 是迟早的事了,有兴趣的可以关注下。