前回の記事へのご指摘
前回の記事を執筆したところ、ご指摘を頂けたのですが、
私の理解が追いついていないところがありました。それが以下です。
a. どこまで行っても 1 は標準出力と同義 ( 同様に 2 は標準エラーと同義 )
b. 子は親のファイルディスクリプタを引き継ぐ
c. リダイレクトを指定すると、親のものを引き継がずに切り替わりが起こる
d. 子は親の標準出力と同じである必要はない
では、順にみていきます。
a.のご指摘の調査
ファイルディスクリプタ0は標準入力、1を標準出力、2を標準エラー出力と呼ばれます。
その関係性が崩れることはありません。そのことは、Linux上のファイルを見る事でも窺い知れます。
# ls -l /dev/std*
lrwxrwxrwx. 1 root root 15 11月 6 11:32 /dev/stderr -> /proc/self/fd/2
lrwxrwxrwx. 1 root root 15 11月 6 11:32 /dev/stdin -> /proc/self/fd/0
lrwxrwxrwx. 1 root root 15 11月 6 11:32 /dev/stdout -> /proc/self/fd/1
この通り、stdinはfd0、stdoutはfd1、stderrはfd2を指し示しています。
では、fdの0から2は何を指しているのでしょうか。
# ls -l /proc/self/fd/{0..2}
lrwx------. 1 root root 64 3月 2 22:30 /proc/self/fd/0 -> /dev/pts/1
lrwx------. 1 root root 64 3月 2 22:30 /proc/self/fd/1 -> /dev/pts/1
lrwx------. 1 root root 64 3月 2 22:30 /proc/self/fd/2 -> /dev/pts/1
/dev/pts/1ですので、疑似端末(sshした時などの接続先端末)となります。
そして、このfdの向き先を最終的に変えるのがリダイレクトとなります。
私は1の先に標準出力があるものだと思っていたのですが、
正しくは1こそが標準出力と呼ばれるもの、そのものだったということですね。
以上より、
a. どこまで行っても 1 は標準出力と同義 ( 同様に 2 は標準エラーと同義 )
については、ご指摘の通りとなります。
b.のご指摘の調査
b. 子は親のファイルディスクリプタを引き継ぐ
これはforkを調べたら分かりました。
The child inherits copies of the parent's set of open file descriptors.
ただ、そのものを引き継ぐわけではなく、コピーを引き継ぐようです。
つまり、プロセス毎にファイルディスクリプタ自体は別ですが、指し示す先は同じです。
c.のご指摘の調査
c. リダイレクトを指定すると、親のものを引き継がずに切り替わりが起こる
これについては、straceを使って、どのような処理が
リダイレクト時に内部的に行われているのかを見て判断します。
では、前回と同じコマンド(正しいほう)でトレースしてみます。
strace -o redirect.txt -ff bash -c "ls dada mama 1>/dev/null 2>&1"
一応straceのオプションについて説明します。
-oはトレース結果をファイル出力します。
-ffは、子プロセスまでトレースし、プロセス毎にファイルを分けて出力するオプションです。
尚、bashにコマンドをいったん嚙ませないと、うまくリダイレクト結果が取れませんでした。
見ていきます。
# ls
dada redirect.txt.817 redirect.txt.818
# grep "/dev/null" redirect.txt.817
execve("/bin/bash", ["bash", "-c", "ls dada mama 1>/dev/null 2>&1"], [/* 18 v
ars */]) = 0
なぜかプロセスが二つ生成されてました。
どうやら、817(親プロセス側)ではリダイレクトらしき事は行われていないようです。
これは、リダイレクトを親プロセス側で実施してしまうと、その後の親プロセスの処理も
リダイレクトの影響を意図せず受けてしまうからかな、と思います。
では、818側を見てみます。
# grep "/dev/null" redirect.txt.818
open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
/dev/nullがopenされています。
openはファイルを開き、オープンしたファイルの
ファイルディスクリプタを返すシステムコールです。
今回は、/dev/nullが3でopenされているようです。
周辺のログを見てみましょう。
# grep -A 6 "/dev/null" redirect.txt.818
open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
dup2(3, 1) = 1
close(3) = 0
dup2(1, 2) = 2
fcntl(1, F_GETFD) = 0
execve("/bin/ls", ["ls", "dada", "mama"], [/* 17 vars */]) = 0
brk(0) = 0x1014000
リダイレクトを行っている部分が見えましたね。
リダイレクトの正体はdup2です。
dup2は、二つ目の引数のファイルディスクリプタを
一つ目の引数のファイルディスクリプタの複製として作る、
というシステムコールです。
一連の流れをまとめると、
- プロセスをcloneする(子プロセスを作る)
- /dev/nullをファイルディスクリプタ3で開く
- 1を3(/dev/null)の複製として作成
- 3をクローズ
- 2を1(/dev/null)の複製として作成
- 2にエラーメッセージを、1にls結果を出力(いずれも/dev/nullに対して出力)
ということをしています。
従いまして、リダイレクトした際、親のものを引き継がずに切り替わりが起こるわけではなく、
親のファイルディスクリプタを引き継いだ上で、dup2によってファイルディスクリプタの
複製を行い、指し示す先を切り替えています。
ついでなので、だめなほう(2>&1 1>/dev/null)の動きもみてみます。
# strace -o redirect2.txt -ff bash -c "ls dada mama 2>&1 1>
/dev/null"
ls: mama にアクセスできません: そのようなファイルやディレクトリはありません
# ls
dada redirect2.txt.3820 redirect2.txt.3821
やはり、プロセスは二つのようです。中見ます。
dup2(1, 2) = 2
fcntl(1, F_GETFD) = 0
open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
dup2(3, 1) = 1
close(3) = 0
execve("/bin/ls", ["ls", "dada", "mama"], [/* 17 vars */]) = 0
brk(0) = 0xa8e000
先ほどと逆になっただけですね。
- プロセスをcloneする(子プロセスを作る)
- 2を1の複製として作成(先は一緒なので意味なし)
- /dev/nullをファイルディスクリプタ3で開く
- 1を3(/dev/null)の複製として作成
- 3をクローズ
- 2にエラーメッセージを、1(/dev/null)にls結果を出力
となります。
d.のご指摘の調査
d. 子は親の標準出力と同じである必要はない
cの検証の通り、切り替わった状態で子供が生まれるわけでないので、
恐らくこれ以外の言い回しになると思います。
〆
自分の無知さを痛感しました。
それなりに時間をかけて調べたつもりだったのに、まだまだでしたね…。
angel_p_57さん、本当にありがとうございました。
取りあえず、前記事は黒歴史として残しておいて、別途ちゃんとしたやつを書いて、
そちらに誘導するようにします。