linux - Bash 命令替换给出奇怪的不一致输出

标签 linux bash shell

出于与此问题无关的某些原因,我不是直接而是通过单独的子 shell 下的命令替换和在后台在 bash 脚本中运行 Java 服务器。目的是让子命令返回 Java 服务器的进程 ID 作为其标准输出。有问题的片段如下:

launch_daemon()
{
  /bin/bash <<EOF
     $JAVA_HOME/bin/java $JAVA_OPTS -jar $JAR_FILE daemon $PWD/config/cl.yml <&- &
     pid=\$!
     echo \${pid} > $PID_FILE
     echo \${pid}   
EOF
}

daemon_pid=$(launch_daemon)

echo ${daemon_pid} > check.out

有问题的 Java 守护进程打印标准错误并在初始化出现问题时退出,否则它关闭标准输出和标准错误并继续前进。稍后在脚本中(未显示)我会检查以确保服务器进程正在运行。现在来解决这个问题。

每当我检查上面的 $PID_FILE 时,它都会在一行中包含正确的进程 ID。

但是当我检查文件 check.out 时,它有时包含正确的 id,有时它包含在同一行上重复两次的进程 id,由空格字符分隔,如下所示:
34056 34056

我稍后在脚本中使用上面脚本中的变量 $daemon_pid 来检查服务器是否正在运行,因此如果它包含重复两次的 pid,这完全会导致测试失败,并且它错误地认为服务器没有运行。通过放入更多 echo 语句等来摆弄运行 CentOS Linux 的服务器机器上的脚本似乎将行为翻转回正确的 $daemon_pid 之一,其中包含进程 ID 一次,但如果我认为已经修复它并 checkin 将此脚本添加到我的源代码存储库并再次构建和部署,我开始看到相同的不良行为。

现在我通过假设 $daemon_pid 可能是坏的并通过 awk 传递它来解决这个问题,如下所示:
mypid=$(echo ${daemon_pid} | awk '{ gsub(" +.*",""); print $0 }')

然后 $mypid 总是包含正确的进程 ID,一切都很好,但不用说,我想了解为什么它的行为方式如此。在你问之前,我已经看了又看,但有问题的 Java 服务器在关闭标准输出之前不会将其进程 ID 打印到其标准输出。

非常感谢专家的意见。

最佳答案

按照@WilliamPursell 的提示,我在 bash 源代码中跟踪了这一点。老实说,我不知道这是否是一个错误;我只能说,这似乎是与可疑用例的不幸交互。

TL;DR:您可以通过删除 <&- 来解决问题从脚本。

闭幕 stdin充其量是有问题的,不仅仅是因为@JonathanLeffler 提到的原因(“程序有权拥有一个开放的标准输入。”),更重要的是因为 stdin bash 正在使用进程本身并在后台关闭它会导致竞争条件。

为了了解发生了什么,请考虑以下相当奇怪的脚本,它可能被称为 Duff's Bash Device,除了我不确定 Duff 是否会批准:(此外,正如所介绍的,它不是那么有用。但是某个地方的某个人已经在一些 hack 中使用过它。或者,如果没有,他们现在会看到它。)

/bin/bash <<EOF
if (($1<8)); then head -n-$1 > /dev/null; fi
echo eight
echo seven
echo six
echo five
echo four
echo three
echo two
echo one
EOF

为此,bashhead双方都要准备好分享stdin ,包括共享文件位置。这意味着 bash需要确保它刷新其读取缓冲区(或不刷新),以及 head需要确保它返回到它使用的输入部分的末尾。

(hack 之所以有效,是因为 bash 通过将 here-documents 复制到临时文件来处理它们。如果它使用管道,则 head 不可能向后查找。)

现在,如果 head 会发生什么?曾在后台运行?答案是,“一切皆有可能”,因为 bashhead正在竞相从同一个文件描述符中读取数据。运行 head在后台将是一个非常糟糕的主意,甚至比至少可以预测的原始黑客更糟糕。

现在,让我们回到手头的实际程序,简化为它的基本要素:
/bin/bash <<EOF
cmd <&- &
echo \$!
EOF

该程序的第 2 行( cmd <&- & ) fork 出一个单独的进程(在后台运行)。在那个过程中,它关闭 stdin然后调用 cmd .

同时,前台进程继续从 stdin 读取命令。 (它的 stdin fd 还没有关闭,所以没关系),这导致它执行 echo命令。

现在问题来了:bash知道需要分享stdin ,所以它不能只是关闭 stdin .需要确保stdin的文件位置指向正确的位置,即使它实际上可能已经提前读取了缓冲区的输入值。所以就在它关闭之前 stdin ,它向后搜索到当前命令行的末尾。 [1]

如果该查找发生在前台 bash 执行之前 echo ,那么就没有问题了。如果它发生在前台 bash 使用 here-document 完成之后,也没有问题。但是如果它发生在 echo 工作时怎么办?在那种情况下,在 echo 之后完成,bash将重读 echo命令因为 stdin已倒带,echo将再次被执行。

这正是 OP 中正在发生的事情。有时,后台搜索在错误的时间完成,并导致 echo \${pid}被执行两次。其实也是造成echo \${pid} > $PID_FILE执行两次,但该行是幂等的;是不是echo \${pid} >> $PID_FILE ,双重执行将是可见的。

所以解决方法很简单:删除 <&-从服务器启动行开始,并可选择将其替换为 </dev/null如果您想确保服务器无法读取 stdin .

备注:

注 1:对于那些比我更熟悉 bash 源代码及其预期行为的人,我相信查找和关闭发生在 case r_close_this: 的末尾。在函数中 do_redirection_internalredir.c ,大约在第 1093 行:
check_bash_input (redirector);
close_buffered_fd (redirector);

第一个调用是 lseek第二个是 close .我看到了使用 strace -f 的行为然后在代码中搜索看似合理的代码 lseek ,但我没有去调试器中验证的麻烦。

关于linux - Bash 命令替换给出奇怪的不一致输出,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/20039771/

相关文章:

linux - 调用bash脚本在if语句和变量赋值上生成错误

python - 在Paramiko中执行多个命令,以便命令受到其前任命令的影响

linux - 如何将可变参数与 wpa_passphrase 一起使用?

c++ - 不同平台的相同源的二进制大小如何变化

linux - 如何在使用 GDB 生成进程核心文件之前预测其大小?

bash - VSCode shell 任务转义单引号

linux - 为什么当我尝试远程执行命令时出现错误?

linux - 如何在不使用 tmp 目录的情况下重新打包 zip 文件?

linux - bash 脚本到一个类轮

bash - 在 for 循环中使用命令行参数 (bash)