在 golang 中运行命令并将其从进程中分离

Goo*_*vie 6 linux daemon process go

问题:

我正在 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
}
Run Code Online (Sandbox Code Playgroud)

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

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

在我的情况下不适用的解决方案:

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

事实上,我可以使用易于import从 github 等导入的库。

Яро*_*лин 4

太长了;

\n

只需使用https://github.com/hashicorp/go-reap

\n

有一句很棒的俄语表达:“不要试图诞生自行车”,意思是不要重新发明轮子保持简单。我认为这也适用于这里。如果我是你,我会重新考虑使用以下之一:

\n\n

这个问题已经解决了;)

\n
\n

您的问题不精确或者您要求的是非标准功能。

\n
\n

事实上,我希望我的后台进程尽可能独立:它与我的程序具有不同的父 pid、组 pid 等。我想将它作为守护进程运行。

\n
\n

这不是进程继承的工作原理。你不能让进程 A 启动进程 B 并以某种方式将 B 的父进程更改为 C。据我所知,这在 Linux 中是不可能的。

\n

换句话说,如果进程 A (pid 55) 启动进程 B (100),则 B 的父进程 pid 必须为 55。

\n

避免这种情况的唯一方法是让其他东西启动 B 进程,例如 atd、crond 或其他东西 - 这不是您所要求的。

\n

如果父进程 55 死亡,那么 PID 1 将成为 100 的父进程,而不是某个任意进程。

\n

你的说法“它有不同的父pid”没有意义。

\n
\n

我想将它作为守护进程运行。

\n
\n

那太好了。然而,在 GNU / Linux 系统中,所有守护进程都有一个父 pid,并且这些父进程都有一个父 pid 一直到 pid 1,严格按照父 -> 子规则。

\n
\n

当我向程序发送 SIGTERM/SIGKILL 时,底层进程崩溃。

\n
\n

我无法重现这种行为。请参阅概念验证存储库中的 case8 和 case7 。

\n
make case8\nexport NOSIGN=1; make build case7 \nunset NOSIGN; make build case7\n
Run Code Online (Sandbox Code Playgroud)\n
\n$ make case8\n{ sleep 6 && killall -s KILL zignal; } &\n./bin/ctrl-c &\nsleep 2; killall -s TERM ctrl-c\nkill with:\n    { pidof ctrl-c; pidof signal ; } | xargs -r -t kill  -9 \nmain() 2476074\nbashed 2476083 (2476081)\nbashed 2476084 (2476081)\nbashed 2476085 (2476081)\nzignal 2476088 (2476090)\ngo main() got 23 urgent I/O condition\ngo main() got 23 urgent I/O condition\nzignal 2476098 (2476097)\ngo main() got 23 urgent I/O condition\nzignal 2476108 (2476099)\nmain() wait...\np  2476088\np  2476098\np  2476108\np  2476088\ngo main() got 15 terminated\nsleep 1; killall -s TERM ctrl-c\np  2476098\np  2476108\np  2476088\ngo main() got 15 terminated\nsleep 1; killall -s TERM ctrl-c\np  2476098\np  2476108\np  2476088\nBash c 2476085 EXITs ELAPSED 4\ngo main() got 17 child exited\ngo main() got 23 urgent I/O condition\nmain() children done: 1 %!s(<nil>)\nmain() wait...\ngo main() got 15 terminated\ngo main() got 23 urgent I/O condition\nsleep 1; killall -s KILL ctrl-c\np  2476098\np  2476108\np  2476088\nbalmora: ~/src/my/go/doodles/sub-process [main]\n$ p  2476098\np  2476108\nBash _ 2476083 EXITs ELAPSED 6\nBash q 2476084 EXITs ELAPSED 8\n\n\n
Run Code Online (Sandbox Code Playgroud)\n

bash 进程在父进程被杀死后继续运行。

\n
killall -s KILL ctrl-c;\n
Run Code Online (Sandbox Code Playgroud)\n

所有 3 个“zignal”子进程都在运行,直到被杀死

\n
killall -s KILL zignal;\n
Run Code Online (Sandbox Code Playgroud)\n

在这两种情况下,尽管主进程收到了 TERM、HUP、INT 信号,但子进程仍继续运行。由于方便原因,此行为在 shell 环境中有所不同。请参阅有关信号的相关问题。这个特定的答案说明了 SIGINT 的一个关键区别。请注意,应用程序无法捕获 SIGSTOP 和 SIGKILL。

\n
\n

在继续讨论问题的其他部分之前,有必要澄清上述内容。

\n

到目前为止,您已经解决了以下问题:

\n
    \n
  • 将子进程的 stdout 重定向到文件
  • \n
  • 设置子进程的所有者UID
  • \n
  • 子进程在父进程死亡后仍然存在(我的程序退出)
  • \n
  • 主程序可以看到子进程的PID
  • \n
