命令替换下的 coproc 和命名管道行为

命令替换下的 coproc 和命名管道行为

我需要在 zsh shell 脚本中创建一个函数,该函数通过命令替换来调用,与对同一命令替换的后续调用进行状态通信。

类似于 C 函数中的静态变量(非常粗略地说)。

为此,我尝试了两种方法 - 一种使用协处理器,一种使用命名管道。命名管道方法,我无法开始工作 - 这很令人沮丧,因为我认为它将解决我在协处理器方面遇到的唯一问题 - 也就是说,如果我从终端进入新的 zsh shell,我似乎不会能够看到父 zsh 会话的 coproc。

我创建了简化的脚本来说明下面的问题 - 如果您对我想要做什么感到好奇 - 它正在向子弹头列车 zsh 主题添加一个新的有状态组件,该组件将由替换的命令调用 build_prompt( ) 函数在这里: https://github.com/caiogondim/bullet-train.zsh/blob/d60f62c34b3d9253292eb8be81fb46fa65d8f048/bullet-train.zsh-theme#L692

脚本 1 - 协处理器

#!/usr/bin/env zsh

coproc cat
disown
print 'Hello World!' >&p

call_me_from_cmd_subst() {
    read get_contents <&p
    print "Retrieved: $get_contents"
    print 'Hello Response!' >&p
    print 'Response Sent!'
}

# Run this first
call_me_from_cmd_subst

# Then comment out the above call
# And run this instead
#print "$(call_me_from_cmd_subst)"

# Hello Response!
read finally <&p
echo $finally

脚本 2 - 命名管道

#!/usr/bin/env zsh

rm -rf /tmp/foo.bar
mkfifo /tmp/foo.bar
print 'Hello World!' > /tmp/foo.bar &

call_me_from_cmd_subst() {
    get_contents=$(cat /tmp/foo.bar)
    print "Retrieved: $get_contents"
    print 'Hello Response!' > /tmp/foo.bar &!
    print 'Response Sent!'
}

# Run this first
call_me_from_cmd_subst

# Then comment out the above call
# And run this instead
#print "$(call_me_from_cmd_subst)"

# Hello Response!
cat /tmp/foo.bar

在它们的初始形式中,它们都产生完全相同的输出:

$ ./named-pipe.zsh
Retrieved: Hello World!
Response Sent!
Hello Response!

$ ./coproc.zsh
Retrieved: Hello World!
Response Sent!
Hello Response!

现在,如果我将 coproc 脚本切换为使用命令替换进行调用,则不会发生任何变化:

# Run this first
#call_me_from_cmd_subst

# Then comment out the above call
# And run this instead
print "$(call_me_from_cmd_subst)"

也就是说,从命令替换创建的子进程读取和写入协进程不会导致任何问题。我对此感到有点惊讶 - 但这是个好消息!

但是,如果我在命名管道示例中进行相同的更改,则脚本会阻塞 - 没有输出。为了尝试弄清楚为什么我用 运行它zsh -x,给出:

+named-pipe.zsh:3> rm -rf /tmp/foo.bar
+named-pipe.zsh:4> mkfifo /tmp/foo.bar
+named-pipe.zsh:15> call_me_from_cmd_subst
+call_me_from_cmd_subst:1> get_contents=+call_me_from_cmd_subst:1> cat /tmp/foo.bar
+named-pipe.zsh:5> print 'Hello World!'
+call_me_from_cmd_subst:1> get_contents='Hello World!'
+call_me_from_cmd_subst:2> print 'Retrieved: Hello World!'
+call_me_from_cmd_subst:4> print 'Response Sent!'

在我看来,由命令替换创建的子进程不会终止,而下一行尚未终止(我已经使用了 using &&!disown这里结果没有变化)。

print 'Hello Response!' > /tmp/foo.bar &!

为了演示这一点,我可以手动触发一只猫来读取响应:

$ cat /tmp/foo.bar
Hello Response!

该脚本现在等待最后的 cat 命令,因为管道中没有任何内容可供读取。


我的问题是:

  1. 是否可以构造命名管道,使其在存在命令替换的情况下表现得与协进程完全相同?
  2. 你能解释一下为什么协进程可以明显地从子进程中读取和写入,但是如果我zsh在控制台中手动创建一个子shell(通过键入),我将无法再访问它(事实上我可以创建一个新的协进程来操作)独立于其父级并退出,并继续使用父级!)。
  3. 如果 1 是可能的,我假设命名管道不会像 2 中那样复杂,因为命名管道没有绑定到特定的 shell 进程?

