在BASH脚本中,来自多个tee接收方的管道输出在命名管道上不按顺序到达的可能竞争条件



UPDATE:虽然我并没有真正解决管道工作中出现的最初问题,但我通过极大地简化它,并完全抛弃管道,解决了我的问题。这里有一个概念验证脚本,它在从磁盘读取一次的同时并行生成CRC32、MD5、SHA1、SHA224、SHA256、SHA384和SHA512校验和,并将它们作为JSON对象返回(将使用PHP和Ruby中的输出)。它很粗糙,没有错误检查,但它有效:

#!/bin/bash
checksums="`tee <"$1" 
        >( cfv -C -q -t sfv -f - - | tail -n 1 | sed -e 's/^.* ([a-fA-F0-9]{8})$/"crc32":"1"/' ) 
        >( md5sum - | sed -e 's/^([a-fA-F0-9]{32}) .*$/"md5":"1"/' ) 
        >( sha1sum - | sed -e 's/^([a-fA-F0-9]{40}) .*$/"sha1":"1"/' ) 
        >( sha224sum - | sed -e 's/^([a-fA-F0-9]{56}) .*$/"sha224":"1"/' ) 
        >( sha256sum - | sed -e 's/^([a-fA-F0-9]{64}) .*$/"sha256":"1"/' ) 
        >( sha384sum - | sed -e 's/^([a-fA-F0-9]{96}) .*$/"sha384":"1"/' ) 
        >( sha512sum - | sed -e 's/^([a-fA-F0-9]{128}) .*$/"sha512":"1"/') 
        >/dev/null` 
"
json="{"
for checksum in $checksums; do json="$json$checksum,"; done
echo "${json:0: -1}}"

最初的问题:

我有点害怕问这个问题,因为我的搜索短语点击率太高了,以至于在应用了从"使用命名管道与bash-数据丢失问题"中获得的知识,并阅读了另外20页之后,我仍然对此有点停滞不前。

因此,为了继续,我正在做一个简单的脚本,使我能够在文件上同时创建CRC32、MD5和SHA1校验和,同时只从磁盘读取一次。我使用cfv就是为了这个目的。

最初,我只是拼凑了一个简单的脚本,用三个cfv命令将文件写入/tmp/下的三个独立文件,然后尝试将它们写入stdout,但最终输出为空,除非我在尝试读取文件之前让脚本休眠一秒钟。我觉得这很奇怪,我认为我在编写脚本时是个白痴,所以我试着用一种不同的方法,让cfv工人输出到一个命名管道。到目前为止,这是我的脚本,在应用了前面提到的链接中的技术之后:

!/bin/bash
# Bail out if argument isn't a file:
[ ! -f "$1" ] && echo "'$1' is not a file!" && exit 1
# Choose a name for a pipe to stuff with CFV output:
pipe="/tmp/pipe.chksms"
# Don't leave an orphaned pipe on exiting or being terminated:
trap "rm -f $pipe; exit" EXIT TERM
# Create the pipe (except if it already exists (e.g. SIGKILL'ed b4)):
[ -p "$pipe" ] || mkfifo $pipe
# Start a background process that reads from the pipe and echoes what it
# receives to stdout (notice the pipe is attached last, at done):
while true; do
        while read line; do
                [ "$line" = "EOP" ] && echo "quitting now" && exit 0
                echo "$line"
        done
done <$pipe 3>$pipe & # This 3> business is to make sure there's always
                      # at least one producer attached to the pipe (the
                      # consumer loop itself) until we're done.
# This sort of works without "hacks", but tail errors out when the pipe is
# killed, naturally, and script seems to "hang" until I press enter after,
# which I believe is actually EOF to tail, so it's no solution anyway:
#tail -f $pipe &
tee <"$1" >( cfv -C -t sfv -f - - >$pipe ) >( cfv -C -t sha1 -f - - >$pipe ) >( cfv -C -t md5 -f - - >$pipe ) >/dev/null
#sleep 1s
echo "EOP" >$pipe
exit

因此,按原样执行,我得到了以下输出:

daniel@lnxsrv:~/tisso$ ./multisfv file
 :  :  : quitting now
- : Broken pipe (CF)
close failed in file object destructor:
sys.excepthook is missing
lost sys.stderr
- : Broken pipe (CF)
close failed in file object destructor:
sys.excepthook is missing
lost sys.stderr
- : Broken pipe (CF)
daniel@lnxsrv:~/tisso$ close failed in file object destructor:
sys.excepthook is missing
lost sys.stderr

但是,随着睡眠1秒的结束,我得到了预期的输出,

daniel@lnxsrv:~/tisso$ ./multisfv file
3bc1b5ff125e03fb35491e7d67014a3e *
-: 1 files, 1 OK.  0.013 seconds, 79311.7K/s
5e3bb0e3ec410a8d8e14fef1a6daababfc48c7ce *
-: 1 files, 1 OK.  0.016 seconds, 62455.0K/s
; Generated by cfv v1.18.3 on 2012-03-09 at 23:45.23
;
2a0feb38
-: 1 files, 1 OK.  0.051 seconds, 20012.9K/s
quitting now

这让我很困惑,因为我认为tee直到每个cfv接收方都退出数据后才会退出,因此echo"EOP"语句将执行,直到所有cfv子流都完成,这意味着他们会将输出写入我的命名管道。。。然后执行echo语句。

由于没有管道,只使用输出临时文件,行为是一样的,我认为这一定是某种竞争条件,与tee将数据推送到其接收方的方式有关?我尝试了一个简单的"等待"命令,但它当然会等待我的bash子进程while循环完成,所以我只得到一个挂起的进程。

有什么想法吗?

TIA,Daniel:)

tee一旦将最后一位输入写入最后一个输出管道并关闭它(即bash创建的未命名管道,而不是您的fifo,也就是"命名管道"),就会退出。它不需要等待读取管道的过程完成;事实上,它甚至不知道自己在给管道写信。由于管道有缓冲区,所以tee很可能在另一端的进程完成读取之前完成写入。因此,脚本将"EOP"写入fifo,导致读取循环终止。这将关闭fifo唯一的读取器,并且所有cfv进程在下次尝试写入stdout时都将获得SIGPIPE。

这里要问的一个显而易见的问题是,为什么不运行三个(或N个)独立的进程来读取文件并计算不同的摘要。如果"文件"实际上是动态生成的,或者是从某个远程站点下载的,或者其他一些缓慢的过程,那么以您尝试的方式做事可能是有意义的,但如果文件存在于本地磁盘上,那么很可能实际上只会进行一次磁盘访问;滞后的汇总器将从缓冲区缓存中读取文件。如果这就是您所需要的,GNU并行应该可以正常工作,或者您可以在bash中启动进程(使用&),然后等待它们。YMMV,但我认为这两种解决方案中的任何一种都比设置所有这些管道和用tee模拟userland中的缓冲区缓存更节省资源。

顺便说一句,如果您想序列化多个进程的输出,可以使用flock实用程序。仅仅使用fifo是不够的;无法保证写入fifo的进程会以原子方式写入整行,如果你知道他们这样做了,你就不需要fifo了。

最新更新