\n

下一个取决于孩子们是否“附着”在贝壳上

\n
    \n
  • 子进程在父进程被杀死后仍然存在
  • \n
\n

最后一个很难重现,但我在 docker 世界中听说过这个问题,所以这个答案的其余部分集中于解决这个问题。

\n
    \n
  • 如果父进程崩溃并且不会变成僵尸,子进程将存活
  • \n
\n

正如您所指出的,Cmd.Wait()避免产生僵尸是必要的。经过一些实验后,我能够使用故意简单的替换/bin/sh. 这个用 go 实现的“shell”只会运行一个命令,在获取子项方面不会执行太多其他命令。您可以在 github 上研究代码。

\n

僵尸解决方案

\n

导致僵尸的简单包装

\n
make case8\nexport NOSIGN=1; make build case7 \nunset NOSIGN; make build case7\n
Run Code Online (Sandbox Code Playgroud)\n

收割者包装纸

\n
\n$ make case8\n{ sleep 6 && killall -s KILL zignal; } &\n./bin/ctrl-c &\nsleep 2; killall -s TERM ctrl-c\nkill with:\n    { pidof ctrl-c; pidof signal ; } | xargs -r -t kill  -9 \nmain() 2476074\nbashed 2476083 (2476081)\nbashed 2476084 (2476081)\nbashed 2476085 (2476081)\nzignal 2476088 (2476090)\ngo main() got 23 urgent I/O condition\ngo main() got 23 urgent I/O condition\nzignal 2476098 (2476097)\ngo main() got 23 urgent I/O condition\nzignal 2476108 (2476099)\nmain() wait...\np  2476088\np  2476098\np  2476108\np  2476088\ngo main() got 15 terminated\nsleep 1; killall -s TERM ctrl-c\np  2476098\np  2476108\np  2476088\ngo main() got 15 terminated\nsleep 1; killall -s TERM ctrl-c\np  2476098\np  2476108\np  2476088\nBash c 2476085 EXITs ELAPSED 4\ngo main() got 17 child exited\ngo main() got 23 urgent I/O condition\nmain() children done: 1 %!s(<nil>)\nmain() wait...\ngo main() got 15 terminated\ngo main() got 23 urgent I/O condition\nsleep 1; killall -s KILL ctrl-c\np  2476098\np  2476108\np  2476088\nbalmora: ~/src/my/go/doodles/sub-process [main]\n$ p  2476098\np  2476108\nBash _ 2476083 EXITs ELAPSED 6\nBash q 2476084 EXITs ELAPSED 8\n\n\n
Run Code Online (Sandbox Code Playgroud)\n

运行其他命令的 init/sh (pid 1) 进程

\n
killall -s KILL ctrl-c;\n
Run Code Online (Sandbox Code Playgroud)\n

Dockerfile

\n
\nFROM scratch\n\n# for sh.go\nENV HANG ""\n\n# for sub-process.go\nENV ABORT ""\nENV CRASH ""\nENV KILL ""\n\n# for ctrl-c.go, signal.go\nENV NOSIGN ""\n\nCOPY bin/sh          /bin/sh ## <---- wrapped or simple /bin/sh or "init"\nCOPY bin/sub-process /bin/sub-process\nCOPY bin/zleep       /bin/zleep\nCOPY bin/fork-if     /bin/fork-if\n\n\nCOPY --from=busybox:latest /bin/find    /bin/find\nCOPY --from=busybox:latest /bin/ls      /bin/ls\nCOPY --from=busybox:latest /bin/ps      /bin/ps\nCOPY --from=busybox:latest /bin/killall /bin/killall\n\n
Run Code Online (Sandbox Code Playgroud)\n

剩余的代码/设置可以在这里看到:

\n\n

案例5(简单/bin/sh)

\n

其要点是我们使用“父”二进制文件从 go 启动两个子进程sub-process。第一个孩子是zleep,第二个孩子是fork-if。第二个启动一个“守护进程”,除了一些短暂的线程之外,它还运行一个永远循环。过了一段时间,我们杀死了sub-procss父母,迫使他们sh接管了这些孩子的抚养权。

\n

由于 sh 的这个简单实现不知道如何处理被遗弃的孩子,因此孩子们变成了僵尸。\n这是标准行为。为了避免这种情况,init 系统通常负责清理任何此类子进程。

\n

查看此存储库并运行案例:

\n
$ make prep build\n$ make prep build2\n
Run Code Online (Sandbox Code Playgroud)\n

第一个将在 docker 容器中使用简单的 /bin/sh,第二个将使用包装在 reaper 中的相同代码。

\n

与僵尸:

