Skip to content

Instantly share code, notes, and snippets.

@Shinpeim
Last active November 2, 2024 00:52
Show Gist options
  • Select an option

  • Save Shinpeim/4736099 to your computer and use it in GitHub Desktop.

Select an option

Save Shinpeim/4736099 to your computer and use it in GitHub Desktop.
プロセスとかスレッドとかプロセス間通信とかについて書く場所

この文書の目的

この文書は*nix系のシステムにおけるプロセスやスレッド、シグナルについて説明することを目的に書かれています。「プロセスとかスレッドとかよくわかってないからちゃんと知りたいな」みたいなひとたちが想定読者です。あと、まとめることによって自分の知ってることの棚卸しをするという目的もあります。

目次(予定)

  • プロセス、スレッドと並列処理
  • プロセスについて
    • プロセスのライフサイクル
    • プロセスツリーについて
    • fork
    • exec
    • シグナル
    • パイプライン処理(標準入出力)
  • スレッドについて
    • プロセスとの違い
    • リソースの共有について

プロセス、スレッドと並列処理

マルチプロセスとスケジューリング

*nix系のシステムは、もともと複数のユーザーが同じコンピューターリソース(CPUやメモリー)を同時に使うことを前提に作られています。そのため、*nix系のシステムでは様々な処理が同時に行われるような仕組みになっています。実際、小規模なwebサービスでは nginx と unicorn と MySQL がひとつのマシンで同時に走っているような状況は珍しくないのではないでしょうか。

いまはカジュアルに「同時に複数の処理が走っている」と言ってしまいましたが、マシンが持っているCPU(脳みそ)は限られた数なのに、どうやって複数の処理を同時に行っているのでしょうか? ひとつの脳みそでどうやって複数のことを同時に考えているのでしょうか? その答えは、「本当は同時に処理は行われていなくて、OSが目にも留まらぬ早さで複数の処理を切り替えているせいであたかも同時に複数の処理が行われているように見える」です。図にするとこういう感じ。

A -----        ----                  ---------         ----------
B      ---         --------                   ----   --          -------
C         -----            ----------             ---                   --

OSは、上記のように処理A,B,Cを順々に切り替えながら少しずつ処理していきます。この切り替えのタイミングがめっちゃ早いため、人間にはまるで同時に処理されているかのように見えるわけです。この切り替えをする処理の単位が、プロセス(やスレッド)です。上図の場合だと、Aというプロセス、Bというプロセス、CというプロセスをOSがすごい早さで切り替えながら処理しているようなイメージですね。このように、プロセスやスレッドを上手に使うことで、同時に複数の計算が可能になるわけです。

ちなみに、この切り替えをどういう戦略やタイミングで行うかのことを、「スケジューリング」と言います。このへんはOSが面倒を見てくれますが、niceというコマンドで「これははあんまり重要じゃないプロセスなんで、優先度低めでいいよ」という情報をOSに教えたりできて、そうするとOSさんはそのプロセスを処理する時間を少なめにスケジューリングしたりします。

マルチコアとの関係

最近のマシンのCPUはコアが複数あるのが普通です。コアがひとつだけならば、ひとつのコアで全てのプロセスをスケジューリングする必要があるわけですが、コアが複数あるため、下記のような感じで今度は「ほんとうの」同時処理が可能になります。

+ ------ + A -----        ----                  ---------         ----------
| core 1 | B      ---         --------                   ----   --          -------
+ ------ + C         -----            ----------             ---                   --
+ ------ + D ---        ----                  ---------         ----------
| core 2 | E     ---         --------                   ----   --          -------
+ ------ + F        ----            ----------             ---                   --

大規模なデータを処理する場合などには、ひとつのコアだけではなく複数のコアを無駄なく使うためにも、複数のプロセスや複数のスレッドで処理を行う必要が出てくるわけです。

ただ、スレッドに関しては、OSが面倒を見てくれるスレッド(いわゆるネイティブスレッド)と、例えば言語処理系やVMが面倒見てくれるスレッド(いわゆるグリーンスレッド)があって、グリーンスレッドの中にはいくらスレッドを増やしてもコアをひとつしか使えないようなものもあります。CRubyの1.8などがその例ですね。スレッドと一口に言ってもどのような実装になっているかによって特徴が変わってくるので、自分が使っている環境の「スレッド」というのがどのような仕組みをさしているのかは意識しておく必要があるでしょう。

次回予告

次回はプロセスについてもう少し深くまでもぐって見ていきます。

プロセスの生成

プロセスの例

前回、プロセスとはOSが処理を切り替えるときの処理の単位だという話をしましたが、まずはプロセスの例を見てみましょう

