排序:写入失败 | 管道损坏

排序:写入失败 | 管道损坏

晚上好,

以下是我在脚本中使用的一段代码。从 SSH 会话启动工作正常,但是,当它通过 cron 运行时,它会在屏幕上显示损坏的管道错误。

我无法通过 SSH 重现它。

代码:

IP=$(sort --random-sort /root/ips.csv | head -n 1); nc -zv -w 2 $IP 443 2>&1 | grep succeeded >> outfile

屏幕错误:

sort: write failed: standard output; Broken pipe
sort: write error

有什么提示/指示吗?

谢谢你!

答案1

head处理第一行后完成时,它退出,关闭管道的另一端。sort可能仍在尝试写入更多内容,并且写入关闭的管道或套接字会返回 EPIPE 错误。但它也会引发 SIGPIPE 信号,杀死进程,除非该信号被忽略或处理。忽略信号后,sort会看到错误、抱怨并退出。如果不忽略该信号,sort就会死掉。

我们可以使用trap内置忽略来自 shell 的信号,出现错误:

$ trap "" PIPE
$ sort bigfile | head -1 > /dev/null 
sort: write failed: standard output: Broken pipe
sort: write error

但可悲的是,我们不能用于trap取消忽略信号并获得所需的行为,如POSIX 要求不允许在非交互式 shell(脚本)中执行此操作。它确实允许交互式 shell,但 Bashtrap在这种情况下也不这样做。

去测试:

sh$ trap '' PIPE                     # ignore the signal    
sh$ PS1='another$ ' bash             # run another shell
another$ trap - PIPE                 # try to reset the signal
                                     # it doesn't work
another$ sort bigfile |head -1 > /dev/null
sort: write failed: 'standard output': Broken pipe
sort: write error

相反,我们可以使用外部工具,例如 Perl 单行代码来运行脚本或命令,并且信号不被忽略(sort此处静默退出):

another$ perl -e '$SIG{PIPE}="DEFAULT"; exec "@ARGV"' \
         'sort bigfile |head -1' > /dev/null 
another$ 

至于你的 cron 情况,原因可能是 systemd显然默认情况下会忽略 SIGPIPE,提到:

[SIGPIPE] 对于普通守护进程来说并不是很有用,当我们试图为守护进程提供一个良好、有用的执行环境时,我们将其关闭。当然,shell 之类的应该再次打开它。

当然,这也提到了在文档中(systemd.exec)

IgnoreSIGPIPE=
采用布尔参数。如果为 true,则导致 SIGPIPE 在执行的进程中被忽略。默认为 true,因为 SIGPIPE 通常仅在 shell 管道中有用。

在我的 Debian 系统上,/lib/systemd/system/cron.service 显式设置IgnoreSIGPIPE=false,撤消 cron 的 systemd 默认值。您可能想检查一下这对您的情况是否有帮助。

答案2

因为--random-sort无论如何它都是一个 GNU 扩展,所以您不妨使用shuf来自同一 GNU 实用程序集合 (GNU coreutils) 的 GNU:

ip=$(shuf -n 1 ips.tsv)

除了避免 SIGPIPE 问题之外,这也更有效,因为它不需要像 GNU 那样对整个文件进行洗牌sort --random-sortshuf使用一些油藏取样算法需要大量投入才能做到这一点)。

答案3

作为一般解决方案,您可以使用这种 Bash 函数来运行管道,而不会出现管道损坏错误,并且不会隐藏其他可能的错误消息:

entire_pipe() {
    ( set -m; (
        trap 'exit 0' INT
        echo $BASHPID
        $1
    ) & wait $! ) |
    (
        trap 'trap - EXIT; kill -s INT -- -$pid 2>/dev/null || :' EXIT
        read -r pid
        $2
    )
}

为了使其更便携,您可以替换$BASHPID$(exec sh -c 'echo "$PPID"').

要在出现此问题时使用该函数,您需要替换

sort --random-sort /root/ips.csv | head -n 1

经过

entire_pipe 'sort --random-sort /root/ips.csv' 'head -n 1'

即使管道的左侧启动后代进程,这也应该起作用;他们都会被打断。

相关内容