Unix上带有多个读取器的命名管道(FIFOs)

49

我有两个程序,Writer和Reader。

我有一个从Writer到Reader的FIFO,所以当我在Writer的stdin中写入内容时,它会从Reader的stdout打印出来。

我尝试使用两个Reader打开,并且只有其中一个Reader程序的输出出现在stdout中。Unix选择从哪个Reader程序打印stdout似乎是每次运行都随机的,但一旦它选择了其中一个程序,每个stdout输出都将从相同的Reader程序中打印出来。

有人知道为什么会这样吗?

如果我有两个WRITER程序,它们都可以正确地写入同一个pipe。


你想知道为什么数据没有“广播”到每个读者,还是为什么数据没有均匀分布在每个读者之间? - Jacob
我相信Writer正向其stdout(而非stdin)写入,这是一个FIFO;每个Reader可能正在从自己的stdin读取数据,然后将数据写入其自己的stdout。 - Jonathan Leffler
Jacob - 我想知道为什么数据只传递给了一个读取器,而不是两个。 - Vlad the Impala
3
+1,这个在SO上存档是个好主意。去年Linux成为默认的开发平台后,我的几个同事也问了我同样的问题。 - Tim Post
5个回答

35

FIFO中的“O”代表“out”。当你的数据“out”时,它就消失了。所以,如果另一个进程出现并且其他人已经发出了读取请求,那么数据不会被再次获取。

要实现您建议的内容,您应该查看Unix域套接字。Manpage 在此。您可以编写一个服务器,可以将数据写入客户端进程,并绑定到文件系统路径。还请参见socket()bind()listen()accept()connect()等,这些都需要使用PF_UNIXAF_UNIXstruct sockaddr_un


13

Linux的tee()函数可能适合你的需求。
请在此处查看tee

注意: 此功能仅适用于Linux。


2
tee 不仅适用于 Linux http://www.opengroup.org/onlinepubs/9699919799/utilities/tee.html;但我不确定它是否能帮助解决原始问题的使用情况。 - Brian Campbell
3
我在提到 C 语言中的 tee 函数,而不是命令行工具 tee。但是,我不确定这个函数是否在其他平台/库中实现过。 - Julian
tee很棒。难点在于,在将数据流连接到30个进程以使得每个进程可以处理1/30的数据之后,如何组装结果...为此,您需要一个读者和多个写入者。诀窍是创建30个FIFO,并让读者在它们上面“选择”,整体输出读取。HADOOP应该为您完成此操作,但它是一个可怕、臃肿的框架。像0mq这样的工具提供轻量级/清洁的IPC,适用于大多数编程语言。 - Erik Aronesty
如果在读取器上使用tee,那么至少最后一个读取器需要消耗数据,否则数据将永远不会被消耗? - CMCDragonkai

3

我认为你观察到的行为只是巧合。请考虑以下跟踪,它使用'sed'作为两个读取器和循环作为写入器:

