如何将一个命名管道的输出反馈到另一个命名管道中?

3
我正在为一个bash脚本添加自定义日志功能,但无法弄清楚为什么它无法将一个命名管道的输出反馈到另一个命名管道中。
以下是该脚本的基本版本(http://pastebin.com/RMt1FYPc):
#!/bin/bash

PROGNAME=$(basename $(readlink -f $0))
LOG="$PROGNAME.log"
PIPE_LOG="$PROGNAME-$$-log"
PIPE_ECHO="$PROGNAME-$$-echo"

# program output to log file and optionally echo to screen (if $1 is "-e")
log () {
  if [ "$1" = '-e' ]; then 
    shift
    $@ > $PIPE_ECHO 2>&1 
  else 
    $@ > $PIPE_LOG 2>&1 
  fi
}

# create named pipes if not exist
if [[ ! -p $PIPE_LOG ]]; then 
  mkfifo -m 600 $PIPE_LOG
fi
if [[ ! -p $PIPE_ECHO ]]; then 
  mkfifo -m 600 $PIPE_ECHO
fi

# cat pipe data to log file
while read data; do
  echo -e "$PROGNAME: $data" >> $LOG 
done < $PIPE_LOG &

# cat pipe data to log file & echo output to screen
while read data; do
  echo -e "$PROGNAME: $data"
  log echo $data   # this doesn't work
  echo -e $data > $PIPE_LOG 2>&1   # and neither does this
  echo -e "$PROGNAME: $data" >> $LOG   # so I have to do this
done < $PIPE_ECHO &

# clean up temp files & pipes
clean_up () {
  # remove named pipes
  rm -f $PIPE_LOG
  rm -f $PIPE_ECHO
}
#execute "clean_up" on exit
trap "clean_up" EXIT 

log echo "Log File Only"
log -e echo "Echo & Log File"

我以为第34行和35行的命令会将$PIPE_ECHO中的$data输出到$PIPE_LOG,但它不起作用。相反,我必须直接将输出发送到日志文件,而不通过$PIPE_LOG
为什么它不能按照我的期望工作?
编辑:我将shebang更改为“bash”。问题仍然存在。
解决方案:A.H.的答案帮助我了解我没有正确使用命名管道。我已经通过不使用命名管道来解决了我的问题。该解决方案在这里:http://pastebin.com/VFLjZpC3

你说这是一个bash脚本,但你在脚本的第一行指定了/bin/sh。你确定在bash中需要这样做吗?请记住,bash并不是每个操作系统都默认安装的。 - ghoti
我认为潜在的问题是,如果你使用脚本路径运行这个脚本,而不是bash [path],你将调用“sh”版本的bash(即使它是相同的可执行文件),它的工作方式并不完全相同(有限制)。 - huelbois
我尝试将“sh”替换为“bash”,但它的工作方式相同(即不工作)。 - kipkoan
我期望第34、35和36行都会向日志文件输出“Echo & Log File”,因此日志文件中应该有3行“Echo & Log File”。但实际上,第34和35行没有输出,只有第36行输出了。我想在34或35中使用该方法,这样我就不必重复输出逻辑。在生产版本中,我将添加其他内容到输出中,例如日期时间戳,因此最好能够将“pipe_echo”输出传递给“pipe_log”输出,而不是在“pipe_echo”函数中重复“pipe_log”例程的逻辑。 - kipkoan
奇怪。对我不起作用。当我只有第34行时,日志文件中只会显示“仅日志文件”,而不是“回显和日志文件”。我在CentOS 6.2上运行此脚本。我使用./<script_name>调用脚本。在RHEL/CentOS中,/bin/sh是符号链接到/bin/bash的。但是,如果您使用bash来运行它,我想它应该与CentOS上的bash一样工作。 - kipkoan
显示剩余3条评论
2个回答

10

在我看来,你可能不理解什么是命名管道。 命名管道不像普通管道一样只有一个流。它是由一系列普通管道组成的,因为命名管道可以被关闭,并且生产者端的关闭操作可能会显示为消费者端的关闭操作。

“可能”部分的意思是:消费者将读取数据直到没有更多数据。没有更多数据意味着在调用read时,没有生产者打开命名管道。这意味着只有当至少有一个生产者存在时,多个生产者才能向一个消费者提供数据。可以把它想象成一个自动关闭的门:如果有一群人源源不断地让门保持开放,无论是通过将门把手递给下一个人还是同时挤过去,那么门就一直开着。但是一旦门关闭,它就会一直关闭。

一个小演示应该能够使差异更加清晰:

打开三个shell。 第一个 shell:

1> mkfifo xxx
1> cat xxx

没有任何输出是因为cat打开了命名管道并等待输入数据。

第二个 shell:

2> cat > xxx 

没有输出,因为这个cat是一个生产者,它会一直保持这个命名管道处于打开状态,直到我们显式地告诉它关闭为止。

第三个 shell:

3> echo Hello > xxx
3>

这个生产者会立即返回。

第一个shell:

Hello
消费者接收数据,将其写入,并且——由于另一个消费者保持连接,继续等待。 第三个 shell
3> echo World > xxx
3> 

第一个 shell:

World
消费者接收到数据,将其写入并且——由于还有一个消费者保持门打开状态——继续等待。 第二个Shell:在cat > xxx窗口中写入。
And good bye!
(control-d key)
2>

第一个shell

And good bye!
1>

在你的情况下,即:

  • 你的 log 函数会尝试多次打开和关闭管道。这不是一个好主意。
  • 你的两个 while 循环都比你想象中提前退出了。(用 (while ... done < $PIPE_X; echo FINISHED; ) & 检查一下)
  • 根据你的各种生产者和消费者的调度,门可能有时候被猛然关上,有时候则不会——你内置了竞争条件。(为了测试,可以在 log 函数的末尾添加一个 sleep 1。)
  • 你的“测试用例”只对每种可能性进行了一次尝试——尽量多次使用它们(你会被阻塞,特别是在有 sleeps 的情况下),因为你的生产者可能找不到任何消费者。

所以我可以解释你代码中存在的问题,但我不能告诉你解决方案,因为你的需求边界不清楚。


非常好的答案,表述得非常清楚。但奇怪的是,在RHEL 6.2和Debian Squeeze中,它的行为非常不同,我不明白为什么。在RHEL中,第一个循环根本不会退出(卡在写入上..而不是读取)。在Debian中,发送多个日志可以正常工作,但添加睡眠会导致整个过程挂起。我想深入研究这个主题。 - huelbois
正如我所写的那样:这是一种竞争条件,由“睡眠”明确定义:如果没有生产者的“持续流”来“保持大门敞开”,消费者将退出并阻止下一个生产者。没有睡眠,“稳定流”只是更有可能出现。此外,不同的操作系统版本表示不同的硬件,因此具有不同的调度行为。 - A.H.
1
@huelbois: 我曾试图隐藏一些内部细节,但现在我认为最好为您命名它们:普通管道和FIFO(也称为命名管道)之间的一个关键区别是阻塞通常发生在哪里:使用标准管道时,消费者和生产者将在read/write调用中阻塞。另一方面,FIFO通常在消费者和生产者的__open__调用中阻塞!在两者“相遇”后,两个open调用都返回,并且消费者和生产者都继续他们的工作(可能再次阻塞)。这意味着同步是在open而不是在read/write期间完成的。 - A.H.
1
谢谢,A.H. 你说得对...我没有正确理解FIFO的工作原理。你能帮我想办法让它工作吗?这是我想要实现的:上面的代码是启动脚本(rc.d/init.d)的一部分,该脚本执行另一个供应商提供的脚本来启动应用程序。当供应商提供的脚本运行时,它会将消息输出到STDOUT(最后退出)。我想捕获这些消息,添加一些内容(时间戳、程序名称),并将结果消息重定向到日志文件(可选地回显到STDOUT/终端)。 - kipkoan
A.H.: 如果需要,让我知道是否应该将此作为新问题重新发布。 我不确定在这方面的礼仪。 谢谢! - kipkoan
@kipkoan:在我看来,提出一个新问题会更好,因为a)你应该先看看是否有其他人已经解决了这样的问题(我认为是这样的),b)这是一个干净的开始-没有混合不同的东西,c)你可以更简洁地描述真正的问题。 - A.H.

