为什么 systemd 不捕获用 shell 脚本编写的用户服务子进程的输出?

为什么 systemd 不捕获用 shell 脚本编写的用户服务子进程的输出?

下面是我用 shell 脚本编写的出色服务:

$ cat ~/junk/demoapp 
#! /bin/bash -eu

while true
do
    echo "in shell"
    ( echo "in subshell" )
    /usr/bin/echo "in subprocess"
    sleep 1
done

它会产生一些重复的输出:

$ ~/junk/demoapp
in shell
in subshell
in subprocess
in shell
in subshell
in subprocess
in shell
in subshell
in subprocess
in shell
in subshell
in subprocess
^C

以下是它的用户服务配置:

$ cat ~/.config/systemd/user/demoapp.service 
[Unit]
Description=Demo App

[Service]
Type=exec
ExecStart=/home/tomanderson/junk/demoapp

但是当我使用 systemd 239 运行此服务时,记录的输出缺少子 shell 和子进程生成的行:

$ systemctl --user daemon-reload

$ systemctl --user start demoapp

$ journalctl --user --unit demoapp

Sep 12 18:53:27 myhost systemd[539847]: Started Demo App.
Sep 12 18:53:27 myhost demoapp[559387]: in shell
Sep 12 18:53:28 myhost demoapp[559387]: in shell
Sep 12 18:53:29 myhost demoapp[559387]: in shell
Sep 12 18:53:30 myhost demoapp[559387]: in shell
Sep 12 18:53:31 myhost demoapp[559387]: in shell
Sep 12 18:53:32 myhost demoapp[559387]: in shell
Sep 12 18:53:33 myhost demoapp[559387]: in shell
Sep 12 18:53:34 myhost demoapp[559387]: in shell
Sep 12 18:53:35 myhost demoapp[559387]: in shell

知道为什么吗?从阅读来看,似乎 systemd 通常会在此处捕获子进程的输出。这是 shell 正在执行的与此交互的特定操作吗?

通过 Google 搜索,我发现人们在使用 Python 时遇到了类似的问题,其中缓冲是罪魁祸首,但我不明白这与这里有什么关系。

编辑:使用两个简单的 C 程序,在将 shell 脚本从方程式中移除后,我看到了完全相同的行为。我没有看到这种行为,因为一个简单的父进程取代了 systemd 并通过管道收集输出。这强烈表明是 systemd 做了一些奇怪的事情。参见:https://github.com/tomwhoiscontrary/child-stdout-demo

编辑 2:一位有根权限的观察同事报告说(a)子进程输出在日志中,它只是与服务无关,并且(b)他只在用户服务;如果他设立了一个系统服务具有相同的代码,子进程输出与其相关联!这肯定是 systemd 错误?

答案1

编辑 2:一位有 root 权限的细心同事报告说:(a) 子进程输出在日志中,只是与服务无关,并且 (b) 他只在用户服务中看到这种行为;如果他用相同的代码设置系统服务,子进程输出就会与之关联!这肯定是 systemd 的一个错误吧?

这是一个已知的、长期存在的问题;问题在于内核没有提供足够的方法来将套接字客户端与 cgroup 关联起来(例如,检索客户端的 PID 的能力)。因此,每当 journald 收到一条消息时,它只知道发送者的 PID,但必须异步地从 查找其单元名称/proc/<pid>/cgroup。如果该进程的寿命很短(例如子 shell),则很可能在 journald 被唤醒之前就退出了 - 而当其消息被处理时,将其输出与服务关联所需的信息已不再可用。

我对细节有点模糊,但据我记得,最近的 systemd 版本有一个部分解决方法,只有当到 journald 的 stdout“管道”(实际上是一个套接字对)已由特权进程设置时才有效,而您的“用户”服务由另一个仅具有与您相同权限的 systemd 实例设置。

答案2

虽然我还没有研究底层的技术细节,但适用于 Python 的相同解决方案(禁用缓冲)也适用于这种情况。如果我使用这个单元文件...

[Unit]
Description=Demo App

[Service]
Type=exec
ExecStart=/usr/bin/unbuffer %h/bin/demoapp

...然后预期的输出被记录在日志中。

unbuffer命令是包的一部分expect


此功能通过强制命令作为附加到 pty 设备的交互式进程执行来实现,从而禁用正常缓冲。

如果您手边没有该unbuffer命令,则可以改用该script命令:

ExecStart=/usr/bin/script -c %h/bin/demoapp /dev/null

相关内容