无需FIFO的进程间通信

13

在BASH脚本中,我们可以有多个在后台运行的进程,它们通过在文件系统上注册的命名管道和FIFO进行相互通信。以下是一个例子:

#!/bin/bash
mkfifo FIFO

# BG process 1
while :; do echo x; done & >FIFO

# BG process 2
while :; do read; done & <FIFO

exit

我想知道是否可能在脚本的后台进程之间进行相同的交互,而不使用文件系统上的FIFO,也许可以使用某种类型的文件描述符重定向。


1
你可以使用 mktemp 来生成一个唯一的名称。 - Alexandre C.
我宁愿不必完全管理文件系统。此外,由于快速创建/删除文件或FIFO,文件系统交互会降低性能。 - davide
遗憾的是,Bourne Shell 对这些事情的处理并不是非常灵活。但是,IPC 虽然没有 FIFO 那么流行,但肯定是可行的,因为它们是相当新的:底层调用是 socketpair/pipe。 - Nicholas Wilson
@NicholasWilson:这听起来很有趣。你能给我指一下文档参考或者举一个非常简短的例子吗? - davide
1
抱歉,我的措辞有些不当。我想说的是:FIFO是最近才出现的,因此IPC显然可以在没有它们的情况下实现。如果您没有使用FIFO,则底层调用将是socketpair(或pipe)。Bash在文件描述符方面有很多有趣的限制,因此通常最好在出现这些问题时直接转向像Python或(我更喜欢的)C包装器这样的东西,以直接执行所需的调用。 - Nicholas Wilson
5个回答

18

这里有一个例子,它运行两个作为同一shell脚本函数实现的子进程…其中一个子进程生成1到5的数字(在打印之间休眠),第二个子进程从固定文件描述符(5)读取(它是第一个FD的STDOUT重定向到的),乘以2后再次打印。主进程将该第二个进程的STDOUT重定向到另一个固定文件描述符(6),然后在循环中从其中一个读取。

它的工作原理与您在C代码中使用pipe(2)系统调用创建的fd对基本相同。要了解发生了什么,请在strace -f下运行脚本!

Bash版本为4.2.24(1),正在Ubuntu/x86上运行。

[ubuntu /home/chris]
$ bash --version
GNU bash, version 4.2.24(1)-release (i686-pc-linux-gnu)
Copyright (C) 2011 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

脚本的输出:

[ubuntu /home/chris]
$ ./read_from_fd.sh
Got number 2.
Got number 4.
Got number 6.
Got number 8.
Got number 10.

源代码:

#!/bin/bash

# Generate data, output to STDOUT.
generate_five_numbers() {
        for n in `seq 5` ; do
                echo $n
                sleep 2
        done
}

# Read data from FD#5, multiply by two, output to STDOUT.
multiply_number_from_fd5_by_two() {
        while read n <&5 ; do
                echo "$(( $n * 2 ))"
        done
}

# choose your FD number wisely ;-)

# run generator with its output dup'ed to FD #5
exec 5< <( generate_five_numbers )

# run multiplyier (reading from fd 5) with output dup'ed to FD #6
exec 6< <( multiply_number_from_fd5_by_two )

# read numbers from fd 6
while read n <&6 ; do
        echo "Got number $n."
done

运行时的进程树:

──read_from_fd.sh(8118)─┬─read_from_fd.sh(8119)───sleep(8123)
                        └─read_from_fd.sh(8120)

我想这在技术上回答了这个问题。虽然我不认为这种方法支持双向通信,所以这比使用标准的bash管道好一点,例如 generate_five_numbers | 0<&5 multiply_number_from_fd5_by_two - DBear

6

Bash 4具有协程

在Bash 2、3或4中,您还可以使用匿名命名管道,也称为进程替换


请纠正我,但我认为这样做无法同时重定向进程的STDIN和STDOUT。 此外,Bash每个实例仅支持一个协处理器;如果我们有许多需要相互通信的后台进程,该怎么办? - davide
@davide:那我会建议你使用除Bash以外的其他东西。顺便说一下,参考手册上说:“在执行的shell和协处理器之间建立了双向管道”[重点标出]。 - Dennis Williamson
抱歉Dennis,我表达不清楚。我的意思是我不确定使用进程替换是否可以进行双向重定向。当然coproc可以,但不幸的是Bash限制了每个实例仅能使用一个。 - davide

3
您可以使用nc(也称为netcat)来连接脚本的标准流到网络套接字。当然,它也适用于本地主机,因此您可以在脚本之间进行IPC。额外的好处是可以在不同主机上运行脚本,这对于FIFO来说是不可能的(好吧,也许在NFS上可以,但除非您已经安装了NFS,否则这将相当麻烦)。

是的,那也可以行得通,但这不需要我(或最终用户)注意不要使用已被同一台计算机上运行的其他服务占用的网络端口(可能有数百个)吗? - davide
可能可以,但如果您从安全范围内选择一个随机端口(不是短暂端口),那么很难发生冲突。此外,如果您告诉nc监听已被占用的端口,它将立即返回错误,然后您可以尝试下一个端口等等。当然,您随后需要通过其他渠道(例如通过文件)将端口通知给其他应用程序。可能在特定的计算机上只需要这样做一次。任何一种解决方案都足够实用。 - Michał Kosmulski
此外,防火墙设置可能会使这种通信方法变得复杂。 - James M. Lay
1
虽然防火墙在技术上可能会出现问题,但这种情况极不可能发生。通常情况下,防火墙会保持回环接口的开放状态,因为如果回环被阻止,很多程序都会崩溃。 - DBear

2
我想指出,丑陋的黑客并不希望以这种方式诞生。

接收数据的部分:

node -e "require('net').createServer(function(s){s.pipe(process.stdout)}).listen(1337)"

发送数据的部分:

echo "write clean code they said" > /dev/tcp/localhost/1337
echo "it will pay off they said" > /dev/tcp/localhost/1337

令我惊讶的是,它甚至可以在MSysGit的Windows Bash中运行。

1

您考虑过使用信号吗?如果您需要的仅是触发事件(而不传递参数),则使用kill和trap非常完美(但要小心语义,例如使用SIGUSR1)。

但您可能需要重新设计逻辑,如下面的示例:

subprocess_finished()
{
    np=$( jobs -p | wc -l )
}

start_processing()
{
    myfile="$1"
    # DO SOMETHING HERE!!
    kill -SIGUSR1 $2
}

CPUS=$( lscpu | grep "^CPU(s):" | rev | cut -f 1 -d ' ' | rev )
POLLPERIOD=5  # 5s between each poll
np=0
trap subprocess_finished SIGUSR1

for myfile in *
do 
        start_processing "$myfile" $$ &
        np=$( jobs -p | wc -l )
        echo "$( date +'%Y-%m-%d %H:%M:%S' ) [$!] Starting #$np on $CPUS: $myfile"

        if [ $np -eq $CPUS ] 
        then
            # Wait for one CPU to be free
            trap subprocess_finished SIGUSR1
            while [ $np -eq $CPUS ]
            do
                sleep $POLLPERIOD
            done
        fi
    done
done

# wait for the last subprocesses
while [ ! -z "$( jobs -rp )" ]
do
    sleep $POLLPERIOD
done

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