【问题标题】:Run command in golang and detach it from process在 golang 中运行命令并将其与进程分离
【发布时间】:2020-04-12 01:48:37
【问题描述】:

问题:

我在 linux 上用 golang 编写程序,需要执行长时间运行的进程,以便:

  1. 我将正在运行的进程的标准输出重定向到文件。
  2. 我控制进程的用户。
  3. 当我的程序退出时进程不会死。
  4. 进程崩溃时不会变成僵尸进程。
  5. 我得到了正在运行的进程的 PID。

我正在以 root 权限运行我的程序。

尝试的解决方案:

func Run(pathToBin string, args []string, uid uint32, stdLogFile *os.File) (int, error) {
    cmd := exec.Command(pathToBin, args...)

    cmd.SysProcAttr = &syscall.SysProcAttr{
        Credential: &syscall.Credential{
            Uid: uid,
        },
    }

    cmd.Stdout = stdLogFile

    if err := cmd.Start(); err != nil {
        return -1, err
    }

    go func() {
        cmd.Wait() //Wait is necessary so cmd doesn't become a zombie
    }()


    return cmd.Process.Pid, nil
}

这个解决方案似乎满足了我几乎所有的要求,除了当我向我的程序发送 SIGTERM/SIGKILL 时,底层进程崩溃了。事实上,我希望我的后台进程尽可能独立:它与我的程序有不同的父 pid、组 pid 等。 我想将它作为守护进程运行。

stackoverflow 上的其他解决方案建议在类似用例中使用 cmd.Process.Release(),但它似乎不起作用。

不适用于我的解决方案:

  1. 我无法控制正在运行的进程的代码。我的解决方案必须适用于任何流程。
  2. 我不能使用外部命令来运行它,只是去。所以使用 systemd 或类似的东西是不适用的。

事实上,我可以使用来自 github 等的 import 轻松导入的库。

【问题讨论】:

  • 设置Setpgid: true够吗?我似乎记得至少在 bash 下运行时这是必要的(大概你在开发中这样做),因为 bash 向进程组中的所有进程发送信号,而不仅仅是你的 Go 程序。另外,你说无论如何你想要一个独立的团体。
  • 我也试过这个。但是由于某种原因,当我运行它时,我得到了权限错误。当我以root身份运行它时,这很奇怪。即使以 root 身份运行此命令也会给我同样的错误。我尝试了解决方法并在运行后使用linksyscall.Setpgid 修改了进程,但我遇到了同样的问题。
  • 你知道它为什么会崩溃吗?我相信您需要考虑谁拥有日志文件的文件描述符。我相信cmd.ExtraFiles 就是为了这个目的。
  • 另外你应该只调用cmd.Process.Release() 而不是在goroutine中等待。 Wait() 的文档没有提到这一点。
  • 我很惊讶与这个问题相关的许多答案都建议使用cmd.Process.Release()。在 unix 系统上,它似乎是...doing nothing.

标签: linux go process daemon


【解决方案1】:

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


【讨论】:

    猜你喜欢
    • 2016-11-13
    • 1970-01-01
    • 1970-01-01
    • 2011-07-15
    • 2016-05-23
    • 2015-10-10
    • 1970-01-01
    • 2015-10-22
    • 1970-01-01
    相关资源
    最近更新 更多