在 linux 中,父进程创建了子进程,例如 exec.Command()
命令启动一个子命令。当父进程因为意外崩溃退出,没来得及控制子进程的生命周期,此时子进程成为孤儿进程继续运行。
本文调研的内容是控制当父进程因为任何原因退出时,子进程必须立即结束。
进程组
进程组确实是一组相关进程的集合,每个进程都有唯一的进程 ID(pid
),同时属于一个进程组,进程组有唯一的进程组 ID(pgid
)。通常,进程组由一个 “领头进程” 创建,其pid
会成为该进程组的pgid
。
当 kill
命令的参数是 pgid
时,kill
会向进程组内的所有进程发送信号,进程可以捕获或忽略信号(SIGTERM)。
- 进程组仅支持 linux,windows 是没有进程组的概念
- 主进程崩溃,子进程还是会成为孤儿进程运行
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
34
35
36
37
38
39
40
41
|
import (
"fmt"
"os/exec"
"syscall"
)
func main() {
// 创建一个命令
cmd := exec.Command("sleep", "1000") // 示例命令,让进程休眠 1000 秒
// 设置 SysProcAttr 以创建新进程组
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, // 创建新进程组
Pgid: 0, // 0 表示使用子进程的 PID 作为进程组 ID
}
// 启动命令
if err := cmd.Start(); err != nil {
fmt.Printf("启动进程失败: %v\n", err)
return
}
// 获取进程组 ID(等于子进程的 PID)
pgid := -cmd.Process.Pid // 在 Linux 中,负 PID 表示进程组
fmt.Printf("子进程 PID: %d\n", cmd.Process.Pid)
fmt.Printf("进程组 ID: %d\n", pgid)
// 向进程组发送信号(例如 SIGTERM)
if err := syscall.Kill(pgid, syscall.SIGTERM); err != nil {
fmt.Printf("发送信号失败: %v\n", err)
return
}
fmt.Println("已向进程组发送终止信号")
// 等待命令执行完成
if err := cmd.Wait(); err != nil {
fmt.Printf("命令执行完成: %v\n", err)
}
}
|
尝试解决方案 异步监听标准输入
1
2
3
4
5
6
7
8
9
10
|
// interrupt 用于阻塞
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
go func() {
_, err := io.Copy(io.Discard, os.Stdin) // 持续读取标准输入(但丢弃数据)
slog.Info("main -> Copy", "err", err) // 记录读取结束时的错误(如 EOF)
interrupt <- syscall.SIGTERM // 发送终止信号
}()
<- interrupt
|
父进程通过 exec.Command()
启动子进程,子进程持续从标准输入读取数据,并直接丢弃。如果输入流关闭(终端关闭,管道断开等), io.Copy
会立即结束,并发送终止信号。
通过监听标准输入的关闭事件,触发程序的优雅退出机制。
但如果是以服务方式启动,子进程会立即退出,服务管理器通过会将标准输入设置为空设备 (/dev/null) 或直接关闭,导致子进程的 os.Stdin
在启动时已经处于 EOF 状态。
1
2
3
4
5
6
7
8
|
// 判断是否在服务模式下运行(stdin 指向 /dev/null 或已关闭)
func isServiceMode() bool {
info, err := os.Stdin.Stat()
if err != nil {
return true // 无法获取状态,假设为服务模式
}
return info.Mode()&os.ModeCharDevice == 0 // 非终端设备(如 /dev/null)
}
|
既然可以监听标准输入的方式判断父进程是否存活,那么通过进程通信管道呢?
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
package main
import (
"bytes"
"log/slog"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
)
func main() {
// 创建用于通信的管道
r, w, err := os.Pipe()
if err != nil {
slog.Error("创建管道失败", "err", err)
return
}
defer w.Close()
// 启动子进程,并将管道写入端传递给子进程
cmd := exec.Command("./child") // 假设子进程程序名为 child
cmd.Stdin = r // 子进程从管道读取数据
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// 启动子进程
if err := cmd.Start(); err != nil {
slog.Error("启动子进程失败", "err", err)
return
}
slog.Info("子进程已启动", "pid", cmd.Process.Pid)
// 父进程定期向管道写入数据(心跳)
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for range ticker.C {
_, err := w.Write([]byte("heartbeat\n"))
if err != nil {
slog.Warn("写入管道失败,子进程可能已退出", "err", err)
break
}
}
}()
// 处理父进程退出信号
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
slog.Info("父进程正在退出...")
w.Close() // 关闭管道,通知子进程
// 等待子进程退出
if err := cmd.Wait(); err != nil {
slog.Error("等待子进程失败", "err", err)
}
slog.Info("父进程已退出")
}
|
子进程
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
package main
import (
"io"
"log/slog"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, syscall.SIGTERM, syscall.SIGINT)
// 获取父进程 PID
parentPID := os.Getppid()
slog.Info("父进程 PID", "pid", parentPID)
// 监听管道关闭事件(父进程崩溃或正常退出)
go func() {
// 从 stdin 读取数据(实际连接到父进程创建的管道)
_, err := io.Copy(io.Discard, os.Stdin)
if err != nil {
slog.Info("管道关闭", "err", err)
} else {
slog.Info("管道正常关闭")
}
interrupt <- syscall.SIGTERM // 触发优雅退出
}()
// 定期检查父进程是否存活(双重保险)
go func() {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for range ticker.C {
// 尝试向父进程发送 0 信号(仅检查进程是否存在)
err := syscall.Kill(parentPID, 0)
if err != nil {
slog.Info("父进程已退出", "err", err)
interrupt <- syscall.SIGTERM // 触发优雅退出
break
}
}
}()
slog.Info("子进程运行中...")
<-interrupt // 阻塞直到收到退出信号
slog.Info("子进程优雅退出")
}
|
总结
主要有两个方案
- 子进程定期检测父进程是否存活,可以监听固定端口也好,pid 状态也好,网络通信也好。(子进程跟随父进程生命周期)
- 父进程启动子进程时,记录子进程的 id,下次启动时,根据需要再停止子进程或继续复用子进程。(允许子进程存活,并在下次启动时接管)