Osiris JL: mkdir fifo
Osiris JL: cd fifo
Osiris JL: mkfifo fifo
Osiris JL: sed 's/^/1: /' < fifo &
[1] 4235
Osiris JL: sed 's/^/2: /' < fifo &
[2] 4237
Osiris JL: while read line ; do echo $line; done > fifo < /etc/passwd
1: ##
1: # User Database
1: #
1: # Note that this file is consulted directly only when the system is running
1: # in single-user mode. At other times this information is provided by
1: # Open Directory.
1: #
1: # This file will not be consulted for authentication unless the BSD local node
1: # is enabled via /Applications/Utilities/Directory Utility.app
1: #
1: # See the DirectoryService(8) man page for additional information about
1: # Open Directory.
1: ##
1: nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
1: root:*:0:0:System Administrator:/var/root:/bin/sh
1: daemon:*:1:1:System Services:/var/root:/usr/bin/false
1: _uucp:*:4:4:Unix to Unix Copy Protocol:/var/spool/uucp:/usr/sbin/uucico
1: _lp:*:26:26:Printing Services:/var/spool/cups:/usr/bin/false
2: _postfix:*:27:27:Postfix Mail Server:/var/spool/postfix:/usr/bin/false
2: _mcxalr:*:54:54:MCX AppLaunch:/var/empty:/usr/bin/false
2: _pcastagent:*:55:55:Podcast Producer Agent:/var/pcast/agent:/usr/bin/false
2: _pcastserver:*:56:56:Podcast Producer Server:/var/pcast/server:/usr/bin/false
2: _serialnumberd:*:58:58:Serial Number Daemon:/var/empty:/usr/bin/false
2: _devdocs:*:59:59:Developer Documentation:/var/empty:/usr/bin/false
2: _sandbox:*:60:60:Seatbelt:/var/empty:/usr/bin/false
2: _mdnsresponder:*:65:65:mDNSResponder:/var/empty:/usr/bin/false
2: _ard:*:67:67:Apple Remote Desktop:/var/empty:/usr/bin/false
2: _www:*:70:70:World Wide Web Server:/Library/WebServer:/usr/bin/false
2: _eppc:*:71:71:Apple Events User:/var/empty:/usr/bin/false
2: _cvs:*:72:72:CVS Server:/var/empty:/usr/bin/false
2: _svn:*:73:73:SVN Server:/var/empty:/usr/bin/false
2: _mysql:*:74:74:MySQL Server:/var/empty:/usr/bin/false
2: _sshd:*:75:75:sshd Privilege separation:/var/empty:/usr/bin/false
2: _qtss:*:76:76:QuickTime Streaming Server:/var/empty:/usr/bin/false
2: _cyrus:*:77:6:Cyrus Administrator:/var/imap:/usr/bin/false
2: _mailman:*:78:78:Mailman List Server:/var/empty:/usr/bin/false
2: _appserver:*:79:79:Application Server:/var/empty:/usr/bin/false
2: _clamav:*:82:82:ClamAV Daemon:/var/virusmails:/usr/bin/false
2: _amavisd:*:83:83:AMaViS Daemon:/var/virusmails:/usr/bin/false
2: _jabber:*:84:84:Jabber XMPP Server:/var/empty:/usr/bin/false
2: _xgridcontroller:*:85:85:Xgrid Controller:/var/xgrid/controller:/usr/bin/false
2: _xgridagent:*:86:86:Xgrid Agent:/var/xgrid/agent:/usr/bin/false
2: _appowner:*:87:87:Application Owner:/var/empty:/usr/bin/false
2: _windowserver:*:88:88:WindowServer:/var/empty:/usr/bin/false
2: _spotlight:*:89:89:Spotlight:/var/empty:/usr/bin/false
2: _tokend:*:91:91:Token Daemon:/var/empty:/usr/bin/false
2: _securityagent:*:92:92:SecurityAgent:/var/empty:/usr/bin/false
2: _calendar:*:93:93:Calendar:/var/empty:/usr/bin/false
2: _teamsserver:*:94:94:TeamsServer:/var/teamsserver:/usr/bin/false
2: _update_sharing:*:95:-2:Update Sharing:/var/empty:/usr/bin/false
2: _installer:*:96:-2:Installer:/var/empty:/usr/bin/false
2: _atsserver:*:97:97:ATS Server:/var/empty:/usr/bin/false
2: _unknown:*:99:99:Unknown User:/var/empty:/usr/bin/false
Osiris JL:  jobs
[1]-  Running                 sed 's/^/1: /' < fifo &
[2]+  Done                    sed 's/^/2: /' < fifo
Osiris JL: echo > fifo
1: 
Osiris JL: jobs
[1]+  Done                    sed 's/^/1: /' < fifo
Osiris JL: 

正如您所看到的,两个读取器都读取了一些数据。任何时候安排哪个读取器取决于操作系统的心情。请注意,我仔细使用了echo打印文件的每一行;这些是原子写入,也是原子读取。
如果我使用一个Perl脚本,并在读取和回显一行后加上延迟,那么我可能会看到更多的确定性行为,即通常情况下Reader 1的两行对应Reader 2的一行。
perl -n -e 'while(<>){ print "1: $_"; sleep 1; }' < fifo &
perl -n -e 'while(<>){ print "2: $_"; sleep 2; }' < fifo &

在MacOS X 10.5.8(Leopard)上进行了实验,但大多数地方可能类似。


哦,顺便说一下,当我尝试在两个读取器脚本中使用“sleep 1”的Perl变体时,所有内容都被第二个读取器处理了。我放置了不对称的睡眠以迫使系统采取行动。 - Jonathan Leffler
1
有趣的是...看起来在一个读取器从FIFO中读取数据后,数据就被清除了,所以其他读取器无法读取相同的数据。 - Vlad the Impala
当然 - 一旦数据被读取,它就被消耗掉了。这就是重点。终端也是如此 - 如果有几个进程竞争数据,一个进程会得到它,而另一个则不会。如果想要一些混乱,请尝试执行“more somebigfile | more”。 - Jonathan Leffler

1
我想补充以上解释,即写(和可能的读取,尽管我无法从manpages中确认)到管道在某个大小(在Linux上为4KiB)内是原子性的。因此,假设我们从一个空管道开始,并且写入者将<=4KiB数据写入管道。这是我认为会发生的事情:
a) 写入者一次性写入所有数据。在此过程中,没有其他进程有机会从(或写入)管道中读取。
b) 选择的读取器中的一个被调度执行I/O操作。
c) 该读取器从管道中一次性读取所有数据,并在稍后的某个时间将它们打印到stdout中。
我认为这可以解释为什么只能看到一个读取器的输出。尝试按照较小的块来写入,并且每次写入后暂停一段时间。
当然,其他人已经回答了为什么每个数据仅由一个进程读取的问题。

作者一次性写入所有数据。在此过程中,没有其他进程有机会从管道中读取(或写入)数据。这在Linux中绝对是错误的。作者将被阻塞,直到读者从管道中读取数据。因此,读取和写入始终同时进行(但不会损坏原子性)。 - DepressedDaniel

0

套接字解决方案可行,但如果服务器崩溃,则变得复杂。为了允许任何进程成为服务器,我在临时文件的末尾使用记录锁来包含对给定文件的位置/长度/数据更改。我使用一个临时命名管道将追加请求通信到临时文件末尾具有写锁的任何进程。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接