プロセスとファイル入出力

さて、前回、プロセスというのは「自分が独占したメモリーの中で動いているので、その中で何をしても他のプロセスのメモリーに影響を与えない」というのを見れたかと思います。でも、そんな自分の中だけで完結してる引きこもりみたいなプロセスじゃあ、意味がないですね。外界からなんかデータをもらって、自分の中で処理して、それを外の世界に知らせる方法が必要になってきます。

そこで、プロセスに外から何かを入力したり、プロセスが外に何かを出力する方法として、「ファイルの入出力」というのがあります。たとえば、ファイルに書かれたデータをプロセスがメモリー上に読み込んでなんか処理をするとか、処理を行った結果をテキストファイルに書き込みをするとか。例を見てみましょう。

まず、以下のようなテキストファイルを nyan.txt という名前で適当な場所に作ってみます。

nyan
nyan nyan
nyan nyan nyan

では、このファイルをプロセスの中に読み込んでみましょう。今日は Ruby を使います。

file = File.open("nyan.txt","r")
lines = file.readlines #ファイルの中身を全部読み込む
file.close

ファイルを open して、その内容を lines という変数に読み込んで、最後にファイルを close しています。ファイルの中のデータはディスクに書かれたものであり、プロセスがもともとメモリー内に持っていたものではありません。このディスクに書かれた内容を

lines = file.readlines

の行でlines変数に読み込むことで、プロセスの「外界」の情報を、プロセスの内部のメモリーに読み込んでいますね。

では今度は出力をしてみましょう。

# nyan_copy.rb
file = File.open("nyan.txt","r")
lines = file.readlines
file.close

file = File.open("nyan_copy.txt","w")
file.write(lines.join)
file.close

nyan_copy.rbを、nyan.txtと同じディレクトリに作って、実行してみましょう。nyan.txtと同じ内容の、nyan_copy.txtというファイルが生まれたかと思います。さきほどディスクから読み込んでメモリー上に展開したデータを、そのまま別のファイルに対して出力したためですね。

こうして、プロセスはファイルを通じて外部との入出力を行うことができます。

すべてがファイル???

さて、いまは「テキストファイル」への読み書きを行ってみましたが、「Linuxではすべてがファイルなんだよ」みたいな話を聞いたことがないでしょうか? そんなこと言われても、「はっ?」って感じの話ですよね。「Linuxではキーボードもファイルだからね」みたいなことを言うひとに至っては「こいつ頭大丈夫か、キーボードはキーボードだろうが」みたいな気持ちになりますよね。わたしは最初にこの話を聞いたときに「なにそれ、禅問答?哲学?頭大丈夫?ファイルはファイルだしキーボードはキーボードだろ」って思いました。

「全てがファイル」とか言われると「世の中のすべてはファイルなのだ、そう、きみも、わたしも」みたいな禅問答をやられてるみたいな気持ちになるので、こういう言い方はあまりよくない感じがしますね。だったら、こんなふうに言われたらどうでしょうか? 「Linuxは、すべての入出力がファイルと同じ感じで扱えるような設計になっているんだよ」。つまり、プロセスが「ここでターミナルからの入力を受け取りたいんだけど」とか、「ネットワーク越しに入力もらってネットワーク越しに出力したいんだけど」みたいなことを言うと、OSさんが「はいよ、実際はHD(さいきんだとSSDかな)上のファイルじゃないんだけど、いつもファイルを通じてディスクを読み書きするのと同じやり方で扱えるように用意しといたよ!」みたいな感じでそのためのインターフェイスを用意してくれてるのです。

例:標準入出力

さて、例を見てみましょうか。

# stdout.rb
file = File.open("nyan.txt","r")
lines = file.readlines
file.close

file = $stdout # この行だけ書き換えた
file.write(lines.join)
file.close

nyan.txt と同じディレクトリに、今度は stdout.rb を作って、実行してみましょう。nyan.txtの内容が、ターミナルに出力されたかと思います。

rubyの組み込みグローバル変数 $stdout には、「標準出力」と言われるものが、すでにFile.openされた状態で入っています。この「標準出力」の出力先は、デフォルトではターミナルをさします。そのため、さっきテキストファイルに内容を出力したのと同じやりかたで、ターミナルに対して出力ができるわけです。

標準出力があるなら標準入力もあるの?当然あります。 rubyだと標準入力はFile.openされた状態で $stdin というグローバル変数に入っています。標準入力のデフォルトの入力ソースはターミナルになります。例を見ましょう。

# stdin.rb
file = $stdin
lines = file.readlines #標準入力からの入力を全部受け取る
file.close

file = $stdout
file.write(lines.join) # 標準出力に対して内容を書き出す
file.close