ターミナルで、

$ ps

と入力してみるましょう。psは今実行中のプロセスの一覧を見ることができるコマンドです。オプションなしで実行すると自分が実行中のプロセスの一覧が見れます。で、psを実行してみると、(環境によって異なるかと思いますが)以下のような文字が出力されるかと思います。

  PID TTY          TIME CMD
 4400 pts/2    00:00:00 bash
 4419 pts/2    00:00:00 ps

一番右を見ると、(この場合は)bashというプロセスとpsというプロセスが実行されていることがわかります。bashはログインシェル、psはいまさっき打ったpsコマンドですね。ちなみに、一番左のPIDという列は、そのプロセスのidで、実行されているプロセスを一意に判別するために使われているものです。

では、今度は & つきでバックグラウンドでコマンドを実行してみましょう。

 $ perl -e 'while(1){sleep}' &

ただsleepし続けるだけのperlのワンライナーです。この状態で、もう一度

 $ ps

と入力してみると、

  PID TTY          TIME CMD
4420 pts/2    00:00:00 perl

のような、さっきは存在していなかったプロセスが新しく増えているのがわかると思います。これがさきほど実行した

 $ perl -e 'while(1){sleep}' &

コマンドのプロセスです。新しく処理を始めたら新しくプロセスが生成されたのがわかるかと思います。

さて、バックグラウンドで実行中のsleepするだけのプロセスですが、今度は

$ fg

でフォアグラウンドに処理を戻して、 Ctrl+C かなんかで処理を止めましょう。その後再度 ps コマンドでプロセスの一覧を確認すると、perlのプロセスが無くなっていることが確認できるかと思います。

プロセスのライフサイクル

プロセスは、なんらかの方法で生成されたあとは、ぐんぐん処理を行っていき、処理が終わったり外部から止められたりすると消滅します。

生成 -> 処理中 -> 終了

というライフサイクルを持っているわけです。今簡単に「処理中」と書いてしまいましたが、大きくわけてこの「処理中」には3つの状態があります。

  • 走行中
  • 待ち状態
  • ブロック中

「えっ待ち状態とブロック中ってなにが違うの」という疑問を持ったかた、ごもっともです。でも、その違いは簡単です。「待ち状態」というのは、「もうすぐにでも処理できるよ!CPUさん、はやくわたしを処理して!」という状態のことです。一方、「ブロック中」というのは、たとえばファイルの読み込みを行うときにdisk I/Oを待っているなどで、「今CPUさんが私を処理しようとしても私まだIO待ちだから何もできないよ!」みたいな状態のことです。

fork

さて、さきほど簡単に「プロセスをなんらかの方法で生成」と言いましたが、たとえば新しくコマンドを叩いて新しいプロセスが生成されるとき、中では何が起きてるのでしょうか?

通常、プロセスは、「親プロセス」がforkというシステムコールをOSに送ることによって生成されます。すると、OSは親プロセスをまるっと複製して、「子プロセス」を新しく生成します。このとき、メモリの状態は親プロセスから子プロセスにまるっとコピーされます1。コピーされて新しい環境が出来上がるため、親プロセスでなにか操作しても(変数に新しい値代入するとか新しくインスタンスを生成するとか)、その操作は子プロセスに影響を与えません。親でなんか変更したからといって、子にもその変更が伝わるみたいなことはないわけです。逆もまたしかりで、子プロセスでなにか操作しても、その変化は親プロセスに影響を与えません。

こうして、forkにより新しくプロセスが生まれると、OSによりそのプロセス専用の環境が用意されて、その中でいろんな処理が行えるようになるわけです。

こうしてforkによって、プロセスは生成されるため、基本的に全てのプロセスには「自分を生んだ親プロセス」が存在することになります。

ちなみに、forkは複数行うことができるので、「子だくさん」なプロセスというのも、あり得ます。preforkのサーバープロセスなんかは子供をたくさん作って、複数の接続のひとつひとつをそれぞれひとつの子供に処理させることで並列性を上げているわけですね。子供たちを酷使するひどいやつです。

プロセスツリー

