从管道的后面杀死管道中的特定进程

问题描述

我正在编写一个 shell 脚本,它使用 telnet 连接到服务器,发送查询,然后获取单行响应以进行进一步处理。经典的解决方案是设置一个子 shell,它 echo 执行查询,然后运行 ​​sleep 以保持连接打开足够长的时间以返回响应:

#!/bin/sh

SERVER_ADD=localhost
SERVER_PORT=9995

result=$( ( echo '{"op":"get","path":"access"}'; sleep 30 ) | telnet "$SERVER_ADD" "$SERVER_PORT" )
    
echo "$result"

这是有效的,除了 sleep 必须长于服务器可能需要的最长响应时间。这意味着每次调用都需要那么长时间,这会将最短时间从几十毫秒推到几十秒。我需要我的脚本等待第一行响应,然后终止进程以便它可以继续做其他事情。

(是的:我知道显而易见的答案是“使用 expect。”不幸的是,我的目标是嵌入式系统,并添加expectTcL 脚本引擎写入会使我的图像大小增加大约 1 兆字节。无法做到。)

经过多次反复试验,我得出了以下结论:

#!/bin/sh

SERVER_ADD=localhost
SERVER_PORT=9995

result=$( ( echo '{"op":"get","path":"access"}'; sleep 30 ) |
    telnet "$SERVER_ADD" "$SERVER_PORT" |
    while read -r line; do
        echo "$line"
        killall sleep
    done ) 2>/dev/null
echo "$result"

说明

  • 设置一个子shell来回显查询,然后休眠足够长的时间以获得最长的响应时间
  • 通过管道连接到 telnet 以建立连接
  • 将服务器响应传送到一个循环中,一次一行
  • 第一行被捕获并回显后,按名称终止 sleep 命令,这将结束子 shell,从而结束 telnet 连接
  • 2>/dev/null 使用 sleep 命令在终止时打印的“已终止”消息

这很有效,除了那个 killall sleep。这将杀死此用户控制下的每个 sleep 实例。大多数情况下这不会成为问题,但其他时候它会成为严重令人困惑的错误来源。

我怎样才能在需要时杀死那个 sleep(或整个子 shell),而不会造成附带损害?

解决方法

如果您有 bash,您可以尝试使用协处理:

coproc telnet "$SERVER_ADD" "$SERVER_PORT" 
echo '{"op":"get","path":"access"}' >&${COPROC[1]}
result=
while IFS= read -r line
do    result+="$line
"
done <&${COPROC[0]}

coproc bash 命令启动一个进程,并创建一个名为 COPROC 的数组,其中包含进程的两个文件描述符,stdin 和 stdout。 然后,您可以随意书写和阅读它们。 如果您的 telnet 在一个命令后没有结束,您可能需要发送第二个命令(例如 echo 'exit' >&${COPROC[1]})来断开连接。

,

您可以在运行 telnet 时尝试在任意文件上使用 flock 命令替换睡眠。然后将挂起第二个 flock 代替睡眠。第三个 flock -u 可以随时释放锁。

尽管典型用法是 flock lockfile command,但此脚本利用了用法 flock filedescriptor。这是因为 flock -u 只能用于文件描述符,不能用于命令(添加命令会强制将文件描述符 arg 作为文件名)。

只需打开锁定文件一次,并为其分配文件描述符 5(任意),flock 5 将锁定它,而 flock -u 5 将解锁它。

result=$(
    exec 5>mylock
    flock 5
    ( echo '{"op":"get","path":"access"}'; flock mylock true ) |
    telnet "$SERVER_ADD" "$SERVER_PORT" |
    while read line; do
        echo "$line"
        flock -u 5
    done 
) 2>/dev/null

查看另一个 question 以进行 flock -u 的简单测试。

,

您可以尝试使用 fifo 将数据发送到 telnet,以便您可以在需要的时间关闭它。

rm -f myfifo
mkfifo myfifo
result=$(
    telnet "$SERVER_ADD" "$SERVER_PORT"  <myfifo  |
    (  echo '{"op":"get","path":"access"}'  >&5
       while read -r line; do
          echo "$line"
          exec 5>&-
       done
    ) 5>myfifo
)

语法 5>myfifo(无空格)打开一个新的输出文件描述号 5,写入先进先出。 echo >&5 写入此 fd。 exec 5>&- 关闭此 fd。此语法应适用于 ash