Rustで Broken Pipeが起きる原因と対策

Rustで以下のようなプログラムを書き、`head -1` など出力を途中で切るプログラムと組み合わせると `Broken Pipe` エラーによってパニックしてしまいます。

```rust:simple-yes.rs
fn main() {
    loop {
        println!("y");
    }
}
```

```bash
$ ./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行のみ出力されます。

```c:y.c
#include <unistd.h>

int main() {
    while(1) {
        write(STDOUT_FILENO, "y\n", 2);
    }
    return 0;
}
```

```bash
$ 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`を引くことで原因となったシグナルが取得できます。 
[パイプしたときの終了ステータスの取得方法](https://qiita.com/zackey2/items/1221d44ab4ced84dad1c) が参考になりました。

`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`を無視するプログラムを書いて確かめてみます。

```c
#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;
}
```

```bash
$ 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で書いたプログラムでのシステムコールをトレースすると、以下のようになります。

```bash
$ 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](https://github.com/rust-lang/rust/issues/62569) というissueが建てられていました。

# 対策

対策としては大まかに3つの方法があります。

## 1つめ

`println!` マクロを使わずに 書き込み時のエラーハンドリングを自分でやる


```rust
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を無視するのを抑制できるようです。

```rust:コード例
#![feature(unix_sigpipe)]

#[unix_sigpipe = "sig_dfl"]
fn main() {
    loop {
        println!("y");
    }
}
```

## 3つ目

libcクレートと `unsafe` を使って自分で `signal` を設定する。

```rust:コード例
unsafe {
    libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
```

`signal(2)` は本来非推奨で `sigaction(2)` を使うべきですが簡単のために `signal(2)` を使用しています。


# まとめ

Rustでは `SIGPIPE` を無視していることで、書き込み時に `BrokenPipe` エラーが発生して `println!` マクロで `unwrap()` されていることで パニックしていることを確認しました。

`SIGPIPE` をデフォルトの挙動に戻すか、 `BrokenPipe` エラーをハンドリングすることでパニックを起こさないようにできることを確認しました。

最近 勉強のために [毎日(?) システムコール](https://zenn.dev/imishinist/scraps/dd7ff22b5894e8) でシステムコールについて勉強し始めたことでRustでのBrokenPipeのエラーがなぜ起きているのか説明できたのでこれからも続けていきたい。