Rustで以下のようなプログラムを書き、head -1
など出力を途中で切るプログラムと組み合わせると Broken Pipe
エラーによってパニックしてしまいます。
fn main() {
loop {
println!("y");
}
}
$ ./simple-yes | head -1
y
thread 'main' panicked at library/std/src/io/stdio.rs:1019:9:
failed printing to stdout: Broken pipe (os error 32)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
どうしてこれが起きるのかとその対策を調べました。
なぜ起きるのか
ここで発生しているエラーは、 write(2)
システムコール時の EPIPE
エラーです。
同様のプログラムをCで実装した場合には以下のように1行のみ出力されます。
#include <unistd.h>
int main() {
while(1) {
write(STDOUT_FILENO, "y\n", 2);
}
return 0;
}
$ cc y.c -o y
$ ./y | head -1
y
$ echo "${PIPESTATUS[@]}"
ここで無限ループにしているにもかかわらず、プログラムが終了しているのは、SIGPIPE
シグナルが発生したときにデフォルトでプロセスが停止するようになっているためです。
signal(7)
を確認すると以下の記述があることから確認できます(これは環境によって異なる可能性があります)
Signal Standard Action Comment
────────────────────────────────────────────────────────────────────────
...
SIGKILL P1990 Term Kill signal
SIGLOST - Term File lock lost (unused)
SIGPIPE P1990 Term Broken pipe: write to pipe with no
readers; see pipe(7)
SIGPOLL P2001 Term Pollable event (Sys V);
synonym for SIGIO
SIGPROF P2001 Term Profiling timer expired
...
また $PIPESTATUS
を確認すると、 y
のプロセスは 141
となっています。
ステータスが 128
以上の場合は、 128
を引くことで原因となったシグナルが取得できます。
パイプしたときの終了ステータスの取得方法 が参考になりました。
141-128=13
となります。
シグナル番号を signal(7)
で確認するとやはり SIGPIPE
で終了していることが確認できます。
Signal x86/ARM Alpha/ MIPS PARISC Notes
most others SPARC
─────────────────────────────────────────────────────────────────
...
SIGKILL 9 9 9 9
SIGUSR1 10 30 16 16
SIGSEGV 11 11 11 11
SIGUSR2 12 31 17 17
SIGPIPE 13 13 13 13
SIGALRM 14 14 14 14
SIGTERM 15 15 15 15
...
また、 write(2)
の EPIPE
の記述を確認すると以下のように記されています。
EPIPE fd is connected to a pipe or socket whose reading end is closed. When this
happens the writing process will also receive a SIGPIPE signal. (Thus, the
write return value is seen only if the program catches, blocks or ignores
this signal.)
つまり、指定された fd
からデータを読んでいるパイプやソケットがすでにcloseされている場合に EPIPE
は発生します。 これが起きるときプロセスは SIGPIPE
シグナルを受け取ります。 そのため、write
の戻り値を確認するためには、対象のシグナルを無視するかブロックする必要がある、と書いてあります。
試しにSIGPIPE
を無視するプログラムを書いて確かめてみます。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
int main() {
signal(SIGPIPE, SIG_IGN);
while(1) {
ssize_t nwrites = write(STDOUT_FILENO, "y\n", 2);
if (nwrites < 0) {
fprintf(stderr, "%d: %s\n", errno, strerror(errno));
return 141;
}
}
return 0;
}
$ cc y.c -o y
$ ./y | head -1
y
32: Broken pipe
$ errno -l | grep EPIPE
EPIPE 32 Broken pipe
SIGPIPE
シグナルを無視するようにしたことで、 write(2)
の返り値を確認できるようになり、errno
に EPIPE
が設定されていることが確認できます。
Rustでは
ここでRustのケースに戻ると、Broken pipe
によってパニックしていることから SIGPIPE
を無視するようになっていることが想像できます。
そこで strace
を使って Rustで書いたプログラムでのシステムコールをトレースすると、以下のようになります。
$ strace -o /tmp/simple-yes.trace ./simple-yes | head -1 > /dev/null
$ grep SIGPIPE /tmp/simple-yes.trace
rt_sigaction(SIGPIPE, {sa_handler=SIG_IGN, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x25d991}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=175199, si_uid=1000} ---
やはり sa_handler
に SIG_IGN
が指定されています。
つまり、Rustでは SIGPIPE
に対して、明示的に SIG_IGN
が指定されることによって EPIPE
エラーが発生し println!
マクロでunwrap()しているためにパニックしていることがわかります。
GitHub を探してみると、 Should Rust still ignore SIGPIPE by default というissueが建てられていました。
対策
対策としては大まかに3つの方法があります。
1つめ
println!
マクロを使わずに 書き込み時のエラーハンドリングを自分でやる
match writeln!(&mut stdout, "y") {
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => std::process:exit(141),
Err(err) => {
eprintln!("{:?}", err);
}
}
ここで注意が必要なのが、"${PIPESTATUS}" が同じ挙動になるようにするためには、 ステータスコードを 141
で終了する必要があります。 ただ、Rustで作られた CLIアプリケーションで普通にステータスコードを 0
で終了しているものもあるため、どの程度こだわるかは場合によると思います。
2つ目
#[unix_sigpipe = "..."]
を使用する(ただしnightly使用)
https://github.com/rust-lang/rust/issues/97889
この issue にあるように、 main()
に unix_sigpipe
アトリビュートを使用して、 sig_dfl
を指定することで signalを無視するのを抑制できるようです。
#![feature(unix_sigpipe)]
#[unix_sigpipe = "sig_dfl"]
fn main() {
loop {
println!("y");
}
}
3つ目
libcクレートと unsafe
を使って自分で signal
を設定する。
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
signal(2)
は本来非推奨で sigaction(2)
を使うべきですが簡単のために signal(2)
を使用しています。
まとめ
Rustでは SIGPIPE
を無視していることで、書き込み時に BrokenPipe
エラーが発生して println!
マクロで unwrap()
されていることで パニックしていることを確認しました。
SIGPIPE
をデフォルトの挙動に戻すか、 BrokenPipe
エラーをハンドリングすることでパニックを起こさないようにできることを確認しました。
最近 勉強のために 毎日(?) システムコール でシステムコールについて勉強し始めたことでRustでのBrokenPipeのエラーがなぜ起きているのか説明できたのでこれからも続けていきたい。