解释一下2和3的意思:

$ coproc cat
[1] 24516
$ print -p test
$ read -ep
test
$ print -p test_parent
$ zsh
$ print -p test_child
print: -p: no coprocess
$ coproc cat
[1] 28424
$ disown
$ print -p test_child
$ read -ep
test_child
$ exit
$ read -ep
test_parent

我无法从子 zsh 内部看到协进程,但我可以从命令替换子进程内部看到它?

最后我使用的是 Ubuntu 18.04:

$ zsh --version
zsh 5.4.2 (x86_64-ubuntu-linux-gnu)

答案1

基于管道的脚本不起作用的原因并不是 zsh 的某些特性。这是由于 shell 命令替换、shell 重定向和管道的工作方式造成的。这是没有多余部分的脚本。

mkfifo /tmp/foo.bar
echo 'Hello World!' > /tmp/foo.bar &

call_me_from_cmd_subst() {
    echo 'Hello Response!' > /tmp/foo.bar &
    echo 'Response Sent!'
}

echo "$(call_me_from_cmd_subst)"
cat /tmp/foo.bar

命令替换$(call_me_from_cmd_subst)创建一个匿名管道,将运行该函数的子 shell 的输出连接到原始 shell 进程。原始进程从该管道读取数据。子进程创建一个孙进程来运行echo 'Hello Response!' > /tmp/foo.bar。两个进程都以相同的打开文件开始,包括匿名管道。孙子执行重定向> /tmp/foo.bar。这会阻塞,因为没有从命名管道读取任何内容/tmp/foo.bar

重定向是一个两步过程(实际上是三步,但第三步在这里并不重要),因为当您打开文件时,您无法选择其文件描述符。操作>员想要重定向标准输出,即它想要将特定文件连接到文件描述符 1。这需要三个系统调用:

  1. 调用fd = open("/tmp/foo.bar", O_RDWR)以打开文件。该文件将在fd进程当前未使用的某个文件描述符上打开。这是阻塞的步骤,直到有东西开始从命名管道读取/tmp/foo.bar:如果没有人在监听,则打开命名管道会阻塞。
  2. dup2(fd, 1)除了内核选择的文件描述符之外,还调用以打开所需文件描述符上的文件。如果新描述符 (1) 上有任何打开的内容(用于命令替换的匿名管道),则此时它会关闭。
  3. 调用close(fd),将重定向目标仅保留在所需的文件描述符上。

同时,子进程打印Reponse Sent!并终止。原来的shell进程仍在从管道中读取数据。由于管道仍然打开以在孙子中写入,因此原始 shell 进程一直在等待。

要解决此死锁,请确保孙子不会让管道保持打开状态超过其必须的时间。例如:

call_me_from_cmd_subst() {
    { exec >&-; /bin/echo 'Hello Response!' > /tmp/foo.bar; } &
    echo 'Response Sent!'
}

或者

call_me_from_cmd_subst() {
    { echo 'Hello Response!' > /tmp/foo.bar; } >/dev/null &
    echo 'Response Sent!'
}

或该主题的任意数量的变体。

对于协进程,您不会遇到此问题,因为它不涉及命名管道,因此死锁的一半不会被阻塞:>/tmp/foo.bar当它打开命名管道时会阻塞,但>&p不会阻塞,因为它只是重定向已经打开的文件描述符。

答案2

对代码进行非常细微的修改即可按预期工作:

#!/usr/bin/env zsh

rm -rf /tmp/foo.bar
mkfifo /tmp/foo.bar
print 'Hello World!' > /tmp/foo.bar &

call_me_from_cmd_subst() {
    get_contents=$(cat /tmp/foo.bar)
    print "Retrieved: $get_contents"
    (print 'Hello Response!' > /tmp/foo.bar &!) >/dev/null
    print 'Response Sent!'
}

# Run this first
#call_me_from_cmd_subst

# Then comment out the above call
# And run this instead
print "$(call_me_from_cmd_subst)"

# Hello Response!
cat /tmp/foo.bar

运行此命令会产生预期的结果:

Retrieved: Hello World!
Response Sent!
Hello Response!

我不完全确定这是为什么 - 似乎 zsh 认为打印到 fifo 可能会在 stdout 上产生某种输出?

相关内容