\n
$ make prep build case5\n(\xe2\x80\xa6)\nmain() Daemon away! 16 (/bin/zleep)\nmain() Daemon away! 22 (/bin/fork-if)\n(\xe2\x80\xa6)\nmain() CRASH imminent\npanic: runtime error: invalid memory address or nil pointer dereference\n[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x49e45c]\ngoroutine 1 [running]:\nmain.main()\n    /home/jaroslav/src/my/go/doodles/sub-process/sub-process.go:137 +0xfc\ncmd [/bin/sub-process /log/case5 3 /bin/zleep 111 2 -- /dev/stderr 3 /bin/fork-if --] err: exit status 2\nChild \'1\' done\nthread done\nSTAT COMMAND          USER     ELAPSED PID   PPID\nR    sh               0         0:02       1     0\nS    zleep            3         0:02      16     1\nZ    fork-if          3         0:02      22     1\nR    fork-child-A     3         0:02      25     1\nR    fork-child-B     3         0:02      26    25\nS    fork-child-C     3         0:02      27    26\nS    fork-daemon      3         0:02      28    27\nR    ps               0         0:01      30     1\nChild \'2\' done\nthread done\ndaemon\n(\xe2\x80\xa6)\nSTAT COMMAND          USER     ELAPSED PID   PPID\nR    sh               0         0:04       1     0\nZ    zleep            3         0:04      16     1\nZ    fork-if          3         0:04      22     1\nZ    fork-child-A     3         0:04      25     1\nR    fork-child-B     3         0:04      26     1\nS    fork-child-C     3         0:04      27    26\nS    fork-daemon      3         0:04      28    27\nR    ps               0         0:01      33     1\n(\xe2\x80\xa6)\n
Run Code Online (Sandbox Code Playgroud)\n

与收割者:

\n
$ make -C ~/src/my/go/doodles/sub-process case5\n(\xe2\x80\xa6)\nmain() CRASH imminent\n(\xe2\x80\xa6)\nChild \'1\' done\nthread done\nraeper pid 24\nSTAT COMMAND          USER     ELAPSED PID   PPID\nS    sh               0         0:02       1     0\nS    zleep            3         0:01      18     1\nR    fork-child-A     3         0:01      27     1\nR    fork-child-B     3         0:01      28    27\nS    fork-child-C     3         0:01      30    28\nS    fork-daemon      3         0:01      31    30\nR    ps               0         0:01      32     1\nChild \'2\' done\nthread done\nraeper pid 27\ndaemon\nSTAT COMMAND          USER     ELAPSED PID   PPID\nS    sh               0         0:03       1     0\nS    zleep            3         0:02      18     1\nR    fork-child-B     3         0:02      28     1\nS    fork-child-C     3         0:02      30    28\nS    fork-daemon      3         0:02      31    30\nR    ps               0         0:01      33     1\nSTAT COMMAND          USER     ELAPSED PID   PPID\nS    sh               0         0:03       1     0\nS    zleep            3         0:02      18     1\nR    fork-child-B     3         0:02      28     1\nS    fork-child-C     3         0:02      30    28\nS    fork-daemon      3         0:02      31    30\nR    ps               0         0:01      34     1\nraeper pid 18\ndaemon\nSTAT COMMAND          USER     ELAPSED PID   PPID\nS    sh               0         0:04       1     0\nR    fork-child-B     3         0:03      28     1\nS    fork-child-C     3         0:03      30    28\nS    fork-daemon      3         0:03      31    30\nR    ps               0         0:01      35     1\n(\xe2\x80\xa6)\n
Run Code Online (Sandbox Code Playgroud)\n

这是相同输出的图片,阅读起来可能不会那么混乱。

\n

僵尸

\n

案例5-僵尸

\n

收割者

\n

案例5——收割者

\n

如何运行 poc 存储库中的案例

\n

获取代码

\n
git clone https://github.com/tox2ik/go-poc-reaper.git\n
Run Code Online (Sandbox Code Playgroud)\n

一个终端:

\n
make tail-cases\n
Run Code Online (Sandbox Code Playgroud)\n

另一个航站楼

\n
make prep\nmake build\nor make build2\nmake case0 case1\n...\n
Run Code Online (Sandbox Code Playgroud)\n
\n

相关问题:

\n

\n\n

信号

\n\n

相关讨论:

\n\n

相关项目:

\n\n

相关散文:

\n
\n

僵尸进程是指执行完成但进程表中仍有条目的进程。僵尸进程通常发生在子进程中,因为父进程仍然需要读取其 child\xe2\x80\x99s 退出状态。一旦使用 wait 系统调用完成此操作,僵尸进程就会从进程表中消除。这称为收获僵尸进程。

\n
\n

来自 https://www.tutorialspoint.com/what-is-zombie-process-in-linux

\n
\n