さきほど「親プロセスがforkで子プロセス作るんだよ〜〜。だからみんな親がいるんだよ〜〜〜」ってゆるふわな感じで言いましたが、当然「えっじゃあ、その親プロセスは誰が作ったの?」という疑問がわいてきますよね。疑問にお答えしましょう。親プロセスは、「親プロセスの親プロセス」がforkで作ったのです。となると、当然「えっじゃあ、その『親プロセスの親プロセス』はだれが作ったの」いう疑問がわいてきますよね。もちろん、「親プロセスの親プロセスの親プロセス」がforkで作ったのです。となると当然(ry

というように、全てのプロセスはどんどんその「親」を辿って行くことができます。そんなわけで、全てのプロセスの祖先となる「最初のプロセス」というものが存在しないといけないわけです。このプロセスはブート時に生成されて、そのあと全てのプロセスがここを祖先としてforkされていきます。この「最初のプロセス」はPIDが1であり、Linuxの場合は init というプロセスがその実体となります。

$ ps ax | grep init
1 ?        Ss     0:10 /sbin/init

このように、プロセスは親子関係の木構造を持っています。この親子関係を「プロセスツリー」と呼びます。プロセスツリーがどうなっているかを調べるためにpstreeというコマンドが使えますので、興味があればpstreeコマンドでどのようなプロセスツリーが生成されているか見てみるのもよいかと思います。ptreeコマンドの使いかたはmanで調べてください(丸投げ)

exec

さて、「すべてのプロセスは祖先からforkされて生まれた」という話と「forkは親プロセスをまるっとコピーして子プロセスを作る」という話をしましたが、これ、なんかおかしいですね。そうです。このままでは、「親の複製のプロセス」しかなくって、すべてが同じことを行うプロセスになってしまいます!

そこで必要になるのが、execというシステムコールです。あるプロセスがexecというシステムコールを呼ぶと、OSはそのプロセスをexecの内容で書き換えてしまいます。つまり、execというのは、「自分自身の内容を別の内容で書き換えて実行してしまう」システムコールなんですね。くらくらしてきた!

まとめると、

  1. forkでプロセスを生成して、独立した環境を用意してあげる
  2. その環境に、execによって別の実行可能なものを読み込んで実行する

ことで、親プロセスとは違うプロセスをどんどん生成していくような仕組みになっているわけです。

「日本語だとよくわかんないよ、コードで書いてよ」という声がわたしの脳内から聞こえてきたので、コードで書きます。

use strict;
use warnings;

print "forking...\n";

# forkシステムコールを呼び出す
my $pid = fork;

# forkに失敗すると返り値はundef
die "fork failed." unless defined $pid;

# ここに来てるということは、正常にプロセスが複製された。
# この時点で親プロセスと子プロセスが *別々の環境で*
# 同時にこのプログラムを実行していることになる。
print "forked!\n";

# forkで生成された子プロセスでは、forkの返り値が 0 となる
# 親プロセスでは、生成された子プロセスのpidが入ってくる
if ($pid == 0){
    #子プロセスはこっちを実行する

    # execシステムコールで、perlのプロセスをrubyのプロセスに書き換えてしまう!
    exec "ruby -e 'loop do;sleep;end'";
}
else{
    #親プロセスはこっちを実行する

    #子プロセスが終了するのを待つ
    waitpid($pid,0);
}

上記のようなPerlスクリプトをfork_exec.plという名前で用意して、バックグラウンドで実行してみましょう。すると、以下のような出力が得られると思います。

$ perl ./fork_exec.pl &
forking...
forked!
forked!

なぜこうなるのか、説明しましょう。

print "forking!\n"; という行は、まだfork前なので、プロセスがひとつだけの状態です。なので、普通にひとつの"forking!"が出力されます。しかし、print "forked!\n"; という行は、forkシステムコールでプロセスが複製されたあとです。そのため、この行は親プロセスとそこから複製された子プロセスが、別のプロセスとして実行します。親プロセスは親プロセスで"forked!"という文字列を標準出力という場所に出力します(perlのprintという関数は、引数に渡された文字列を標準出力に出力します)、一方、別の環境で動いている子プロセスも、"forked!"という文字列を標準出力という場所に出力します。今回の場合、親プロセスも子プロセスも標準出力はターミナルを意味するので(このあたりの話はまたあとで詳しくやります)、ターミナルに親プロセスと子プロセスの二つ分のforked!が出力されるわけです。

さて、今バックグラウンドで実行したこのスクリプトですが、ではプロセスはどのようになっているでしょうか。psコマンドで確認して見ましょう。

$ ps
  PID TTY           TIME CMD
81996 ttys003    0:00.01 perl fork_exec.pl
81998 ttys003    0:00.01 ruby -e loop do;sleep;end

psコマンドの出力に、上記のようなふたつの行が見つかるかと思います。上の perl fork_exec.pl というプロセスが私たちがさっき「$ perl fork_exec.pl &」と実行したプロセスで、下の ruby -e loop do;sleep;end というプロセスが、forkされた子プロセスです。pstreeで見てみましょう。

$ pstree 81996 (さっきpsで確認した "perl fork_exec.pl" のPIDを指定)
-+= 81996 shinpeim perl fork_exec.pl
 \--- 81998 shinpeim ruby -e loop do;sleep;end

というような出力が得られ、"perl fork_exec.pl" というプロセスから "ruby -e loop do;sleep;end" というプロセスが生成されているのがわかるかと思います。

さて、今バックグラウンドで実行しているプロセス(親プロセスです)を fg コマンドでフォアグランドに移して、Ctrl+Cで止めてしまいましょう。その後もう一度psコマンドを叩くと、子プロセスごと消えているのがわかるかと思います。なぜこうなるのかについては、シグナルについて見るときに説明しましょう。

今は、「forkで子プロセスを生成できて、execでそのプロセスの内容を書き換えられた」ということがわかれば十分です。コマンドを叩いて新しいプロセスを生成する場合とかも、内部ではこのようにforkでプロセスを生成して、確保された環境の内容をexecで書き換えるという形で生まれているのです。ちなみに、シェルからコマンドを叩いてプロセスを生成するときには、「親プロセス」に当たるのはシェルのプロセスになります。

今後の予定

  • forkしたpidを看取る話と子供がゾンビになっちゃう話
  • あらゆる入出力はファイルとして扱われてるよって話からの、forkした際の file descripter と open file description について

あたりを書きたい気持ちがある

Footnotes

  1. 「えっ、まるまるメモリーをコピーするの、そんなのメモリーの無駄じゃないの」と思われる方もいるかもしれませんが、そこはよくできていて、COW(Copy On Write)という方法を使うことで、うまいこと無駄なメモリーを食わないようになっています。

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

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

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

まず、以下のようなテキストファイルを 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」の略で、「ここでこのファイルはおしまいだよ」というのを伝える制御文字です。rubyのプロセスは、この「EOF」を受け取ることで、「よし、標準入力を全部読み込んだぞ」と理解して、IO待ちのブロック状態から抜けるわけですね。

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

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

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

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

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

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

リダイレクト

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

例えば、

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

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

$ ruby print_mew.rb 1>mew.txt

とすると、mew とだけ書かれた mew.txt というファイルができあがります。その上で

$ ruby print_mew.rb 1>>mew.txt

とすると、 mew.txt にさらに mew が追記され、mew.txt の中身は mew(改行)mew というものになります。さらに

$ ruby print_mew.rb 1>mew.txt

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

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

$ ruby print_mew.rb > mew.txt

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

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

$ ruby stdin.rb < mew.txt
mew

mew.txtの内容がstdin.rbによって読み込まれ、ターミナルに書き出されたかと思います。

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

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

上記の場合、mew.txt の内容が stdin.rb によって読み込まれ、標準出力に書き出されます。標準出力は mew_copy.txt をさしているので、mew_copy.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" で標準出力をout.txtに、"2>err.txt" で標準エラー出力を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っていう名前のファイルじゃなくて標準出力を表す数字だよ!」ってことを言っているわけですね。さあ、またまた新しい疑問がわいてきました。なんで&付けるとそれがファイル名じゃなくて標準出力ってことになるの? そもそもなんで1とか2とかって謎っぽい数字使ってるの? 疲れてきたので、そのあたりは次回にまわします。

パイプ

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

$ command_a | command_b

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

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

次回予告

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

@remnant
Copy link

remnant commented Feb 8, 2013

大学で丁度習ったところだったので理解が進みました。ありがとうございました。

誤字報告:
× COW(Coy On Write)
○ COW(Copy On Write)

@Shinpeim
Copy link
Author

Shinpeim commented Feb 8, 2013

ありがとうございます。修正しました

Copy link

ghost commented Feb 11, 2013

「プロセス、スレッドと並列処理」という記事におきまして「いわゆる「並列処理」が可能になるわけです」と書いておられますが、こちらの「並列処理」は「並行処理 = concurrent processing = pseudo parallel processing」ではないでしょうか。当方の勘違いでしたら誠に申し訳ございません。ご教示いただけましたら幸いです。

@kenkou25
Copy link

とてもわかりやすく、参考になりました。

1点だけ、漢字の変換ミスと思われますが、

[002.md 157行目]
誤 : 子プロセス毎消えて
正 : 子プロセスごと消えて

以上です。
次回も楽しみにしています。

@Shinpeim
Copy link
Author

h-kawさん

そのとおりですね。ご指摘ありがとうございます。並列処理と平行処理の違いに踏み込むと論点がブレるため、今回は「同時に複数の計算が可能になる」という表現に改めました。

kenkou25さん

ありがとうございます。修正しました。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment