Bash命令替换产生奇怪的不一致输出

时间:2013-11-18 03:50:05

标签: linux bash shell

由于某些与此问题无关的原因,我在bash脚本中运行Java服务器不是直接运行,而是通过单独的子shell下的命令替换运行,并在后台运行。目的是让子命令将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,有时它包含在由空格charcater分隔的同一行上重复两次的进程id,如下所示:

34056 34056

我在脚本中使用上面脚本中的变量$ daemon_pid来检查服务器是否正在运行,所以如果它包含重复两次的pid,那么这将完全抛出测试并且错误地认为服务器没有运行。通过输入更多的echo语句来摆弄运行CentOS Linux的服务器盒上的脚本似乎将行为转回到包含进程id的$ daemon_pid中的正确的一个,但如果我认为已修复它并检入这个脚本到我的源代码仓库并再次进行构建和部署,我开始看到同样的不良行为。

现在我已经解决了这个问题,假设$ daemon_pid可能不好并通过awk传递它,如下所示:

mypid=$(echo ${daemon_pid} | awk '{ gsub(" +.*",""); print $0 }')

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

非常感谢专家的意见。

1 个答案:

答案 0 :(得分:4)

根据@WilliamPursell的提示,我在bash源代码中跟踪了这一点。老实说,我不知道这是不是一个错误;我只能说,这似乎是一个令人遗憾的与可疑用例的互动。

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

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

为了看看发生了什么,请考虑以下相当奇怪的脚本,可能被称为Duff的Bash设备,除了我不确定甚至Duff会批准:(同样,如上所述,它没有那么有用。但有人在某个地方使用过它。或者,如果没有,他们现在就会看到它。)

/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需要确保它回寻到它使用的输入部分的末尾

(黑客只能工作,因为bash处理这里 - 文件通过将它们复制到临时文件中。如果它使用了管道,head就不可能向后搜索。)< / p>

现在,如果head在后​​台运行会发生什么?答案是“几乎任何事情都有可能”,因为bashhead正在竞相从同一个文件描述符中读取。在后台运行head将是一个非常糟糕的主意,甚至比最初可预测的原始黑客更糟糕。

现在,让我们回到手边的实际程序,简化为基本要素:

/bin/bash <<EOF
cmd <&- &
echo \$!
EOF

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

同时,前台进程继续从stdin读取命令(其stdin fd尚未关闭,因此没问题,这使得它执行echo命令。 / p>

现在请注意:bash知道它需要共享stdin,所以它不能只关闭stdin。它需要确保stdin的文件位置指向正确的位置,即使它实际上已经预先读取了缓冲区的输入值。因此,在它关闭stdin之前,它会向后搜索当前命令行的末尾。 [1]

如果在前台bash执行echo之前发生了搜索,则没有问题。如果在使用here-document完成前景bash后发生,也没问题。但是如果它在回声工作时发生会怎样?在这种情况下,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的{​​{1}}末{} 1}},在大约1093行:

check_bash_input (redirector);
close_buffered_fd (redirector);

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