0

看起来问题出在“将数据通过管道写入日志文件”的部分。

让我们看看:你使用“&”将循环放在后台运行,我想你的意思是它必须与第二个循环并行运行。

但问题是你甚至不需要“&”,因为一旦fifo中没有更多数据可用,while..read就会停止。(但首先你必须有一些数据才能使第一个读取工作)。如果没有更多数据可用,下一个读取不会挂起(这会带来另一个问题:你的程序如何停止?)。

我猜while read在执行读取之前检查文件中是否还有更多数据,并在没有数据时停止。

你可以使用以下示例进行检查:

mkfifo foo
while read data; do echo $data; done < foo

这个脚本会一直挂起,直到你从另一个 shell 中写入任何内容(或将第一个 shell 放到后台)。但只要读取成功,它就会结束。

编辑: 我在 RHEL 6.2 上进行了测试,结果和你说的一样(例如:糟糕!)。

问题在于,在运行脚本之后(假设是脚本“a”),你会得到一个剩余的“a”进程。所以,是的,在某种程度上,脚本会像我之前写的那样挂起(不是我当时想的那么愚蠢的答案 :))。除非你只写一个日志(无论是仅日志文件还是 echo,在这种情况下它可以工作)。

(当写入 PIPE_LOG 并每次留下一个进程时,PIPE_ECHO 的读取循环会挂起)。

我添加了一些调试消息,以下是我看到的:

  • 只有一行从 PIPE_LOG 中读取,然后循环结束
  • 然后第二条消息被发送到 PIPE_LOG(在从 PIPE_ECHO 接收到之后),但进程不再从 PIPE_LOG 中读取 => 写入挂起。

当你执行 ls -l /proc/[pid]/fd 命令时,你会发现 fifo 文件仍然处于打开状态(但已被删除)。 实际上,脚本已经退出并删除了 fifos 文件,但仍有一个进程在使用它。 如果你在清理时不删除日志 fifo 并且使用 cat 命令,它将释放挂起的进程。

希望这能帮到你...


你可以尝试运行这个脚本。它不会挂起。问题在于第34和35行没有将任何内容输出到日志文件中。 - kipkoan
你是对的,抱歉。但是我今天学会了使用while read和fifo! - huelbois

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