![如何从后台运行的进程中读取stderr?](https://linux22.com/image/154516/%E5%A6%82%E4%BD%95%E4%BB%8E%E5%90%8E%E5%8F%B0%E8%BF%90%E8%A1%8C%E7%9A%84%E8%BF%9B%E7%A8%8B%E4%B8%AD%E8%AF%BB%E5%8F%96stderr%EF%BC%9F.png)
我想将守护进程发送到后台,并且仅当守护进程将特定行输出到 stderr 时才继续执行脚本,其中包括以下行:
# Fictional daemon
{
for x in {1..9}; do
sleep 1
if [ "$x" != "5" ]; then
echo $x 1>&2
else
echo now 1>&2
fi
done
} &
# Daemon PID
PID="$!"
# Wait until the particular output...
until { read -r line && grep -q "now" <<<"$line"; } do
sleep 1
done </proc/$PID/fd/2
#
# Do more stuff...
#
fg
答案1
其mydaemon
行为如下:
#! /bin/sh -
i=1 stage=init
while true; do
if [ "$i" -eq 5 ]; then
echo >&2 ready
stage=working
fi
echo >&2 "$stage $i"
sleep 1
i=$((i + 1))
done
也就是说,初始化需要 4 秒,ready
准备好后输出,然后继续工作,所有这些都在同一个过程中,您可以编写start-mydaemon
如下脚本:
#! /bin/sh -
DAEMON=./mydaemon
LOGFILE=mydaemon.log
umask 027
: >> "$LOGFILE" # ensures it exists
pid=$(
sh -c 'echo "$$"; exec "$0" "$@"' tail -fn0 -- "$LOGFILE" | {
IFS= read -r tail_pid
export LOGFILE DAEMON
setsid -f sh -c '
echo "$$"
exec "$DAEMON" < /dev/null >> "$LOGFILE" 2>&1'
grep -q ready
kill -s PIPE "$tail_pid"
}
)
printf '%s\n' "$DAEMON started in process $pid and now ready"
$ time ./start-mydaemon
./mydaemon started in process 230254 and now ready
./start-mydaemon 0.01s user 0.01s system 0% cpu 4.029 total
$ ps -fjC mydaemon
UID PID PPID PGID SID C STIME TTY TIME CMD
chazelas 230254 6175 230254 230254 0 10:28 ? 00:00:00 /bin/sh - ./mydaemon
start-mydaemon
mydaemon
直到表明它已准备好才返回。这里,mydaemon
的 stdout 和 stderr 转到日志文件,而其 stdin 则从/dev/null
. setsid -f
(不是标准命令,但在大多数 Linux 发行版上都有)应保证守护进程与终端分离(如果从终端启动)。
但请注意,如果mydaemon
初始化失败并在没有写入的情况下死亡ready
,则该脚本将永远等待ready
永远不会出现的(或者下次mydaemon
成功启动时会出现)。
另请注意,sh -c ...tail
和 守护进程是同时启动的。如果在开始时mydaemon
已初始化并打印并查找到日志文件的末尾,则会错过该消息。ready
tail
tail
ready
您可以通过以下方式解决这些问题:
#! /bin/sh -
DAEMON=./mydaemon
LOGFILE=mydaemon.log
export DAEMON LOGFILE
umask 027
died=false ready=false canary_pid= tail_pid= daemon_pid=
: >> "$LOGFILE" # ensures it exists
{
exec 3>&1
{
tail -c1 <&5 5<&- > /dev/null # skip to the end synchronously
(
sh -c 'echo "tail_pid=$$" >&3; exec tail -fn+1' |
{ grep -q ready && echo ready=true; }
) <&5 5<&- &
} 5< "$LOGFILE"
setsid -f sh -c '
echo "daemon_pid=$$" >&3
exec "$DAEMON" < /dev/null 3>&- 4>&1 >> "$LOGFILE" 2>&1' 4>&1 |
(read anything; echo died=true) &
echo "canary_pid=$!"
} | {
while
IFS= read -r line &&
eval "$line" &&
! "$died" &&
! { [ -n "$daemon_pid" ] && "$ready" ; }
do
continue
done
if "$ready"; then
printf '%s\n' "$DAEMON started in process $daemon_pid and now ready"
else
printf >&2 '%s\n' "$DAEMON failed to start"
fi
kill -s PIPE "$tail_pid" "$canary_pid" 2> /dev/null
"$ready"
}
尽管这开始变得相当复杂。另请注意,由于tail -f
现在在 Linux 上的 stdin 上运行,因此它不会使用 inotify 来检测文件中何时有新数据,并诉诸通常的方法每秒检查一次这意味着可能需要额外一秒钟的时间才能ready
在日志文件中进行检测。
答案2
... done </proc/$PID/fd/2
这并不像你想象的那样有效。
的 stderr$PID
是
- 控制 tty,在这种情况下,您将尝试读取用户输入的字符串,该字符串也可能是标准输入的
$PID
- 一个管道——你会与已经在读取它的人竞争,导致完全混乱
/dev/null
——EOF!- 还有别的事吗;-)?
只有一些黑客方法可以将文件描述符从正在运行的进程重定向到其他地方,因此最好的选择是让输入等待代码将自身降级为cat >/dev/null
在后台运行。
例如,这将“等待”直到守护程序输出4
:
% cat /tmp/daemon
#! /bin/sh
while sleep 1; do echo $((i=i+1)) >&2; done
% (/tmp/daemon 2>&1 &) | (sed /4/q; cat >/dev/null &)
1
2
3
4
%
之后/tmp/daemon
将继续写入cat >/dev/null &
,不受 shell 控制。
另一个解决方案是将守护进程的 stderr 重定向到某个常规文件并tail -f
在其上,但是守护进程将继续用垃圾填充您的磁盘(即使您的rm
文件,它占用的空间也不会被释放,直到守护进程关闭它),这比资源匮乏的情况更糟糕cat
。
当然,最好的办法是编写/tmp/daemon
一个真正的守护进程,它在初始化后将自身置于后台,关闭其 std 文件描述符,用于syslog(3)
打印错误等。
答案3
其他答案涉及使用文件或命名管道。你也不需要。一个简单的管道就足够了。
#!/bin/bash
run_daemon() {
# Fictional daemon
{
for x in {1..9}; do
sleep 1
if [ "$x" != "5" ]; then
echo $x 1>&2
else
echo now 1>&2
fi
done
}
}
run_daemon 2>&1 | (
exec 9<&0 0</dev/null
while read -u 9 line && test "$line" != now
do
echo "$line" ## or ## :
done
echo "$line" ## or ##
cat /dev/fd/9 & ## or ## cat /dev/fd/9 >/dev/null &
#fictional stuff
for a in 1 2 3
do
echo do_stuff $a
sleep 1
done
echo done
wait
)
## or ##
如果您不想显示混合的守护进程的输出,则标记的部分是替代方案。请注意,您不能省略 cat,因为它是阻止守护进程在输出时获取 SIGPIPE 的唯一因素。
我尝试不将输入重定向到不同的文件描述符(exec
),但如果没有这个,某些东西会关闭输入并且守护程序终止。
您可能还注意到,我没有明确地将守护进程设置为后台。有了管道就不需要了。等待wait
的是猫,而不是守护进程。如果守护进程处于后台,它仍然有效。
wait
类似于fg
,但不使控制台进行控制。 (它也老得多。)
答案4
mkfifo
可以做你需要的。看这个答案:
https://stackoverflow.com/questions/43949166/reading-value-of-stdout-and-stderr
使用 FIFO 代替
从技术上讲,/dev/stdout 和 /dev/stderr 实际上是文件描述符,而不是 FIFO 或命名管道。在我的系统上,它们实际上只是 /dev/fd/1 和 /dev/fd/2 的符号链接。这些描述符通常链接到您的 TTY 或 PTY。因此,您无法真正按照您想要的方式阅读它们。
这段代码可以满足您的需求(我删除了处理步骤)。读取循环中不需要睡眠。请注意,当从 fifo 读取的代码停止时,守护进程将继续尝试写入,直到填满,并且守护进程将阻塞,直到从 fifo 读取数据。
fifo="daemon_errors"
{
mkfifo $fifo
for x in {1..9}; do
sleep 1
if [ "$x" != "5" ]; then
echo $x 1>&2
else
echo now 1>&2
fi
done 2>$fifo
} &
sleep 1 # need to wait until the file is created
# Read all output
while read -r line; do
echo "just read: $line"
done < $fifo