【図解】ファイルディスクリプターと標準入出力、パイプやリダイレクトの仕組み

ファイルディスクリプターとは

全てのプロセスは『ファイルディスクリプター』というファイル書き込み用の通信チャネルを持っています。

ファイルディスクリプターは番号で書き込み先ファイルを識別しており、必要なタイミングで必要な分だけ生成されますが、その中でも以下の3つはプロセス生成時に必ず作られれます。

fd:0 = 標準入力
fd:1 = 標準出力
fd:2 = 標準エラー出力

0 だけが特殊で、プロセスへの入力を行うための標準的な入力チャネルです。

1 以降はプロセスが出力するためのチャネルですが、中でも 1 は標準的な出力チャネル、2 は標準的なエラー出力チャネルです。3以降は汎用としてプログラムが任意に利用できます。

なお、ここで言うファイルというのは「一般ファイル」に限らず、キーボード入力やモニターといったキャラクタデバイス等の「スペシャルファイル」も含まれます。

具体的に見てみましょう。

ファイルディスクリプターは概念なのでその実体を捉えることはできませんが、以下コマンドで番号と出力先を確認することができます。

# ls -l /proc/[プロセスのpid]/fd/

例えば他に誰もログインしていない状態で ssh 接続すると、bash プロセスが生成されると同時に、pts/0 という擬似端末が与えられます。

ssh接続を行ったコンソールは /dev/pts/0 というキャラクタデバイスとリンクしています。

[root@localhost ~]# ps aux | grep bash
root     15223  0.0  0.2 115440  2032 pts/0    Ss   17:48   0:00 -bash
root     15242  0.0  0.0 112724   980 pts/0    R+   17:48   0:00 grep --color=auto bash
[root@localhost ~]# ls -l | /proc/15223/fd/
合計 0
lrwx------. 1 root root 64  2月 28 17:48 0 -> /dev/pts/0
lrwx------. 1 root root 64  2月 28 17:48 1 -> /dev/pts/0
lrwx------. 1 root root 64  2月 28 17:48 2 -> /dev/pts/0
lrwx------. 1 root root 64  2月 28 17:48 255 -> /dev/pts/0

fd ディレクトリの配下にファイルディスクリプタの番号のファイルがシンボリックリンクとして存在しており、リンク先が出力先のファイルになっています。

この出力結果より、この bash プロセスが標準入力,標準出力,標準エラー3つとも /dev/pts/0 に向いていることが分かります。

なのでコンソールから入力したコマンドは bash に渡されるし、その結果はコンソールに戻ってきます。

# echo test
test

もう少し別のものも見てみましょう。例えば rsyslogd のプロセス。

[root@localhost ~]# ps aux | grep rsyslog
root      3195  0.0  0.4 214560  4352 ?        Ssl   2月27   0:06 /usr/sbin/rsyslogd -n
root     15245  0.0  0.0 112724   984 pts/0    R+   17:49   0:00 grep --color=auto rsyslog
[root@localhost ~]# ls -l /proc/3195/fd/
合計 0
lr-x------. 1 root root 64  2月 27 13:18 0 -> /dev/null
l-wx------. 1 root root 64  2月 27 13:18 1 -> /dev/null
l-wx------. 1 root root 64  2月 27 13:18 2 -> /dev/null
lr-x------. 1 root root 64  2月 27 13:18 3 -> anon_inode:inotify
lrwx------. 1 root root 64  2月 27 13:18 4 -> socket:[24126]
lr-x------. 1 root root 64  2月 27 13:18 5 -> /run/log/journal/ea94430614514eb692303cbd86320983/system.journal
l-wx------. 1 root root 64  2月 27 13:18 6 -> /var/log/messages
l-wx------. 1 root root 64  2月 27 13:18 7 -> /var/log/cron
l-wx------. 1 root root 64  2月 27 13:18 8 -> /var/log/secure
l-wx------. 1 root root 64  2月 27 13:18 9 -> /var/log/maillog

標準入出力やエラー出力は /dev/null となっています。つまり標準入出力やエラー出力は使えないということです。

代わりに journald からの入力を fd:5 で受けており、fd:6 から fd:9 まででログファイルに出力していることが分かります。

ファイルディスクリプタの継承

先ほどの例で bash から echo を呼び出しましたが、実は正確に言うと、(bash プロセスの標準出力ではなく) echo コマンドのプロセスの標準出力が /dev/pts/0 になっているため、結果がコンソールに戻ってきているのです。

bash からコマンドを実行すると、(fork という子プロセスを生成するシステムコールが呼び出され、) bash の子プロセスとして実行されます。

fork により生成された子プロセスはファイルディスクリプタを継承するため、このような結果になります。

パイプの仕組み

パイプとは、あるプロセスの標準出力を、別のプロセスの標準入力と繋げることです。

例えば ファイル test.txt というファイルから secret という文字列が存在する行だけを抽出したい場合は以下のように打ちます。

# cat test.txt | grep secret

このときの状態を図示すると以下のようになります。

リダイレクトの仕組み

リダイレクトとは、あるプロセスの標準出力を変更することです。

例えば

# echo test

とした場合は /dev/pts/0 等のコンソールに出力しますが、

# echo test > /root/hoge.txt

とした場合はコンソールには出力されず、代わりに /root/hoge.txt が生成され、中身が test となります。

# cat /root/hoge.txt
test

これを応用してみましょう。

もう1つ ssh 接続を増やし、pts/1 という擬似端末を作ってみます。これも同様に標準入出力を持っています。

# ls -l /proc/[pid]/fd/

このとき、1つ目のSSHセッションからリダイレクトを使って、2つ目のSSHセッションのコンソールに出力することもできます。

pts/0 のコンソール(下図の左側)から

echo test > /dev/pts/1

とすると、2つ目のSSHセッションのコンソール(下図の右側)に test と表示されます。

 ↓