梓瑶在上周分享的 rust x SIGPIPE 的 issue 很有意思: https://t.me/ziyao233channel/3879
issue: https://github.com/rust-lang/rust/issues/62569

这里涉及了命令行软件和服务端软件的一些默认实践,我翻了一下几年前的笔记恢复了记忆。

服务端软件(socket 网络编程)通过 write (writev/sendto/sendmsg/sendmmsg) syscall 发送 tcp 流量,这里有个几乎无法避免的情况是,进程正在 write(sock),对端的 tcp reset 飞过来了,内核会抛出 SIGPIPE 信号通知进程这个 socket 不可写。

这个行为对于服务端软件是完全有害且多余。
- 说有害是因为 SIGPIPE 信号的默认处理是崩掉进程,然而 socket 收到 tcp reset 本来就是预期之内的情况,所以必须忽视 SIGPIPE。
- 说多余是因为忽视 SIGPIPE 之后,write syscall 返回 EPIPE 错误码,进程不可能错过这个报错,“正在给一个已经收到 tcp reset 的 socket 写数据” 的这个信息量没有得到任何丢失,反而由于同步化的 EPIPE 返回简化了信号处理(否则 pthread_sigmask + sigtimedwait 是有多想不开)

因此写网络程序直接 ignore SIGPIPE 百利无害,此事在 Unix Network Programming 中亦有记载,UNP 还是太权威了[1]

既然如此那为何要设计 SIGPIPE,全部用 EPIPE 错误码不就好了?答:unix 历史遗留 https://stackoverflow.com/questions/8369506/why-does-sigpipe-exist
这里带出了 SIGPIPE 的(几乎唯一?)需要抛出的场景,考虑命令行:
$ (echo 1; sleep 1; echo 2) | head -1
1
~ $ echo "${PIPESTATUS[@]}"
141 0


管道下游的 head -1 读到第一个 echo 1 的输出就退出进程了,关闭了 pipe_r,导致 pipe_w 不可写,上游的 echo 2 再 write(pipe_w) 的时候会遭遇 SIGPIPE 中止进程。
古典的 unix 命令行要求此时在 retval 里反映信号,141 == 128 + SIGPIPE(13)。

这个要求导致我们不能无脑 ignore SIGPIPE,否则看起来有点业余。(当然可以在命令行软件里看到 EPIPE 再手动 raise SIGPIPE / return 141😀

所以 rust 目前默认 ignore SIGPIPE 就常常会使 rust 编写的命令行工具在本应该返回 141 return code 时候返回 0(again,这是可以仔细处理好的:在 EPIPE 时 return 141😀

如果去看下 go 是怎么处理的,会发现它(貌似)更智慧一点:
A write to a broken pipe on file descriptors 1 or 2 (standard output or standard error) will cause the program to exit with a SIGPIPE signal. A write to a broken pipe on some other file descriptor will take no action on the SIGPIPE signal, and the write will fail with a syscall.EPIPE error.

https://pkg.go.dev/os/signal#hdr-SIGPIPE

更能照顾到绝大多数场景。

不过在 gdb 调试 go 进程时,由于 SIGPIPE 没有被 ignore,会触发 ptrace 的 signal-delivery-stop,很烦,但都是非常细微的 debugger 细节,正常人类不用管。。。

[1] https://flylib.com/books/en/3.225.1.83/1/

(再不分享点技术内容就要沦陷为飞升为男同🏳️‍🌈交友频道了)
 
 
Back to Top