上記のような stdin.rb というファイルを作成して、実行してみましょう。何も出力されず、かつプロンプトも返ってこない状態になると思います。これはなぜかと言うと、

lines = file.readlines #標準入力からの入力を全部受け取る

の行で、プロセスが「ブロック中」になっているからです。前回の内容を思い出してください。プロセスの実行中の状態のうちのひとつに、「ブロック中」があったと思いますが、ブロック中というのは、「IOとかを待ってて今は処理できないよ」という状態でしたね。

この行では、標準入力からの入力を「全部」読み込もうとしています。そして、標準入力のデフォルトはターミナルからの読み込みを行います。しかし、すでに何が書かれているか決まっているdisk上のファイルと違って、ターミナルへの入力は「終わり」がいつ来るものなのかわかりません。だから、このプロセスは「終わり」が入力されるまで、ずっとずっと「ブロック中」の状態で待ち続けているのです。けなげですね。

では、ひとまず以下のような感じで、プロンプトが戻ってきてないターミナルに何かを打ち込んでみてください。

 $ ruby stdin.rb #さっき実行したコマンド
 aaaa
 bbbbbb
 ccc

打ち込みましたか?そうしたら、改行したあと、おもむろにCtrlキーを押しながらDを押してみましょう。すると、ターミナルに、あたらしく

aaaa
bbbbbb
ccc

と、さっき自分で入力したのと同じ内容が出力されるはずです。

Ctrl+D を押すと、EOFというものが入力されます。この「EOF」というのは「End Of File」の略で、「ここでこのファイルはおしまいだよ」というのを伝える制御文字です。プロセスは、この「EOF」を受け取ることで、「よし、標準入力を全部読み込んだぞ」と理解して、IO待ちのブロック状態から抜けるわけですね。

ところで、最初の例では標準入力ではなくてnyan.txtを読み込んでいましたが、実はその間にも、一瞬プロセスは「ブロック中」状態になっています。ディスクからデータを読みこんでくるのが一瞬なので、普段はあまり意識しないかもしれませんが(とはいえ、コンピューターの処理の中ではdiskIOというのはかなり遅い処理の部類です。だから、パフォーマンスが必要になってくるようなソフトウェアを書くときには、なるべくIOをしないことでブロックされないようにしてパフォーマンスを稼ぐみたいな手法が取られたりするわけです)。

こんな感じで、「実際はdisk上のファイルじゃないもの」も、「disk上のファイルとおなじように」扱える。そういう仕組みがLinuxには備わっています。今はそれが「すべてがファイル」の意味だと思ってください。

ちなみに、標準入力/出力の他にも、「標準エラー出力」というのがあり、これもデフォルトの出力先はターミナルになっています。

余談ですが、IO#readlinesは「ファイルの内容を全部読み込む」という挙動をしますが、では一行だけ読み込む IO#readline を使うとどういう挙動をするかなど、自分で確かめてみると、「あっブロックしてる」「あっ今読み込んでブロック中じゃなくなった」みたいなのがわかっておもしろいかもしれません。

じゃあデフォルトじゃないのはなんなんだよ

先ほどから標準入出力のデフォルトはどうこうみたいな話をしていますが、それはつまり標準入出力はその他の場所にもできるってことですね。そのための機能が「リダイレクト」と「パイプ」です。

リダイレクト

リダイレクトを使うと、標準入出力に別のファイルを指定することができます。ちなみに、シェル上(sh,bash,zshを想定)では、標準入力は「0」という数字、標準出力は「1」という数字、標準エラー出力は「2」という数字で表されます(なんでこんな謎っぽい数字使ってるのかは後で説明します)。出力系のリダイレクトは ">" という記号、あるいは">>"という記号で行えます。">"の場合、指定されたファイルがすでに存在する場合はそれを上書きします。">>"の場合、指定されたファイルがすでに存在する場合はファイルの末尾に追記します。

標準出力のリダイレクト

例えば、

# print_mew.rb
puts "mew" # putsは標準出力に対して引数を出力する

というrubyスクリプトがあるとき、

$ ruby print_mew.rb 1>mew.txt

とすると、mew とだけ書かれた mew.txt というファイルができあがります。"1>mew.txt"が、「標準出力(1)の出力先はmew.txtだよ」を意味するわけですね。その上で

$ ruby print_mew.rb 1>>mew.txt

とすると、 mew.txt にさらに mew が追記され、mew.txt の中身は mew(改行)mew というものになります。"1>>mew.txt"が、「標準出力の出力先はmew.txtだよ。ファイルが存在してたら末尾に追記してね」を意味するわけです。さらにもう一度

$ ruby print_mew.rb 1>mew.txt

