Skip to content

Instantly share code, notes, and snippets.

@Shinpeim
Last active November 2, 2024 00:52
Show Gist options
  • Save Shinpeim/4736099 to your computer and use it in GitHub Desktop.
Save Shinpeim/4736099 to your computer and use it in GitHub Desktop.

Revisions

  1. Shinpeim revised this gist Mar 22, 2013. 1 changed file with 0 additions and 17 deletions.
    17 changes: 0 additions & 17 deletions 000.md
    Original file line number Diff line number Diff line change
    @@ -10,20 +10,3 @@ URL 変わっちゃうの申し訳ないんだけど、一覧性が高くなる

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

    # 目次(予定)

    [第一回:導入](https://gist.github.com/Shinpeim/4994708)

    [第二回:プロセスの生成](https://gist.github.com/Shinpeim/4994709)

    [第三回:プロセスとファイル入出力](https://gist.github.com/Shinpeim/4994710)

    [第四回:ファイルディスクリプタ](https://gist.github.com/Shinpeim/4994711)

    [第五回:preforkサーバーを書く](https://gist.github.com/Shinpeim/4994713)

    [第六回:ゾンビプロセスと孤児プロセス](https://gist.github.com/Shinpeim/5056638)

    [第七回:シグナル と kill](https://gist.github.com/Shinpeim/5194454)

    [第八回:プロセスグループ と フォアグランドプロセス](https://gist.github.com/Shinpeim/5205353)
  2. Shinpeim revised this gist Mar 22, 2013. No changes.
  3. Shinpeim revised this gist Mar 22, 2013. 1 changed file with 8 additions and 0 deletions.
    8 changes: 8 additions & 0 deletions 000.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,11 @@
    # 重要

    サイズがあまりに大きくなってしまったので、gist ではなくて github 上で管理するようにしました。

    https://github.com/Shinpeim/process-book

    URL 変わっちゃうの申し訳ないんだけど、一覧性が高くなるのと pull req が受け取れるメリットのほうを取ります。せっかく読みにきてくれたのにひと手間かかっちゃってすみません。

    # この文書の目的

    この文書は\*nix系のシステムにおけるプロセスやシグナルなどについて説明することを目的に書かれています。「プロセスとかよくわかってないからちゃんと知りたいな」みたいなひとたちが想定読者です。あと、まとめることによって自分の知ってることの棚卸しをするという目的もあります。
  4. Shinpeim revised this gist Mar 20, 2013. 1 changed file with 1 addition and 3 deletions.
    4 changes: 1 addition & 3 deletions 000.md
    Original file line number Diff line number Diff line change
    @@ -1,8 +1,6 @@
    # この文書の目的

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

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

    # 目次(予定)

  5. Shinpeim revised this gist Mar 20, 2013. 1 changed file with 8 additions and 8 deletions.
    16 changes: 8 additions & 8 deletions 000.md
    Original file line number Diff line number Diff line change
    @@ -6,18 +6,18 @@

    # 目次(予定)

    [第一回](https://gist.github.com/Shinpeim/4994708)
    [第一回:導入](https://gist.github.com/Shinpeim/4994708)

    [第二回](https://gist.github.com/Shinpeim/4994709)
    [第二回:プロセスの生成](https://gist.github.com/Shinpeim/4994709)

    [第三回](https://gist.github.com/Shinpeim/4994710)
    [第三回:プロセスとファイル入出力](https://gist.github.com/Shinpeim/4994710)

    [第四回](https://gist.github.com/Shinpeim/4994711)
    [第四回:ファイルディスクリプタ](https://gist.github.com/Shinpeim/4994711)

    [第五回](https://gist.github.com/Shinpeim/4994713)
    [第五回:preforkサーバーを書く](https://gist.github.com/Shinpeim/4994713)

    [第六回](https://gist.github.com/Shinpeim/5056638)
    [第六回:ゾンビプロセスと孤児プロセス](https://gist.github.com/Shinpeim/5056638)

    [第七回](https://gist.github.com/Shinpeim/5194454)
    [第七回:シグナル と kill](https://gist.github.com/Shinpeim/5194454)

    [第八回](https://gist.github.com/Shinpeim/5205353)
    [第八回:プロセスグループ と フォアグランドプロセス](https://gist.github.com/Shinpeim/5205353)
  6. Shinpeim revised this gist Mar 20, 2013. 1 changed file with 3 additions and 17 deletions.
    20 changes: 3 additions & 17 deletions 000.md
    Original file line number Diff line number Diff line change
    @@ -6,22 +6,6 @@

    # 目次(予定)

    * プロセス、スレッド
    * プロセスについて
    * プロセスのライフサイクル
    * プロセスツリーについて
    * fork
    * exec
    * プロセスとファイル入出力
    * シグナル
    * スレッドについて
    * プロセスとの違い
    * リソースの共有について

    # 本文

    ファイルが増えてきてgist上で表示されなくなったので、分けました。

    [第一回](https://gist.github.com/Shinpeim/4994708)

    [第二回](https://gist.github.com/Shinpeim/4994709)
    @@ -34,4 +18,6 @@

    [第六回](https://gist.github.com/Shinpeim/5056638)

    [第七回](https://gist.github.com/Shinpeim/5194454)
    [第七回](https://gist.github.com/Shinpeim/5194454)

    [第八回](https://gist.github.com/Shinpeim/5205353)
  7. Shinpeim revised this gist Mar 19, 2013. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion 000.md
    Original file line number Diff line number Diff line change
    @@ -32,4 +32,6 @@

    [第五回](https://gist.github.com/Shinpeim/4994713)

    [第六回](https://gist.github.com/Shinpeim/5056638)
    [第六回](https://gist.github.com/Shinpeim/5056638)

    [第七回](https://gist.github.com/Shinpeim/5194454)
  8. Shinpeim revised this gist Feb 28, 2013. 1 changed file with 7 additions and 1 deletion.
    8 changes: 7 additions & 1 deletion 000.md
    Original file line number Diff line number Diff line change
    @@ -23,7 +23,13 @@
    ファイルが増えてきてgist上で表示されなくなったので、分けました。

    [第一回](https://gist.github.com/Shinpeim/4994708)

    [第二回](https://gist.github.com/Shinpeim/4994709)

    [第三回](https://gist.github.com/Shinpeim/4994710)

    [第四回](https://gist.github.com/Shinpeim/4994711)
    [第五回](https://gist.github.com/Shinpeim/4994713)

    [第五回](https://gist.github.com/Shinpeim/4994713)

    [第六回](https://gist.github.com/Shinpeim/5056638)
  9. Shinpeim revised this gist Feb 20, 2013. 10 changed files with 11 additions and 784 deletions.
    12 changes: 11 additions & 1 deletion 000.md
    Original file line number Diff line number Diff line change
    @@ -16,4 +16,14 @@
    * シグナル
    * スレッドについて
    * プロセスとの違い
    * リソースの共有について
    * リソースの共有について

    # 本文

    ファイルが増えてきてgist上で表示されなくなったので、分けました。

    [第一回](https://gist.github.com/Shinpeim/4994708)
    [第二回](https://gist.github.com/Shinpeim/4994709)
    [第三回](https://gist.github.com/Shinpeim/4994710)
    [第四回](https://gist.github.com/Shinpeim/4994711)
    [第五回](https://gist.github.com/Shinpeim/4994713)
    36 changes: 0 additions & 36 deletions 001.md
    Original file line number Diff line number Diff line change
    @@ -1,36 +0,0 @@
    # プロセス、スレッド

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

    \*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などがその例ですね。スレッドと一口に言ってもどのような実装になっているかによって特徴が変わってくるので、自分が使っている環境の「スレッド」というのがどのような仕組みをさしているのかは意識しておく必要があるでしょう。

    ## 次回予告

    次回はプロセスについてもう少し深くまでもぐって見ていきます。
    166 changes: 0 additions & 166 deletions 002.md
    Original file line number Diff line number Diff line change
    @@ -1,166 +0,0 @@
    # プロセスの生成

    ## プロセスの例

    前回、プロセスとは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]。コピーされて新しい環境が出来上がるため、親プロセスでなにか操作しても(変数に新しい値代入するとか新しくインスタンスを生成するとか)、その操作は子プロセスに影響を与えません。親でなんか変更したからといって、子にもその変更が伝わるみたいなことはないわけです。逆もまたしかりで、子プロセスでなにか操作しても、その変化は親プロセスに影響を与えません。

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

    こうして、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でプロセスを生成して、独立した環境を用意してあげる
    1. その環境に、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 について

    あたりを書きたい気持ちがある
    199 changes: 0 additions & 199 deletions 003.md
    Original file line number Diff line number Diff line change
    @@ -1,199 +0,0 @@
    # プロセスとファイル入出力

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

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

    まず、以下のようなテキストファイルを 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とかって謎っぽい数字使ってるの? 疲れてきたので、そのあたりは次回にまわします。

    # パイプ

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

    $ 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の話ができたらいいな!さーて、次回も、サービス!サービスゥ!
    170 changes: 0 additions & 170 deletions 004.md
    Original file line number Diff line number Diff line change
    @@ -1,170 +0,0 @@
    # ファイルディスクリプタ

    さて、前回、プロセスがファイルを通じて外部との入出力する様を見て見ました。今回はさらにプロセスとファイル入出力について詳しく見てみましょう。

    前回はさらっと流してしまいましたが、実はプロセスは自分自身で実際にファイルを開いたりディスクに書き込んだりディスクからデータを読み出したりすることはありません。そういう低レイヤーの処理は、プロセスがシステムコールをOSに送ることで、OSが代わりに行ってくれます。そのあたりの話を、きちんと見て行きましょう。

    さて、なにはともあれ、プロセスが入出力をしたいと思ったら、ファイルを開くところから始めないといけません。

    * OSは、プロセスから「ファイルを開いてね」というシステムコールを受け取ると、実際にファイルを開きます
    * OSは、その開いたファイルを表す「番号札」作成します
    * OSは、その番号札をプロセスに対して返します。

    さて、ファイルを開いたら、今度はそこになにかを書き込んでみましょうか

    * プロセスは、さっき受け取った「番号札」をつかって、「n番の番号札で表されてるファイルにこれ書き込んでおいて」っていうシステムコールを送ります。
    * OSは、「番号札」で表された、すでに開かれているファイルに対して書き込みを行います

    じゃあ、今度はファイルを閉じましょう

    * プロセスは、不要になった番号札をcloseというシステムコールでOSに返却します
    * OSは、番号札が返却されたので、「もうこれは使わないんだな」と判断して、ファイルを閉じます

    と、こんな感じでファイルの入出力が行われているのですが、この「番号札」のことを、「ファイルディスクリプタ」と呼びます。実際、ファイルディスクリプタは整数値で表現されています。

    例を見てみましょう。今回もRubyを使います。

    # fd.rb
    file = File.open("nyan.txt","w") # openシステムコールでnyan.txtを書き込みモードでopen
    p file.fileno # fileno メソッドで、ファイルディスクリプタ(番号札)を取得
    file.close #fileをclose

    1行目で、openシステムコールをOSに対して送っています。正常にopenされると、ファイルディスクリプタを内部に持ったfileオブジェクトが生成されます。2行目で、fileオブジェクトが保持しているファイルディスクリプタを取得してターミナルに出力しています。3行目で、fileを閉じていますが、これはRubyが内部でfileオブジェクトが保持しているファイルディスクリプタを使って、OSにcloseシステムコールを送っているわけです。IO#readlineとかIO#writeメソッドなんかも、内部ではIOオブジェクトが保持しているファイルディスクリプタを使って、読み込みのためのシステムコールを送ったり書き込みのためのシステムコールを使ってりしているわけですね。

    さて、説明がすんだところで、実際にfd.rbを実行してみましょう。

    $ ruby fd.rb
    5

    「nyna.txtが書き込みモードで開かれたもの」についてる番号札が、5番なのが確認できましたね。

    # 標準入出力のファイルディスクリプタ

    さて、勘のいいひとはそろそろ例の標準入力は0、標準出力は1、標準エラー出力は2、という謎の数字の正体について、感付きつつあるのではないでしょうか。そうです。実は、「標準入力のファイルディスクリプタは0、標準出力のファイルディスクリプタは1、標準エラー出力のファイルディスクプタは2」なのです。実際に確かめてみましょう

    # std_fds.rb
    p $stdin.fileno # => 0
    p $stdout.fileno # => 1
    p $stderr.fileno # => 2

    おー。

    つまり、前回出てきた & という記号は、「ファイルパスじゃなくてファイルディスクリプタを指定してるぜ」という意味の記号だったわけですね!そして、なぜリダイレクトのときに標準入力や標準出力にあのような数字が使われているのかが理解できたと思います。

    # オープンファイル記述

    さて、今はプロセスの側からがファイルディスクリプタをどう扱っているかについて見てみましたが、今度はOSの側から見てみましょう。

    OSのお仕事は、「プロセスからファイルの操作を頼まれたら、代わりにやってあげること」です。そのためには、OSは実際のdiskの読み書きの他に、少なくとも以下の仕事をしないといけません。

    * プロセスに「ファイル開いて」って言われたら開いてあげる
    * ファイルを開いたら、そのファイル専用の「ファイルの状況どうなってるっけメモ」を作る
    * 開いたファイルの情報(書き込みモードなのか読み込みモードなのか、とか、ファイルパスはどこなのかとか、どこまで読み込んだかあるいは書き込んだかとか)を「ファイルの状況どうなってるっけメモ」に書いておく
    * プロセスのために番号札を作って、さっき書いた「ファイルの状況どうなってるっけメモ」がどのプロセスのどの番号札のものなのかを覚えておく
    * プロセスに番号札を貸してあげる

    「ファイルの状況どうなってるっけメモ」を保持しておかないと、「次の行読み込んでよ」ってプロセスから言われたときに「ふぇぇ、次の行ってどこ〜〜〜〜〜」ってなっちゃいますよね。あるいは、どの「ファイルの状況どうなってるっけメモ」がどのプロセスの何番の番号札と紐づいているのかを覚えておかないと、あるプロセスが「5番の番号のやつに書き込んでよ」って言ってきても、「ふぇぇ、書き込みたいけどどのメモ見ればいいのか忘れちゃったよ〜〜〜」ってなっちゃいます。

    このとき、この「ファイルの状況どうなってるっけメモ」にあたるのが、オープンファイル記述と呼ばれるものです。OSは、「ファイル開いて」っていうシステムコールを受け取ると、オープンファイル記述を作り出して自分で保持しておきます。さらに、システムコールを送ってきたプロセスのidに対して、新しい番号札(ファイルディスクリプタ)を返します。このとき、オープンファイル記述とプロセスidと番号札の関連も、自分の中に保持しておきます。

    これで、たとえばpidが100番のプロセスから「5番のファイルの、次の行読み込んでよ」と言われても、「ふぇぇ」ってならずに、「100番のプロセスさんの5番の番号札に紐づいたメモはこれだな」「メモには/path/to/fileの3行目まで読み込んだって書いてあるな」「じゃあこのファイルの4行目を読み込めばいいね!」「はいできた!」と言ってデータを返すことができるわけですね!

    このあたりのイメージは、このファイルの下に attach してある 004_01.png と 004_02.pngを見てみてください。

    # ファイルディスクリプタ/オープンファイル記述とfork

    さて、では、forkしたとき、ファイルディスクリプタやオープンファイル記述はどうなるのでしょうか?

    先に答えを言ってしまいましょう。forkした場合、ファイルディスクリプタは複製されますが、複製されたファイルディスクリプタは同一のオープンファイル記述を参照します。

    言い方を変えると、forkした場合、OSは新しいpidのために新しい番号札は作るけど、その番号札は同じ「ファイルの状況どうなってるっけメモ」に紐づけられてる、ということです。つまり、「ファイルの状況どうなってるっけメモ」は、親プロセスと子プロセスで共有するメモになります。

    そのため、forkしたときに同じ番号札(ファイルディスクリプタ)にたいして親プロセスと子プロセス両方で操作をすると、おかしなことになることがあります。

    ## オープンファイル記述は複製されない
    例を見ましょう。

    # fork_fd.rb
    # -*- coding: utf-8 -*-

    read_file = File.new("nyan.txt","r")

    # ファイルをopenしたあとにforkしてみる
    pid = Process.fork

    if pid.nil?
    # 子プロセス
    lines = []
    while line = read_file.gets
    lines << line
    end
    write_file = File.new("child.txt","w")
    write_file.write(lines.join)
    write_file.close
    else
    # 親プロセス
    lines = []
    while line = read_file.gets
    lines << line
    end
    write_file = File.new("parent.txt","w")
    write_file.write(lines.join)
    write_file.close
    end
    read_file.close

    子プロセスと親プロセスで、nyan.txtから一行ずつ入力を受け取っています。もしもforkされたときに「ファイルの状況どうなってるっけメモ」まで複製されているならば、親プロセスが一行読み込んだとき親プロセスの「ファイルの状況どうなってるっけメモ」は一行分進みますが、子プロセスの「ファイルの状況どうなってるっけメモ」は書き変わらないので、親プロセスでの読み込みは子プロセスでの読み込みに影響を与えないはずですね。つまり、親プロセスでも子プロセスでも、同じくファイルの内容をすべて読み込むことができるはずです。逆に、親と子が共通の「ファイルの状況どうなってるっけメモ」を参照しているならば、親プロセスで一行読み込んだら、共通の「ファイルの状況どうなってるっけメモ」が1行分進んでしまい、子プロセスではその行を読み込むことができなくなってしまいます。

    では実際に確かめて見ましょう。nyan.txtに以下の内容を書き込んだ上で、fork_fd.rbを実行してみましょう

    nyan
    nyan nyan
    nyan nyan nyan
    nyan nyan nyan nyan
    nyan nyan nyan nyan nyan
    nyan nyan nyan nyan nyan nyan

    実行します

    $ ruby fork_fd.rb

    さて、結果はどうなったでしょうか?オープンファイル記述が複製されていないことが実感できたかと思います。

    ## ファイルディスクリプタは複製される

    では今度は、ファイルディスクリプタは複製されているのを見てみましょう


    # -*- coding: utf-8 -*-
    file = File.open("nyan.txt","r")

    # ファイルをopenしてからforkする

    pid = Process.fork

    if pid.nil?
    #子プロセス
    sleep 1 # 親プロセスがfileを閉じるのを待つ

    # 親プロセスがfdを閉じてても、自分はまだ番号札を持ってるから読み込める
    puts file.readlines.join

    file.close #自分も番号札を返す
    else
    # 親プロセス
    file.close #番号札をOSに返す
    Process.wait(pid) #子プロセスが終わるの待つ
    end

    実行してみると、親プロセスがすでに番号札をOSに返してしまっても、子プロセスは複製された番号札を持っているので問題なくファイル操作ができているのが見て取れると思います。

    このあたりのイメージは、このファイルの下にattachしてある 004_03.png と 004_04.png を見てみてください。

    ## どうするのがベストプラクティスなの?

    すでにfileがopenされている状態でforkすると、以上に見たように予期せぬ動作で混乱することがあります。そのため、forkした場合、親プロセスで使わないファイルは親プロセスですぐ閉じる、子プロセスで使わないファイルは子プロセスですぐ閉じるとすると、最も問題が起きにくいと思います。子プロセスでファイルを閉じたとしても、親プロセスでファイル使いたい場合に問題なく扱える(またはその逆も)のは、上に見た通りですからね


    # 次回予告

    ソケットの話してpreforkサーバーを自分で書いてみるつもり
    Binary file removed 004_01.png
    Binary file not shown.
    Binary file removed 004_02.png
    Binary file not shown.
    Binary file removed 004_03.png
    Binary file not shown.
    Binary file removed 004_04.png
    Binary file not shown.
    212 changes: 0 additions & 212 deletions 005.md
    Original file line number Diff line number Diff line change
    @@ -1,212 +0,0 @@
    # 今日のお題:preforkサーバーを作ってみよう

    さて、前回は、fork するとファイルディスクリプタ(以下fdと表記)は複製されるけどオープンファイル記述は共有されるというのを見ました。これを利用して、preforkモデルのサーバーを実際に書いてみましょう。

    # tcp socketはファイルである

    以前見たとおり、Linuxではプロセスの入出力はファイルを通じて行います。とうぜん、ネットワークごしに入出力する tcp socket もファイルです。ここで「ファイルです」が意味するのは、プロセスがソケットを通じて入出力をしようと思えば、socket の fd を通じて write や read を行うということですね。では、実際に socket がファイルであるところを見てみましょう

    # -*- coding: utf-8 -*-
    require "socket"

    # 12345 portで待ち受けるソケットを開く
    listening_socket = TCPServer.open(12345)

    # ソケットもファイルなので fd がある
    p listening_socket.fileno

    # ひとまずなにもせず閉じる
    listening_socket.close

    上記のような Ruby スクリプトを実行してみると、openしたソケットがfdを持つことが確認できるかと思います。

    # クライアントの接続を受け入れる

    今は socket を開いてなにもせずにすぐ閉じてしまいましたが、今度はクライアントの接続を受け入れてしてみましょう。

    # -*- coding: utf-8 -*-
    require "socket"

    # 12345 portで待ち受けるソケットを開く
    listening_socket = TCPServer.open(12345)

    p listening_socket.fileno

    # acceptでクライアントからの接続を待つ
    # 接続されるまでブロックする
    puts "accepting..."
    socket = listening_socket.accept
    puts "accepted!"

    # 接続されると新しいsocketが作られる
    # このsocketを通じてクライアントと通信する
    # あたらしいsocketなのでfdの番号がlistening_socketと違う
    p socket.fileno

    # なにもせずクライアントとのコネクションを切る
    socket.close

    # 待ち受けソケットも閉じる
    listening_socket.close

    上記のような Rubyスクリプトを適当な名前で作って、実行してみましょう。listening_socket の fd が出力されたあとに、accepting…と出力されて、そこで止まってしまいプロンプトが帰ってこないかと思います。なぜこういう動きをするか、いままでこのドキュメントを読み進めてきたみなさんはもう理解できますね。listen している socket で accept を呼び出すと、プロセスはそこでブロックして、クライアントからの接続を待ちます。そこでブロック中になっているため、プロセスがそれ以上進まないわけですね。

    では、今度はそのままターミナルをもうひとつ開いて、ここにコネクションを貼ってみましょう。

    # べつのターミナルで
    $ telnet localhost 12345

    上記のように、 telnet コマンドで、さっきのプロセスが待ち受けてる 12345 ポートに接続してみましょう。一瞬で接続が切られてしまうかと思います。

    一方、今度はさっきプロンプトが返ってこないままになっていたターミナルを再度見てみてください。 accepted! のあとに、listening_socket の fd とはまた違う数字の fd が出力されて、プロンプトが返ってきたかと思います。これは、telnetでクライアントから接続されたことにより、accept の行でブロック中になっていたプロセスが動き出したためです。accept はクライアントから接続されるとブロック中から抜け出し、新しい socket を作り出して返します。サーバーのプロセスは、この新しい socket を通じてクライアントと通信をします。この socket にたいして write をすればクライアントへデータを送ることになるし、この socket から read をすれば、クライアントからの入力を受け取るという感じですね。とうぜん、この socket を close するとクライアントとのコネクションは切断されます。

    今回はなにもせずに socket を close したので、クライアント側(telnetコマンドを打った側)ではすぐにサーバーからコネクションが切られてしまったわけですね。

    # クライアントから送られてくるデータを読み込む

    さっきはなにもせず socket を close してしまいましたが、今度はクライアントからデータが送られてきたらそれを読む、という動きにしてみましょう。

    # -*- coding: utf-8 -*-
    require "socket"

    listening_socket = TCPServer.open(12345)

    # クライアント受け入れ無限地獄
    loop do
    puts "accepting..."
    socket = listening_socket.accept
    puts "accepted a client!"

    # クライアントからの入力受け取り無限地獄
    loop do
    # クライアントからの入力を1行読む
    # 入力されるまでブロックする
    line = socket.gets
    line.gsub!(/[\r\n]/,"") #改行コード捨てる

    # exitと入力されてたらソケット閉じてループを抜ける
    if line == "exit"
    socket.close
    puts "closed a connection!"
    break
    end

    # そうでなければ標準出力に出力
    puts line
    end
    end

    はい、ちょっと書き換えてみました。

    ターミナルを立ち上げて、これを実行してみましょう。このターミナル上で動いてるのがサーバープロセスになります。今は accepting… が出力されたところでプロセスがブロックしてると思います。ここまではさっきとおなじですね。では、またべつのターミナルを開いて、telnetコマンドでサーバープロセスに接続してみましょう。

    $ telnet localhost 12345

    今度は切断されないと思います。

    ではまたサーバープロセスが走ってるほうのターミナルを見てみましょう。"accepted a client"と出力されて、そこでプロセスがブロックしていると思います。line = socket.gets のところで、クライアントからのデータを読み込もうとしていますが、クライアントがまだなにもデータを送っていないのでここでブロックしているわけですね。

    では今度は telnet のほうのターミナルで、なんかを入力して、改行してみましょう。

    再度サーバープロセスのほうを見てみると、今 telnet で入力した一行が、標準出力に書き出されているのが見て取れると思います。

    では telnet のほうに戻って(何度も往復してたいへんだ!)、今度は exit と入力して改行してみましょう。すると、サーバープロセスが socket を close したことにより、接続が切れるかと思います。

    サーバープロセスのほうを見てみると、"closed a connection!" が出力されたあと、また "accepting…" が出力されて、ブロックしてると思います。これは、break でクライアントからの入力受け取り無限地獄を抜けたはいいけれど、今度はクライアント受け入れ無限地獄loopによりまた listening_socket.accept しているところでブロックしてるわけですね。

    動きを確認したら、サーバープロセスのほうで Ctrl + C を入力して、プロセスを終了してあげましょう。

    いまは puts line で標準出力にクライアントからの入力を出力していますが、この行を socket に対する書き込みにすれば、いわゆるエコーサーバーとして動くプロセスになります。そのあたりは宿題とするので、自分で書き換えて動きを見てみてください。

    # このサーバーは出来損ないだ、たべられないよ

    さて、これでクライアントから接続を待って、クライアントに接続されたらそのクライアントとネットワーク越しに入出力することができました。しかし、このサーバーには欠陥があります。わかりますか?

    そう、このままでは、同時にひとつのクライアントしか処理できないのです。クライアントからの接続を accept したあとは、このプロセスは「クライアントの入力受け取り無限地獄」にいます。その無限地獄にいる限り、このプロセスは次の listening_socket.accept に到達することはありません。なので、「クライアントの入力受け取り無限地獄」を抜けるまでは新しく接続してこようとするクライアントを受け入れることができないのです。これは困りましたね。

    じっさい、このサーバープロセスを立ち上げた状態で、さらにターミナルをふたつ立ち上げて、両方で

    $ telnet localhost 12345

    をしてみると、先に telnet したほうは普通に動くのだけれど、もういっこのほうはいくら入力してもサーバープロセスがうんともすんとも言わないのが見て取れると思います。

    # 明日の同じ時間にここに来てください。本当のサーバーってやつを見せてあげますよ

    べつに用意する食材もないので、明日の同じ時間を待つ必要はありません。すぐにコードを書き換えてしまいましょう。

    # -*- coding: utf-8 -*-
    require "socket"

    number_of_workers = 3
    listening_socket = TCPServer.open(12345)

    number_of_workers.times do
    pid = fork

    if pid
    # 親プロセスは次々に fork で子プロセスを作る
    next
    else
    # 子プロセス

    # クライアント受け入れ無限地獄
    loop do
    puts "accepting..."
    # 子プロセスは全部ここでブロックする。
    socket = listening_socket.accept
    puts "accepted a client!"

    # クライアントの入力受け取り無限地獄
    loop do
    line = socket.gets
    line.gsub!(/[\r\n]/,"")

    if line == "exit"
    socket.close
    puts "closed a connection!"
    break
    end

    puts line
    end
    end
    end
    end

    # 子プロセスは無限ループしてるからここには届かない
    # 親プロセスでは子プロセスの終了を待ち続ける
    Process.waitall

    listening_socket を作ったあとに、3回 fork するようにしてみました。親プロセスでは fork したあとに何もしないで子プロセスの終了を待ちます。一方、子プロセスでは、 accept を呼んでブロックしていますね。

    さて、ここで前回の内容が役に立ちますよ。

    listening_socket はファイルでした。そのため、fd を持ちます。そして、forkした場合、fd は複製されるけど、複製された fd は複製もとと同じオープンファイル記述を参照しているのでしたね。

    というわけで、今、listening_socket を作ったあとに fork したことで、12345 portで待ち受ける同じソケットを全てのプロセスで共有している状態になっているわけです。ここまではいいですか?

    そして、親プロセスではその listening_socket に対して何もせず、子プロセスで accept していますね。この3つの子プロセスは、クライアントからの接続を獲得して、新しい socket を作ろうと身構えてブロックしている状態なわけです。ここで、あるクライアントが 12345 ポートに対して接続してきたとしましょう。なにが起こりますか?

    3つの子プロセスは、それぞれがクライアントからの接続を受け入れて新しい socket を作ろうとしますが、オープンファイル記述が共有されているため、クライアントからの接続を受け入れられるのはたったひとつの子プロセスだけです。前回の内容を思い出して下さい。file を open したあと fork して、両方のプロセスでその file を読み込んだ場合、片方のプロセスでしか読み込むことができていなかったと思います。これと同じことが accept でも起こっているわけですね。

    さて、首尾よく accept できて新しいソケットを獲得した子プロセスは、クライアントからの入力受け取り無限地獄へと突入します。というわけで、今接続してきたクライアントとのやり取りは、この子プロセスにまかせることができました。一方、残念ながら accept できなかった他の子プロセスは、さっきとおなじところでブロックしたままです。

    さて、ここに、さらに新しいクライアントが接続してきた場合はどうなるでしょうか?accept でブロック中になっている、他の子プロセスがそのクライアントを accept してくれるわけですね。

    こんなふうに、あらかじめ子プロセスをいくらか fork しておいて、その子プロセスでクライアントからの接続を受け入れて処理するような仕組みを、「prefork」といいます。先に(pre)forkしておくサーバーってことですね。

    さて、これで無事、同時に複数のクライアントからの接続を受け入れることが可能になりました。今回は 3 回forkしたので、同時接続数は 3 つまでですね。サーバープロセスの他にもターミナルをたくさん立ち上げて、それぞれで telnet localhost 12345 してみてください。3つまでは同時に処理できるけど、4つを超えると同時に処理できてないことが見て取れるかと思います。

    今までの話で、preforkサーバーが書けて、さらにどうしてそんなことが可能なのかも理解できましたね!

    # preforkの利点、欠点

    さて、上に見たように、prefork サーバーはひとつのプロセスがひとつのクライアントを受け持つようなアーキテクチャになっています。このことは、いくつかの利点と欠点をもたらします。

    まず、上に見たように、worker(子プロセス)の数が少ないと、同時に処理できるクライアントの数が少なくなってしまいます。同時処理の数を増やしたければ、その数だけプロセスを生成する必要があるわけです。プロセスが生成されればそのぶんだけメモリーは消費されるので、この方法は意外と効率がよくないんですね。

    一方で、アーキテクチャが単純なので、コードが追いやすいです。シンプルであることは、とても大切なことです。さらに、ひとつのクライアントがひとつのプロセスで処理されるためたとえばプロセスが暴走したとかそういうときの影響範囲がせまくなります。ひとつのプロセス内でたくさんのクライアントを受け持つと、そのプロセスがなんかおかしなことになったときに影響範囲が大きくなって困りますね。

    # 次回予告

    次回はちょっと話を戻して、forkした際に親が先に死んだり終わったりしたらどうなるのとかそういう話をしたいなって思います。
  10. Shinpeim revised this gist Feb 20, 2013. 1 changed file with 212 additions and 0 deletions.
    212 changes: 212 additions & 0 deletions 005.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,212 @@
    # 今日のお題:preforkサーバーを作ってみよう

    さて、前回は、fork するとファイルディスクリプタ(以下fdと表記)は複製されるけどオープンファイル記述は共有されるというのを見ました。これを利用して、preforkモデルのサーバーを実際に書いてみましょう。

    # tcp socketはファイルである

    以前見たとおり、Linuxではプロセスの入出力はファイルを通じて行います。とうぜん、ネットワークごしに入出力する tcp socket もファイルです。ここで「ファイルです」が意味するのは、プロセスがソケットを通じて入出力をしようと思えば、socket の fd を通じて write や read を行うということですね。では、実際に socket がファイルであるところを見てみましょう

    # -*- coding: utf-8 -*-
    require "socket"

    # 12345 portで待ち受けるソケットを開く
    listening_socket = TCPServer.open(12345)

    # ソケットもファイルなので fd がある
    p listening_socket.fileno

    # ひとまずなにもせず閉じる
    listening_socket.close

    上記のような Ruby スクリプトを実行してみると、openしたソケットがfdを持つことが確認できるかと思います。

    # クライアントの接続を受け入れる

    今は socket を開いてなにもせずにすぐ閉じてしまいましたが、今度はクライアントの接続を受け入れてしてみましょう。

    # -*- coding: utf-8 -*-
    require "socket"

    # 12345 portで待ち受けるソケットを開く
    listening_socket = TCPServer.open(12345)

    p listening_socket.fileno

    # acceptでクライアントからの接続を待つ
    # 接続されるまでブロックする
    puts "accepting..."
    socket = listening_socket.accept
    puts "accepted!"

    # 接続されると新しいsocketが作られる
    # このsocketを通じてクライアントと通信する
    # あたらしいsocketなのでfdの番号がlistening_socketと違う
    p socket.fileno

    # なにもせずクライアントとのコネクションを切る
    socket.close

    # 待ち受けソケットも閉じる
    listening_socket.close

    上記のような Rubyスクリプトを適当な名前で作って、実行してみましょう。listening_socket の fd が出力されたあとに、accepting…と出力されて、そこで止まってしまいプロンプトが帰ってこないかと思います。なぜこういう動きをするか、いままでこのドキュメントを読み進めてきたみなさんはもう理解できますね。listen している socket で accept を呼び出すと、プロセスはそこでブロックして、クライアントからの接続を待ちます。そこでブロック中になっているため、プロセスがそれ以上進まないわけですね。

    では、今度はそのままターミナルをもうひとつ開いて、ここにコネクションを貼ってみましょう。

    # べつのターミナルで
    $ telnet localhost 12345

    上記のように、 telnet コマンドで、さっきのプロセスが待ち受けてる 12345 ポートに接続してみましょう。一瞬で接続が切られてしまうかと思います。

    一方、今度はさっきプロンプトが返ってこないままになっていたターミナルを再度見てみてください。 accepted! のあとに、listening_socket の fd とはまた違う数字の fd が出力されて、プロンプトが返ってきたかと思います。これは、telnetでクライアントから接続されたことにより、accept の行でブロック中になっていたプロセスが動き出したためです。accept はクライアントから接続されるとブロック中から抜け出し、新しい socket を作り出して返します。サーバーのプロセスは、この新しい socket を通じてクライアントと通信をします。この socket にたいして write をすればクライアントへデータを送ることになるし、この socket から read をすれば、クライアントからの入力を受け取るという感じですね。とうぜん、この socket を close するとクライアントとのコネクションは切断されます。

    今回はなにもせずに socket を close したので、クライアント側(telnetコマンドを打った側)ではすぐにサーバーからコネクションが切られてしまったわけですね。

    # クライアントから送られてくるデータを読み込む

    さっきはなにもせず socket を close してしまいましたが、今度はクライアントからデータが送られてきたらそれを読む、という動きにしてみましょう。

    # -*- coding: utf-8 -*-
    require "socket"

    listening_socket = TCPServer.open(12345)

    # クライアント受け入れ無限地獄
    loop do
    puts "accepting..."
    socket = listening_socket.accept
    puts "accepted a client!"

    # クライアントからの入力受け取り無限地獄
    loop do
    # クライアントからの入力を1行読む
    # 入力されるまでブロックする
    line = socket.gets
    line.gsub!(/[\r\n]/,"") #改行コード捨てる

    # exitと入力されてたらソケット閉じてループを抜ける
    if line == "exit"
    socket.close
    puts "closed a connection!"
    break
    end

    # そうでなければ標準出力に出力
    puts line
    end
    end

    はい、ちょっと書き換えてみました。

    ターミナルを立ち上げて、これを実行してみましょう。このターミナル上で動いてるのがサーバープロセスになります。今は accepting… が出力されたところでプロセスがブロックしてると思います。ここまではさっきとおなじですね。では、またべつのターミナルを開いて、telnetコマンドでサーバープロセスに接続してみましょう。

    $ telnet localhost 12345

    今度は切断されないと思います。

    ではまたサーバープロセスが走ってるほうのターミナルを見てみましょう。"accepted a client"と出力されて、そこでプロセスがブロックしていると思います。line = socket.gets のところで、クライアントからのデータを読み込もうとしていますが、クライアントがまだなにもデータを送っていないのでここでブロックしているわけですね。

    では今度は telnet のほうのターミナルで、なんかを入力して、改行してみましょう。

    再度サーバープロセスのほうを見てみると、今 telnet で入力した一行が、標準出力に書き出されているのが見て取れると思います。

    では telnet のほうに戻って(何度も往復してたいへんだ!)、今度は exit と入力して改行してみましょう。すると、サーバープロセスが socket を close したことにより、接続が切れるかと思います。

    サーバープロセスのほうを見てみると、"closed a connection!" が出力されたあと、また "accepting…" が出力されて、ブロックしてると思います。これは、break でクライアントからの入力受け取り無限地獄を抜けたはいいけれど、今度はクライアント受け入れ無限地獄loopによりまた listening_socket.accept しているところでブロックしてるわけですね。

    動きを確認したら、サーバープロセスのほうで Ctrl + C を入力して、プロセスを終了してあげましょう。

    いまは puts line で標準出力にクライアントからの入力を出力していますが、この行を socket に対する書き込みにすれば、いわゆるエコーサーバーとして動くプロセスになります。そのあたりは宿題とするので、自分で書き換えて動きを見てみてください。

    # このサーバーは出来損ないだ、たべられないよ

    さて、これでクライアントから接続を待って、クライアントに接続されたらそのクライアントとネットワーク越しに入出力することができました。しかし、このサーバーには欠陥があります。わかりますか?

    そう、このままでは、同時にひとつのクライアントしか処理できないのです。クライアントからの接続を accept したあとは、このプロセスは「クライアントの入力受け取り無限地獄」にいます。その無限地獄にいる限り、このプロセスは次の listening_socket.accept に到達することはありません。なので、「クライアントの入力受け取り無限地獄」を抜けるまでは新しく接続してこようとするクライアントを受け入れることができないのです。これは困りましたね。

    じっさい、このサーバープロセスを立ち上げた状態で、さらにターミナルをふたつ立ち上げて、両方で

    $ telnet localhost 12345

    をしてみると、先に telnet したほうは普通に動くのだけれど、もういっこのほうはいくら入力してもサーバープロセスがうんともすんとも言わないのが見て取れると思います。

    # 明日の同じ時間にここに来てください。本当のサーバーってやつを見せてあげますよ

    べつに用意する食材もないので、明日の同じ時間を待つ必要はありません。すぐにコードを書き換えてしまいましょう。

    # -*- coding: utf-8 -*-
    require "socket"

    number_of_workers = 3
    listening_socket = TCPServer.open(12345)

    number_of_workers.times do
    pid = fork

    if pid
    # 親プロセスは次々に fork で子プロセスを作る
    next
    else
    # 子プロセス

    # クライアント受け入れ無限地獄
    loop do
    puts "accepting..."
    # 子プロセスは全部ここでブロックする。
    socket = listening_socket.accept
    puts "accepted a client!"

    # クライアントの入力受け取り無限地獄
    loop do
    line = socket.gets
    line.gsub!(/[\r\n]/,"")

    if line == "exit"
    socket.close
    puts "closed a connection!"
    break
    end

    puts line
    end
    end
    end
    end

    # 子プロセスは無限ループしてるからここには届かない
    # 親プロセスでは子プロセスの終了を待ち続ける
    Process.waitall

    listening_socket を作ったあとに、3回 fork するようにしてみました。親プロセスでは fork したあとに何もしないで子プロセスの終了を待ちます。一方、子プロセスでは、 accept を呼んでブロックしていますね。

    さて、ここで前回の内容が役に立ちますよ。

    listening_socket はファイルでした。そのため、fd を持ちます。そして、forkした場合、fd は複製されるけど、複製された fd は複製もとと同じオープンファイル記述を参照しているのでしたね。

    というわけで、今、listening_socket を作ったあとに fork したことで、12345 portで待ち受ける同じソケットを全てのプロセスで共有している状態になっているわけです。ここまではいいですか?

    そして、親プロセスではその listening_socket に対して何もせず、子プロセスで accept していますね。この3つの子プロセスは、クライアントからの接続を獲得して、新しい socket を作ろうと身構えてブロックしている状態なわけです。ここで、あるクライアントが 12345 ポートに対して接続してきたとしましょう。なにが起こりますか?

    3つの子プロセスは、それぞれがクライアントからの接続を受け入れて新しい socket を作ろうとしますが、オープンファイル記述が共有されているため、クライアントからの接続を受け入れられるのはたったひとつの子プロセスだけです。前回の内容を思い出して下さい。file を open したあと fork して、両方のプロセスでその file を読み込んだ場合、片方のプロセスでしか読み込むことができていなかったと思います。これと同じことが accept でも起こっているわけですね。

    さて、首尾よく accept できて新しいソケットを獲得した子プロセスは、クライアントからの入力受け取り無限地獄へと突入します。というわけで、今接続してきたクライアントとのやり取りは、この子プロセスにまかせることができました。一方、残念ながら accept できなかった他の子プロセスは、さっきとおなじところでブロックしたままです。

    さて、ここに、さらに新しいクライアントが接続してきた場合はどうなるでしょうか?accept でブロック中になっている、他の子プロセスがそのクライアントを accept してくれるわけですね。

    こんなふうに、あらかじめ子プロセスをいくらか fork しておいて、その子プロセスでクライアントからの接続を受け入れて処理するような仕組みを、「prefork」といいます。先に(pre)forkしておくサーバーってことですね。

    さて、これで無事、同時に複数のクライアントからの接続を受け入れることが可能になりました。今回は 3 回forkしたので、同時接続数は 3 つまでですね。サーバープロセスの他にもターミナルをたくさん立ち上げて、それぞれで telnet localhost 12345 してみてください。3つまでは同時に処理できるけど、4つを超えると同時に処理できてないことが見て取れるかと思います。

    今までの話で、preforkサーバーが書けて、さらにどうしてそんなことが可能なのかも理解できましたね!

    # preforkの利点、欠点

    さて、上に見たように、prefork サーバーはひとつのプロセスがひとつのクライアントを受け持つようなアーキテクチャになっています。このことは、いくつかの利点と欠点をもたらします。

    まず、上に見たように、worker(子プロセス)の数が少ないと、同時に処理できるクライアントの数が少なくなってしまいます。同時処理の数を増やしたければ、その数だけプロセスを生成する必要があるわけです。プロセスが生成されればそのぶんだけメモリーは消費されるので、この方法は意外と効率がよくないんですね。

    一方で、アーキテクチャが単純なので、コードが追いやすいです。シンプルであることは、とても大切なことです。さらに、ひとつのクライアントがひとつのプロセスで処理されるためたとえばプロセスが暴走したとかそういうときの影響範囲がせまくなります。ひとつのプロセス内でたくさんのクライアントを受け持つと、そのプロセスがなんかおかしなことになったときに影響範囲が大きくなって困りますね。

    # 次回予告

    次回はちょっと話を戻して、forkした際に親が先に死んだり終わったりしたらどうなるのとかそういう話をしたいなって思います。
  11. Shinpeim revised this gist Feb 13, 2013. 1 changed file with 4 additions and 0 deletions.
    4 changes: 4 additions & 0 deletions 004.md
    Original file line number Diff line number Diff line change
    @@ -69,6 +69,8 @@ OSのお仕事は、「プロセスからファイルの操作を頼まれたら

    これで、たとえばpidが100番のプロセスから「5番のファイルの、次の行読み込んでよ」と言われても、「ふぇぇ」ってならずに、「100番のプロセスさんの5番の番号札に紐づいたメモはこれだな」「メモには/path/to/fileの3行目まで読み込んだって書いてあるな」「じゃあこのファイルの4行目を読み込めばいいね!」「はいできた!」と言ってデータを返すことができるわけですね!

    このあたりのイメージは、このファイルの下に attach してある 004_01.png と 004_02.pngを見てみてください。

    # ファイルディスクリプタ/オープンファイル記述とfork

    さて、では、forkしたとき、ファイルディスクリプタやオープンファイル記述はどうなるのでしょうか?
    @@ -156,6 +158,8 @@ OSのお仕事は、「プロセスからファイルの操作を頼まれたら

    実行してみると、親プロセスがすでに番号札をOSに返してしまっても、子プロセスは複製された番号札を持っているので問題なくファイル操作ができているのが見て取れると思います。

    このあたりのイメージは、このファイルの下にattachしてある 004_03.png と 004_04.png を見てみてください。

    ## どうするのがベストプラクティスなの?

    すでにfileがopenされている状態でforkすると、以上に見たように予期せぬ動作で混乱することがあります。そのため、forkした場合、親プロセスで使わないファイルは親プロセスですぐ閉じる、子プロセスで使わないファイルは子プロセスですぐ閉じるとすると、最も問題が起きにくいと思います。子プロセスでファイルを閉じたとしても、親プロセスでファイル使いたい場合に問題なく扱える(またはその逆も)のは、上に見た通りですからね
  12. Shinpeim revised this gist Feb 13, 2013. 4 changed files with 0 additions and 0 deletions.
    Binary file added 004_01.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
    Binary file added 004_02.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
    Binary file added 004_03.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
    Binary file added 004_04.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
  13. Shinpeim revised this gist Feb 13, 2013. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion 004.md
    Original file line number Diff line number Diff line change
    @@ -67,7 +67,7 @@ OSのお仕事は、「プロセスからファイルの操作を頼まれたら

    このとき、この「ファイルの状況どうなってるっけメモ」にあたるのが、オープンファイル記述と呼ばれるものです。OSは、「ファイル開いて」っていうシステムコールを受け取ると、オープンファイル記述を作り出して自分で保持しておきます。さらに、システムコールを送ってきたプロセスのidに対して、新しい番号札(ファイルディスクリプタ)を返します。このとき、オープンファイル記述とプロセスidと番号札の関連も、自分の中に保持しておきます。

    これで、たとえばpidが100番のプロセスから「5番のファイルの、次の行読み込んでよ」と言われても、「ふぇぇ」ってならずに、「100番のプロセスさんの5番の番号札に紐づいたメモはこれだな」「じゃあこのファイルのここを読み込めばいいね!」「はいできた!」と言ってデータを返すことができるわけですね!
    これで、たとえばpidが100番のプロセスから「5番のファイルの、次の行読み込んでよ」と言われても、「ふぇぇ」ってならずに、「100番のプロセスさんの5番の番号札に紐づいたメモはこれだな」「メモには/path/to/fileの3行目まで読み込んだって書いてあるな」「じゃあこのファイルの4行目を読み込めばいいね!」「はいできた!」と言ってデータを返すことができるわけですね!

    # ファイルディスクリプタ/オープンファイル記述とfork

  14. Shinpeim revised this gist Feb 13, 2013. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion 004.md
    Original file line number Diff line number Diff line change
    @@ -58,7 +58,8 @@
    OSのお仕事は、「プロセスからファイルの操作を頼まれたら、代わりにやってあげること」です。そのためには、OSは実際のdiskの読み書きの他に、少なくとも以下の仕事をしないといけません。

    * プロセスに「ファイル開いて」って言われたら開いてあげる
    * 開いたファイルの情報(書き込みモードなのか読み込みモードなのか、とか、ファイルパスはどこなのかとか、どこまで読み込んだかあるいは書き込んだかとか)を「ファイルの状況どうなってるっけメモ」に書いておく
    * ファイルを開いたら、そのファイル専用の「ファイルの状況どうなってるっけメモ」を作る
    * 開いたファイルの情報(書き込みモードなのか読み込みモードなのか、とか、ファイルパスはどこなのかとか、どこまで読み込んだかあるいは書き込んだかとか)を「ファイルの状況どうなってるっけメモ」に書いておく
    * プロセスのために番号札を作って、さっき書いた「ファイルの状況どうなってるっけメモ」がどのプロセスのどの番号札のものなのかを覚えておく
    * プロセスに番号札を貸してあげる

  15. Shinpeim revised this gist Feb 12, 2013. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion 004.md
    Original file line number Diff line number Diff line change
    @@ -66,7 +66,7 @@ OSのお仕事は、「プロセスからファイルの操作を頼まれたら

    このとき、この「ファイルの状況どうなってるっけメモ」にあたるのが、オープンファイル記述と呼ばれるものです。OSは、「ファイル開いて」っていうシステムコールを受け取ると、オープンファイル記述を作り出して自分で保持しておきます。さらに、システムコールを送ってきたプロセスのidに対して、新しい番号札(ファイルディスクリプタ)を返します。このとき、オープンファイル記述とプロセスidと番号札の関連も、自分の中に保持しておきます。

    これで、たとえばpidが100番のプロセスから「5番の次の行読み込んでよ」と言われても、「ふぇぇ」ってならずに、「100番のプロセスさんの5番の番号札に紐づいたメモはこれだな」「じゃあこのファイルのここを読み込めばいいね!」「はいできた!」と言ってデータを返すことができるわけですね!
    これで、たとえばpidが100番のプロセスから「5番のファイルの、次の行読み込んでよ」と言われても、「ふぇぇ」ってならずに、「100番のプロセスさんの5番の番号札に紐づいたメモはこれだな」「じゃあこのファイルのここを読み込めばいいね!」「はいできた!」と言ってデータを返すことができるわけですね!

    # ファイルディスクリプタ/オープンファイル記述とfork

  16. Shinpeim revised this gist Feb 12, 2013. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion 004.md
    Original file line number Diff line number Diff line change
    @@ -59,7 +59,7 @@ OSのお仕事は、「プロセスからファイルの操作を頼まれたら

    * プロセスに「ファイル開いて」って言われたら開いてあげる
    * 開いたファイルの情報(書き込みモードなのか読み込みモードなのか、とか、ファイルパスはどこなのかとか、どこまで読み込んだかあるいは書き込んだかとか)を「ファイルの状況どうなってるっけメモ」に書いておく
    * プロセスに番号札を作って、さっき書いた「ファイルの状況どうなってるっけメモ」がどのプロセスのどの番号札のものなのかを覚えておく
    * プロセスのために番号札を作って、さっき書いた「ファイルの状況どうなってるっけメモ」がどのプロセスのどの番号札のものなのかを覚えておく
    * プロセスに番号札を貸してあげる

    「ファイルの状況どうなってるっけメモ」を保持しておかないと、「次の行読み込んでよ」ってプロセスから言われたときに「ふぇぇ、次の行ってどこ〜〜〜〜〜」ってなっちゃいますよね。あるいは、どの「ファイルの状況どうなってるっけメモ」がどのプロセスの何番の番号札と紐づいているのかを覚えておかないと、あるプロセスが「5番の番号のやつに書き込んでよ」って言ってきても、「ふぇぇ、書き込みたいけどどのメモ見ればいいのか忘れちゃったよ〜〜〜」ってなっちゃいます。
  17. Shinpeim revised this gist Feb 12, 2013. 1 changed file with 25 additions and 10 deletions.
    35 changes: 25 additions & 10 deletions 004.md
    Original file line number Diff line number Diff line change
    @@ -4,15 +4,29 @@

    前回はさらっと流してしまいましたが、実はプロセスは自分自身で実際にファイルを開いたりディスクに書き込んだりディスクからデータを読み出したりすることはありません。そういう低レイヤーの処理は、プロセスがシステムコールをOSに送ることで、OSが代わりに行ってくれます。そのあたりの話を、きちんと見て行きましょう。

    さて、なにはともあれ、プロセスが入出力をしたいと思ったら、ファイルを開くところから始めないといけません。OSは、プロセスから「ファイルを開いてね」というシステムコールを受け取ると、実際にファイルを開いたあと、そのファイルを表す「番号札」をプロセスに対して返します。プロセスは、ファイルから入力を受け取りたいときにはこの「番号札」をつかって、「n番の番号札で表されてるファイルから入力を読み込んでくれー」っていうシステムコールを送ったり、「n番の番号札で表されてるファイルにこれ書き込んでおいて」っていうシステムコールを送ったりすることで、OSにファイル入出力をお願いします。ファイルを閉じるときには、「n番の番号札で表されてるファイル、もう閉じていいよ」というシステムコールを送るさけですね。
    さて、なにはともあれ、プロセスが入出力をしたいと思ったら、ファイルを開くところから始めないといけません。

    この「番号札」のことを、「ファイルディスクリプタ」と呼びます。実際、ファイルディスクリプタは整数値で表現されています。
    * OSは、プロセスから「ファイルを開いてね」というシステムコールを受け取ると、実際にファイルを開きます
    * OSは、その開いたファイルを表す「番号札」作成します
    * OSは、その番号札をプロセスに対して返します。

    さて、ファイルを開いたら、今度はそこになにかを書き込んでみましょうか

    * プロセスは、さっき受け取った「番号札」をつかって、「n番の番号札で表されてるファイルにこれ書き込んでおいて」っていうシステムコールを送ります。
    * OSは、「番号札」で表された、すでに開かれているファイルに対して書き込みを行います

    じゃあ、今度はファイルを閉じましょう

    * プロセスは、不要になった番号札をcloseというシステムコールでOSに返却します
    * OSは、番号札が返却されたので、「もうこれは使わないんだな」と判断して、ファイルを閉じます

    と、こんな感じでファイルの入出力が行われているのですが、この「番号札」のことを、「ファイルディスクリプタ」と呼びます。実際、ファイルディスクリプタは整数値で表現されています。

    例を見てみましょう。今回もRubyを使います。

    # fd.rb
    file = File.open("nyan.txt","w") # openシステムコールでnyan.txtを書き込みモードでopen
    p file.fileno # fileno メソッドで、ファイルディスクリプタを取得
    p file.fileno # fileno メソッドで、ファイルディスクリプタ(番号札)を取得
    file.close #fileをclose

    1行目で、openシステムコールをOSに対して送っています。正常にopenされると、ファイルディスクリプタを内部に持ったfileオブジェクトが生成されます。2行目で、fileオブジェクトが保持しているファイルディスクリプタを取得してターミナルに出力しています。3行目で、fileを閉じていますが、これはRubyが内部でfileオブジェクトが保持しているファイルディスクリプタを使って、OSにcloseシステムコールを送っているわけです。IO#readlineとかIO#writeメソッドなんかも、内部ではIOオブジェクトが保持しているファイルディスクリプタを使って、読み込みのためのシステムコールを送ったり書き込みのためのシステムコールを使ってりしているわけですね。
    @@ -39,19 +53,20 @@

    # オープンファイル記述

    さて、今はプロセスの側からファイルディスクリプタについて見てみましたが、今度はOSの側から見てみましょう。
    さて、今はプロセスの側からがファイルディスクリプタをどう扱っているかについて見てみましたが、今度はOSの側から見てみましょう。

    OSのお仕事は、「プロセスからファイルの操作を頼まれたら、代わりにやってあげること」です。そのためには、OSは実際のdiskの読み書きの他に、少なくとも以下の仕事をしないといけません。

    * 「開いて」って言われたら開いてあげる
    * 開いたファイルの情報(書き込みモードなのか読み込みモードなのか、とか、ファイルパスはどこなのかとか、どこまで読み込んだかあるいは書き込んだかとか)を保持しておく「ファイルの状況どうなってるっけメモ」を書いておく
    * 「ファイルの状況どうなってるっけメモ」が、どのプロセスの何番の番号札とヒモづいているのかを覚えておく
    * プロセスに「ファイル開いて」って言われたら開いてあげる
    * 開いたファイルの情報(書き込みモードなのか読み込みモードなのか、とか、ファイルパスはどこなのかとか、どこまで読み込んだかあるいは書き込んだかとか)を「ファイルの状況どうなってるっけメモ」に書いておく
    * プロセスに番号札を作って、さっき書いた「ファイルの状況どうなってるっけメモ」がどのプロセスのどの番号札のものなのかを覚えておく
    * プロセスに番号札を貸してあげる

    「ファイルの状況どうなってるっけメモ」を保持しておかないと、「次の行読み込んでよ」ってプロセスから言われたときに「ふぇぇ、次の行ってどこ〜〜〜〜〜」ってなっちゃいますよね。あるいは、「ファイルの状況どうなってるっけメモ」が、どのプロセスの何番の番号札とヒモづいているのかを覚えておかないと、あるプロセスが「5番の番号のやつに書き込んでよ」って言ってきても、「ふぇぇ、書き込みたいけどメモが見つからなくてどのファイルのどんなところに書き込めばいいの〜〜〜〜」ってなっちゃいます。
    「ファイルの状況どうなってるっけメモ」を保持しておかないと、「次の行読み込んでよ」ってプロセスから言われたときに「ふぇぇ、次の行ってどこ〜〜〜〜〜」ってなっちゃいますよね。あるいは、どの「ファイルの状況どうなってるっけメモ」がどのプロセスの何番の番号札と紐づいているのかを覚えておかないと、あるプロセスが「5番の番号のやつに書き込んでよ」って言ってきても、「ふぇぇ、書き込みたいけどどのメモ見ればいいのか忘れちゃったよ〜〜〜」ってなっちゃいます。

    このとき、この「ファイルの状況どうなってるっけメモ」にあたるのが、オープンファイル記述と呼ばれるものです。OSは、「ファイル開いて」っていうシステムコールを受け取ると、オープンファイル記述を作り出して自分で保持しておきます。さらに、システムコールを送ってきたプロセスのidに対して、新しい番号札(ファイルディスクリプタ)を返します。このとき、オープンファイル記述とプロセスidと番号札の関連も、自分の中に保持しておきます。

    これで、たとえばpidが100番のプロセスから「5番の次の行読み込んでよ」と言われても、「ふぇぇ」ってならずに、「はいよ」と言って読み込んでデータを返すことができるわけですね
    これで、たとえばpidが100番のプロセスから「5番の次の行読み込んでよ」と言われても、「ふぇぇ」ってならずに、「100番のプロセスさんの5番の番号札に紐づいたメモはこれだな」「じゃあこのファイルのここを読み込めばいいね!」「はいできた!」と言ってデータを返すことができるわけですね

    # ファイルディスクリプタ/オープンファイル記述とfork

    @@ -95,7 +110,7 @@ OSのお仕事は、「プロセスからファイルの操作を頼まれたら
    end
    read_file.close

    子プロセスと親プロセスで、nyan.txtから一行ずつ入力を受け取っています。もし、forkされたときに「ファイルの状況どうなってるっけメモ」まで複製されているならば、親プロセスが一行読み込んだとき親プロセスの「ファイルの状況どうなってるっけメモ」は一行分進みますが、子プロセスの「ファイルの状況どうなってるっけメモ」は書き変わらないので、親プロセスでの読み込みは子プロセスでの読み込みに影響を与えないはずですね。つまり、親プロセスでも子プロセスでも、同じくファイルの内容を読み込むことができるはずです。逆に、同じ「ファイルの状況どうなってるっけメモ」を参照しているならば、親プロセスで一行読み込んだら、共通の「ファイルの状況どうなってるっけメモ」が1行分進んでしまい、子プロセスではその行を読み込むことができなくなってしまいます。
    子プロセスと親プロセスで、nyan.txtから一行ずつ入力を受け取っています。もしもforkされたときに「ファイルの状況どうなってるっけメモ」まで複製されているならば、親プロセスが一行読み込んだとき親プロセスの「ファイルの状況どうなってるっけメモ」は一行分進みますが、子プロセスの「ファイルの状況どうなってるっけメモ」は書き変わらないので、親プロセスでの読み込みは子プロセスでの読み込みに影響を与えないはずですね。つまり、親プロセスでも子プロセスでも、同じくファイルの内容をすべて読み込むことができるはずです。逆に、親と子が共通の「ファイルの状況どうなってるっけメモ」を参照しているならば、親プロセスで一行読み込んだら、共通の「ファイルの状況どうなってるっけメモ」が1行分進んでしまい、子プロセスではその行を読み込むことができなくなってしまいます。

    では実際に確かめて見ましょう。nyan.txtに以下の内容を書き込んだ上で、fork_fd.rbを実行してみましょう

  18. Shinpeim revised this gist Feb 12, 2013. 1 changed file with 150 additions and 0 deletions.
    150 changes: 150 additions & 0 deletions 004.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,150 @@
    # ファイルディスクリプタ

    さて、前回、プロセスがファイルを通じて外部との入出力する様を見て見ました。今回はさらにプロセスとファイル入出力について詳しく見てみましょう。

    前回はさらっと流してしまいましたが、実はプロセスは自分自身で実際にファイルを開いたりディスクに書き込んだりディスクからデータを読み出したりすることはありません。そういう低レイヤーの処理は、プロセスがシステムコールをOSに送ることで、OSが代わりに行ってくれます。そのあたりの話を、きちんと見て行きましょう。

    さて、なにはともあれ、プロセスが入出力をしたいと思ったら、ファイルを開くところから始めないといけません。OSは、プロセスから「ファイルを開いてね」というシステムコールを受け取ると、実際にファイルを開いたあと、そのファイルを表す「番号札」をプロセスに対して返します。プロセスは、ファイルから入力を受け取りたいときにはこの「番号札」をつかって、「n番の番号札で表されてるファイルから入力を読み込んでくれー」っていうシステムコールを送ったり、「n番の番号札で表されてるファイルにこれ書き込んでおいて」っていうシステムコールを送ったりすることで、OSにファイル入出力をお願いします。ファイルを閉じるときには、「n番の番号札で表されてるファイル、もう閉じていいよ」というシステムコールを送るさけですね。

    この「番号札」のことを、「ファイルディスクリプタ」と呼びます。実際、ファイルディスクリプタは整数値で表現されています。

    例を見てみましょう。今回もRubyを使います。

    # fd.rb
    file = File.open("nyan.txt","w") # openシステムコールでnyan.txtを書き込みモードでopen
    p file.fileno # fileno メソッドで、ファイルディスクリプタを取得
    file.close #fileをclose

    1行目で、openシステムコールをOSに対して送っています。正常にopenされると、ファイルディスクリプタを内部に持ったfileオブジェクトが生成されます。2行目で、fileオブジェクトが保持しているファイルディスクリプタを取得してターミナルに出力しています。3行目で、fileを閉じていますが、これはRubyが内部でfileオブジェクトが保持しているファイルディスクリプタを使って、OSにcloseシステムコールを送っているわけです。IO#readlineとかIO#writeメソッドなんかも、内部ではIOオブジェクトが保持しているファイルディスクリプタを使って、読み込みのためのシステムコールを送ったり書き込みのためのシステムコールを使ってりしているわけですね。

    さて、説明がすんだところで、実際にfd.rbを実行してみましょう。

    $ ruby fd.rb
    5

    「nyna.txtが書き込みモードで開かれたもの」についてる番号札が、5番なのが確認できましたね。

    # 標準入出力のファイルディスクリプタ

    さて、勘のいいひとはそろそろ例の標準入力は0、標準出力は1、標準エラー出力は2、という謎の数字の正体について、感付きつつあるのではないでしょうか。そうです。実は、「標準入力のファイルディスクリプタは0、標準出力のファイルディスクリプタは1、標準エラー出力のファイルディスクプタは2」なのです。実際に確かめてみましょう

    # std_fds.rb
    p $stdin.fileno # => 0
    p $stdout.fileno # => 1
    p $stderr.fileno # => 2

    おー。

    つまり、前回出てきた & という記号は、「ファイルパスじゃなくてファイルディスクリプタを指定してるぜ」という意味の記号だったわけですね!そして、なぜリダイレクトのときに標準入力や標準出力にあのような数字が使われているのかが理解できたと思います。

    # オープンファイル記述

    さて、今はプロセスの側からファイルディスクリプタについて見てみましたが、今度はOSの側から見てみましょう。

    OSのお仕事は、「プロセスからファイルの操作を頼まれたら、代わりにやってあげること」です。そのためには、OSは実際のdiskの読み書きの他に、少なくとも以下の仕事をしないといけません。

    * 「開いて」って言われたら開いてあげる
    * 開いたファイルの情報(書き込みモードなのか読み込みモードなのか、とか、ファイルパスはどこなのかとか、どこまで読み込んだかあるいは書き込んだかとか)を保持しておく「ファイルの状況どうなってるっけメモ」を書いておく
    * 「ファイルの状況どうなってるっけメモ」が、どのプロセスの何番の番号札とヒモづいているのかを覚えておく

    「ファイルの状況どうなってるっけメモ」を保持しておかないと、「次の行読み込んでよ」ってプロセスから言われたときに「ふぇぇ、次の行ってどこ〜〜〜〜〜」ってなっちゃいますよね。あるいは、「ファイルの状況どうなってるっけメモ」が、どのプロセスの何番の番号札とヒモづいているのかを覚えておかないと、あるプロセスが「5番の番号のやつに書き込んでよ」って言ってきても、「ふぇぇ、書き込みたいけどメモが見つからなくてどのファイルのどんなところに書き込めばいいの〜〜〜〜」ってなっちゃいます。

    このとき、この「ファイルの状況どうなってるっけメモ」にあたるのが、オープンファイル記述と呼ばれるものです。OSは、「ファイル開いて」っていうシステムコールを受け取ると、オープンファイル記述を作り出して自分で保持しておきます。さらに、システムコールを送ってきたプロセスのidに対して、新しい番号札(ファイルディスクリプタ)を返します。このとき、オープンファイル記述とプロセスidと番号札の関連も、自分の中に保持しておきます。

    これで、たとえばpidが100番のプロセスから「5番の次の行読み込んでよ」と言われても、「ふぇぇ」ってならずに、「はいよ」と言って読み込んでデータを返すことができるわけですね!

    # ファイルディスクリプタ/オープンファイル記述とfork

    さて、では、forkしたとき、ファイルディスクリプタやオープンファイル記述はどうなるのでしょうか?

    先に答えを言ってしまいましょう。forkした場合、ファイルディスクリプタは複製されますが、複製されたファイルディスクリプタは同一のオープンファイル記述を参照します。

    言い方を変えると、forkした場合、OSは新しいpidのために新しい番号札は作るけど、その番号札は同じ「ファイルの状況どうなってるっけメモ」に紐づけられてる、ということです。つまり、「ファイルの状況どうなってるっけメモ」は、親プロセスと子プロセスで共有するメモになります。

    そのため、forkしたときに同じ番号札(ファイルディスクリプタ)にたいして親プロセスと子プロセス両方で操作をすると、おかしなことになることがあります。

    ## オープンファイル記述は複製されない
    例を見ましょう。

    # fork_fd.rb
    # -*- coding: utf-8 -*-

    read_file = File.new("nyan.txt","r")

    # ファイルをopenしたあとにforkしてみる
    pid = Process.fork

    if pid.nil?
    # 子プロセス
    lines = []
    while line = read_file.gets
    lines << line
    end
    write_file = File.new("child.txt","w")
    write_file.write(lines.join)
    write_file.close
    else
    # 親プロセス
    lines = []
    while line = read_file.gets
    lines << line
    end
    write_file = File.new("parent.txt","w")
    write_file.write(lines.join)
    write_file.close
    end
    read_file.close

    子プロセスと親プロセスで、nyan.txtから一行ずつ入力を受け取っています。もし、forkされたときに「ファイルの状況どうなってるっけメモ」まで複製されているならば、親プロセスが一行読み込んだとき親プロセスの「ファイルの状況どうなってるっけメモ」は一行分進みますが、子プロセスの「ファイルの状況どうなってるっけメモ」は書き変わらないので、親プロセスでの読み込みは子プロセスでの読み込みに影響を与えないはずですね。つまり、親プロセスでも子プロセスでも、同じくファイルの内容を読み込むことができるはずです。逆に、同じ「ファイルの状況どうなってるっけメモ」を参照しているならば、親プロセスで一行読み込んだら、共通の「ファイルの状況どうなってるっけメモ」が1行分進んでしまい、子プロセスではその行を読み込むことができなくなってしまいます。

    では実際に確かめて見ましょう。nyan.txtに以下の内容を書き込んだ上で、fork_fd.rbを実行してみましょう

    nyan
    nyan nyan
    nyan nyan nyan
    nyan nyan nyan nyan
    nyan nyan nyan nyan nyan
    nyan nyan nyan nyan nyan nyan

    実行します

    $ ruby fork_fd.rb

    さて、結果はどうなったでしょうか?オープンファイル記述が複製されていないことが実感できたかと思います。

    ## ファイルディスクリプタは複製される

    では今度は、ファイルディスクリプタは複製されているのを見てみましょう


    # -*- coding: utf-8 -*-
    file = File.open("nyan.txt","r")

    # ファイルをopenしてからforkする

    pid = Process.fork

    if pid.nil?
    #子プロセス
    sleep 1 # 親プロセスがfileを閉じるのを待つ

    # 親プロセスがfdを閉じてても、自分はまだ番号札を持ってるから読み込める
    puts file.readlines.join

    file.close #自分も番号札を返す
    else
    # 親プロセス
    file.close #番号札をOSに返す
    Process.wait(pid) #子プロセスが終わるの待つ
    end

    実行してみると、親プロセスがすでに番号札をOSに返してしまっても、子プロセスは複製された番号札を持っているので問題なくファイル操作ができているのが見て取れると思います。

    ## どうするのがベストプラクティスなの?

    すでにfileがopenされている状態でforkすると、以上に見たように予期せぬ動作で混乱することがあります。そのため、forkした場合、親プロセスで使わないファイルは親プロセスですぐ閉じる、子プロセスで使わないファイルは子プロセスですぐ閉じるとすると、最も問題が起きにくいと思います。子プロセスでファイルを閉じたとしても、親プロセスでファイル使いたい場合に問題なく扱える(またはその逆も)のは、上に見た通りですからね


    # 次回予告

    ソケットの話してpreforkサーバーを自分で書いてみるつもり
  19. Shinpeim revised this gist Feb 12, 2013. 1 changed file with 4 additions and 2 deletions.
    6 changes: 4 additions & 2 deletions 000.md
    Original file line number Diff line number Diff line change
    @@ -2,16 +2,18 @@

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

    少しずつ書いてますが、更新したら http://nekogata.hatenablog.com/ でお知らせします

    # 目次(予定)

    * プロセス、スレッドと並列処理
    * プロセス、スレッド
    * プロセスについて
    * プロセスのライフサイクル
    * プロセスツリーについて
    * fork
    * exec
    * プロセスとファイル入出力
    * シグナル
    * パイプライン処理(標準入出力)
    * スレッドについて
    * プロセスとの違い
    * リソースの共有について
  20. Shinpeim revised this gist Feb 12, 2013. No changes.
  21. Shinpeim revised this gist Feb 12, 2013. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion 001.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    # プロセス、スレッドと並列処理
    # プロセス、スレッド

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

  22. Shinpeim revised this gist Feb 12, 2013. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions 003.md
    Original file line number Diff line number Diff line change
    @@ -126,7 +126,7 @@ Ctrl+D を押すと、EOFというものが入力されます。この「EOF」

    $ ruby print_mew.rb 1>>mew.txt

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

    $ ruby print_mew.rb 1>mew.txt

    @@ -137,6 +137,7 @@ Ctrl+D を押すと、EOFというものが入力されます。この「EOF」
    $ ruby print_mew.rb > mew.txt

    ## 標準入力のリダイレクト

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

    試しに、さっき作った mew.txt というファイルを標準入力としてみましょう。
    @@ -151,7 +152,6 @@ Ctrl+D を押すと、EOFというものが入力されます。この「EOF」
    $ ruby stdin.rb < mew.txt
    mew


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

    $ ruby stdin.rb 0<mew.txt 1>mew_copy.txt
  23. Shinpeim revised this gist Feb 12, 2013. 1 changed file with 11 additions and 5 deletions.
    16 changes: 11 additions & 5 deletions 003.md
    Original file line number Diff line number Diff line change
    @@ -112,6 +112,7 @@ Ctrl+D を押すと、EOFというものが入力されます。この「EOF」

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

    ## 標準出力のリダイレクト
    例えば、

    # print_mew.rb
    @@ -121,7 +122,7 @@ Ctrl+D を押すと、EOFというものが入力されます。この「EOF」

    $ ruby print_mew.rb 1>mew.txt

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

    $ ruby print_mew.rb 1>>mew.txt

    @@ -135,14 +136,15 @@ Ctrl+D を押すと、EOFというものが入力されます。この「EOF」

    $ ruby print_mew.rb > mew.txt

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

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

    $ ruby stdin.rb 0<mew.txt
    mew

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

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

    @@ -152,9 +154,13 @@ Ctrl+D を押すと、EOFというものが入力されます。この「EOF」

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

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

    上記の場合、mew.txt の内容が stdin.rb によって読み込まれ、標準出力に書き出されます。標準出力は mew_copy.txt をさしているので、mew_copy.txtという新しいファイルに mew が書き込まれることになります。
    上記の場合、stdin.rbの標準入力はmew.txtとなり、標準出力は mew_copy.txt となります。

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

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

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

    @@ -168,7 +174,7 @@ Ctrl+D を押すと、EOFというものが入力されます。この「EOF」

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

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

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

  24. Shinpeim revised this gist Feb 12, 2013. 1 changed file with 13 additions and 7 deletions.
    20 changes: 13 additions & 7 deletions 003.md
    Original file line number Diff line number Diff line change
    @@ -110,7 +110,7 @@ Ctrl+D を押すと、EOFというものが入力されます。この「EOF」

    # リダイレクト

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

    例えば、

    @@ -121,11 +121,11 @@ Ctrl+D を押すと、EOFというものが入力されます。この「EOF」

    $ ruby print_mew.rb 1>mew.txt

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

    $ ruby print_mew.rb 1>>mew.txt

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

    $ ruby print_mew.rb 1>mew.txt

    @@ -135,22 +135,28 @@ Ctrl+D を押すと、EOFというものが入力されます。この「EOF」

    $ ruby print_mew.rb > mew.txt

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

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

    $ ruby stdin.rb 0<mew.txt
    mew

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

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

    $ 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"
    @@ -170,7 +176,7 @@ mew.txtの内容がstdin.rbによって読み込まれ、ターミナルに書

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

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

    # パイプ

  25. Shinpeim revised this gist Feb 12, 2013. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion 003.md
    Original file line number Diff line number Diff line change
    @@ -94,7 +94,7 @@ rubyの組み込みグローバル変数 $stdout には、「標準出力」と

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

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

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

  26. Shinpeim revised this gist Feb 12, 2013. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion 003.md
    Original file line number Diff line number Diff line change
    @@ -96,7 +96,7 @@ rubyの組み込みグローバル変数 $stdout には、「標準出力」と

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

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

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

  27. Shinpeim revised this gist Feb 12, 2013. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion 003.md
    Original file line number Diff line number Diff line change
    @@ -77,7 +77,7 @@ rubyの組み込みグローバル変数 $stdout には、「標準出力」と

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

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

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

    @@ -102,6 +102,8 @@ Ctrl+D を押すと、EOFというものが入力されます。この「EOF」

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

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

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

    先ほどから標準入出力のデフォルトはどうこうみたいな話をしていますが、それはつまり標準入出力はその他の場所にもできるってことですね。そのための機能が「リダイレクト」と「パイプ」です。
  28. Shinpeim revised this gist Feb 12, 2013. 1 changed file with 185 additions and 0 deletions.
    185 changes: 185 additions & 0 deletions 003.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,185 @@
    # プロセスとファイル入出力

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

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

    まず、以下のようなテキストファイルを 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というのはかなり遅い処理の部類です)なので、普段はあまり意識しないかもしれませんが。

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

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

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

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

    # リダイレクト

    リダイレクトを使うと、標準入出力に別の場所を指定することができます。ちなみに、シェル上(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の話ができたらいいな!さーて、次回も、サービス!サービスゥ!
  29. Shinpeim revised this gist Feb 12, 2013. 1 changed file with 0 additions and 185 deletions.
    185 changes: 0 additions & 185 deletions 003.md
    Original file line number Diff line number Diff line change
    @@ -1,185 +0,0 @@
    # プロセスとファイル入出力

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

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

    まず、以下のようなテキストファイルを 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というのはかなり遅い処理の部類です)なので、普段はあまり意識しないかもしれませんが。

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

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

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

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

    # リダイレクト

    リダイレクトを使うと、標準入出力に別の場所を指定することができます。ちなみに、シェル上では、標準出力は「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"が書き出されているかと思います。

    ちなみに、標準エラー出力を標準出力へ向けることもできます。

    $ 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の話ができたらいいな!さーて、次回も、サービス!サービスゥ!
  30. Shinpeim revised this gist Feb 12, 2013. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion 003.md
    Original file line number Diff line number Diff line change
    @@ -98,7 +98,7 @@ Ctrl+D を押すと、EOFというものが入力されます。この「EOF」

    ところで、最初の例では標準入力ではなくてnyan.txtを読み込んでいましたが、実はその間にも、一瞬プロセスは「ブロック中」状態になっています。ディスクからデータを読みこんでくるのが一瞬(とはいえ、コンピューターの処理の中ではdiskIOというのはかなり遅い処理の部類です)なので、普段はあまり意識しないかもしれませんが。

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

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