在写单行时是回声原子

时间:2012-03-29 13:44:14

标签: bash scripting concurrency

我目前正在尝试使用脚本将其他已启动命令的输出正确写入日志文件。该脚本将使用echo将它自己的消息写入日志文件,并且有一种方法可以管理来自其他程序的行。

主要问题是,产生输出的程序是在后台启动的,所以我执行读取的函数可以写入日志文件。这可能是个问题吗? Echo总是只写一行,所以不应该很难确保原子性。但是我已经查看了谷歌,我发现无法确保它实际上是原子的。

这是当前的脚本:

LOG_FILE=/path/to/logfile

write_log() {
  echo "$(date +%Y%m%d%H%M%S);$1" >> ${LOG_FILE}
}

write_output() {
  while read data; do
    write_log "Message from SUB process: [ $data ]"
  done
}

write_log "Script started"
# do some stuff
call_complicated_program 2>&1 | write_output &
SUB_PID=$!
#do some more stuff
write_log "Script exiting"
wait $SUB_PID

正如您所看到的,脚本可能会自行编写,也可能因为重定向输出。这会导致文件中的havok吗?

3 个答案:

答案 0 :(得分:28)

echo只是一个围绕write的简单包装(这是一个简化;请参阅下面的编辑,了解血腥细节),因此要确定回声是否为原子,查找写入很有用。来自single UNIX specification

  

原子/非原子:如果在一次操作中写入的全部量与来自任何其他进程的数据不交错,则写入是原子的。当有多个写入器将数据发送到单个读取器时,这很有用。应用程序需要知道可以预期以原子方式执行写入请求的大小。此最大值称为{PIPE_BUF}。 IEEE Std 1003.1-2001的这个体积并未说明超过{PIPE_BUF}字节的写请求是否是原子的,但要求{PIPE_BUF}或更少字节的写入必须是原子的。

您可以使用简单的C程序检查系统上的PIPE_BUF。如果你只打印一行输出,这不是非常长,它应该是原子的。

这是一个检查PIPE_BUF的价值的简单程序:

#include <limits.h>
#include <stdio.h>

int main(void) {
  printf("%d\n", PIPE_BUF);

  return 0;
}

在Mac OS X上,它给了我512(PIPE_BUF的{​​{3}})。在Linux上,我得到4096.所以如果你的行很长,请确保你在有问题的系统上检查它。

编辑添加:我决定在Bash中检查minimum allowed value,确认它会以原子方式打印。事实证明,echo使用putcharprintf取决于您是否使用-e选项。这些是缓冲的stdio操作,这意味着它们填满了一个缓冲区,并且实际上只在达到换行符时(在行缓冲模式下),缓冲区被填充(在块缓冲模式下),或者你明确刷新输出fflush。默认情况下,如果流是交互式终端,则流将处于行缓冲模式;如果是任何其他文件,则流将处于缓冲模式。 Bash从不设置缓冲类型,因此对于您的日志文件,它应该默认为阻止缓冲模式。然后在the implementation of echo,Bash end of the echo builtin刷新输出流。因此,输出将始终在echo的末尾刷新,但如果它不适合缓冲区,则可以提前刷新。

使用的缓冲区大小可能是BUFSIZ,但可能不同;如果使用BUFSIZ显式设置缓冲区,setbuf是默认大小,但是没有可移植的方法来确定缓冲区的实际大小。 BUFSIZ也没有便携式指南,但是当我在Mac OS X和Linux上测试它时,它的大小是PIPE_BUF的两倍。

这一切意味着什么?由于echo的输出都是缓冲的,因此在填充缓冲区或调用write之前,它实际上不会调用fflush。此时,应该写出输出,并且我应该应用上面提到的原子性保证。如果stdout缓冲区大小大于PIPE_BUF,则PIPE_BUF将是可以写出的最小原子单位。如果PIPE_BUF大于stdout缓冲区大小,则当缓冲区填满时,流将写入缓冲区。

因此,echo仅保证以原子方式编写短于PIPE_BUF的较小序列和stdout缓冲区的大小,这很可能是BUFSIZ。在大多数系统中,BUFSIZ大于PIPE_BUF

tl; dr echo将自动输出线条,只要这些线条足够短。在现代系统上,您可能最多可以安全地使用512字节,但是无法移植地确定限制。

答案 1 :(得分:0)

没有非自愿的文件锁定,但&gt;&gt;操作员是安全的,&gt;操作员不安全。所以你的做法是安全的。

答案 2 :(得分:0)

我尝试了方法from user:pizza,但无法使它像答案from user:Brian Campbell那样工作。让我知道我是否正在做某事,我将更新答案。 (是的,这是一个答案,因为我实际上正在提供完整的工作演示。)

基本并发

这仅说明了问题

$ for n in {1..5}; do (curl -svo /dev/null example.com 2>&1 &) done | grep GET
> GET / HTTP/1.1
>>  GET / HTTP/1.1
GET / HTTP/1.1
>>>  GET / HTTP/1.1
>>GET / HTTP/1.1

在输出的每一行上使用回声

这使用Brian Campbell's method解决了问题。 (请注意,此行的长度受到限制。)

$ for n in {1..5}; do (curl -svo /dev/null example.com 2>&1 | while read; do echo "${REPLY}"; done &) done | grep GET
> GET / HTTP/1.1
> GET / HTTP/1.1
> GET / HTTP/1.1
> GET / HTTP/1.1
> GET / HTTP/1.1

重定向for循环以附加到stdout

本能会告诉您这是行不通的,因为它会在合并所有叉状卷发的输出之后重新定向。

$ for n in {1..5}; do (curl -svo /dev/null example.com 2>&1 &) done >> /dev/stdout | grep GET
> GET / HTTP/1.1
> GET / HTTP/1.1
>> >GET / HTTP/1.1
 >  GET / HTTP/1.1
 GET / HTTP/1.1

重定向每个curl以追加到stdout

我怀疑这种失败是由于每个curl的全部内容都被重定向并且其大小大于内核愿意阻止的大小这一事实。我没有花时间确认这一点,但是Brian Campbell的分享似乎支持了这一点。

$ for n in {1..5}; do (curl -svo /dev/null example.com >>/dev/stdout 2>&1 &) done | grep GET
>>  GET / HTTP/1.1
GET / HTTP/1.1
> GET / HTTP/1.1
GET / HTTP/1.1
> GET / HTTP/1.1