とすると、mew.txtは上書きされてしまい、「mew」とだけ書かれたファイルになります。

ちなみに、標準出力をリダイレクトする際は、「1」を省略した書き方も可能です。

$ ruby print_mew.rb > mew.txt

標準入力のリダイレクト

当然、標準入力もリダイレクトすることが可能です。そのためには、"<"という記号を使います。

試しに、さっき作った mew.txt というファイルを標準入力としてみましょう。

$ ruby stdin.rb 0<mew.txt
mew

"0<mew.txt"が、「mew.txtを標準入力(0)の入力ソースとするよ」を意味しているわけですね。mew.txtの内容がstdin.rbによって読み込まれ、ターミナルに書き出されたかと思います。

これも、0を省略した書き方が可能です。

$ ruby stdin.rb < mew.txt
mew

当然ながら、複数のリダイレクトを同時に行うことも可能です

$ ruby stdin.rb 0<mew.txt 1>mew_copy.txt

上記の場合、stdin.rbの標準入力はmew.txtとなり、標準出力は mew_copy.txt となります。

stdin.rbの内容は標準入力を読み込んで標準出力にそのまま書き出すものなので、mew_copy.txtという新しいファイルに、mew.txtの内容、つまり「mew」 が書き込まれることになります。

標準エラー出力のリダイレクト

標準入出力について見てみたので、標準エラー出力についても見てみましょう。

# stdout_stderr.rb
puts "this is stdout"
warn "this is stderr" # warnは標準エラー出力に引数を出力する

普通にstdout_stderr.rbを実行すると、標準出力も標準エラー出力もターミナルに向いているので、どちらもターミナルに出力されます。

では、以下のようにしてみましょう。

$ ruby stdout_stderr.rb 1>out.txt 2>err.txt

"1>out.txt" で「標準出力(1)をout.txt」に、"2>err.txt" で「標準エラー出力(2)をerr.txt」に向けています。

すると、out.txtには "this is stdout"が、err.txt には"this is stderr"が書き出されているかと思います。

ちなみに、"2>&1"みたいにして標準エラー出力を標準出力へ向けることもできます。

$ ruby stdout_stderr.rb 1>out.txt 2>&1

&を付けることによって、「この1ってのは、1っていう名前のファイルじゃなくて標準出力を表す数字だよ!」ってことを言っているわけですね。さあ、またまた新しい疑問がわいてきました。なんで&付けるとそれがファイル名じゃなくて標準出力ってことになるの? そもそもなんで0とか1とか2とかって謎っぽい数字使ってるの? 疲れてきたので、そのあたりは次回にまわします。

リダイレクトの順序

$ ruby stdout_stderr.rb 1>out.txt 2>&1

とすると、プロセス内で標準出力に書き出したものも標準エラー出力に書き出したものも out.txt に出力されます。しかし、

$ ruby stdout_stderr.rb 2>&1 1>out.txt

とすると、標準エラー出力に対する出力は、依然としてコンソールに出力されてしまいます。

このような動きをするのはなぜでしょうか?その説明をするためには、次回説明する「ファイルディスクリプタ」というものを知る必要があります。これも次回説明しますので、今は「そういうもんなんだな」と思っておいてください。

パイプ

パイプについても簡単にみておきましょう。シェル上では、パイプは「|」という記号で実現されます。

$ command_a | command_b

とすると、command_aの標準出力に出力された内容がcommand_bの標準入力に入力されます。この時、command_aの出力が全部終わってなくても(EOFに達しなくても)、command_bのプロセスは「来たデータから順々に」処理していきます。データがcommand_aから出力されたら、すぐにcommand_bはそのデータを処理します。まだEOFが来てないけどcommand_aからの出力が来ないぞ、というときにはcommand_bはどうするでしょうか。そうですね、標準入力からのデータを読み込む部分で「ブロック中」になって、command_aが標準出力になにかを吐くのを待ち続けるわけです。けなげですね。ちなみに、このように入力と出力をパイプでつないで、「ファイルの終わりを待たずにきたデータから順々に」なにか処理をするのを、パイプライン処理、とか、ストリーム処理、と言います。

また、パイプはシェル上でふたつのプロセスの標準入出力をつなぐだけではなく、プロセス上でも新しい入出力のペアを作ることができます。RubyだったらIO.pipeを使うと実現できるでしょう。Perlだったらpipe関数ですね。詳しくはrubyの公式リファレンスやperldoc,pipe(2)を参照してください。

次回予告

次回はファイルの入出力について、もっと深くまで潜っていきますよ!ファイルディスクリプタの話をして、ソケットの話をします。そのあとようやくファイルディスクリプタとforkの話ができたらいいな!さーて、次回も、サービス!サービスゥ!