TLDR;
只需使用https://github.com/hashicorp/go-reap
有一句很棒的俄语表达方式是“不要试图生下自行车”,意思是不要重新发明轮子和保持简单。我认为这里适用。如果我是你,我会重新考虑使用以下之一:
这个问题已经解决了;)
您的问题不准确,或者您要求的是非标准功能。
事实上,我希望我的后台进程尽可能独立:它与我的程序有不同的父 pid、组 pid 等。我想把它作为守护进程运行。
这不是进程继承的工作方式。您不能让进程 A 启动进程 B 并以某种方式将 B 的父进程更改为 C。据我所知,这在 Linux 中是不可能的。
换句话说,如果进程 A (pid 55) 启动进程 B (100),那么 B 必须有父 pid 55。
避免这种情况的唯一方法是让其他东西启动 B 进程,例如 atd、crond 或其他东西 - 这不是您想要的。
如果父 55 死了,那么 PID 1 将是 100 的父,而不是任意进程。
您的说法“它有不同的父 pid”没有意义。
我想把它作为守护进程运行。
那太好了。但是,在 GNU / Linux 系统中,所有守护进程都有一个父 pid,并且这些父进程的父 pid 一直到 pid 1,严格按照父 -> 子规则。
当我向我的程序发送 SIGTERM/SIGKILL 时,底层进程崩溃。
我无法重现该行为。请参阅the proof-of-concept repo 中的 case8 和 case7。
make case8
export NOSIGN=1; make build case7
unset NOSIGN; make build case7
$ make case8
{ sleep 6 && killall -s KILL zignal; } &
./bin/ctrl-c &
sleep 2; killall -s TERM ctrl-c
kill with:
{ pidof ctrl-c; pidof signal ; } | xargs -r -t kill -9
main() 2476074
bashed 2476083 (2476081)
bashed 2476084 (2476081)
bashed 2476085 (2476081)
zignal 2476088 (2476090)
go main() got 23 urgent I/O condition
go main() got 23 urgent I/O condition
zignal 2476098 (2476097)
go main() got 23 urgent I/O condition
zignal 2476108 (2476099)
main() wait...
p 2476088
p 2476098
p 2476108
p 2476088
go main() got 15 terminated
sleep 1; killall -s TERM ctrl-c
p 2476098
p 2476108
p 2476088
go main() got 15 terminated
sleep 1; killall -s TERM ctrl-c
p 2476098
p 2476108
p 2476088
Bash c 2476085 EXITs ELAPSED 4
go main() got 17 child exited
go main() got 23 urgent I/O condition
main() children done: 1 %!s(<nil>)
main() wait...
go main() got 15 terminated
go main() got 23 urgent I/O condition
sleep 1; killall -s KILL ctrl-c
p 2476098
p 2476108
p 2476088
balmora: ~/src/my/go/doodles/sub-process [main]
$ p 2476098
p 2476108
Bash _ 2476083 EXITs ELAPSED 6
Bash q 2476084 EXITs ELAPSED 8
bash 进程在父进程被杀死后继续运行。
killall -s KILL ctrl-c;
所有 3 个“zignal”子进程都在运行,直到被杀死
killall -s KILL zignal;
在这两种情况下,尽管用 TERM、HUP、INT 向主进程发出信号,但子进程仍会继续运行。由于方便的原因,这种行为在 shell 环境中是不同的。请参阅有关信号的相关问题。 This particular answer 说明了 SIGINT 的一个关键区别。请注意,应用程序无法捕获 SIGSTOP 和 SIGKILL。
在继续处理问题的其他部分之前,有必要澄清上述内容。
到目前为止,您已经解决了以下问题:
- 将子进程的标准输出重定向到文件
- 设置子进程的所有者UID
- 子进程在父进程中幸存下来(我的程序退出)
- 子进程的PID可以被主程序看到
下一个取决于孩子是否“附着”到外壳上
最后一个很难复现,但我在 docker 世界里听说过这个问题,所以这个答案的其余部分都集中在解决这个问题上。
正如您所指出的,Cmd.Wait() 是避免创建僵尸所必需的。经过一些实验后,我能够在 docker 环境中使用有意简单的 /bin/sh 替换来一致地生成僵尸。在 go 中实现的这个“shell”只会运行一个命令,在获取子节点方面不会运行太多其他命令。你可以研究一下代码over at github。
僵尸解决方案
导致僵尸的简单包装
package main
func main() {
Sh()
}
收割者包装器
package main
import (
"fmt"
"sync"
"github.com/fatih/color"
"github.com/hashicorp/go-reap"
)
func main() {
if reap.IsSupported() {
done := make(chan struct{})
var reapLock sync.RWMutex
pids := make(reap.PidCh, 1)
errors := make(reap.ErrorCh, 1)
go reap.ReapChildren(pids, errors, done, &reapLock)
go report(pids, errors, done)
Sh()
close(done)
} else {
fmt.Println("Sorry, go-reap isn't supported on your platform.")
}
}
func report(pids reap.PidCh, errors reap.ErrorCh, done chan struct{}) {
sprintf := color.New(color.FgWhite, color.Bold).SprintfFunc()
for ;; {
select {
case pid := <-pids:
println(sprintf("raeper pid %d", pid))
case err := <-errors:
println(sprintf("raeper er %s", err))
case <-done:
return
}
}
}
运行其他命令的 init / sh (pid 1) 进程
package main
import (
"os"
"os/exec"
"strings"
"time"
"github.com/google/shlex"
"github.com/tox2ik/go-poc-reaper/fn"
)
func Sh() {
args := os.Args[1:]
script := args[0:0]
if len(args) >= 1 {
if args[0] == "-c" {
script = args[1:]
}
}
if len(script) == 0 {
fn.CyanBold("cmd: expecting sh -c 'foobar'")
os.Exit(111)
}
var cmd *exec.Cmd
parts, _ := shlex.Split(strings.Join(script, " "))
if len(parts) >= 2 {
cmd = fn.Merge(exec.Command(parts[0], parts[1:]...), nil)
}
if len(parts) == 1 {
cmd = fn.Merge(exec.Command(parts[0]), nil)
}
if fn.IfEnv("HANG") {
fn.CyanBold("cmd: %v\n start", parts)
ex := cmd.Start()
if ex != nil {
fn.CyanBold("cmd %v err: %s", parts, ex)
}
go func() {
time.Sleep(time.Millisecond * 100)
errw := cmd.Wait()
if errw != nil {
fn.CyanBold("cmd %v err: %s", parts, errw)
} else {
fn.CyanBold("cmd %v all done.", parts)
}
}()
fn.CyanBold("cmd: %v\n dispatched, hanging forever (i.e. to keep docker running)", parts)
for {
time.Sleep(time.Millisecond * time.Duration(fn.EnvInt("HANG", 2888)))
fn.SystemCyan("/bin/ps", "-e", "-o", "stat,comm,user,etime,pid,ppid")
}
} else {
if fn.IfEnv("NOWAIT") {
ex := cmd.Start()
if ex != nil {
fn.CyanBold("cmd %v start err: %s", parts, ex)
}
} else {
ex := cmd.Run()
if ex != nil {
fn.CyanBold("cmd %v run err: %s", parts, ex)
}
}
fn.CyanBold("cmd %v\n dispatched, exit docker.", parts)
}
}
Dockerfile
FROM scratch
# for sh.go
ENV HANG ""
# for sub-process.go
ENV ABORT ""
ENV CRASH ""
ENV KILL ""
# for ctrl-c.go, signal.go
ENV NOSIGN ""
COPY bin/sh /bin/sh ## <---- wrapped or simple /bin/sh or "init"
COPY bin/sub-process /bin/sub-process
COPY bin/zleep /bin/zleep
COPY bin/fork-if /bin/fork-if
COPY --from=busybox:latest /bin/find /bin/find
COPY --from=busybox:latest /bin/ls /bin/ls
COPY --from=busybox:latest /bin/ps /bin/ps
COPY --from=busybox:latest /bin/killall /bin/killall
剩余的代码/设置可以在这里看到:
案例5(简单/bin/sh)
它的要点是我们从 go 启动两个子进程,使用“父”sub-process 二进制文件。第一个孩子是zleep,第二个孩子是fork-if。第二个启动一个“守护进程”,除了一些短命线程之外,它还运行一个永久循环。一段时间后,我们杀死了sub-procss 父母,迫使sh 接管这些孩子的养育工作。
由于sh这个简单的实现不知道如何处理被遗弃的孩子,孩子们变成了僵尸。
这是标准行为。为避免这种情况,请init systems are usually responsible 清理任何此类孩子。
查看这个 repo 并运行案例:
$ make prep build
$ make prep build2
第一个将使用 docker 容器中的简单 /bin/sh,第二个将使用包装在 reaper 中的相同代码。
僵尸:
$ make prep build case5
(…)
main() Daemon away! 16 (/bin/zleep)
main() Daemon away! 22 (/bin/fork-if)
(…)
main() CRASH imminent
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x49e45c]
goroutine 1 [running]:
main.main()
/home/jaroslav/src/my/go/doodles/sub-process/sub-process.go:137 +0xfc
cmd [/bin/sub-process /log/case5 3 /bin/zleep 111 2 -- /dev/stderr 3 /bin/fork-if --] err: exit status 2
Child '1' done
thread done
STAT COMMAND USER ELAPSED PID PPID
R sh 0 0:02 1 0
S zleep 3 0:02 16 1
Z fork-if 3 0:02 22 1
R fork-child-A 3 0:02 25 1
R fork-child-B 3 0:02 26 25
S fork-child-C 3 0:02 27 26
S fork-daemon 3 0:02 28 27
R ps 0 0:01 30 1
Child '2' done
thread done
daemon
(…)
STAT COMMAND USER ELAPSED PID PPID
R sh 0 0:04 1 0
Z zleep 3 0:04 16 1
Z fork-if 3 0:04 22 1
Z fork-child-A 3 0:04 25 1
R fork-child-B 3 0:04 26 1
S fork-child-C 3 0:04 27 26
S fork-daemon 3 0:04 28 27
R ps 0 0:01 33 1
(…)
有收割者:
$ make -C ~/src/my/go/doodles/sub-process case5
(…)
main() CRASH imminent
(…)
Child '1' done
thread done
raeper pid 24
STAT COMMAND USER ELAPSED PID PPID
S sh 0 0:02 1 0
S zleep 3 0:01 18 1
R fork-child-A 3 0:01 27 1
R fork-child-B 3 0:01 28 27
S fork-child-C 3 0:01 30 28
S fork-daemon 3 0:01 31 30
R ps 0 0:01 32 1
Child '2' done
thread done
raeper pid 27
daemon
STAT COMMAND USER ELAPSED PID PPID
S sh 0 0:03 1 0
S zleep 3 0:02 18 1
R fork-child-B 3 0:02 28 1
S fork-child-C 3 0:02 30 28
S fork-daemon 3 0:02 31 30
R ps 0 0:01 33 1
STAT COMMAND USER ELAPSED PID PPID
S sh 0 0:03 1 0
S zleep 3 0:02 18 1
R fork-child-B 3 0:02 28 1
S fork-child-C 3 0:02 30 28
S fork-daemon 3 0:02 31 30
R ps 0 0:01 34 1
raeper pid 18
daemon
STAT COMMAND USER ELAPSED PID PPID
S sh 0 0:04 1 0
R fork-child-B 3 0:03 28 1
S fork-child-C 3 0:03 30 28
S fork-daemon 3 0:03 31 30
R ps 0 0:01 35 1
(…)
这是相同输出的图片,阅读起来可能不会那么混乱。
僵尸
收割者
如何在 poc repo 中运行案例
获取代码
git clone https://github.com/tox2ik/go-poc-reaper.git
一个终端:
make tail-cases
另一个终端
make prep
make build
or make build2
make case0 case1
...
相关问题:
去
信号
相关讨论:
相关项目:
相关散文:
僵尸进程是执行完成但在进程表中仍有一个条目的进程。僵尸进程通常发生在子进程中,因为父进程仍然需要读取其子进程的退出状态。一旦使用等待系统调用完成此操作,僵尸进程就会从进程表中消除。这被称为收割僵尸进程。
来自 https://www.tutorialspoint.com/what-is-zombie-process-in-linux