背景
$ コマンド > outfile 2>&1
と
$ コマンド 2>&1 > outfile
は結果が異なります。
前者は標準出力、標準エラーともにoutfileに出力されるのに対し、後者は標準出力はoutfileに、標準エラーはターミナルに出力されます。
この違いを理解するためにUnixのシステムを深掘ってみます。
カーネルデータ構造
Unixシステムでファイルをオープンした場合は以下のようなデータ構造になります。
ref: https://www.usna.edu/Users/cs/aviv/classes/ic221/s16/lec/21/lec.html
- プロセステーブルエントリ
- ファイルテーブルエントリ
- v-nodeテーブルエントリ
の3つの階層構造になっており、それぞれ説明します。
プロセステーブルエントリ
各プロセスはプロセステーブルに1つエントリを持ちます。プロセステーブルエントリにはオープンしているfdの表があり、以下を含みます。
- fdフラグ
- ファイルテーブルエントリへのポインタ
ファイルテーブルエントリ
カーネルはオープンしているすべてのファイルについてファイルテーブルを管理しています。
ファイルテーブルエントリは以下を含みます。
- 読み取り、書き出し、追加書き、同期、非ブロックなどのファイルステータスフラグ
- カレントファイルオフセット
- ファイルのv-nodeテーブルエントリへのポインタ
v-nodeテーブルエントリとi-node
v-nodeは複数のファイルシステムを扱えるようにしたための抽象化メカニズムです。i-nodeへのポインタを含みます。
i-nodeはファイルに関する属性を持ち、実際のデータブロックへのポインタを持ちます。
ref: Section 4.14. File Systems
stdoutとstderrはどうなっているか
一般的にプロセスのfd 1
はstdout用のファイルテーブルエントリと、fd 2
はstderr用のファイルテーブルエントリと紐付いています。
そして実際に紐づくファイルはtty(ターミナル)になっています。
ファイルテーブルエントリにはオフセットの概念があり、そこをベースにread()/write()するのでstdout用のファイルテーブルエントリとstderr用のファイルテーブルエントリはどのプロセスも共有していると考えられます。
つまり左の構造ではなく、右の構造ですね。
リダイレクトをするとどういうことが起きるのか
まずは
2>&1
> outfile
とそれぞれがどういう挙動になるかを説明します。
2>&1パターン
2>&1
のようにfd同士を扱う場合はdup2(1, 2)
がコールされます。dup2()はfdの複製を作るシステムコールですが、中身は同じファイルテーブルエントリを参照するfdを生成する仕組みです。
dup2(oldfd, newfd)はnewfdが開いている場合はまずクローズし、その後oldfdを複製します。なのでfd 2
は一度閉じられます。
strace
システムコールを追うとこうです。
$ strace sh -c "echo foo 2>&1" ~~省略~~ fcntl(2, F_DUPFD, 10) = 10 close(2) = 0 fcntl(10, F_SETFD, FD_CLOEXEC) = 0 dup2(1, 2) = 2 write(1, "foo\n", 4foo ) = 4 dup2(10, 2) = 2 close(10) = 0
標準エラー出力のttyへ繋がってるファイルテーブルエントリの参照が途切れないようにfd 10
に退避してますね。
> outfileパターン
> outfile
の場合は、新しくそのファイルを開くのでopen()
が呼ばれて新しいファイルテーブルエントリが用意されます。
そしてfd 1
の向き先をそのファイルテーブルエントリに変更します。
strace
$ strace sh -c 'echo foo > foo.txt' openat(AT_FDCWD, "foo.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3 fcntl(1, F_DUPFD, 10) = 10 close(1) = 0 fcntl(10, F_SETFD, FD_CLOEXEC) = 0 dup2(3, 1) = 1 close(3) = 0 write(1, "foo\n", 4) = 4 dup2(10, 1) = 1 close(10) = 0
こちらも標準出力のttyへ繋がってるファイルテーブルエントリの参照が途切れないようにfd 10
に退避してますね。
一緒に使うとどうなるか
それでは組み合わせた場合どうなるかを考えます。
ポイントはシェルは左から右に処理を実行する点です。
コマンド > outfile 2>&1
fd 1
もfd 2
もそれぞれターミナルに紐づくファイルテーブルエントリを参照している> outfile
はoutfileのファイルテーブルエントリを作成し、fd 1
をそのファイルテーブルエントリに向ける2>&1
はfd 2
がfd 1
の向いているファイルテーブルエントリ(outfile)を参照するようにする
よってfd 1
もfd 2
もoutfileへ出力されます。
コマンド 2>&1 > outfile
fd 1
もfd 2
もそれぞれターミナルに紐づくファイルテーブルエントリを参照している2>&1
はfd 2
がfd 1
の向いているファイルテーブルエントリ(ターミナル)を参照するようにする> outfile
はoutfileのファイルテーブルエントリを作成し、fd 1
をそのファイルテーブルエントリに向ける
よってfd 1
はoutfileへ、fd 2
はターミナルへ出力されます。
まとめ
カーネルのデータ構造を学ぶことでリダイレクトの仕組みをきちんと理解することができました。