├── .gitignore ├── images ├── 004_01.png ├── 004_02.png ├── 004_03.png └── 004_04.png ├── Gemfile ├── release ├── process_book.pdf ├── process_book.epub └── process_book.html ├── CONTRIBUTING ├── Rakefile ├── README.md ├── script └── export_html.rb ├── 001.md ├── 006.md ├── styles └── epub.css ├── 002.md ├── 007.md ├── 008.md ├── 003.md ├── 004.md └── 005.md /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | .bundle/ 3 | working/ 4 | -------------------------------------------------------------------------------- /images/004_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rummelonp/process-book/master/images/004_01.png -------------------------------------------------------------------------------- /images/004_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rummelonp/process-book/master/images/004_02.png -------------------------------------------------------------------------------- /images/004_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rummelonp/process-book/master/images/004_03.png -------------------------------------------------------------------------------- /images/004_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rummelonp/process-book/master/images/004_04.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rake' 4 | gem 'redcarpet' 5 | gem 'wkhtmltopdf' 6 | -------------------------------------------------------------------------------- /release/process_book.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rummelonp/process-book/master/release/process_book.pdf -------------------------------------------------------------------------------- /release/process_book.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rummelonp/process-book/master/release/process_book.epub -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | markdown本文を修正した場合、releaseディレクトリの中身も更新してください。 2 | 3 | calibreというツールとそのコマンドラインツールをインストールして、 4 | 5 | $ bundle install 6 | $ bundle exec rake 7 | 8 | すればOKです。 9 | 10 | calibreインストールするのがむずかしいとかruby環境持ってないみたいな感じならば、そのまま Pull Request ください。こちらでやります。 -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :default => "book" 2 | 3 | task book: [:epub, :pdf, :clean] 4 | 5 | if File.exists?('working') 6 | sh "rm -rf ./working" 7 | end 8 | sh "mkdir ./working" 9 | sh "cp *.md working/" 10 | sh "mv working/README.md working/000.README.md" 11 | 12 | file html: Dir.glob('./working/*.md') do |task| 13 | sh "script/export_html.rb #{task.prerequisites.join(' ')} > release/process_book.html" 14 | end 15 | 16 | task epub: %w[html] do 17 | sh "ebook-convert release/process_book.html release/process_book.epub --no-default-epub-cover" 18 | end 19 | 20 | task pdf: %w[html] do 21 | sh "wkhtmltopdf release/process_book.html release/process_book.pdf --encoding utf8" 22 | end 23 | 24 | task :clean do 25 | sh "rm -rf working" 26 | end 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## この文書はなんですか? 2 | 3 | この文書は*nix系のシステムにおけるプロセスやシグナルなどについて説明することを目的に書かれました。「プロセスとかよくわかってないからちゃんと知りたいな」みたいなひとたちが想定読者です。 4 | 5 | 書いているあいだは gist で管理されていたのですが、ボリュームが大きくなったので github で管理するように変えました。 6 | 7 | ## 目次 8 | 9 | [導入](https://github.com/Shinpeim/process-book/blob/master/001.md) 10 | 11 | [プロセスの生成](https://github.com/Shinpeim/process-book/blob/master/002.md) 12 | 13 | [プロセスとファイル入出力](https://github.com/Shinpeim/process-book/blob/master/003.md) 14 | 15 | [ファイルディスクリプタ](https://github.com/Shinpeim/process-book/blob/master/004.md) 16 | 17 | [preforkサーバーを作ってみよう](https://github.com/Shinpeim/process-book/blob/master/005.md) 18 | 19 | [ゾンビプロセスと孤児プロセス](https://github.com/Shinpeim/process-book/blob/master/006.md) 20 | 21 | [シグナルとkill](https://github.com/Shinpeim/process-book/blob/master/007.md) 22 | 23 | [プロセスグループとフォアグランドプロセス](https://github.com/Shinpeim/process-book/blob/master/008.md) 24 | -------------------------------------------------------------------------------- /script/export_html.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | require 'redcarpet' 5 | 6 | contributors = `git log --pretty=format:'%cn'` 7 | contributors = contributors.split("\n") 8 | contributors.sort! 9 | contributors.uniq! 10 | contributors.delete_if do |e| 11 | e =~ /shinpei maruyama/i 12 | end 13 | 14 | # TODO title とAuthor, 著作権表示は書き換えて下さい 15 | HEADER = < 17 | 18 | Process Book 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 |

Process Book

28 |
29 |

Author: Shinpeim

30 |

Contributors: #{contributors.join ", "}

31 |

32 |
33 |
34 | HEAD 35 | 36 | def replace_index(html) 37 | h2_id = 0 38 | a_id = 2 39 | mod = '' 40 | html.each_line do |line| 41 | if line.gsub! %r!

!, %!

! 42 | h2_id += 1 43 | elsif line.gsub! %r!

!, %!

! 44 | a_id += 1 45 | end 46 | mod += line 47 | end 48 | mod 49 | end 50 | 51 | def replace(html) 52 | mod = replace_index(html) 53 | mod.gsub %r! true, :space_after_headers => true) 57 | STDOUT.write HEADER 58 | STDOUT.write replace(markdown.render(ARGF.readlines.join '')) 59 | STDOUT.write "\n" 60 | -------------------------------------------------------------------------------- /001.md: -------------------------------------------------------------------------------- 1 | ## 導入 2 | 3 | ### マルチプロセスとスケジューリング 4 | 5 | \*nix系のシステムは、もともと複数のユーザーが同じコンピューターリソース(CPUやメモリー)を同時に使うことを前提に作られています。そのため、\*nix系のシステムでは様々な処理が同時に行われるような仕組みになっています。実際、小規模なwebサービスでは nginx と unicorn と MySQL がひとつのマシンで同時に走っているような状況は珍しくないのではないでしょうか。 6 | 7 | いまはカジュアルに「同時に複数の処理が走っている」と言ってしまいましたが、マシンが持っているCPU(脳みそ)は限られた数なのに、どうやって複数の処理を同時に行っているのでしょうか? ひとつの脳みそでどうやって複数のことを同時に考えているのでしょうか? その答えは、「本当は同時に処理は行われていなくて、OSが目にも留まらぬ早さで複数の処理を切り替えているせいであたかも同時に複数の処理が行われているように見える」です。図にするとこういう感じ。 8 | 9 | A ----- ---- --------- ---------- 10 | B --- -------- ---- -- ------- 11 | C ----- ---------- --- -- 12 | 13 | 14 | OSは、上記のように処理A,B,Cを順々に切り替えながら少しずつ処理していきます。この切り替えのタイミングがめっちゃ早いため、人間にはまるで同時に処理されているかのように見えるわけです。この切り替えをする処理の単位が、プロセス(やスレッド)です。上図の場合だと、Aというプロセス、Bというプロセス、CというプロセスをOSがすごい早さで切り替えながら処理しているようなイメージですね。このように、プロセスやスレッドを上手に使うことで、同時に複数の計算が可能になるわけです。 15 | 16 | ちなみに、この切り替えをどういう戦略やタイミングで行うかのことを、「スケジューリング」と言います。このへんはOSが面倒を見てくれますが、niceというコマンドで「これははあんまり重要じゃないプロセスなんで、優先度低めでいいよ」という情報をOSに教えたりできて、そうするとOSさんはそのプロセスを処理する時間を少なめにスケジューリングしたりします。 17 | 18 | ### マルチコアとの関係 19 | 20 | 最近のマシンのCPUはコアが複数あるのが普通です。コアがひとつだけならば、ひとつのコアで全てのプロセスをスケジューリングする必要があるわけですが、コアが複数あるため、下記のような感じで今度は「ほんとうの」同時処理が可能になります。 21 | 22 | 23 | + ------ + A ----- ---- --------- ---------- 24 | | core 1 | B --- -------- ---- -- ------- 25 | + ------ + C ----- ---------- --- -- 26 | + ------ + D --- ---- --------- ---------- 27 | | core 2 | E --- -------- ---- -- ------- 28 | + ------ + F ---- ---------- --- -- 29 | 30 | 大規模なデータを処理する場合などには、ひとつのコアだけではなく複数のコアを無駄なく使うためにも、複数のプロセスや複数のスレッドで処理を行う必要が出てくるわけです。 31 | 32 | ただ、スレッドに関しては、OSが面倒を見てくれるスレッド(いわゆるネイティブスレッド)と、例えば言語処理系やVMが面倒見てくれるスレッド(いわゆるグリーンスレッド)があって、グリーンスレッドの中にはいくらスレッドを増やしてもコアをひとつしか使えないようなものもあります。CRubyの1.8などがその例ですね。スレッドと一口に言ってもどのような実装になっているかによって特徴が変わってくるので、自分が使っている環境の「スレッド」というのがどのような仕組みをさしているのかは意識しておく必要があるでしょう。 33 | 34 | ### 次回予告 35 | 36 | 次回はプロセスについてもう少し深くまでもぐって見ていきます。 37 | -------------------------------------------------------------------------------- /006.md: -------------------------------------------------------------------------------- 1 | ## ゾンビプロセスと孤児プロセス 2 | 3 | さて、前回までで fork とかファイルとかのことはだいたいわかってきたかと思います。今回は、「親が死んだ子供は養子になるしかない」「子供が親の見てないところで死ぬとゾンビになってしまう」という話をします。 4 | 5 | ### 親が死んだ子供は養子になるしかない 6 | 7 | 今回はここ数日なにかと話題の Perl で行きましょう。 8 | 9 | use strict; 10 | use warnings; 11 | 12 | my $pid = fork; 13 | 14 | if ( $pid ) { 15 | # 親プロセスで waitpid しないで 16 | # 死んじゃう 17 | sleep 1; 18 | exit; 19 | } 20 | else { 21 | #子プロセス 22 | 23 | # getppid で親プロセスのpidを取得する 24 | print getppid."\n"; 25 | 26 | #親が死ぬまで待つ 27 | sleep 2; 28 | 29 | # 親が死んだあとの親プロセスのpidって?? 30 | print getppid."\n"; 31 | } 32 | 33 | さて、上のような perl スクリプトを実行すると、結果はどうなるでしょうか。子プロセスのほうの一回目の getppid では、まだ親プロセスが生きているので、当然そのプロセスの pid が表示されます。1秒後、親プロセスが終わるのでプロンプトが戻ってきます。そして、そのさらに1秒後、二度目の getppid が実行され、すでにプロンプトが戻っているターミナルに「1」と表示されるはずです。 34 | 35 | これはどういうことかと言うと、親が死んだから、init さんのところに養子に入ったわけですね。親プロセスに先立たれて親の pid が存在しなくなったプロセスは、init が代わりに親プロセスとして振る舞ってくれます。 36 | 37 | ### 「子供が親の見てないところで死ぬとゾンビになってしまう」 38 | 39 | 「ゾンビプロセス」ってのを聞いたことあると思うんですけど、これはどういうプロセスなんでしょうか。コードで見てみましょう 40 | 41 | use strict; 42 | use warnings; 43 | 44 | my $pid = fork; 45 | 46 | if ( $pid ) { 47 | print "$pid\n"; 48 | 49 | # 無限ループに忙しくて子プロセスを wait してない 50 | while ( 1 ) { 51 | sleep; 52 | } 53 | } 54 | else { 55 | # 子プロセスは即死する 56 | exit; 57 | } 58 | 59 | 上記のようなスクリプトを zombie.pl として保存して、バックグラウンド実行してみましょう 60 | 61 | $ perl zombie.pl & 62 | 63 | 親プロセスの print "$pid\n" が利いて、子プロセスのpidが出力されたかと思います。さて、この親プロセスは、まだバックグラウンドで無限ループしています。一方、子プロセスは即 exit しているので、もう実行が終了しています。しかし、親はこの終了を wait していません。この子プロセスは、実行がおわってもう死んでいるのに、誰にも看取られていない(wait されていない)状態です。 64 | 65 | そこで、先ほどターミナルに表示された pid がどうなっているのか、ps コマンドで確認してみましょう。 66 | 67 | $ ps <さっきターミナルに表示されたpid> 68 | 69 | どうなりましたか?環境によって多少の違いはあるかもしれませんが、私の環境では 70 | 71 | PID TTY STAT TIME COMMAND 72 | 3668 pts/2 Z 0:00 [perl] 73 | 74 | と表示されました。STAT の部分に Z と出ていますね。これは、このプロセスがゾンビプロセスとなっていることを表します。 75 | 76 | それでは、無限ループ中の親プロセスを fg でフォアグラウンドに戻して、Ctrl + Cで止めましょう。その状態で再度 ps でプロセスの状態を見てみると、さっきまでゾンビだったプロセスも、無事に成仏してなくなっていることが確認できると思います。 77 | 78 | なにが起こったのか、説明しましょう。 79 | 80 | さきほど、Ctrl+C で親プロセスを終了しましたが、この親は子を wait しないで死んでしまいました。ゾンビ状態だった子プロセスは、親プロセスが死んでしまったことにより、init さんの養子に入ります。すると、init さんが即座に wait でこのプロセスを看取ってくれて、無事にゾンビ状態だったプロセスが終了できたわけです。 81 | 82 | ### まとめ 83 | 84 | さて、このことから、以下のようなことがわかります。 85 | 86 | * 子プロセスが実行終了しているにもかかわらず、親プロセスに wait されないとプロセスが回収されず、ゾンビプロセスとして残ってしまう 87 | * 親プロセスが子プロセスを wait せずに先に死んでしまったときには、initプロセスが代わりに親となって子プロセスを wait してくれる 88 | 89 | 短いですが、今日はこんなところで。 90 | 91 | ### 次回予告 92 | 93 | 次回はようやくシグナルについて書く予定です。共有メモリの話と、スレッドの話もその後にできればしたいけど、気力ないかもしれないので次回が最終回の可能性が微レ存…… 94 | -------------------------------------------------------------------------------- /styles/epub.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | /* reset */ 4 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, 5 | p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, 6 | dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, 7 | sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, 8 | label, legend, table, caption, tbody, tfoot, thead, tr, th, td { 9 | margin: 0; 10 | padding: 0; 11 | border: 0; 12 | outline: 0; 13 | font-size: 100%; 14 | vertical-align: baseline; 15 | background: transparent; } 16 | /* end reset */ 17 | 18 | @page { 19 | margin: 5px !important; 20 | } 21 | 22 | /* p margin */ 23 | p { 24 | margin: 8px 0 0; 25 | } 26 | 27 | /* inlines; code and monospace in the code section below */ 28 | span.bolditalic { 29 | font-weight: bold; 30 | font-style: italic; 31 | } 32 | 33 | strong, span.bold { 34 | font-weight: bold; 35 | font-family: serif; 36 | } 37 | 38 | a.ulink, a.xref, a.email { 39 | text-decoration: none; 40 | color: #8e0012; 41 | } 42 | 43 | sup { /* bug in webkit? */ 44 | font-size: x-small; 45 | vertical-align: super; 46 | } 47 | 48 | sub { 49 | font-size: smaller; 50 | vertical-align: sub; 51 | } 52 | 53 | /* headings */ 54 | h1 { 55 | font-size: 1.5em; 56 | font-weight: bold; 57 | font-family: "Free Sans Bold", sans-serif; 58 | margin-top: 20px !important; 59 | } 60 | 61 | h2 { 62 | font-size: 1.3em; 63 | font-weight: bold; 64 | font-family: "Free Sans Bold", sans-serif; 65 | color: #8e0012; 66 | margin: 15px 0 8px 0 !important; 67 | text-decoration:underline; 68 | } 69 | 70 | h3 { 71 | font-size: 1.1em; 72 | font-weight: bold; 73 | font-family: "Free Sans Bold", sans-serif; 74 | margin: 10px 0 8px 0 !important; 75 | } 76 | 77 | h4 { 78 | font-size: bold; 79 | font-weight: 1em; 80 | font-family: "Free Sans Bold", sans-serif; 81 | color: #555; 82 | margin: 9px 0 !important; 83 | } 84 | 85 | /* Appropriate heading hyphenation suppression for different ereaders */ 86 | h1, h2, h3, h4 { 87 | -webkit-hyphens: none; 88 | hyphens: none; 89 | adobe-hyphenate: none; 90 | } 91 | 92 | /* images */ 93 | img { 94 | max-width: 95%; 95 | margin: 0 auto; 96 | padding: 0; 97 | } 98 | 99 | /* code */ 100 | pre { 101 | white-space: pre-wrap; 102 | font-family: "Ubuntu Mono", monospace; 103 | margin: 10px 0 10px 20px; 104 | font-size: 85%; 105 | display: block; 106 | -webkit-hyphens: none; 107 | hyphens: none; 108 | adobe-hyphenate: none; 109 | } 110 | 111 | code { 112 | font-family: "Ubuntu Mono", monospace; 113 | } 114 | 115 | code strong em, code em strong, pre em strong, pre strong em, 116 | strong code em code, em code strong code, span.bolditalic code { /* yes, all of these variations are needed */ 117 | font-weight: bold; 118 | font-style: italic; 119 | font-family: "Ubuntu Mono BoldItal", monospace; 120 | } 121 | 122 | code em, em code, pre em, em.replaceable { 123 | font-family: "Ubuntu Mono Ital", monospace; 124 | font-style: italic; 125 | } 126 | 127 | code strong, strong code, pre strong, strong.userinput { 128 | font-family: "Ubuntu Mono Bold", monospace; 129 | font-weight: bold; 130 | } 131 | 132 | /* title and copyright page */ 133 | div.book div.titlepage h1.title { 134 | font-size: 2.2em; 135 | text-align: center; 136 | margin-bottom: 10px !important; 137 | line-height: 1; 138 | } 139 | 140 | div.book div.titlepage div.author h3 { 141 | font-size: 1.6em; 142 | font-family: "Free Sans Bold", sans-serif; 143 | text-align: center; 144 | color: #8e0012; 145 | margin: 10px 0 !important; 146 | } 147 | 148 | h2.subtitle { 149 | font: bold italic 1.1em; 150 | font-family: "Free Sans Bold", sans-serif; 151 | text-align: center; 152 | color: #000; 153 | margin-top: 5px !important; 154 | } 155 | 156 | div.titlepage p.copyright { margin: 20px 0 !important; } 157 | 158 | div.legalnotice { 159 | margin: 0 !important; 160 | font-size: 90%; 161 | } 162 | 163 | div.legalnotice p { margin: 5px 0 !important; } 164 | 165 | div.titlepage hr { 166 | width: 50%; 167 | } 168 | 169 | div.editor h4 { 170 | color: #000; 171 | margin: 20px 0 0 0 !important; 172 | font-weight: bold; 173 | font-style: normal; 174 | font-size: 1em; 175 | } 176 | 177 | div.editor h3 { 178 | margin: 0; 179 | font-family: serif; 180 | font-size: 1em; 181 | font-weight: normal 182 | } 183 | 184 | div.dedication p { 185 | text-align: center; 186 | font-style: italic; 187 | font-family: "Free Serif", serif; 188 | } 189 | 190 | div.dedication div.titlepage h1.title { 191 | text-transform: uppercase; 192 | font-size: 1em; 193 | } 194 | 195 | div.affiliation span.orgname, span.jobtitle { 196 | padding-left: 30px !important; 197 | font-size: smaller; 198 | } 199 | 200 | /* keep this body statement last */ 201 | body { 202 | font-family: "Free Serif", serif; 203 | } 204 | -------------------------------------------------------------------------------- /002.md: -------------------------------------------------------------------------------- 1 | ## プロセスの生成 2 | 3 | ### プロセスの例 4 | 5 | 前回、プロセスとはOSが処理を切り替えるときの処理の単位だという話をしましたが、まずはプロセスの例を見てみましょう 6 | 7 | ターミナルで、 8 | 9 | $ ps 10 | 11 | と入力してみるましょう。psは今実行中のプロセスの一覧を見ることができるコマンドです。オプションなしで実行すると自分が実行中のプロセスの一覧が見れます。で、psを実行してみると、(環境によって異なるかと思いますが)以下のような文字が出力されるかと思います。 12 | 13 | PID TTY TIME CMD 14 | 4400 pts/2 00:00:00 bash 15 | 4419 pts/2 00:00:00 ps 16 | 17 | 一番右を見ると、(この場合は)bashというプロセスとpsというプロセスが実行されていることがわかります。bashはログインシェル、psはいまさっき打ったpsコマンドですね。ちなみに、一番左のPIDという列は、そのプロセスのidで、実行されているプロセスを一意に判別するために使われているものです。 18 | 19 | では、今度は & つきでバックグラウンドでコマンドを実行してみましょう。 20 | 21 | $ perl -e 'while(1){sleep}' & 22 | 23 | ただsleepし続けるだけのperlのワンライナーです。この状態で、もう一度 24 | 25 | $ ps 26 | 27 | と入力してみると、 28 | 29 | PID TTY TIME CMD 30 | 4420 pts/2 00:00:00 perl 31 | 32 | のような、さっきは存在していなかったプロセスが新しく増えているのがわかると思います。これがさきほど実行した 33 | 34 | $ perl -e 'while(1){sleep}' & 35 | 36 | コマンドのプロセスです。新しく処理を始めたら新しくプロセスが生成されたのがわかるかと思います。 37 | 38 | さて、バックグラウンドで実行中のsleepするだけのプロセスですが、今度は 39 | 40 | $ fg 41 | 42 | でフォアグラウンドに処理を戻して、 Ctrl+C かなんかで処理を止めましょう。その後再度 ps コマンドでプロセスの一覧を確認すると、perlのプロセスが無くなっていることが確認できるかと思います。 43 | 44 | ### プロセスのライフサイクル 45 | 46 | プロセスは、なんらかの方法で生成されたあとは、ぐんぐん処理を行っていき、処理が終わったり外部から止められたりすると消滅します。 47 | 48 | 生成 -> 処理中 -> 終了 49 | 50 | というライフサイクルを持っているわけです。今簡単に「処理中」と書いてしまいましたが、大きくわけてこの「処理中」には3つの状態があります。 51 | 52 | * 走行中 53 | * 待ち状態 54 | * ブロック中 55 | 56 | 「えっ待ち状態とブロック中ってなにが違うの」という疑問を持ったかた、ごもっともです。でも、その違いは簡単です。「待ち状態」というのは、「もうすぐにでも処理できるよ!CPUさん、はやくわたしを処理して!」という状態のことです。一方、「ブロック中」というのは、たとえばファイルの読み込みを行うときにdisk I/Oを待っているなどで、「今CPUさんが私を処理しようとしても私まだIO待ちだから何もできないよ!」みたいな状態のことです。 57 | 58 | ### fork 59 | 60 | さて、さきほど簡単に「プロセスをなんらかの方法で生成」と言いましたが、たとえば新しくコマンドを叩いて新しいプロセスが生成されるとき、中では何が起きてるのでしょうか? 61 | 62 | 通常、プロセスは、「親プロセス」がforkというシステムコールをOSに送ることによって生成されます。すると、OSは親プロセスをまるっと複製して、「子プロセス」を新しく生成します。このとき、メモリの状態は親プロセスから子プロセスにまるっとコピーされます[^1]。コピーされて新しい環境が出来上がるため、親プロセスでなにか操作しても(変数に新しい値代入するとか新しくインスタンスを生成するとか)、その操作は子プロセスに影響を与えません。親でなんか変更したからといって、子にもその変更が伝わるみたいなことはないわけです。逆もまたしかりで、子プロセスでなにか操作しても、その変化は親プロセスに影響を与えません。 63 | 64 | [^1]:「えっ、まるまるメモリーをコピーするの、そんなのメモリーの無駄じゃないの」と思われる方もいるかもしれませんが、そこはよくできていて、COW(Copy On Write)という方法を使うことで、うまいこと無駄なメモリーを食わないようになっています。 65 | 66 | こうして、forkにより新しくプロセスが生まれると、OSによりそのプロセス専用の環境が用意されて、その中でいろんな処理が行えるようになるわけです。 67 | 68 | こんなふうに、forkによってプロセスは生成されるため、基本的に全てのプロセスには「自分を生んだ親プロセス」が存在することになります。 69 | 70 | ちなみに、forkは複数行うことができるので、「子だくさん」なプロセスというのも、あり得ます。preforkのサーバープロセスなんかは子供をたくさん作って、複数の接続のひとつひとつをそれぞれひとつの子供に処理させることで並列性を上げているわけですね。子供たちを酷使するひどいやつです。 71 | 72 | ### プロセスツリー 73 | 74 | さきほど「親プロセスがforkで子プロセス作るんだよ〜〜。だからみんな親がいるんだよ〜〜〜」ってゆるふわな感じで言いましたが、当然「えっじゃあ、その親プロセスは誰が作ったの?」という疑問がわいてきますよね。疑問にお答えしましょう。親プロセスは、「親プロセスの親プロセス」がforkで作ったのです。となると、当然「えっじゃあ、その『親プロセスの親プロセス』はだれが作ったの」いう疑問がわいてきますよね。もちろん、「親プロセスの親プロセスの親プロセス」がforkで作ったのです。となると当然(ry 75 | 76 | というように、全てのプロセスはどんどんその「親」を辿って行くことができます。そんなわけで、全てのプロセスの祖先となる「最初のプロセス」というものが存在しないといけないわけです。このプロセスはブート時に生成されて、そのあと全てのプロセスがここを祖先としてforkされていきます。この「最初のプロセス」はPIDが1であり、Linuxの場合は init というプロセスがその実体となります。 77 | 78 | $ ps ax | grep init 79 | 1 ? Ss 0:10 /sbin/init 80 | 81 | このように、プロセスは親子関係の木構造を持っています。この親子関係を「プロセスツリー」と呼びます。プロセスツリーがどうなっているかを調べるためにpstreeというコマンドが使えますので、興味があればpstreeコマンドでどのようなプロセスツリーが生成されているか見てみるのもよいかと思います。pstree コマンドの使いかたはmanで調べてください(丸投げ) 82 | 83 | ### exec 84 | 85 | さて、「すべてのプロセスは祖先からforkされて生まれた」という話と「forkは親プロセスをまるっとコピーして子プロセスを作る」という話をしましたが、これ、なんかおかしいですね。そうです。このままでは、「親の複製のプロセス」しかなくって、すべてが同じことを行うプロセスになってしまいます! 86 | 87 | そこで必要になるのが、execというシステムコールです。あるプロセスがexecというシステムコールを呼ぶと、OSはそのプロセスをexecの内容で書き換えてしまいます。つまり、execというのは、「自分自身の内容を別の内容で書き換えて実行してしまう」システムコールなんですね。くらくらしてきた! 88 | 89 | まとめると、 90 | 91 | 1. forkでプロセスを生成して、独立した環境を用意してあげる 92 | 1. その環境に、execによって別の実行可能なものを読み込んで実行する 93 | 94 | ことで、親プロセスとは違うプロセスをどんどん生成していくような仕組みになっているわけです。 95 | 96 | ### 例 97 | 98 | 「日本語だとよくわかんないよ、コードで書いてよ」という声がわたしの脳内から聞こえてきたので、コードで書きます。 99 | 100 | use strict; 101 | use warnings; 102 | 103 | print "forking...\n"; 104 | 105 | # forkシステムコールを呼び出す 106 | my $pid = fork; 107 | 108 | # forkに失敗すると返り値はundef 109 | die "fork failed." unless defined $pid; 110 | 111 | # ここに来てるということは、正常にプロセスが複製された。 112 | # この時点で親プロセスと子プロセスが *別々の環境で* 113 | # 同時にこのプログラムを実行していることになる。 114 | print "forked!\n"; 115 | 116 | # forkで生成された子プロセスでは、forkの返り値が 0 となる 117 | # 親プロセスでは、生成された子プロセスのpidが入ってくる 118 | if ($pid == 0){ 119 | #子プロセスはこっちを実行する 120 | 121 | # execシステムコールで、perlのプロセスをrubyのプロセスに書き換えてしまう! 122 | exec "ruby -e 'loop do;sleep;end'"; 123 | } 124 | else{ 125 | #親プロセスはこっちを実行する 126 | 127 | #子プロセスが終了するのを待つ 128 | waitpid($pid,0); 129 | } 130 | 131 | 上記のようなPerlスクリプトをfork_exec.plという名前で用意して、バックグラウンドで実行してみましょう。すると、以下のような出力が得られると思います。 132 | 133 | $ perl ./fork_exec.pl & 134 | forking... 135 | forked! 136 | forked! 137 | 138 | なぜこうなるのか、説明しましょう。 139 | 140 | print "forking!\n"; という行は、まだfork前なので、プロセスがひとつだけの状態です。なので、普通にひとつの"forking!"が出力されます。しかし、print "forked!\n"; という行は、forkシステムコールでプロセスが複製されたあとです。そのため、この行は親プロセスとそこから複製された子プロセスが、別のプロセスとして実行します。親プロセスは親プロセスで"forked!"という文字列を標準出力という場所に出力します(perlのprintという関数は、引数に渡された文字列を標準出力に出力します)、一方、別の環境で動いている子プロセスも、"forked!"という文字列を標準出力という場所に出力します。今回の場合、親プロセスも子プロセスも標準出力はターミナルを意味するので(このあたりの話はまたあとで詳しくやります)、ターミナルに親プロセスと子プロセスの二つ分のforked!が出力されるわけです。 141 | 142 | さて、今バックグラウンドで実行したこのスクリプトですが、ではプロセスはどのようになっているでしょうか。psコマンドで確認して見ましょう。 143 | 144 | $ ps 145 | PID TTY TIME CMD 146 | 81996 ttys003 0:00.01 perl fork_exec.pl 147 | 81998 ttys003 0:00.01 ruby -e loop do;sleep;end 148 | 149 | psコマンドの出力に、上記のようなふたつの行が見つかるかと思います。上の perl fork_exec.pl というプロセスが私たちがさっき「$ perl fork_exec.pl &」と実行したプロセスで、下の ruby -e loop do;sleep;end というプロセスが、forkされた子プロセスです。pstreeで見てみましょう。 150 | 151 | $ pstree 81996 (さっきpsで確認した "perl fork_exec.pl" のPIDを指定) 152 | -+= 81996 shinpeim perl fork_exec.pl 153 | \--- 81998 shinpeim ruby -e loop do;sleep;end 154 | 155 | というような出力が得られ、"perl fork_exec.pl" というプロセスから "ruby -e loop do;sleep;end" というプロセスが生成されているのがわかるかと思います。 156 | 157 | さて、今バックグラウンドで実行しているプロセス(親プロセスです)を fg コマンドでフォアグランドに移して、Ctrl+Cで止めてしまいましょう。その後もう一度psコマンドを叩くと、子プロセスごと消えているのがわかるかと思います。なぜこうなるのかについては、シグナルについて見るときに説明しましょう。 158 | 159 | 今は、「forkで子プロセスを生成できて、execでそのプロセスの内容を書き換えられた」ということがわかれば十分です。コマンドを叩いて新しいプロセスを生成する場合とかも、内部ではこのようにforkでプロセスを生成して、確保された環境の内容をexecで書き換えるという形で生まれているのです。ちなみに、シェルからコマンドを叩いてプロセスを生成するときには、「親プロセス」に当たるのはシェルのプロセスになります。 160 | 161 | ### 今後の予定 162 | 163 | * forkしたpidを看取る話と子供がゾンビになっちゃう話 164 | * あらゆる入出力はファイルとして扱われてるよって話からの、forkした際の file descripter と open file description について 165 | 166 | あたりを書きたい気持ちがある 167 | -------------------------------------------------------------------------------- /007.md: -------------------------------------------------------------------------------- 1 | ## シグナルとkill 2 | 3 | さて、前回までで fork とプロセスの関係についてはなんとなく概要が把握できたんじゃないかなと思います。今回は、シグナルについてです。 4 | 5 | ### プロセスに外から影響を与えたい(シグナル編) 6 | 7 | プロセスが外界とコミュニケーションを取るための方法として、ファイルディスクリプタを通じた入出力というものがあることは前回までで見てきたとおりです。じつは、プロセスが外界とコミュニケーションを取る方法としてもうひとつ、「シグナル」というものがあります。第二回で見たとおり、プロセスは生成されたあとは実行中となり、処理が終わるまでは一心不乱に決められた動きを行っています。しかしたとえば、無限ループに陥ってしまったプロセスなどは、外から「あっちょっと君、ストップ!ストップ!」という感じで止めてあげられる仕組みがないと困りますよね。そういう感じで、外からプロセスに対して「割り込み」を行うための仕組みが「シグナル」です。 8 | 9 | なにはともあれ、ためしてみましょう。 10 | 11 | ### シグナルを送ってみる 12 | 13 | まずはプロセスを作りましょう。 14 | 15 | $ perl -e 'while(1){sleep}' & 16 | $ ps 17 | 18 | 毎度おなじみ、sleep するだけの perl プロセスです。ps でpid を確認しておきましょう。 19 | 20 | このプロセスに対して、シグナルを送ってみます。 21 | 22 | $ kill -INT <さっき確認したpid> 23 | 24 | kill というのが、プロセスに対してシグナルを送るコマンドです。今回は -INT を指定することで、「SIGINT」というシグナルを送ってみました。「SIGINT」の他にもいろんなシグナルがありますが、今は置いておきます。さて、ではここでもう一度 ps コマンドでプロセスの様子を見てみましょう。 25 | 26 | $ ps 27 | 28 | すると、さきほどまで存在していた perl プロセスが無くなっていることがわかると思います。これはいったいどうしたことでしょうか。実は、SIGINTというプロセスを受け取ると、デフォルト値ではそのプロセスは実行を停止するのです。sleep し続けていたプロセスに SIGINT というシグナルを送ったことによりプロセスに「割り込み」をして、そのプロセスの実行を止めてしまったわけですね。 29 | 30 | ### シグナルを受け取ったときの動作を変えてみる 31 | 32 | さきほど、「デフォルト値では」と言いましたが、ということは、シグナルを受け取ったときの動作を変更することだってできるわけです。やってみましょうか。 33 | 34 | # papas.pl 35 | use strict; 36 | use warnings; 37 | 38 | # SIGINTを受け取ったときは sub {} の中身を実行する 39 | $SIG{INT} = sub { 40 | warn "ぬわーーーーっっ!!"; 41 | }; 42 | 43 | # スリープし続ける 44 | while (1) { 45 | sleep; 46 | } 47 | 48 | papas.pl という名前で上のようなスクリプトを作成して、バックグラウンドで実行してみましょう 49 | 50 | $ perl papas.pl & 51 | 52 | さて、それではこのプロセスに対して、SIGINTを送ってみましょう。 53 | 54 | $ kill -INT <"perl papas.pl" の pid> 55 | 56 | 標準エラーに「ぬわーーーーっっ!!」が表示されたかと思います。そして再度 ps してみると、さっきは SIGINT を受け取って停止していたプロセスが、今回はまだ生きていることが見て取れるかと思います。これで、何度 SIGINT を送っても「ぬわーーーーっっ!!」と叫ぶだけで、死なないプロセスの完成です。パパスも適切にシグナル処理さえしていればゲマに殺されることもなかったというのに……。 57 | 58 | さて、このままではこのプロセスは生き続けてしまうので、SIGTERMというシグナルを送信して適切に殺してあげましょう。 59 | 60 | $ kill -TERM <"perl papas.pl" の pid> 61 | 62 | これで無事にパパスは死にました。 63 | 64 | ### シグナルにはどんなものがあるの? 65 | 66 | 上に見たように、シグナルには SIGINT 以外にもいろいろないろいろなシグナルがあります。man 7 signal や man kill に一度目を通しておくと良いでしょう。それぞれのシグナルに、受け取ったときのデフォルトの動作が定義されています。 67 | 68 | とりあえずここでは、signal(7) から、 POSIX.1-1990 で規定されているシグナルの種類を引いておきましょう。 69 | 70 | Signal Value Action Comment 71 | ------------------------------------------------------------------------- 72 | SIGHUP 1 Term Hangup detected on controlling terminal 73 | or death of controlling process 74 | SIGINT 2 Term Interrupt from keyboard 75 | SIGQUIT 3 Core Quit from keyboard 76 | SIGILL 4 Core Illegal Instruction 77 | SIGABRT 6 Core Abort signal from abort(3) 78 | SIGFPE 8 Core Floating point exception 79 | SIGKILL 9 Term Kill signal 80 | SIGSEGV 11 Core Invalid memory reference 81 | SIGPIPE 13 Term Broken pipe: write to pipe with no readers 82 | SIGALRM 14 Term Timer signal from alarm(2) 83 | SIGTERM 15 Term Termination signal 84 | SIGUSR1 30,10,16 Term User-defined signal 1 85 | SIGUSR2 31,12,17 Term User-defined signal 2 86 | SIGCHLD 20,17,18 Ign Child stopped or terminated 87 | SIGCONT 19,18,25 Cont Continue if stopped 88 | SIGSTOP 17,19,23 Stop Stop process 89 | SIGTSTP 18,20,24 Stop Stop typed at tty 90 | SIGTTIN 21,21,26 Stop tty input for background process 91 | SIGTTOU 22,22,27 Stop tty output for background process 92 | 93 | Signal のところがシグナルの名前、Value というところがそのシグナルを表す番号(kill -n pid でプロセスにそのシグナルを送ることができます)、Action のところがそのシグナルを受け取ったときのデフォルトの動作です。Term ならばプロセスを終了し、Coreならばコアダンプを吐いて終了します。Ignならばそのシグナルを無視します(なにもしない)し、Stopならば実行を一時停止、Contならば一時停止していたプロセスを再開します。Commentのところに、どのようなときにそのシグナルが送られてくるかが書かれていますね。たとえば SIGCHLD を見てみると、Child stopped or terminatedと書かれています。つまり、子プロセスが止まったり止められたりしたときに、その親プロセスはSIGCHLDを受け取るようになっているわけですね。 94 | 95 | 微妙なハマりポイントとして、SIGHUP や SIGPIPE があるので、そこだけ少し説明しておきましょう。 96 | 97 | まずは SIGHUP についてですが、ログインシェルが死んだときに、そのログインシェルが起動したプロセスにはSIGHUPが送られてきます(じつはこれは正確な説明ではないのだけれど、このあたりの正確な説明は次回できたらします)。これがなにを意味するかというと、たとえば ssh でサーバーにログインして、バックグラウンドでなにかを動かしたまま logout したりすると、そのバックグラウンドプロセスに SIGHUP が送られます。SIGHUP のデフォルトの動作は Term なので、そのバッググラウンドプロセスは死んでしまいます。これを防ぐためには、 nohup コマンドを使ってプロセスを起動するか、プロセス側で SIGHUP を受け取ったときの動作を変更する必要があります。 98 | 99 | つぎに SIGPIPE についてです。SIGPIPEは、壊れた pipe に対して書き込みを行ったときに受信されるシグナルです。これが問題を引き起こすことが多いのが、ネットワークサーバーを書いているときです。なんらかのトラブルなどですでに切断されてしまっているソケットに対してなにかを書き込みしようとすると(いくらでもその理由は考えられます)、プロセスは SIGPIPE を受け取ります。SIGPIPE のデフォルトの動作はTermなので、この時点でサーバーは突然の死を迎えることになるわけです。 100 | 101 | _人人人人人_ 102 | > SIGPIPE < 103 |  ̄YYYYY ̄ 104 | 105 | 動かし続けることを前提としたプロセスでは、このあたりのシグナルをきちんとハンドリングしてあげないとハマることが多いので、頭の片隅に置いておくといいかもしれません。 106 | 107 | ### 次回へ続くなぞ 108 | 109 | さて、シグナルについて基本的なことは見て来れたかと思います。では、forkなどと組み合わせて使った時にはどういう動きをするのでしょうか?見てみましょう。 110 | 111 | まずは以下のようなスクリプトを用意してみます。 112 | 113 | # fork_and_sleep.pl 114 | use strict; 115 | use warnings; 116 | 117 | fork; 118 | 119 | # スリープし続ける 120 | while (1) { 121 | sleep; 122 | } 123 | 124 | forkして子プロセスを作ったあと、親プロセスも子プロセスもスリープし続けるものですね。バックグラウンドで実行します。 125 | 126 | $ perl fork_and_sleep.pl & 127 | 128 | ps コマンドで様子を見てみましょう 129 | 130 | $ ps f 131 | PID TTY STAT TIME COMMAND 132 | 16753 pts/2 Ss 0:00 -bash 133 | 16891 pts/2 S 0:00 \_ perl hoge.pl 134 | 16892 pts/2 S 0:00 | \_ perl hoge.pl 135 | 16928 pts/2 R+ 0:00 \_ ps f 136 | 137 | 「f」を付けて ps を実行すると親子関係が一目でわかります。この場合は 16891 が親プロセス、16892 が子プロセスですね。では、fg でフォアグラウンドに戻して、Ctrl + C を押してみましょう。Ctrl+C は、プロセスに対してSIGINTを送信します。 138 | 139 | 140 | OKですか? そうしたら、ここで再度 ps を実行してみましょう 141 | 142 | $ ps f 143 | PID TTY STAT TIME COMMAND 144 | 16753 pts/2 Ss 0:00 -bash 145 | 17140 pts/2 R+ 0:00 \_ ps f 146 | 147 | 子プロセスも一緒に消えていますね。では、今度は fg -> Ctrl+C のコンボではなく、kill -INT で SIGINT を送ってみましょう。 148 | 149 | $ perl fork_and_sleep.pl & 150 | 151 | $ ps -f 152 | PID TTY STAT TIME COMMAND 153 | 16753 pts/2 Ss 0:00 -bash 154 | 17288 pts/2 S 0:00 \_ perl hoge.pl 155 | 17289 pts/2 S 0:00 | \_ perl hoge.pl 156 | 17293 pts/2 R+ 0:00 \_ ps f 157 | 158 | $ kill -INT 17288 # 親プロセスにSIGINTを送る 159 | 160 | $ ps f 161 | PID TTY STAT TIME COMMAND 162 | 16753 pts/2 Ss 0:00 -bash 163 | 17352 pts/2 R+ 0:00 \_ ps f 164 | 17289 pts/2 S 0:00 perl hoge.pl 165 | 166 | *「!?」* 今度は子プロセスが残っています(親プロセスが死んだからinitの子供になっており、ツリーの表示も変わっています)。 167 | 168 | さて、なぜこのようなことが起こるのでしょうか。この挙動を理解するには、「プロセスグループ」という新しい概念を学ぶ必要があります。 169 | 170 | ### というわけで次回予告 171 | 172 | 次回はプロセスグループについて見てみましょう。多分次回が最終回! 173 | 174 | ### see also 175 | [Perl Hackers Hub 第6回 UNIXプログラミングの勘所(3)](http://gihyo.jp/dev/serial/01/perl-hackers-hub/000603) 176 | -------------------------------------------------------------------------------- /008.md: -------------------------------------------------------------------------------- 1 | ## プロセスグループ と フォアグランドプロセス 2 | 3 | 前回はプロセスとシグナル、そしてシグナルを明示的にプロセスに送るためのコマンド kill について見ました。そして最後にひとつ謎が残ったわけですが、今回はその謎を解いて行きましょう。 4 | 5 | ### プロセスグループ 6 | 7 | さて、じつは今まで一度も意識したことはありませんでしたが、プロセスというのはかならずひとつのプロセスグループというものに属します。見てみましょう。 8 | 9 | $ perl -e 'sleep' & 10 | 11 | $ ps o pid,pgid,command 12 | PID PGID COMMAND 13 | 1620 1620 -bash 14 | 1638 1638 perl -e sleep 15 | 1639 1639 ps o pid,pgid,command 16 | 17 | 毎度おなじみ sleep し続ける perl プロセスをバックグラウンドで実行して、ps を "o pid,pgid,command" 付きで実行してみました。「pidとpgidとcommandを表示する」くらいの意味です。おや、見慣れない PGID というものがありますね。これが、プロセスグループのidです。こんな感じで、プロセスがかならずひとつのプロセスグループに属していることが見て取れるかと思います。なんだか今は PID と同じ数字が PGID のところにも表示されていて、この PGID ってあまり意味や意義がわからない感じですね。 18 | 19 | では、ここで、fork と組み合わせてみましょうか。 20 | 21 | # fork.pl 22 | use strict; 23 | use warnings; 24 | 25 | fork; 26 | 27 | sleep; 28 | 29 | 上記のような、 fork して sleep し続けるだけの fork.pl というスクリプトを作ってバックグラウンドで実行してみましょう。 30 | 31 | $ perl fork.pl & 32 | 33 | $ ps o pid,pgid,command f 34 | PID PGID COMMAND 35 | 1620 1620 -bash 36 | 1646 1646 \_ perl fork.pl 37 | 1647 1646 | \_ perl fork.pl 38 | 1652 1652 \_ ps o pid,pgid,command f 39 | 40 | 今回は ps に f オプションを付けて tree 状に表示してみました。(BSD系だと f 利かないので Macで試すなら f なしで実行してください) 41 | 42 | さて、こうして見てみると、親プロセスであるプロセス(pid 1646)は PID と PGID が同じ数字ですが、そこから fork で生成された子プロセス(pid 1647)は、PID と PGID が別の数字になっています。そして、子プロセスのほうの PGID は、fork元である親プロセスの PGID になっているのがわかるでしょうか。 43 | 44 | こんな感じで、実は fork された子プロセスは、親プロセスと同じプロセスグループに属するようになります。逆の言い方をすると、forkで子プロセスを作ることによって、「自分と同じプロセスグループに属するプロセス」が一個ふえるわけですね。 45 | 46 | ちなみに、プロセスグループにはリーダーが存在して、PGID と同じ数字の PID のプロセスが、プロセスグループのリーダーです。forkすると、同じグループに属する子分ができる、みたいな感じですね。 47 | 48 | ### プロセスグループをいじってみよう 49 | 50 | さて、今かんたんに「fork すると子プロセスは自分と同じプロセスグループに属するようになる」と言いましたが、これはちょっとおかしいですね。そうです、以前見たように、すべてのプロセスは pid 1 のプロセスから fork で作られたのでした。そうなると、すべてのプロセスは pid 1 のプロセスと同じプロセスグループに属することになってしまいます。すべてのプロセスが同じグループに属すなら、グループの意味がないですね。だから、forkしたあと、プロセスグループをいじる仕組みが必要になってきます。それが setpgrp システムコールです。では例を見てみましょう。 51 | 52 | #fork_setpgrp.pl 53 | use strict; 54 | use warnings; 55 | 56 | my $pid = fork; 57 | 58 | die "fork failed" unless defined $pid; 59 | 60 | if ($pid) { 61 | # 親プロセス 62 | sleep; 63 | } 64 | else { 65 | # 子プロセス 66 | 67 | # setpgidシステムコールを 68 | # 引数なしで呼び出すと、 69 | # 自分のプロセスグループを作ってそこのリーダーになる 70 | setpgrp; 71 | sleep; 72 | } 73 | 74 | 上記のようなスクリプトを fork_setpgrp.pl という名前で保存して、バックグラウンドで実行、ps で確認してみましょう 75 | 76 | $ ps o pid,pgid,command f 77 | PID PGID COMMAND 78 | 1620 1620 -bash 79 | 1666 1666 \_ perl fork_setpgrp.pl 80 | 1667 1667 | \_ perl fork_setpgrp.pl 81 | 1673 1673 \_ ps o pid,pgid,command f 82 | 83 | 今度は、子プロセスは親プロセスと同じ PGID ではなくなりました。setpgrp システムコールを引数なしで呼び出したことにより、今までグループ内で子分役をやっていた子プロセスが、新しく自分のグループを作り、リーダーになっていることが見て取れるかと思います。なんだかベンチャー界隈でよく聴く独立譚みたいな話ですね。 84 | 85 | ちなみに、PGID は、親の側からいじることもできます。 86 | 87 | use strict; 88 | use warnings; 89 | 90 | my $pid = fork; 91 | 92 | die "fork failed" unless defined $pid; 93 | 94 | if ($pid) { 95 | # 親プロセス 96 | 97 | # setpgrp を引数付きで呼び出す 98 | my $pgid = $pid; 99 | setpgrp $pid, $pgid; 100 | sleep; 101 | } 102 | else { 103 | # 子プロセス 104 | sleep; 105 | } 106 | 107 | こういう感じで親プロセスのほうで引数付きで setpgrp を呼び出すことで、子プロセスの PGID を設定することもできます。 108 | 109 | ### いったんまとめ 110 | 111 | こんな感じで、プロセスが fork で子プロセスを作ったとき、その時点ではその子プロセスは親プロセスと同じプロセスグループに属しています。プロセスグループを変更したいときには、この子プロセスの PGID を setpgrp システムコールでいじってあげれば良いわけですね。 112 | 113 | ちなみに、シェルから起動されたプロセスは、シェルが勝手に setpgrp を呼んでくれるので、それぞれがプロセスグループのリーダーとなっています。 114 | 115 | ### プロセスグループ全体に kill をしてみよう 116 | 117 | さて、いままでの話だけでは、「プロセスグループってのがあるのはわかったけど、そんなもんがあってなにがうれしいの」という感じがしますね。うれしいことのひとつとして、kill でプロセスグループに属する全てのプロセスに一気にシグナルを送れる、というものがあります。kill で pid を指定する部分に、"-" を付けてあげると、pid ではなくて pgid を指定したことになります。やってみましょう。 118 | 119 | $ perl fork.pl & 120 | 121 | $ ps o pid,pgid,command f 122 | PID PGID COMMAND 123 | 1678 1678 -bash 124 | 1699 1699 \_ perl fork.pl 125 | 1700 1699 | \_ perl fork.pl 126 | 1701 1701 \_ ps o pid,pgid,command f 127 | 128 | $ kill -INT -1699 # 1699 ではなくて -1699 としている 129 | 130 | $ ps o pid,pgid,command f # 一気にふたつのプロセスが消えている 131 | PID PGID COMMAND 132 | 1678 1678 -bash 133 | 1702 1702 \_ ps o pid,pgid,command f 134 | 135 | ### 前回の謎に回答する 136 | 137 | ではここで、前回の謎に回答しましょう。前回謎だった挙動は、「fg でプロセスをフォアグラウンドにしてから Ctrl+C で SIGINT を送信したときは子プロセスごと殺されたのに、 kill -INT でバックグラウンドのプロセスに SIGINT を送信したら親プロセスだけが殺される」という挙動でしたね。 138 | 139 | 勘のいいひとはすでにお気づきかもしれないですが、実は、「フォアグラウンド」とされる範囲は、プロセス単位ではなくて、プロセスグループ単位で決まっているのです。いくつか、例を見てみましょう。 140 | 141 | # fork_and_trap_sigint.pl 142 | use strict; 143 | use warnings; 144 | 145 | $SIG{INT} = sub { 146 | die "got SIGINT!"; 147 | }; 148 | 149 | fork; 150 | 151 | sleep; 152 | 153 | 上記のようなスクリプトをフォアグランドで実行して、Ctrl+C でSIGINTを送ってみましょう。すると、"got SIGINT!" がふたつ標準エラーに出力されるはずです。これは、子プロセスと親プロセスが同じプロセスグループに属しているため、このふたつのプロセスがフォラグランドで実行されているからですね。 154 | 155 | では今度は、プロセスグループが別の場合を見てみましょう。 156 | 157 | # fork_and_setpgrp_and_trap_sigint.pl 158 | use strict; 159 | use warnings; 160 | 161 | $SIG{INT} = sub { 162 | die "got SIGINT!"; 163 | }; 164 | 165 | my $pid = fork; 166 | die "fork failed" unless defined $pid; 167 | 168 | if ($pid) { 169 | # 親プロセス 170 | sleep; 171 | } 172 | else { 173 | # 子プロセス 174 | 175 | # setpgrpを呼び出して新しいプロセスグループのリーダーになる 176 | # これにより、子プロセスは親プロセスと異なるプロセスグループに 177 | # 属すことになり、フォアグランドで実行されている 178 | # プロセスグループから抜ける 179 | setpgrp; 180 | sleep; 181 | } 182 | 183 | 上記のようなスクリプトをフォアグランドで実行して、Ctrl+C でSIGINTを送ってみましょう。すると、"got SIGINT!" が今度はひとつだけ出力されるはずです。これは、子プロセスが親プロセスのプロセスグループを抜けて別のプロセスグループになったため、フォアグランドから抜けてしまったためです。 184 | 185 | 別の例も見てみましょう。 186 | 187 | # read_stdin_in_child.pl 188 | use strict; 189 | use warnings; 190 | 191 | my $pid = fork; 192 | die "fork failed" unless defined $pid; 193 | 194 | if ($pid) { 195 | # 親ではstdin閉じる 196 | close(STDIN); 197 | 198 | # 子の終了待つ 199 | waitpid($pid,0); 200 | } 201 | else { 202 | # STDINからの入力をエコーする 203 | while(my $line = ) { 204 | print $line; 205 | } 206 | } 207 | 208 | 上記のようなスクリプトを作成し、フォアグラウンドで実行してみましょう。親プロセスは子プロセスが終わるまで待ってるのでそこでブロックしています。子プロセスは標準入力からの入力を受け取ろうとそこでブロックしています。 209 | 210 | ここでターミナルになんか文字を打ち込めば、子プロセスがその入力を受け取ってエコーしてくれます。 211 | 212 | ではこれをsetpgrpとの合わせ技でやるとどうなるでしょう? 213 | 214 | # setpgrp_and_read_stdin_in_child.pl 215 | use strict; 216 | use warnings; 217 | 218 | my $pid = fork; 219 | die "fork failed" unless defined $pid; 220 | 221 | if ($pid) { 222 | # 親ではstdin閉じる 223 | close(STDIN); 224 | 225 | # 子の終了待つ 226 | waitpid($pid,0); 227 | } 228 | else { 229 | # setpgrpを呼び出して新しいプロセスグループのリーダーになる 230 | # これにより、子プロセスは親プロセスと異なるプロセスグループに 231 | # 属すことになり、フォアグランドで実行されている 232 | # プロセスグループから抜ける 233 | setpgrp; 234 | 235 | 236 | # STDINからの入力をエコーする 237 | while(my $line = ) { 238 | print $line; 239 | } 240 | } 241 | 242 | 上記のようなスクリプトをフォアグラウンドで実行してみましょう。さっきとは異なり、ターミナルになにかを打ち込んでもおうむがえししてこないのが見て取れると思います。これは子プロセスが親プロセスとは別のPGIDに属したことによって、フォアグランドで実行されているプロセスグループから抜けたためですね。 243 | 244 | さて、これで前回謎だった挙動にも説明がつきましたね。これで、プロセスグループの解説はおしまいにします。 245 | 246 | ### おわりに 247 | 248 | これにてこのシリーズはおしまいです。いかがだったでしょうか? 一度プロセスまわりについてまとめておきたいという動機で書き始めたのですが、これを書きながらわたしも理解があやふやなところが洗い出せたりして、なにかと有意義でした。 249 | 250 | もしもこのドキュメントが役に立つと思っていただけたなら、勉強会とかそういうのであなたが属すコミュニティや会社に役立ててもらえたらとても嬉しいです。そのとき、「使ったよ!」とコメント欄とかメールとかで知らせてくれると、単純にわたしが喜びます(言わなくても自由に使っていただいてかまわないですけど)。 251 | -------------------------------------------------------------------------------- /003.md: -------------------------------------------------------------------------------- 1 | ## プロセスとファイル入出力 2 | 3 | さて、前回、プロセスというのは「自分が独占したメモリーの中で動いているので、その中で何をしても他のプロセスのメモリーに影響を与えない」というのを見れたかと思います。でも、そんな自分の中だけで完結してる引きこもりみたいなプロセスじゃあ、意味がないですね。外界からなんかデータをもらって、自分の中で処理して、それを外の世界に知らせる方法が必要になってきます。 4 | 5 | そこで、プロセスに外から何かを入力したり、プロセスが外に何かを出力する方法として、「ファイルの入出力」というのがあります。たとえば、ファイルに書かれたデータをプロセスがメモリー上に読み込んでなんか処理をするとか、処理を行った結果をテキストファイルに書き込みをするとか。例を見てみましょう。 6 | 7 | まず、以下のようなテキストファイルを nyan.txt という名前で適当な場所に作ってみます。 8 | 9 | nyan 10 | nyan nyan 11 | nyan nyan nyan 12 | 13 | では、このファイルをプロセスの中に読み込んでみましょう。今日は Ruby を使います。 14 | 15 | file = File.open("nyan.txt","r") 16 | lines = file.readlines #ファイルの中身を全部読み込む 17 | file.close 18 | 19 | ファイルを open して、その内容を lines という変数に読み込んで、最後にファイルを close しています。ファイルの中のデータはディスクに書かれたものであり、プロセスがもともとメモリー内に持っていたものではありません。このディスクに書かれた内容を 20 | 21 | lines = file.readlines 22 | 23 | の行でlines変数に読み込むことで、プロセスの「外界」の情報を、プロセスの内部のメモリーに読み込んでいますね。 24 | 25 | では今度は出力をしてみましょう。 26 | 27 | # nyan_copy.rb 28 | file = File.open("nyan.txt","r") 29 | lines = file.readlines 30 | file.close 31 | 32 | file = File.open("nyan_copy.txt","w") 33 | file.write(lines.join) 34 | file.close 35 | 36 | nyan_copy.rbを、nyan.txtと同じディレクトリに作って、実行してみましょう。nyan.txtと同じ内容の、nyan_copy.txtというファイルが生まれたかと思います。さきほどディスクから読み込んでメモリー上に展開したデータを、そのまま別のファイルに対して出力したためですね。 37 | 38 | こうして、プロセスはファイルを通じて外部との入出力を行うことができます。 39 | 40 | ### すべてがファイル??? 41 | 42 | さて、いまは「テキストファイル」への読み書きを行ってみましたが、「Linuxではすべてがファイルなんだよ」みたいな話を聞いたことがないでしょうか? そんなこと言われても、「はっ?」って感じの話ですよね。「Linuxではキーボードもファイルだからね」みたいなことを言うひとに至っては「こいつ頭大丈夫か、キーボードはキーボードだろうが」みたいな気持ちになりますよね。わたしは最初にこの話を聞いたときに「なにそれ、禅問答?哲学?頭大丈夫?ファイルはファイルだしキーボードはキーボードだろ」って思いました。 43 | 44 | 「全てがファイル」とか言われると「世の中のすべてはファイルなのだ、そう、きみも、わたしも」みたいな禅問答をやられてるみたいな気持ちになるので、こういう言い方はあまりよくない感じがしますね。だったら、こんなふうに言われたらどうでしょうか? 「Linuxは、すべての入出力がファイルと同じ感じで扱えるような設計になっているんだよ」。つまり、プロセスが「ここでターミナルからの入力を受け取りたいんだけど」とか、「ネットワーク越しに入力もらってネットワーク越しに出力したいんだけど」みたいなことを言うと、OSさんが「はいよ、実際はHD(さいきんだとSSDかな)上のファイルじゃないんだけど、いつもファイルを通じてディスクを読み書きするのと同じやり方で扱えるように用意しといたよ!」みたいな感じでそのためのインターフェイスを用意してくれてるのです。 45 | 46 | ### 例:標準入出力 47 | 48 | さて、例を見てみましょうか。 49 | 50 | # stdout.rb 51 | file = File.open("nyan.txt","r") 52 | lines = file.readlines 53 | file.close 54 | 55 | file = $stdout # この行だけ書き換えた 56 | file.write(lines.join) 57 | file.close 58 | 59 | nyan.txt と同じディレクトリに、今度は stdout.rb を作って、実行してみましょう。nyan.txtの内容が、ターミナルに出力されたかと思います。 60 | 61 | rubyの組み込みグローバル変数 $stdout には、「標準出力」と言われるものが、すでにFile.openされた状態で入っています。この「標準出力」の出力先は、デフォルトではターミナルをさします。そのため、さっきテキストファイルに内容を出力したのと同じやりかたで、ターミナルに対して出力ができるわけです。 62 | 63 | 標準出力があるなら標準入力もあるの?当然あります。 rubyだと標準入力はFile.openされた状態で $stdin というグローバル変数に入っています。標準入力のデフォルトの入力ソースはターミナルになります。例を見ましょう。 64 | 65 | # stdin.rb 66 | file = $stdin 67 | lines = file.readlines #標準入力からの入力を全部受け取る 68 | file.close 69 | 70 | file = $stdout 71 | file.write(lines.join) # 標準出力に対して内容を書き出す 72 | file.close 73 | 74 | 上記のような stdin.rb というファイルを作成して、実行してみましょう。何も出力されず、かつプロンプトも返ってこない状態になると思います。これはなぜかと言うと、 75 | 76 | lines = file.readlines #標準入力からの入力を全部受け取る 77 | 78 | の行で、プロセスが「ブロック中」になっているからです。前回の内容を思い出してください。プロセスの実行中の状態のうちのひとつに、「ブロック中」があったと思いますが、ブロック中というのは、「IOとかを待ってて今は処理できないよ」という状態でしたね。 79 | 80 | この行では、標準入力からの入力を「全部」読み込もうとしています。そして、標準入力のデフォルトはターミナルからの読み込みを行います。しかし、すでに何が書かれているか決まっているdisk上のファイルと違って、ターミナルへの入力は「終わり」がいつ来るものなのかわかりません。だから、このプロセスは「終わり」が入力されるまで、ずっとずっと「ブロック中」の状態で待ち続けているのです。けなげですね。 81 | 82 | では、ひとまず以下のような感じで、プロンプトが戻ってきてないターミナルに何かを打ち込んでみてください。 83 | 84 | $ ruby stdin.rb #さっき実行したコマンド 85 | aaaa 86 | bbbbbb 87 | ccc 88 | 89 | 打ち込みましたか?そうしたら、改行したあと、おもむろにCtrlキーを押しながらDを押してみましょう。すると、ターミナルに、あたらしく 90 | 91 | aaaa 92 | bbbbbb 93 | ccc 94 | 95 | と、さっき自分で入力したのと同じ内容が出力されるはずです。 96 | 97 | Ctrl+D を押すと、EOFというものが入力されます。この「EOF」というのは「End Of File」の略で、「ここでこのファイルはおしまいだよ」というのを伝える制御文字です。プロセスは、この「EOF」を受け取ることで、「よし、標準入力を全部読み込んだぞ」と理解して、IO待ちのブロック状態から抜けるわけですね。 98 | 99 | ところで、最初の例では標準入力ではなくてnyan.txtを読み込んでいましたが、実はその間にも、一瞬プロセスは「ブロック中」状態になっています。ディスクからデータを読みこんでくるのが一瞬なので、普段はあまり意識しないかもしれませんが(とはいえ、コンピューターの処理の中ではdiskIOというのはかなり遅い処理の部類です。だから、パフォーマンスが必要になってくるようなソフトウェアを書くときには、なるべくIOをしないことでブロックされないようにしてパフォーマンスを稼ぐみたいな手法が取られたりするわけです)。 100 | 101 | こんな感じで、「実際はdisk上のファイルじゃないもの」も、「disk上のファイルとおなじように」扱える。そういう仕組みがLinuxには備わっています。今はそれが「すべてがファイル」の意味だと思ってください。 102 | 103 | ちなみに、標準入力/出力の他にも、「標準エラー出力」というのがあり、これもデフォルトの出力先はターミナルになっています。 104 | 105 | 余談ですが、IO#readlinesは「ファイルの内容を全部読み込む」という挙動をしますが、では一行だけ読み込む IO#readline を使うとどういう挙動をするかなど、自分で確かめてみると、「あっブロックしてる」「あっ今読み込んでブロック中じゃなくなった」みたいなのがわかっておもしろいかもしれません。 106 | 107 | ### じゃあデフォルトじゃないのはなんなんだよ 108 | 109 | 先ほどから標準入出力のデフォルトはどうこうみたいな話をしていますが、それはつまり標準入出力はその他の場所にもできるってことですね。そのための機能が「リダイレクト」と「パイプ」です。 110 | 111 | ### リダイレクト 112 | 113 | リダイレクトを使うと、標準入出力に別のファイルを指定することができます。ちなみに、シェル上(sh,bash,zshを想定)では、標準入力は「0」という数字、標準出力は「1」という数字、標準エラー出力は「2」という数字で表されます(なんでこんな謎っぽい数字使ってるのかは後で説明します)。出力系のリダイレクトは ">" という記号、あるいは">>"という記号で行えます。">"の場合、指定されたファイルがすでに存在する場合はそれを上書きします。">>"の場合、指定されたファイルがすでに存在する場合はファイルの末尾に追記します。 114 | 115 | ### 標準出力のリダイレクト 116 | 例えば、 117 | 118 | # print_mew.rb 119 | puts "mew" # putsは標準出力に対して引数を出力する 120 | 121 | というrubyスクリプトがあるとき、 122 | 123 | $ ruby print_mew.rb 1>mew.txt 124 | 125 | とすると、mew とだけ書かれた mew.txt というファイルができあがります。"1>mew.txt"が、「標準出力(1)の出力先はmew.txtだよ」を意味するわけですね。その上で 126 | 127 | $ ruby print_mew.rb 1>>mew.txt 128 | 129 | とすると、 mew.txt にさらに mew が追記され、mew.txt の中身は mew(改行)mew というものになります。"1>>mew.txt"が、「標準出力の出力先はmew.txtだよ。ファイルが存在してたら末尾に追記してね」を意味するわけです。さらにもう一度 130 | 131 | $ ruby print_mew.rb 1>mew.txt 132 | 133 | とすると、mew.txtは上書きされてしまい、「mew」とだけ書かれたファイルになります。 134 | 135 | ちなみに、標準出力をリダイレクトする際は、「1」を省略した書き方も可能です。 136 | 137 | $ ruby print_mew.rb > mew.txt 138 | 139 | ### 標準入力のリダイレクト 140 | 141 | 当然、標準入力もリダイレクトすることが可能です。そのためには、"<"という記号を使います。 142 | 143 | 試しに、さっき作った mew.txt というファイルを標準入力としてみましょう。 144 | 145 | $ ruby stdin.rb 0mew_copy.txt 158 | 159 | 上記の場合、stdin.rbの標準入力はmew.txtとなり、標準出力は mew_copy.txt となります。 160 | 161 | stdin.rbの内容は標準入力を読み込んで標準出力にそのまま書き出すものなので、mew_copy.txtという新しいファイルに、mew.txtの内容、つまり「mew」 が書き込まれることになります。 162 | 163 | ### 標準エラー出力のリダイレクト 164 | 165 | 標準入出力について見てみたので、標準エラー出力についても見てみましょう。 166 | 167 | # stdout_stderr.rb 168 | puts "this is stdout" 169 | warn "this is stderr" # warnは標準エラー出力に引数を出力する 170 | 171 | 普通にstdout_stderr.rbを実行すると、標準出力も標準エラー出力もターミナルに向いているので、どちらもターミナルに出力されます。 172 | 173 | では、以下のようにしてみましょう。 174 | 175 | $ ruby stdout_stderr.rb 1>out.txt 2>err.txt 176 | 177 | "1>out.txt" で「標準出力(1)をout.txt」に、"2>err.txt" で「標準エラー出力(2)をerr.txt」に向けています。 178 | 179 | すると、out.txtには "this is stdout"が、err.txt には"this is stderr"が書き出されているかと思います。 180 | 181 | ちなみに、"2>&1"みたいにして標準エラー出力を標準出力へ向けることもできます。 182 | 183 | $ ruby stdout_stderr.rb 1>out.txt 2>&1 184 | 185 | &を付けることによって、「この1ってのは、1っていう名前のファイルじゃなくて標準出力を表す数字だよ!」ってことを言っているわけですね。さあ、またまた新しい疑問がわいてきました。なんで&付けるとそれがファイル名じゃなくて標準出力ってことになるの? そもそもなんで0とか1とか2とかって謎っぽい数字使ってるの? 疲れてきたので、そのあたりは次回にまわします。 186 | 187 | ### リダイレクトの順序 188 | 189 | $ ruby stdout_stderr.rb 1>out.txt 2>&1 190 | 191 | とすると、プロセス内で標準出力に書き出したものも標準エラー出力に書き出したものも out.txt に出力されます。しかし、 192 | 193 | $ ruby stdout_stderr.rb 1>out.txt 2>&1 194 | 195 | とすると、標準エラー出力に対する出力は、依然としてコンソールに出力されてしまいます。 196 | 197 | このような動きをするのはなぜでしょうか?その説明をするためには、次回説明する「ファイルディスクリプタ」というものを知る必要があります。これも次回説明しますので、今は「そういうもんなんだな」と思っておいてください。 198 | 199 | ### パイプ 200 | 201 | パイプについても簡単にみておきましょう。シェル上では、パイプは「|」という記号で実現されます。 202 | 203 | $ command_a | command_b 204 | 205 | とすると、command_aの標準出力に出力された内容がcommand_bの標準入力に入力されます。この時、command_aの出力が全部終わってなくても(EOFに達しなくても)、command_bのプロセスは「来たデータから順々に」処理していきます。データがcommand_aから出力されたら、すぐにcommand_bはそのデータを処理します。まだEOFが来てないけどcommand_aからの出力が来ないぞ、というときにはcommand_bはどうするでしょうか。そうですね、標準入力からのデータを読み込む部分で「ブロック中」になって、command_aが標準出力になにかを吐くのを待ち続けるわけです。けなげですね。ちなみに、このように入力と出力をパイプでつないで、「ファイルの終わりを待たずにきたデータから順々に」なにか処理をするのを、パイプライン処理、とか、ストリーム処理、と言います。 206 | 207 | また、パイプはシェル上でふたつのプロセスの標準入出力をつなぐだけではなく、プロセス上でも新しい入出力のペアを作ることができます。RubyだったらIO.pipeを使うと実現できるでしょう。Perlだったらpipe関数ですね。詳しくはrubyの公式リファレンスやperldoc,pipe(2)を参照してください。 208 | 209 | ### 次回予告 210 | 211 | 次回はファイルの入出力について、もっと深くまで潜っていきますよ!ファイルディスクリプタの話をして、ソケットの話をします。そのあとようやくファイルディスクリプタとforkの話ができたらいいな!さーて、次回も、サービス!サービスゥ! 212 | -------------------------------------------------------------------------------- /004.md: -------------------------------------------------------------------------------- 1 | ## ファイルディスクリプタ 2 | 3 | さて、前回、プロセスがファイルを通じて外部との入出力する様を見て見ました。今回はさらにプロセスとファイル入出力について詳しく見てみましょう。 4 | 5 | 前回はさらっと流してしまいましたが、実はプロセスは自分自身で実際にファイルを開いたりディスクに書き込んだりディスクからデータを読み出したりすることはありません。そういう低レイヤーの処理は、プロセスがシステムコールをOSに送ることで、OSが代わりに行ってくれます。そのあたりの話を、きちんと見て行きましょう。 6 | 7 | さて、なにはともあれ、プロセスが入出力をしたいと思ったら、ファイルを開くところから始めないといけません。 8 | 9 | * OSは、プロセスから「ファイルを開いてね」というシステムコールを受け取ると、実際にファイルを開きます 10 | * OSは、その開いたファイルを表す「番号札」作成します 11 | * OSは、その番号札をプロセスに対して返します。 12 | 13 | さて、ファイルを開いたら、今度はそこになにかを書き込んでみましょうか 14 | 15 | * プロセスは、さっき受け取った「番号札」をつかって、「n番の番号札で表されてるファイルにこれ書き込んでおいて」っていうシステムコールを送ります。 16 | * OSは、「番号札」で表された、すでに開かれているファイルに対して書き込みを行います 17 | 18 | じゃあ、今度はファイルを閉じましょう 19 | 20 | * プロセスは、不要になった番号札をcloseというシステムコールでOSに返却します 21 | * OSは、番号札が返却されたので、「もうこれは使わないんだな」と判断して、ファイルを閉じます 22 | 23 | と、こんな感じでファイルの入出力が行われているのですが、この「番号札」のことを、「ファイルディスクリプタ」と呼びます。実際、ファイルディスクリプタは整数値で表現されています。 24 | 25 | 例を見てみましょう。今回もRubyを使います。 26 | 27 | # fd.rb 28 | file = File.open("nyan.txt","w") # openシステムコールでnyan.txtを書き込みモードでopen 29 | p file.fileno # fileno メソッドで、ファイルディスクリプタ(番号札)を取得 30 | file.close #fileをclose 31 | 32 | 1行目で、openシステムコールをOSに対して送っています。正常にopenされると、ファイルディスクリプタを内部に持ったfileオブジェクトが生成されます。2行目で、fileオブジェクトが保持しているファイルディスクリプタを取得してターミナルに出力しています。3行目で、fileを閉じていますが、これはRubyが内部でfileオブジェクトが保持しているファイルディスクリプタを使って、OSにcloseシステムコールを送っているわけです。IO#readlineとかIO#writeメソッドなんかも、内部ではIOオブジェクトが保持しているファイルディスクリプタを使って、読み込みのためのシステムコールを送ったり書き込みのためのシステムコールを送ったりしているわけですね。 33 | 34 | さて、説明がすんだところで、実際にfd.rbを実行してみましょう。 35 | 36 | $ ruby fd.rb 37 | 5 38 | 39 | 「nyan.txtが書き込みモードで開かれたもの」についてる番号札が、5番なのが確認できましたね。 40 | 41 | ### 標準入出力のファイルディスクリプタ 42 | 43 | さて、勘のいいひとはそろそろ例の標準入力は0、標準出力は1、標準エラー出力は2、という謎の数字の正体について、感付きつつあるのではないでしょうか。そうです。実は、「標準入力のファイルディスクリプタは0、標準出力のファイルディスクリプタは1、標準エラー出力のファイルディスクプタは2」なのです。実際に確かめてみましょう 44 | 45 | # std_fds.rb 46 | p $stdin.fileno # => 0 47 | p $stdout.fileno # => 1 48 | p $stderr.fileno # => 2 49 | 50 | おー。 51 | 52 | つまり、前回出てきた & という記号は、「ファイルパスじゃなくてファイルディスクリプタを指定してるぜ」という意味の記号だったわけですね!そして、なぜリダイレクトのときに標準入力や標準出力にあのような数字が使われているのかが理解できたと思います。 53 | 54 | ### オープンファイル記述 55 | 56 | さて、今はプロセスの側からがファイルディスクリプタをどう扱っているかについて見てみましたが、今度はOSの側から見てみましょう。 57 | 58 | OSのお仕事は、「プロセスからファイルの操作を頼まれたら、代わりにやってあげること」です。そのためには、OSは実際のdiskの読み書きの他に、少なくとも以下の仕事をしないといけません。 59 | 60 | * プロセスに「ファイル開いて」って言われたら開いてあげる 61 | * ファイルを開いたら、そのファイル専用の「ファイルの状況どうなってるっけメモ」を作る 62 | * 開いたファイルの情報(書き込みモードなのか読み込みモードなのか、とか、ファイルパスはどこなのかとか、どこまで読み込んだかあるいは書き込んだかとか)を「ファイルの状況どうなってるっけメモ」に書いておく 63 | * プロセスのために番号札を作って、さっき書いた「ファイルの状況どうなってるっけメモ」がどのプロセスのどの番号札のものなのかを覚えておく 64 | * プロセスに番号札を貸してあげる 65 | 66 | 「ファイルの状況どうなってるっけメモ」を保持しておかないと、「次の行読み込んでよ」ってプロセスから言われたときに「ふぇぇ、次の行ってどこ〜〜〜〜〜」ってなっちゃいますよね。あるいは、どの「ファイルの状況どうなってるっけメモ」がどのプロセスの何番の番号札と紐づいているのかを覚えておかないと、あるプロセスが「5番の番号のやつに書き込んでよ」って言ってきても、「ふぇぇ、書き込みたいけどどのメモ見ればいいのか忘れちゃったよ〜〜〜」ってなっちゃいます。 67 | 68 | このとき、この「ファイルの状況どうなってるっけメモ」にあたるのが、オープンファイル記述と呼ばれるものです。OSは、「ファイル開いて」っていうシステムコールを受け取ると、オープンファイル記述を作り出して自分で保持しておきます。さらに、システムコールを送ってきたプロセスのidに対して、新しい番号札(ファイルディスクリプタ)を返します。このとき、オープンファイル記述とプロセスidと番号札の関連も、自分の中に保持しておきます。 69 | 70 | これで、たとえばpidが100番のプロセスから「5番のファイルの、次の行読み込んでよ」と言われても、「ふぇぇ」ってならずに、「100番のプロセスさんの5番の番号札に紐づいたメモはこれだな」「メモには/path/to/fileの3行目まで読み込んだって書いてあるな」「じゃあこのファイルの4行目を読み込めばいいね!」「はいできた!」と言ってデータを返すことができるわけですね! 71 | 72 | イメージを図にすると、こんな感じになります。 73 | 74 | ![ファイルディスクリプタの作成](https://raw.github.com/Shinpeim/process-book/master/images/004_01.png) 75 | ![ファイルへの書き込み](https://raw.github.com/Shinpeim/process-book/master/images/004_02.png) 76 | 77 | ### ファイルディスクリプタ/オープンファイル記述とfork 78 | 79 | さて、では、forkしたとき、ファイルディスクリプタやオープンファイル記述はどうなるのでしょうか? 80 | 81 | 先に答えを言ってしまいましょう。forkした場合、ファイルディスクリプタは複製されますが、複製されたファイルディスクリプタは同一のオープンファイル記述を参照します。 82 | 83 | 言い方を変えると、forkした場合、OSは新しいpidのために新しい番号札は作るけど、その番号札は同じ「ファイルの状況どうなってるっけメモ」に紐づけられてる、ということです。つまり、「ファイルの状況どうなってるっけメモ」は、親プロセスと子プロセスで共有するメモになります。 84 | 85 | そのため、forkしたときに同じ番号札(ファイルディスクリプタ)にたいして親プロセスと子プロセス両方で操作をすると、おかしなことになることがあります。 86 | 87 | ### オープンファイル記述は複製されない 88 | 例を見ましょう。 89 | 90 | # fork_fd.rb 91 | # -*- coding: utf-8 -*- 92 | 93 | read_file = File.new("nyan.txt","r") 94 | 95 | # ファイルをopenしたあとにforkしてみる 96 | pid = Process.fork 97 | 98 | if pid.nil? 99 | # 子プロセス 100 | lines = [] 101 | while line = read_file.gets 102 | lines << line 103 | end 104 | write_file = File.new("child.txt","w") 105 | write_file.write(lines.join) 106 | write_file.close 107 | else 108 | # 親プロセス 109 | lines = [] 110 | while line = read_file.gets 111 | lines << line 112 | end 113 | write_file = File.new("parent.txt","w") 114 | write_file.write(lines.join) 115 | write_file.close 116 | end 117 | read_file.close 118 | 119 | 子プロセスと親プロセスで、nyan.txtから一行ずつ入力を受け取っています。もしもforkされたときに「ファイルの状況どうなってるっけメモ」まで複製されているならば、親プロセスが一行読み込んだとき親プロセスの「ファイルの状況どうなってるっけメモ」は一行分進みますが、子プロセスの「ファイルの状況どうなってるっけメモ」は書き変わらないので、親プロセスでの読み込みは子プロセスでの読み込みに影響を与えないはずですね。つまり、親プロセスでも子プロセスでも、同じくファイルの内容をすべて読み込むことができるはずです。逆に、親と子が共通の「ファイルの状況どうなってるっけメモ」を参照しているならば、親プロセスで一行読み込んだら、共通の「ファイルの状況どうなってるっけメモ」が1行分進んでしまい、子プロセスではその行を読み込むことができなくなってしまいます。 120 | 121 | では実際に確かめて見ましょう。nyan.txtに以下の内容を書き込んだ上で、fork_fd.rbを実行してみましょう 122 | 123 | nyan 124 | nyan nyan 125 | nyan nyan nyan 126 | nyan nyan nyan nyan 127 | nyan nyan nyan nyan nyan 128 | nyan nyan nyan nyan nyan nyan 129 | 130 | 実行します 131 | 132 | $ ruby fork_fd.rb 133 | 134 | さて、結果はどうなったでしょうか?オープンファイル記述が複製されていないことが実感できたかと思います。 135 | 136 | ### ファイルディスクリプタは複製される 137 | 138 | では今度は、ファイルディスクリプタは複製されているのを見てみましょう 139 | 140 | 141 | # -*- coding: utf-8 -*- 142 | file = File.open("nyan.txt","r") 143 | 144 | # ファイルをopenしてからforkする 145 | 146 | pid = Process.fork 147 | 148 | if pid.nil? 149 | #子プロセス 150 | sleep 1 # 親プロセスがfileを閉じるのを待つ 151 | 152 | # 親プロセスがfdを閉じてても、自分はまだ番号札を持ってるから読み込める 153 | puts file.readlines.join 154 | 155 | file.close #自分も番号札を返す 156 | else 157 | # 親プロセス 158 | file.close #番号札をOSに返す 159 | Process.wait(pid) #子プロセスが終わるの待つ 160 | end 161 | 162 | 実行してみると、親プロセスがすでに番号札をOSに返してしまっても、子プロセスは複製された番号札を持っているので問題なくファイル操作ができているのが見て取れると思います。 163 | 164 | このあたりのイメージを図にするとこんな感じです。 165 | 166 | ![forkされたときのイメージ](https://raw.github.com/Shinpeim/process-book/master/images/004_03.png) 167 | ![オープンファイル記述が共有されている](https://raw.github.com/Shinpeim/process-book/master/images/004_04.png) 168 | 169 | ### どうするのがベストプラクティスなの? 170 | 171 | すでにfileがopenされている状態でforkすると、以上に見たように予期せぬ動作で混乱することがあります。そのため、forkした場合、親プロセスで使わないファイルは親プロセスですぐ閉じる、子プロセスで使わないファイルは子プロセスですぐ閉じるとすると、最も問題が起きにくいと思います。子プロセスでファイルを閉じたとしても、親プロセスでファイル使いたい場合に問題なく扱える(またはその逆も)のは、上に見た通りですからね 172 | 173 | ### リダイレクトの順序ふたたび 174 | 175 | さて、forkした際のファイルディスクリプタ、オープンファイル記述の振る舞いについては上に見たとおりです。では今度は前回謎の挙動として上げておいた、「リダイレクトの順序」について見てみましょう。 176 | 177 | まずは、リダイレクトの順序の謎はどのようなものだったか簡単に復讐してみましょう。 178 | 179 | $ ruby stdout_stderr.rb 1>out.txt 2>&1 180 | 181 | とすると、プロセス内で標準出力に書き出したものも標準エラー出力に書き出したものも out.txt に出力されるが 182 | 183 | $ ruby stdout_stderr.rb 2>&1 1>out.txt 184 | 185 | とすると、標準エラー出力に対する出力は、依然としてコンソールに出力されてしまう、というのがその謎の挙動でしたね。 186 | 187 | このような挙動が何故起こるのか。それは、リダイレクトが実際にどのように実現されているのかを理解すると見えてきます。 188 | 189 | ### リダイレクトはファイルディスクリプタの複製である 190 | 191 | 1>out.txt とすると、標準出力に対して出力した出力が、なぜコンソールにではなく out.txt に出力されるのか、その動きを見てみましょう。実は、1>out.txt というのは、「out.txtを書き込みモードで開いて、そのファイルディスクリプタを複製したものを fd:1(標準出力) とする」という意味なのです。 192 | 193 | さて、ではここで、標準出力になにかを出力してみましょう。標準出力に対する書き込みは fd:1 に対する書き込みです。今、fd:1 は、out.txt を指していますね。こんな具合で、標準出力に対する書き込みは、out.txt に書き込まれることになるわけです。 194 | 195 | では今度は、 2>&1 としたときのことを考えてみましょう。これは、「fd:1 を複製したものをfd:2 とする」という意味になりますね。これにより、fd:2 に対する書き込みは、fd:1 と同じ、ターミナルへ出力されることになります。 196 | 197 | では、合わせ技を行ってみるとどうなるでしょうか。まずは意図通り動くパターンから見てみます。 198 | 199 | $ command 1>out.txt 2>&1 200 | 201 | まず、"1>out.txt" が評価されます。それによって、fd:1 は、out.txtを指すことになります。つぎに、"2>&1" が評価されます。この時点でfd:1 は out.txt を指していますから、fd:2 もout.txtを指すようになります。これで、無事に fd:1 (標準出力)に対する書き込みも out.txt に書かれるし、fd:2 (標準エラー出力)に対する書き込みも、out.txt に書かれるようになりました。Yay! 202 | 203 | 次に意図通りでないパターンを見ましょう。 204 | 205 | $ command 2>&1 1>out.txt 206 | 207 | まず、"2>&1"が評価されます。fd:2 は fd:1を複製したものになりますね。このとき、fd:1 はまだ変更されていないため、デフォルトのターミナルを指しています。というわけで、fd:2 はターミナルを指すことになります。次に、"1>out.txt" が評価されます。out.txt を書き込みモードで open して、そのファイルディスクリプタの複製が fd:1 になります。これで fd:1 は out.txt を指すようになりました。今、ファイルディスクリプタはどうなっているでしょうか? fd:1はout.txtを指していますが、fd:2はターミナルを指していますね。ここで、標準エラー出力(fd:2)に対して書き込みを行えば、当然、その出力結果はターミナルに出力されることになるわけです。Oops! 208 | 209 | ### 次回予告 210 | 211 | ソケットの話してpreforkサーバーを自分で書いてみるつもり 212 | -------------------------------------------------------------------------------- /005.md: -------------------------------------------------------------------------------- 1 | ## preforkサーバーを作ってみよう 2 | 3 | さて、前回は、fork するとファイルディスクリプタ(以下fdと表記)は複製されるけどオープンファイル記述は共有されるというのを見ました。これを利用して、preforkモデルのサーバーを実際に書いてみましょう。 4 | 5 | ### tcp socketはファイルである 6 | 7 | 以前見たとおり、Linuxではプロセスの入出力はファイルを通じて行います。とうぜん、ネットワークごしに入出力する tcp socket もファイルです。ここで「ファイルです」が意味するのは、プロセスがソケットを通じて入出力をしようと思えば、socket の fd を通じて write や read を行うということですね。では、実際に socket がファイルであるところを見てみましょう 8 | 9 | # -*- coding: utf-8 -*- 10 | require "socket" 11 | 12 | # 12345 portで待ち受けるソケットを開く 13 | listening_socket = TCPServer.open(12345) 14 | 15 | # ソケットもファイルなので fd がある 16 | p listening_socket.fileno 17 | 18 | # ひとまずなにもせず閉じる 19 | listening_socket.close 20 | 21 | 上記のような Ruby スクリプトを実行してみると、openしたソケットがfdを持つことが確認できるかと思います。 22 | 23 | ### クライアントの接続を受け入れる 24 | 25 | 今は socket を開いてなにもせずにすぐ閉じてしまいましたが、今度はクライアントの接続を受け入れてしてみましょう。 26 | 27 | # -*- coding: utf-8 -*- 28 | require "socket" 29 | 30 | # 12345 portで待ち受けるソケットを開く 31 | listening_socket = TCPServer.open(12345) 32 | 33 | p listening_socket.fileno 34 | 35 | # acceptでクライアントからの接続を待つ 36 | # 接続されるまでブロックする 37 | puts "accepting..." 38 | socket = listening_socket.accept 39 | puts "accepted!" 40 | 41 | # 接続されると新しいsocketが作られる 42 | # このsocketを通じてクライアントと通信する 43 | # あたらしいsocketなのでfdの番号がlistening_socketと違う 44 | p socket.fileno 45 | 46 | # なにもせずクライアントとのコネクションを切る 47 | socket.close 48 | 49 | # 待ち受けソケットも閉じる 50 | listening_socket.close 51 | 52 | 上記のような Rubyスクリプトを適当な名前で作って、実行してみましょう。listening_socket の fd が出力されたあとに、accepting…と出力されて、そこで止まってしまいプロンプトが帰ってこないかと思います。なぜこういう動きをするか、いままでこのドキュメントを読み進めてきたみなさんはもう理解できますね。listen している socket で accept を呼び出すと、プロセスはそこでブロックして、クライアントからの接続を待ちます。そこでブロック中になっているため、プロセスがそれ以上進まないわけですね。 53 | 54 | では、今度はそのままターミナルをもうひとつ開いて、ここにコネクションを貼ってみましょう。 55 | 56 | # べつのターミナルで 57 | $ telnet localhost 12345 58 | 59 | 上記のように、 telnet コマンドで、さっきのプロセスが待ち受けてる 12345 ポートに接続してみましょう。一瞬で接続が切られてしまうかと思います。 60 | 61 | 一方、今度はさっきプロンプトが返ってこないままになっていたターミナルを再度見てみてください。 accepted! のあとに、listening_socket の fd とはまた違う数字の fd が出力されて、プロンプトが返ってきたかと思います。これは、telnetでクライアントから接続されたことにより、accept の行でブロック中になっていたプロセスが動き出したためです。accept はクライアントから接続されるとブロック中から抜け出し、新しい socket を作り出して返します。サーバーのプロセスは、この新しい socket を通じてクライアントと通信をします。この socket にたいして write をすればクライアントへデータを送ることになるし、この socket から read をすれば、クライアントからの入力を受け取るという感じですね。とうぜん、この socket を close するとクライアントとのコネクションは切断されます。 62 | 63 | 今回はなにもせずに socket を close したので、クライアント側(telnetコマンドを打った側)ではすぐにサーバーからコネクションが切られてしまったわけですね。 64 | 65 | ### クライアントから送られてくるデータを読み込む 66 | 67 | さっきはなにもせず socket を close してしまいましたが、今度はクライアントからデータが送られてきたらそれを読む、という動きにしてみましょう。 68 | 69 | # -*- coding: utf-8 -*- 70 | require "socket" 71 | 72 | listening_socket = TCPServer.open(12345) 73 | 74 | # クライアント受け入れ無限地獄 75 | loop do 76 | puts "accepting..." 77 | socket = listening_socket.accept 78 | puts "accepted a client!" 79 | 80 | # クライアントからの入力受け取り無限地獄 81 | loop do 82 | # クライアントからの入力を1行読む 83 | # 入力されるまでブロックする 84 | line = socket.gets 85 | line.gsub!(/[\r\n]/,"") #改行コード捨てる 86 | 87 | # exitと入力されてたらソケット閉じてループを抜ける 88 | if line == "exit" 89 | socket.close 90 | puts "closed a connection!" 91 | break 92 | end 93 | 94 | # そうでなければ標準出力に出力 95 | puts line 96 | end 97 | end 98 | 99 | はい、ちょっと書き換えてみました。 100 | 101 | ターミナルを立ち上げて、これを実行してみましょう。このターミナル上で動いてるのがサーバープロセスになります。今は accepting… が出力されたところでプロセスがブロックしてると思います。ここまではさっきとおなじですね。では、またべつのターミナルを開いて、telnetコマンドでサーバープロセスに接続してみましょう。 102 | 103 | $ telnet localhost 12345 104 | 105 | 今度は切断されないと思います。 106 | 107 | ではまたサーバープロセスが走ってるほうのターミナルを見てみましょう。"accepted a client"と出力されて、そこでプロセスがブロックしていると思います。line = socket.gets のところで、クライアントからのデータを読み込もうとしていますが、クライアントがまだなにもデータを送っていないのでここでブロックしているわけですね。 108 | 109 | では今度は telnet のほうのターミナルで、なんかを入力して、改行してみましょう。 110 | 111 | 再度サーバープロセスのほうを見てみると、今 telnet で入力した一行が、標準出力に書き出されているのが見て取れると思います。 112 | 113 | では telnet のほうに戻って(何度も往復してたいへんだ!)、今度は exit と入力して改行してみましょう。すると、サーバープロセスが socket を close したことにより、接続が切れるかと思います。 114 | 115 | サーバープロセスのほうを見てみると、"closed a connection!" が出力されたあと、また "accepting…" が出力されて、ブロックしてると思います。これは、break でクライアントからの入力受け取り無限地獄を抜けたはいいけれど、今度はクライアント受け入れ無限地獄loopによりまた listening_socket.accept しているところでブロックしてるわけですね。 116 | 117 | 動きを確認したら、サーバープロセスのほうで Ctrl + C を入力して、プロセスを終了してあげましょう。 118 | 119 | いまは puts line で標準出力にクライアントからの入力を出力していますが、この行を socket に対する書き込みにすれば、いわゆるエコーサーバーとして動くプロセスになります。そのあたりは宿題とするので、自分で書き換えて動きを見てみてください。 120 | 121 | ### このサーバーは出来損ないだ、たべられないよ 122 | 123 | さて、これでクライアントから接続を待って、クライアントに接続されたらそのクライアントとネットワーク越しに入出力することができました。しかし、このサーバーには欠陥があります。わかりますか? 124 | 125 | そう、このままでは、同時にひとつのクライアントしか処理できないのです。クライアントからの接続を accept したあとは、このプロセスは「クライアントの入力受け取り無限地獄」にいます。その無限地獄にいる限り、このプロセスは次の listening_socket.accept に到達することはありません。なので、「クライアントの入力受け取り無限地獄」を抜けるまでは新しく接続してこようとするクライアントを受け入れることができないのです。これは困りましたね。 126 | 127 | じっさい、このサーバープロセスを立ち上げた状態で、さらにターミナルをふたつ立ち上げて、両方で 128 | 129 | $ telnet localhost 12345 130 | 131 | をしてみると、先に telnet したほうは普通に動くのだけれど、もういっこのほうはいくら入力してもサーバープロセスがうんともすんとも言わないのが見て取れると思います。 132 | 133 | ### 明日の同じ時間にここに来てください。本当のサーバーってやつを見せてあげますよ 134 | 135 | べつに用意する食材もないので、明日の同じ時間を待つ必要はありません。すぐにコードを書き換えてしまいましょう。 136 | 137 | # -*- coding: utf-8 -*- 138 | require "socket" 139 | 140 | number_of_workers = 3 141 | listening_socket = TCPServer.open(12345) 142 | 143 | number_of_workers.times do 144 | pid = fork 145 | 146 | if pid 147 | # 親プロセスは次々に fork で子プロセスを作る 148 | next 149 | else 150 | # 子プロセス 151 | 152 | # クライアント受け入れ無限地獄 153 | loop do 154 | puts "accepting..." 155 | # 子プロセスは全部ここでブロックする。 156 | socket = listening_socket.accept 157 | puts "accepted a client!" 158 | 159 | # クライアントの入力受け取り無限地獄 160 | loop do 161 | line = socket.gets 162 | line.gsub!(/[\r\n]/,"") 163 | 164 | if line == "exit" 165 | socket.close 166 | puts "closed a connection!" 167 | break 168 | end 169 | 170 | puts line 171 | end 172 | end 173 | end 174 | end 175 | 176 | # 子プロセスは無限ループしてるからここには届かない 177 | # 親プロセスでは子プロセスの終了を待ち続ける 178 | Process.waitall 179 | 180 | listening_socket を作ったあとに、3回 fork するようにしてみました。親プロセスでは fork したあとに何もしないで子プロセスの終了を待ちます。一方、子プロセスでは、 accept を呼んでブロックしていますね。 181 | 182 | さて、ここで前回の内容が役に立ちますよ。 183 | 184 | listening_socket はファイルでした。そのため、fd を持ちます。そして、forkした場合、fd は複製されるけど、複製された fd は複製もとと同じオープンファイル記述を参照しているのでしたね。 185 | 186 | というわけで、今、listening_socket を作ったあとに fork したことで、オープンファイル記述、つまり「ソケットは12345 portで待ち受けてるよ」と書かれた「ファイルどうなってたっけメモ」を全てのプロセスで共有している状態になっているわけです。ここまではいいですか? 187 | 188 | そして、親プロセスではその listening_socket に対して何もせず、子プロセスで accept していますね。この3つの子プロセスは、「クライアントからの接続を獲得して新しい socket を作ろう」と身構えてブロックしている状態なわけです。ここで、あるクライアントが 12345 ポートに対して接続してきたとしましょう。なにが起こりますか? 189 | 190 | 3つの子プロセスは、それぞれがクライアントからの接続を受け入れて新しい socket を作ろうとしますが、オープンファイル記述が共有されているため、クライアントからの接続を受け入れられるのはたったひとつの子プロセスだけです。前回の内容を思い出して下さい。file を open したあと fork して、両方のプロセスでその file を読み込んだ場合、片方のプロセスでしか読み込むことができていなかったと思います。これと同じことが accept でも起こっているわけですね。 191 | 192 | さて、首尾よく accept できて新しいソケットを獲得した子プロセスは、クライアントからの入力受け取り無限地獄へと突入します。というわけで、今接続してきたクライアントとのやり取りは、この子プロセスにまかせることができました。一方、残念ながら accept できなかった他の子プロセスは、さっきとおなじところでブロックしたままです。 193 | 194 | さて、ここに、さらに新しいクライアントが接続してきた場合はどうなるでしょうか?子プロセスのうちひとつはまだクライアントからの入力受け取り無限地獄にいますが、ふたつのプロセスは accept でブロック中になっています。こんどはこのふたつのプロセスのうちのどちらかが accpet に成功して新しいソケットを作ってクライアントとやりとりすることになるわけです。 195 | 196 | こんなふうに、あらかじめ子プロセスをいくらか fork しておいて、その子プロセスでクライアントからの接続を受け入れて処理するような仕組みを、「prefork」といいます。先に(pre)forkしておくサーバーってことですね。 197 | 198 | さて、これで無事、同時に複数のクライアントからの接続を受け入れることが可能になりました。今回は 3 回forkしたので、同時接続数は 3 つまでですね。サーバープロセスの他にもターミナルをたくさん立ち上げて、それぞれで telnet localhost 12345 してみてください。3つまでは同時に処理できるけど、4つを超えると同時に処理できてないことが見て取れるかと思います。 199 | 200 | 今までの話で、preforkサーバーが書けて、さらにどうしてそんなことが可能なのかも理解できましたね! 201 | 202 | ### preforkの利点、欠点 203 | 204 | さて、上に見たように、prefork サーバーはひとつのプロセスがひとつのクライアントを受け持つようなアーキテクチャになっています。このことは、いくつかの利点と欠点をもたらします。 205 | 206 | まず、上に見たように、worker(子プロセス)の数が少ないと、同時に処理できるクライアントの数が少なくなってしまいます。同時処理の数を増やしたければ、その数だけプロセスを生成する必要があるわけです。プロセスが生成されればそのぶんだけメモリーは消費されるので、この方法は意外と効率がよくないんですね。 207 | 208 | 一方で、アーキテクチャが単純なので、コードが追いやすいです。シンプルであることは、とても大切なことです。さらに、ひとつのクライアントがひとつのプロセスで処理されるためたとえばプロセスが暴走したとかそういうときの影響範囲がせまくなります。ひとつのプロセス内でたくさんのクライアントを受け持つと、そのプロセスがなんかおかしなことになったときに影響範囲が大きくなって困りますね。 209 | 210 | ### 次回予告 211 | 212 | 次回はちょっと話を戻して、forkした際に親が先に死んだり終わったりしたらどうなるのとかそういう話をしたいなって思います。 213 | -------------------------------------------------------------------------------- /release/process_book.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Process Book 4 | 5 | 6 | 7 | 8 | 9 | 10 |

11 |
12 |

Process Book

13 |
14 |

Author: Shinpeim

15 |

Contributors: Kazuya Takeshima, Seizan Shimazaki, kkosuge, moznion

16 |

17 |
18 |
19 |

この文書はなんですか?

20 | 21 |

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

22 | 23 |

書いているあいだは gist で管理されていたのですが、ボリュームが大きくなったので github で管理するように変えました。

24 | 25 |

目次

26 | 27 |

導入

28 | 29 |

プロセスの生成

30 | 31 |

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

32 | 33 |

ファイルディスクリプタ

34 | 35 |

preforkサーバーを作ってみよう

36 | 37 |

ゾンビプロセスと孤児プロセス

38 | 39 |

シグナルとkill

40 | 41 |

プロセスグループとフォアグランドプロセス

42 | 43 |

導入

44 | 45 |

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

46 | 47 |

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

48 | 49 |

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

50 | 51 |
A -----        ----                  ---------         ----------
  52 | B      ---         --------                   ----   --          -------
  53 | C         -----            ----------             ---                   --
  54 | 
55 | 56 |

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

57 | 58 |

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

59 | 60 |

マルチコアとの関係

61 | 62 |

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

63 | 64 |
+ ------ + A -----        ----                  ---------         ----------
  65 | | core 1 | B      ---         --------                   ----   --          -------
  66 | + ------ + C         -----            ----------             ---                   --
  67 | + ------ + D ---        ----                  ---------         ----------
  68 | | core 2 | E     ---         --------                   ----   --          -------
  69 | + ------ + F        ----            ----------             ---                   --
  70 | 
71 | 72 |

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

73 | 74 |

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

75 | 76 |

次回予告

77 | 78 |

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

79 | 80 |

プロセスの生成

81 | 82 |

プロセスの例

83 | 84 |

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

85 | 86 |

ターミナルで、

87 | 88 |
$ ps
  89 | 
90 | 91 |

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

92 | 93 |
  PID TTY          TIME CMD
  94 |  4400 pts/2    00:00:00 bash
  95 |  4419 pts/2    00:00:00 ps
  96 | 
97 | 98 |

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

99 | 100 |

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

101 | 102 |
 $ perl -e 'while(1){sleep}' &
 103 | 
104 | 105 |

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

106 | 107 |
 $ ps
 108 | 
109 | 110 |

と入力してみると、

111 | 112 |
  PID TTY          TIME CMD
 113 | 4420 pts/2    00:00:00 perl
 114 | 
115 | 116 |

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

117 | 118 |
 $ perl -e 'while(1){sleep}' &
 119 | 
120 | 121 |

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

122 | 123 |

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

124 | 125 |
$ fg
 126 | 
127 | 128 |

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

129 | 130 |

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

131 | 132 |

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

133 | 134 |

生成 -> 処理中 -> 終了

135 | 136 |

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

137 | 138 | 143 | 144 |

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

145 | 146 |

fork

147 | 148 |

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

149 | 150 |

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

151 | 152 |

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

153 | 154 |

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

155 | 156 |

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

157 | 158 |

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

159 | 160 |

プロセスツリー

161 | 162 |

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

163 | 164 |

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

165 | 166 |
$ ps ax | grep init
 167 | 1 ?        Ss     0:10 /sbin/init
 168 | 
169 | 170 |

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

171 | 172 |

exec

173 | 174 |

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

175 | 176 |

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

177 | 178 |

まとめると、

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

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

186 | 187 |

188 | 189 |

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

190 | 191 |
use strict;
 192 | use warnings;
 193 | 
 194 | print "forking...\n";
 195 | 
 196 | # forkシステムコールを呼び出す
 197 | my $pid = fork;
 198 | 
 199 | # forkに失敗すると返り値はundef
 200 | die "fork failed." unless defined $pid;
 201 | 
 202 | # ここに来てるということは、正常にプロセスが複製された。
 203 | # この時点で親プロセスと子プロセスが *別々の環境で*
 204 | # 同時にこのプログラムを実行していることになる。
 205 | print "forked!\n";
 206 | 
 207 | # forkで生成された子プロセスでは、forkの返り値が 0 となる
 208 | # 親プロセスでは、生成された子プロセスのpidが入ってくる
 209 | if ($pid == 0){
 210 |     #子プロセスはこっちを実行する
 211 | 
 212 |     # execシステムコールで、perlのプロセスをrubyのプロセスに書き換えてしまう!
 213 |     exec "ruby -e 'loop do;sleep;end'";
 214 | }
 215 | else{
 216 |     #親プロセスはこっちを実行する
 217 | 
 218 |     #子プロセスが終了するのを待つ
 219 |     waitpid($pid,0);
 220 | }
 221 | 
222 | 223 |

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

224 | 225 |
$ perl ./fork_exec.pl &
 226 | forking...
 227 | forked!
 228 | forked!
 229 | 
230 | 231 |

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

232 | 233 |

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

234 | 235 |

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

236 | 237 |
$ ps
 238 |   PID TTY           TIME CMD
 239 | 81996 ttys003    0:00.01 perl fork_exec.pl
 240 | 81998 ttys003    0:00.01 ruby -e loop do;sleep;end
 241 | 
242 | 243 |

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

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

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

251 | 252 |

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

253 | 254 |

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

255 | 256 |

今後の予定

257 | 258 | 262 | 263 |

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

264 | 265 |

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

266 | 267 |

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

268 | 269 |

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

270 | 271 |

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

272 | 273 |
nyan
 274 | nyan nyan
 275 | nyan nyan nyan
 276 | 
277 | 278 |

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

279 | 280 |
file = File.open("nyan.txt","r")
 281 | lines = file.readlines #ファイルの中身を全部読み込む
 282 | file.close
 283 | 
284 | 285 |

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

286 | 287 |
lines = file.readlines
 288 | 
289 | 290 |

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

291 | 292 |

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

293 | 294 |
# nyan_copy.rb
 295 | file = File.open("nyan.txt","r")
 296 | lines = file.readlines
 297 | file.close
 298 | 
 299 | file = File.open("nyan_copy.txt","w")
 300 | file.write(lines.join)
 301 | file.close
 302 | 
303 | 304 |

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

305 | 306 |

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

307 | 308 |

すべてがファイル???

309 | 310 |

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

311 | 312 |

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

313 | 314 |

例:標準入出力

315 | 316 |

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

317 | 318 |
# stdout.rb
 319 | file = File.open("nyan.txt","r")
 320 | lines = file.readlines
 321 | file.close
 322 | 
 323 | file = $stdout # この行だけ書き換えた
 324 | file.write(lines.join)
 325 | file.close
 326 | 
327 | 328 |

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

329 | 330 |

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

331 | 332 |

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

333 | 334 |
# stdin.rb
 335 | file = $stdin
 336 | lines = file.readlines #標準入力からの入力を全部受け取る
 337 | file.close
 338 | 
 339 | file = $stdout
 340 | file.write(lines.join) # 標準出力に対して内容を書き出す
 341 | file.close
 342 | 
343 | 344 |

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

345 | 346 |
lines = file.readlines #標準入力からの入力を全部受け取る
 347 | 
348 | 349 |

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

350 | 351 |

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

352 | 353 |

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

354 | 355 |
 $ ruby stdin.rb #さっき実行したコマンド
 356 |  aaaa
 357 |  bbbbbb
 358 |  ccc
 359 | 
360 | 361 |

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

362 | 363 |
aaaa
 364 | bbbbbb
 365 | ccc
 366 | 
367 | 368 |

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

369 | 370 |

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

371 | 372 |

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

373 | 374 |

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

375 | 376 |

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

377 | 378 |

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

379 | 380 |

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

381 | 382 |

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

383 | 384 |

リダイレクト

385 | 386 |

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

387 | 388 |

標準出力のリダイレクト

389 | 390 |

例えば、

391 | 392 |
# print_mew.rb
 393 | puts "mew" # putsは標準出力に対して引数を出力する
 394 | 
395 | 396 |

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

397 | 398 |
$ ruby print_mew.rb 1>mew.txt
 399 | 
400 | 401 |

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

402 | 403 |
$ ruby print_mew.rb 1>>mew.txt
 404 | 
405 | 406 |

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

407 | 408 |
$ ruby print_mew.rb 1>mew.txt
 409 | 
410 | 411 |

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

412 | 413 |

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

414 | 415 |
$ ruby print_mew.rb > mew.txt
 416 | 
417 | 418 |

標準入力のリダイレクト

419 | 420 |

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

421 | 422 |

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

423 | 424 |
$ ruby stdin.rb 0<mew.txt
 425 | mew
 426 | 
427 | 428 |

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

429 | 430 |

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

431 | 432 |
$ ruby stdin.rb < mew.txt
 433 | mew
 434 | 
435 | 436 |

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

437 | 438 |
$ ruby stdin.rb 0<mew.txt 1>mew_copy.txt
 439 | 
440 | 441 |

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

442 | 443 |

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

444 | 445 |

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

446 | 447 |

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

448 | 449 |
# stdout_stderr.rb
 450 | puts "this is stdout"
 451 | warn "this is stderr" # warnは標準エラー出力に引数を出力する
 452 | 
453 | 454 |

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

455 | 456 |

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

457 | 458 |
$ ruby stdout_stderr.rb 1>out.txt 2>err.txt
 459 | 
460 | 461 |

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

462 | 463 |

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

464 | 465 |

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

466 | 467 |
$ ruby stdout_stderr.rb 1>out.txt 2>&1
 468 | 
469 | 470 |

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

471 | 472 |

リダイレクトの順序

473 | 474 |
$ ruby stdout_stderr.rb 1>out.txt 2>&1
 475 | 
476 | 477 |

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

478 | 479 |
$ ruby stdout_stderr.rb 1>out.txt 2>&1
 480 | 
481 | 482 |

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

483 | 484 |

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

485 | 486 |

パイプ

487 | 488 |

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

489 | 490 |
$ command_a | command_b
 491 | 
492 | 493 |

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

494 | 495 |

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

496 | 497 |

次回予告

498 | 499 |

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

500 | 501 |

ファイルディスクリプタ

502 | 503 |

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

504 | 505 |

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

506 | 507 |

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

508 | 509 | 514 | 515 |

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

516 | 517 | 521 | 522 |

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

523 | 524 | 528 | 529 |

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

530 | 531 |

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

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

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

540 | 541 |

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

542 | 543 |
$ ruby fd.rb
 544 | 5
 545 | 
546 | 547 |

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

548 | 549 |

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

550 | 551 |

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

552 | 553 |
# std_fds.rb
 554 | p $stdin.fileno  # => 0
 555 | p $stdout.fileno # => 1
 556 | p $stderr.fileno # => 2
 557 | 
558 | 559 |

おー。

560 | 561 |

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

562 | 563 |

オープンファイル記述

564 | 565 |

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

566 | 567 |

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

568 | 569 | 576 | 577 |

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

578 | 579 |

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

580 | 581 |

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

582 | 583 |

イメージを図にすると、こんな感じになります。

584 | 585 |

ファイルディスクリプタの作成 586 | ファイルへの書き込み

587 | 588 |

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

589 | 590 |

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

591 | 592 |

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

593 | 594 |

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

595 | 596 |

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

597 | 598 |

オープンファイル記述は複製されない

599 | 600 |

例を見ましょう。

601 | 602 |
# fork_fd.rb
 603 | # -*- coding: utf-8 -*-
 604 | 
 605 | read_file = File.new("nyan.txt","r")
 606 | 
 607 | # ファイルをopenしたあとにforkしてみる
 608 | pid = Process.fork
 609 | 
 610 | if pid.nil?
 611 |   # 子プロセス
 612 |   lines = []
 613 |   while line = read_file.gets
 614 |     lines << line
 615 |   end
 616 |   write_file = File.new("child.txt","w")
 617 |   write_file.write(lines.join)
 618 |   write_file.close
 619 | else
 620 |   # 親プロセス
 621 |   lines = []
 622 |   while line = read_file.gets
 623 |     lines << line
 624 |   end
 625 |   write_file = File.new("parent.txt","w")
 626 |   write_file.write(lines.join)
 627 |   write_file.close
 628 | end
 629 | read_file.close
 630 | 
631 | 632 |

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

633 | 634 |

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

635 | 636 |
nyan
 637 | nyan nyan
 638 | nyan nyan nyan
 639 | nyan nyan nyan nyan
 640 | nyan nyan nyan nyan nyan
 641 | nyan nyan nyan nyan nyan nyan
 642 | 
643 | 644 |

実行します

645 | 646 |
$ ruby fork_fd.rb
 647 | 
648 | 649 |

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

650 | 651 |

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

652 | 653 |

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

654 | 655 |
# -*- coding: utf-8 -*-
 656 | file = File.open("nyan.txt","r")
 657 | 
 658 | # ファイルをopenしてからforkする
 659 | 
 660 | pid = Process.fork
 661 | 
 662 | if pid.nil?
 663 |   #子プロセス
 664 |   sleep 1 # 親プロセスがfileを閉じるのを待つ
 665 | 
 666 |   # 親プロセスがfdを閉じてても、自分はまだ番号札を持ってるから読み込める
 667 |   puts file.readlines.join
 668 | 
 669 |   file.close #自分も番号札を返す
 670 | else
 671 |   # 親プロセス
 672 |   file.close #番号札をOSに返す
 673 |   Process.wait(pid) #子プロセスが終わるの待つ
 674 | end
 675 | 
676 | 677 |

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

678 | 679 |

このあたりのイメージを図にするとこんな感じです。

680 | 681 |

forkされたときのイメージ 682 | オープンファイル記述が共有されている

683 | 684 |

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

685 | 686 |

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

687 | 688 |

リダイレクトの順序ふたたび

689 | 690 |

さて、forkした際のファイルディスクリプタ、オープンファイル記述の振る舞いについては上に見たとおりです。では今度は前回謎の挙動として上げておいた、「リダイレクトの順序」について見てみましょう。

691 | 692 |

まずは、リダイレクトの順序の謎はどのようなものだったか簡単に復讐してみましょう。

693 | 694 |
$ ruby stdout_stderr.rb 1>out.txt 2>&1
 695 | 
696 | 697 |

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

698 | 699 |
$ ruby stdout_stderr.rb 2>&1 1>out.txt
 700 | 
701 | 702 |

とすると、標準エラー出力に対する出力は、依然としてコンソールに出力されてしまう、というのがその謎の挙動でしたね。

703 | 704 |

このような挙動が何故起こるのか。それは、リダイレクトが実際にどのように実現されているのかを理解すると見えてきます。

705 | 706 |

リダイレクトはファイルディスクリプタの複製である

707 | 708 |

1>out.txt とすると、標準出力に対して出力した出力が、なぜコンソールにではなく out.txt に出力されるのか、その動きを見てみましょう。実は、1>out.txt というのは、「out.txtを書き込みモードで開いて、そのファイルディスクリプタを複製したものを fd:1(標準出力) とする」という意味なのです。

709 | 710 |

さて、ではここで、標準出力になにかを出力してみましょう。標準出力に対する書き込みは fd:1 に対する書き込みです。今、fd:1 は、out.txt を指していますね。こんな具合で、標準出力に対する書き込みは、out.txt に書き込まれることになるわけです。

711 | 712 |

では今度は、 2>&1 としたときのことを考えてみましょう。これは、「fd:1 を複製したものをfd:2 とする」という意味になりますね。これにより、fd:2 に対する書き込みは、fd:1 と同じ、ターミナルへ出力されることになります。

713 | 714 |

では、合わせ技を行ってみるとどうなるでしょうか。まずは意図通り動くパターンから見てみます。

715 | 716 |
$ command 1>out.txt 2>&1
 717 | 
718 | 719 |

まず、"1>out.txt" が評価されます。それによって、fd:1 は、out.txtを指すことになります。つぎに、"2>&1" が評価されます。この時点でfd:1 は out.txt を指していますから、fd:2 もout.txtを指すようになります。これで、無事に fd:1 (標準出力)に対する書き込みも out.txt に書かれるし、fd:2 (標準エラー出力)に対する書き込みも、out.txt に書かれるようになりました。Yay!

720 | 721 |

次に意図通りでないパターンを見ましょう。

722 | 723 |
$ command 2>&1 1>out.txt
 724 | 
725 | 726 |

まず、"2>&1"が評価されます。fd:2 は fd:1を複製したものになりますね。このとき、fd:1 はまだ変更されていないため、デフォルトのターミナルを指しています。というわけで、fd:2 はターミナルを指すことになります。次に、"1>out.txt" が評価されます。out.txt を書き込みモードで open して、そのファイルディスクリプタの複製が fd:1 になります。これで fd:1 は out.txt を指すようになりました。今、ファイルディスクリプタはどうなっているでしょうか? fd:1はout.txtを指していますが、fd:2はターミナルを指していますね。ここで、標準エラー出力(fd:2)に対して書き込みを行えば、当然、その出力結果はターミナルに出力されることになるわけです。Oops!

727 | 728 |

次回予告

729 | 730 |

ソケットの話してpreforkサーバーを自分で書いてみるつもり

731 | 732 |

preforkサーバーを作ってみよう

733 | 734 |

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

735 | 736 |

tcp socketはファイルである

737 | 738 |

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

739 | 740 |
# -*- coding: utf-8 -*-
 741 | require "socket"
 742 | 
 743 | # 12345 portで待ち受けるソケットを開く
 744 | listening_socket = TCPServer.open(12345)
 745 | 
 746 | # ソケットもファイルなので fd がある
 747 | p listening_socket.fileno
 748 | 
 749 | # ひとまずなにもせず閉じる
 750 | listening_socket.close
 751 | 
752 | 753 |

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

754 | 755 |

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

756 | 757 |

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

758 | 759 |
# -*- coding: utf-8 -*-
 760 | require "socket"
 761 | 
 762 | # 12345 portで待ち受けるソケットを開く
 763 | listening_socket = TCPServer.open(12345)
 764 | 
 765 | p listening_socket.fileno
 766 | 
 767 | # acceptでクライアントからの接続を待つ
 768 | # 接続されるまでブロックする
 769 | puts "accepting..."
 770 | socket = listening_socket.accept
 771 | puts "accepted!"
 772 | 
 773 | # 接続されると新しいsocketが作られる
 774 | # このsocketを通じてクライアントと通信する
 775 | # あたらしいsocketなのでfdの番号がlistening_socketと違う
 776 | p socket.fileno
 777 | 
 778 | # なにもせずクライアントとのコネクションを切る
 779 | socket.close
 780 | 
 781 | # 待ち受けソケットも閉じる
 782 | listening_socket.close
 783 | 
784 | 785 |

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

786 | 787 |

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

788 | 789 |
# べつのターミナルで
 790 | $ telnet localhost 12345
 791 | 
792 | 793 |

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

794 | 795 |

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

796 | 797 |

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

798 | 799 |

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

800 | 801 |

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

802 | 803 |
# -*- coding: utf-8 -*-
 804 | require "socket"
 805 | 
 806 | listening_socket = TCPServer.open(12345)
 807 | 
 808 | # クライアント受け入れ無限地獄
 809 | loop do
 810 |   puts "accepting..."
 811 |   socket = listening_socket.accept
 812 |   puts "accepted a client!"
 813 | 
 814 |   # クライアントからの入力受け取り無限地獄
 815 |   loop do
 816 |     # クライアントからの入力を1行読む
 817 |     # 入力されるまでブロックする
 818 |     line = socket.gets
 819 |     line.gsub!(/[\r\n]/,"") #改行コード捨てる
 820 | 
 821 |     # exitと入力されてたらソケット閉じてループを抜ける
 822 |     if line == "exit"
 823 |       socket.close
 824 |       puts "closed a connection!"
 825 |       break
 826 |     end
 827 | 
 828 |     # そうでなければ標準出力に出力
 829 |     puts line
 830 |   end
 831 | end
 832 | 
833 | 834 |

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

835 | 836 |

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

837 | 838 |
$ telnet localhost 12345
 839 | 
840 | 841 |

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

842 | 843 |

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

844 | 845 |

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

846 | 847 |

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

848 | 849 |

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

850 | 851 |

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

852 | 853 |

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

854 | 855 |

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

856 | 857 |

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

858 | 859 |

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

860 | 861 |

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

862 | 863 |

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

864 | 865 |
$ telnet localhost 12345
 866 | 
867 | 868 |

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

869 | 870 |

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

871 | 872 |

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

873 | 874 |
# -*- coding: utf-8 -*-
 875 | require "socket"
 876 | 
 877 | number_of_workers = 3
 878 | listening_socket = TCPServer.open(12345)
 879 | 
 880 | number_of_workers.times do
 881 |   pid = fork
 882 | 
 883 |   if pid
 884 |     # 親プロセスは次々に fork で子プロセスを作る
 885 |     next
 886 |   else
 887 |     # 子プロセス
 888 | 
 889 |     # クライアント受け入れ無限地獄
 890 |     loop do
 891 |       puts "accepting..."
 892 |       # 子プロセスは全部ここでブロックする。
 893 |       socket = listening_socket.accept
 894 |       puts "accepted a client!"
 895 | 
 896 |       # クライアントの入力受け取り無限地獄
 897 |       loop do
 898 |         line = socket.gets
 899 |         line.gsub!(/[\r\n]/,"")
 900 | 
 901 |         if line == "exit"
 902 |           socket.close
 903 |           puts "closed a connection!"
 904 |           break
 905 |         end
 906 | 
 907 |         puts line
 908 |       end
 909 |     end
 910 |   end
 911 | end
 912 | 
 913 | # 子プロセスは無限ループしてるからここには届かない
 914 | # 親プロセスでは子プロセスの終了を待ち続ける
 915 | Process.waitall
 916 | 
917 | 918 |

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

919 | 920 |

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

921 | 922 |

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

923 | 924 |

というわけで、今、listening_socket を作ったあとに fork したことで、オープンファイル記述、つまり「ソケットは12345 portで待ち受けてるよ」と書かれた「ファイルどうなってたっけメモ」を全てのプロセスで共有している状態になっているわけです。ここまではいいですか?

925 | 926 |

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

927 | 928 |

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

929 | 930 |

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

931 | 932 |

さて、ここに、さらに新しいクライアントが接続してきた場合はどうなるでしょうか?子プロセスのうちひとつはまだクライアントからの入力受け取り無限地獄にいますが、ふたつのプロセスは accept でブロック中になっています。こんどはこのふたつのプロセスのうちのどちらかが accpet に成功して新しいソケットを作ってクライアントとやりとりすることになるわけです。

933 | 934 |

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

935 | 936 |

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

937 | 938 |

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

939 | 940 |

preforkの利点、欠点

941 | 942 |

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

943 | 944 |

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

945 | 946 |

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

947 | 948 |

次回予告

949 | 950 |

次回はちょっと話を戻して、forkした際に親が先に死んだり終わったりしたらどうなるのとかそういう話をしたいなって思います。

951 | 952 |

ゾンビプロセスと孤児プロセス

953 | 954 |

さて、前回までで fork とかファイルとかのことはだいたいわかってきたかと思います。今回は、「親が死んだ子供は養子になるしかない」「子供が親の見てないところで死ぬとゾンビになってしまう」という話をします。

955 | 956 |

親が死んだ子供は養子になるしかない

957 | 958 |

今回はここ数日なにかと話題の Perl で行きましょう。

959 | 960 |
use strict;
 961 | use warnings;
 962 | 
 963 | my $pid = fork;
 964 | 
 965 | if ( $pid ) {
 966 |     # 親プロセスで waitpid しないで
 967 |     # 死んじゃう
 968 |     sleep 1;
 969 |     exit;
 970 | }
 971 | else {
 972 |     #子プロセス
 973 | 
 974 |     # getppid で親プロセスのpidを取得する
 975 |     print getppid."\n";
 976 | 
 977 |     #親が死ぬまで待つ
 978 |     sleep 2;
 979 | 
 980 |     # 親が死んだあとの親プロセスのpidって??
 981 |     print getppid."\n";
 982 | }
 983 | 
984 | 985 |

さて、上のような perl スクリプトを実行すると、結果はどうなるでしょうか。子プロセスのほうの一回目の getppid では、まだ親プロセスが生きているので、当然そのプロセスの pid が表示されます。1秒後、親プロセスが終わるのでプロンプトが戻ってきます。そして、そのさらに1秒後、二度目の getppid が実行され、すでにプロンプトが戻っているターミナルに「1」と表示されるはずです。

986 | 987 |

これはどういうことかと言うと、親が死んだから、init さんのところに養子に入ったわけですね。親プロセスに先立たれて親の pid が存在しなくなったプロセスは、init が代わりに親プロセスとして振る舞ってくれます。

988 | 989 |

「子供が親の見てないところで死ぬとゾンビになってしまう」

990 | 991 |

「ゾンビプロセス」ってのを聞いたことあると思うんですけど、これはどういうプロセスなんでしょうか。コードで見てみましょう

992 | 993 |
use strict;
 994 | use warnings;
 995 | 
 996 | my $pid = fork;
 997 | 
 998 | if ( $pid ) {
 999 |     print "$pid\n";
1000 | 
1001 |     # 無限ループに忙しくて子プロセスを wait してない
1002 |     while ( 1 ) {
1003 |         sleep;
1004 |     }
1005 | }
1006 | else {
1007 |     # 子プロセスは即死する
1008 |     exit;
1009 | }
1010 | 
1011 | 1012 |

上記のようなスクリプトを zombie.pl として保存して、バックグラウンド実行してみましょう

1013 | 1014 |
$ perl zombie.pl &
1015 | 
1016 | 1017 |

親プロセスの print "$pid\n" が利いて、子プロセスのpidが出力されたかと思います。さて、この親プロセスは、まだバックグラウンドで無限ループしています。一方、子プロセスは即 exit しているので、もう実行が終了しています。しかし、親はこの終了を wait していません。この子プロセスは、実行がおわってもう死んでいるのに、誰にも看取られていない(wait されていない)状態です。

1018 | 1019 |

そこで、先ほどターミナルに表示された pid がどうなっているのか、ps コマンドで確認してみましょう。

1020 | 1021 |
$ ps <さっきターミナルに表示されたpid>
1022 | 
1023 | 1024 |

どうなりましたか?環境によって多少の違いはあるかもしれませんが、私の環境では

1025 | 1026 |
  PID TTY      STAT   TIME COMMAND
1027 |  3668 pts/2    Z      0:00 [perl] <defunct>
1028 | 
1029 | 1030 |

と表示されました。STAT の部分に Z と出ていますね。これは、このプロセスがゾンビプロセスとなっていることを表します。

1031 | 1032 |

それでは、無限ループ中の親プロセスを fg でフォアグラウンドに戻して、Ctrl + Cで止めましょう。その状態で再度 ps でプロセスの状態を見てみると、さっきまでゾンビだったプロセスも、無事に成仏してなくなっていることが確認できると思います。

1033 | 1034 |

なにが起こったのか、説明しましょう。

1035 | 1036 |

さきほど、Ctrl+C で親プロセスを終了しましたが、この親は子を wait しないで死んでしまいました。ゾンビ状態だった子プロセスは、親プロセスが死んでしまったことにより、init さんの養子に入ります。すると、init さんが即座に wait でこのプロセスを看取ってくれて、無事にゾンビ状態だったプロセスが終了できたわけです。

1037 | 1038 |

まとめ

1039 | 1040 |

さて、このことから、以下のようなことがわかります。

1041 | 1042 | 1046 | 1047 |

短いですが、今日はこんなところで。

1048 | 1049 |

次回予告

1050 | 1051 |

次回はようやくシグナルについて書く予定です。共有メモリの話と、スレッドの話もその後にできればしたいけど、気力ないかもしれないので次回が最終回の可能性が微レ存……

1052 | 1053 |

シグナルとkill

1054 | 1055 |

さて、前回までで fork とプロセスの関係についてはなんとなく概要が把握できたんじゃないかなと思います。今回は、シグナルについてです。

1056 | 1057 |

プロセスに外から影響を与えたい(シグナル編)

1058 | 1059 |

プロセスが外界とコミュニケーションを取るための方法として、ファイルディスクリプタを通じた入出力というものがあることは前回までで見てきたとおりです。じつは、プロセスが外界とコミュニケーションを取る方法としてもうひとつ、「シグナル」というものがあります。第二回で見たとおり、プロセスは生成されたあとは実行中となり、処理が終わるまでは一心不乱に決められた動きを行っています。しかしたとえば、無限ループに陥ってしまったプロセスなどは、外から「あっちょっと君、ストップ!ストップ!」という感じで止めてあげられる仕組みがないと困りますよね。そういう感じで、外からプロセスに対して「割り込み」を行うための仕組みが「シグナル」です。

1060 | 1061 |

なにはともあれ、ためしてみましょう。

1062 | 1063 |

シグナルを送ってみる

1064 | 1065 |

まずはプロセスを作りましょう。

1066 | 1067 |
$ perl -e 'while(1){sleep}' &
1068 | $ ps
1069 | 
1070 | 1071 |

毎度おなじみ、sleep するだけの perl プロセスです。ps でpid を確認しておきましょう。

1072 | 1073 |

このプロセスに対して、シグナルを送ってみます。

1074 | 1075 |
$ kill -INT <さっき確認したpid>
1076 | 
1077 | 1078 |

kill というのが、プロセスに対してシグナルを送るコマンドです。今回は -INT を指定することで、「SIGINT」というシグナルを送ってみました。「SIGINT」の他にもいろんなシグナルがありますが、今は置いておきます。さて、ではここでもう一度 ps コマンドでプロセスの様子を見てみましょう。

1079 | 1080 |
$ ps
1081 | 
1082 | 1083 |

すると、さきほどまで存在していた perl プロセスが無くなっていることがわかると思います。これはいったいどうしたことでしょうか。実は、SIGINTというプロセスを受け取ると、デフォルト値ではそのプロセスは実行を停止するのです。sleep し続けていたプロセスに SIGINT というシグナルを送ったことによりプロセスに「割り込み」をして、そのプロセスの実行を止めてしまったわけですね。

1084 | 1085 |

シグナルを受け取ったときの動作を変えてみる

1086 | 1087 |

さきほど、「デフォルト値では」と言いましたが、ということは、シグナルを受け取ったときの動作を変更することだってできるわけです。やってみましょうか。

1088 | 1089 |
# papas.pl
1090 | use strict;
1091 | use warnings;
1092 | 
1093 | # SIGINTを受け取ったときは sub {} の中身を実行する
1094 | $SIG{INT} = sub {
1095 |     warn "ぬわーーーーっっ!!";
1096 | };
1097 | 
1098 | # スリープし続ける
1099 | while (1) {
1100 |     sleep;
1101 | }
1102 | 
1103 | 1104 |

papas.pl という名前で上のようなスクリプトを作成して、バックグラウンドで実行してみましょう

1105 | 1106 |
$ perl papas.pl &
1107 | 
1108 | 1109 |

さて、それではこのプロセスに対して、SIGINTを送ってみましょう。

1110 | 1111 |
$ kill -INT <"perl papas.pl" の pid>
1112 | 
1113 | 1114 |

標準エラーに「ぬわーーーーっっ!!」が表示されたかと思います。そして再度 ps してみると、さっきは SIGINT を受け取って停止していたプロセスが、今回はまだ生きていることが見て取れるかと思います。これで、何度 SIGINT を送っても「ぬわーーーーっっ!!」と叫ぶだけで、死なないプロセスの完成です。パパスも適切にシグナル処理さえしていればゲマに殺されることもなかったというのに……。

1115 | 1116 |

さて、このままではこのプロセスは生き続けてしまうので、SIGTERMというシグナルを送信して適切に殺してあげましょう。

1117 | 1118 |
$ kill -TERM <"perl papas.pl" の pid>
1119 | 
1120 | 1121 |

これで無事にパパスは死にました。

1122 | 1123 |

シグナルにはどんなものがあるの?

1124 | 1125 |

上に見たように、シグナルには SIGINT 以外にもいろいろないろいろなシグナルがあります。man 7 signal や man kill に一度目を通しておくと良いでしょう。それぞれのシグナルに、受け取ったときのデフォルトの動作が定義されています。

1126 | 1127 |

とりあえずここでは、signal(7) から、 POSIX.1-1990 で規定されているシグナルの種類を引いておきましょう。

1128 | 1129 |
Signal     Value     Action   Comment
1130 | -------------------------------------------------------------------------
1131 | SIGHUP        1       Term    Hangup detected on controlling terminal
1132 |                               or death of controlling process
1133 | SIGINT        2       Term    Interrupt from keyboard
1134 | SIGQUIT       3       Core    Quit from keyboard
1135 | SIGILL        4       Core    Illegal Instruction
1136 | SIGABRT       6       Core    Abort signal from abort(3)
1137 | SIGFPE        8       Core    Floating point exception
1138 | SIGKILL       9       Term    Kill signal
1139 | SIGSEGV      11       Core    Invalid memory reference
1140 | SIGPIPE      13       Term    Broken pipe: write to pipe with no readers
1141 | SIGALRM      14       Term    Timer signal from alarm(2)
1142 | SIGTERM      15       Term    Termination signal
1143 | SIGUSR1   30,10,16    Term    User-defined signal 1
1144 | SIGUSR2   31,12,17    Term    User-defined signal 2
1145 | SIGCHLD   20,17,18    Ign     Child stopped or terminated
1146 | SIGCONT   19,18,25    Cont    Continue if stopped
1147 | SIGSTOP   17,19,23    Stop    Stop process
1148 | SIGTSTP   18,20,24    Stop    Stop typed at tty
1149 | SIGTTIN   21,21,26    Stop    tty input for background process
1150 | SIGTTOU   22,22,27    Stop    tty output for background process
1151 | 
1152 | 1153 |

Signal のところがシグナルの名前、Value というところがそのシグナルを表す番号(kill -n pid でプロセスにそのシグナルを送ることができます)、Action のところがそのシグナルを受け取ったときのデフォルトの動作です。Term ならばプロセスを終了し、Coreならばコアダンプを吐いて終了します。Ignならばそのシグナルを無視します(なにもしない)し、Stopならば実行を一時停止、Contならば一時停止していたプロセスを再開します。Commentのところに、どのようなときにそのシグナルが送られてくるかが書かれていますね。たとえば SIGCHLD を見てみると、Child stopped or terminatedと書かれています。つまり、子プロセスが止まったり止められたりしたときに、その親プロセスはSIGCHLDを受け取るようになっているわけですね。

1154 | 1155 |

微妙なハマりポイントとして、SIGHUP や SIGPIPE があるので、そこだけ少し説明しておきましょう。

1156 | 1157 |

まずは SIGHUP についてですが、ログインシェルが死んだときに、そのログインシェルが起動したプロセスにはSIGHUPが送られてきます(じつはこれは正確な説明ではないのだけれど、このあたりの正確な説明は次回できたらします)。これがなにを意味するかというと、たとえば ssh でサーバーにログインして、バックグラウンドでなにかを動かしたまま logout したりすると、そのバックグラウンドプロセスに SIGHUP が送られます。SIGHUP のデフォルトの動作は Term なので、そのバッググラウンドプロセスは死んでしまいます。これを防ぐためには、 nohup コマンドを使ってプロセスを起動するか、プロセス側で SIGHUP を受け取ったときの動作を変更する必要があります。

1158 | 1159 |

つぎに SIGPIPE についてです。SIGPIPEは、壊れた pipe に対して書き込みを行ったときに受信されるシグナルです。これが問題を引き起こすことが多いのが、ネットワークサーバーを書いているときです。なんらかのトラブルなどですでに切断されてしまっているソケットに対してなにかを書き込みしようとすると(いくらでもその理由は考えられます)、プロセスは SIGPIPE を受け取ります。SIGPIPE のデフォルトの動作はTermなので、この時点でサーバーは突然の死を迎えることになるわけです。

1160 | 1161 |
_人人人人人_
1162 | > SIGPIPE <
1163 |  ̄YYYYY ̄
1164 | 
1165 | 1166 |

動かし続けることを前提としたプロセスでは、このあたりのシグナルをきちんとハンドリングしてあげないとハマることが多いので、頭の片隅に置いておくといいかもしれません。

1167 | 1168 |

次回へ続くなぞ

1169 | 1170 |

さて、シグナルについて基本的なことは見て来れたかと思います。では、forkなどと組み合わせて使った時にはどういう動きをするのでしょうか?見てみましょう。

1171 | 1172 |

まずは以下のようなスクリプトを用意してみます。

1173 | 1174 |
# fork_and_sleep.pl
1175 | use strict;
1176 | use warnings;
1177 | 
1178 | fork;
1179 | 
1180 | # スリープし続ける
1181 | while (1) {
1182 |     sleep;
1183 | }
1184 | 
1185 | 1186 |

forkして子プロセスを作ったあと、親プロセスも子プロセスもスリープし続けるものですね。バックグラウンドで実行します。

1187 | 1188 |
$ perl fork_and_sleep.pl &
1189 | 
1190 | 1191 |

ps コマンドで様子を見てみましょう

1192 | 1193 |
$ ps f
1194 |   PID TTY      STAT   TIME COMMAND
1195 | 16753 pts/2    Ss     0:00 -bash
1196 | 16891 pts/2    S      0:00  \_ perl hoge.pl
1197 | 16892 pts/2    S      0:00  |   \_ perl hoge.pl
1198 | 16928 pts/2    R+     0:00  \_ ps f
1199 | 
1200 | 1201 |

「f」を付けて ps を実行すると親子関係が一目でわかります。この場合は 16891 が親プロセス、16892 が子プロセスですね。では、fg でフォアグラウンドに戻して、Ctrl + C を押してみましょう。Ctrl+C は、プロセスに対してSIGINTを送信します。

1202 | 1203 |

OKですか? そうしたら、ここで再度 ps を実行してみましょう

1204 | 1205 |
$ ps f
1206 |   PID TTY      STAT   TIME COMMAND
1207 | 16753 pts/2    Ss     0:00 -bash
1208 | 17140 pts/2    R+     0:00  \_ ps f
1209 | 
1210 | 1211 |

子プロセスも一緒に消えていますね。では、今度は fg -> Ctrl+C のコンボではなく、kill -INT で SIGINT を送ってみましょう。

1212 | 1213 |
$ perl fork_and_sleep.pl &
1214 | 
1215 | $ ps -f
1216 |   PID TTY      STAT   TIME COMMAND
1217 | 16753 pts/2    Ss     0:00 -bash
1218 | 17288 pts/2    S      0:00  \_ perl hoge.pl
1219 | 17289 pts/2    S      0:00  |   \_ perl hoge.pl
1220 | 17293 pts/2    R+     0:00  \_ ps f
1221 | 
1222 | $ kill -INT 17288 # 親プロセスにSIGINTを送る
1223 | 
1224 | $ ps f
1225 |   PID TTY      STAT   TIME COMMAND
1226 | 16753 pts/2    Ss     0:00 -bash
1227 | 17352 pts/2    R+     0:00  \_ ps f
1228 | 17289 pts/2    S      0:00 perl hoge.pl
1229 | 
1230 | 1231 |

「!?」 今度は子プロセスが残っています(親プロセスが死んだからinitの子供になっており、ツリーの表示も変わっています)。

1232 | 1233 |

さて、なぜこのようなことが起こるのでしょうか。この挙動を理解するには、「プロセスグループ」という新しい概念を学ぶ必要があります。

1234 | 1235 |

というわけで次回予告

1236 | 1237 |

次回はプロセスグループについて見てみましょう。多分次回が最終回!

1238 | 1239 |

see also

1240 | 1241 |

Perl Hackers Hub 第6回 UNIXプログラミングの勘所(3)

1242 | 1243 |

プロセスグループ と フォアグランドプロセス

1244 | 1245 |

前回はプロセスとシグナル、そしてシグナルを明示的にプロセスに送るためのコマンド kill について見ました。そして最後にひとつ謎が残ったわけですが、今回はその謎を解いて行きましょう。

1246 | 1247 |

プロセスグループ

1248 | 1249 |

さて、じつは今まで一度も意識したことはありませんでしたが、プロセスというのはかならずひとつのプロセスグループというものに属します。見てみましょう。

1250 | 1251 |
$ perl -e 'sleep' &
1252 | 
1253 | $ ps o pid,pgid,command
1254 |  PID  PGID COMMAND
1255 | 1620  1620 -bash
1256 | 1638  1638 perl -e sleep
1257 | 1639  1639 ps o pid,pgid,command
1258 | 
1259 | 1260 |

毎度おなじみ sleep し続ける perl プロセスをバックグラウンドで実行して、ps を "o pid,pgid,command" 付きで実行してみました。「pidとpgidとcommandを表示する」くらいの意味です。おや、見慣れない PGID というものがありますね。これが、プロセスグループのidです。こんな感じで、プロセスがかならずひとつのプロセスグループに属していることが見て取れるかと思います。なんだか今は PID と同じ数字が PGID のところにも表示されていて、この PGID ってあまり意味や意義がわからない感じですね。

1261 | 1262 |

では、ここで、fork と組み合わせてみましょうか。

1263 | 1264 |
# fork.pl
1265 | use strict;
1266 | use warnings;
1267 | 
1268 | fork;
1269 | 
1270 | sleep;
1271 | 
1272 | 1273 |

上記のような、 fork して sleep し続けるだけの fork.pl というスクリプトを作ってバックグラウンドで実行してみましょう。

1274 | 1275 |
$ perl fork.pl &
1276 | 
1277 | $ ps o pid,pgid,command f
1278 |  PID  PGID COMMAND
1279 | 1620  1620 -bash
1280 | 1646  1646  \_ perl fork.pl
1281 | 1647  1646  |   \_ perl fork.pl
1282 | 1652  1652  \_ ps o pid,pgid,command f
1283 | 
1284 | 1285 |

今回は ps に f オプションを付けて tree 状に表示してみました。(BSD系だと f 利かないので Macで試すなら f なしで実行してください)

1286 | 1287 |

さて、こうして見てみると、親プロセスであるプロセス(pid 1646)は PID と PGID が同じ数字ですが、そこから fork で生成された子プロセス(pid 1647)は、PID と PGID が別の数字になっています。そして、子プロセスのほうの PGID は、fork元である親プロセスの PGID になっているのがわかるでしょうか。

1288 | 1289 |

こんな感じで、実は fork された子プロセスは、親プロセスと同じプロセスグループに属するようになります。逆の言い方をすると、forkで子プロセスを作ることによって、「自分と同じプロセスグループに属するプロセス」が一個ふえるわけですね。

1290 | 1291 |

ちなみに、プロセスグループにはリーダーが存在して、PGID と同じ数字の PID のプロセスが、プロセスグループのリーダーです。forkすると、同じグループに属する子分ができる、みたいな感じですね。

1292 | 1293 |

プロセスグループをいじってみよう

1294 | 1295 |

さて、今かんたんに「fork すると子プロセスは自分と同じプロセスグループに属するようになる」と言いましたが、これはちょっとおかしいですね。そうです、以前見たように、すべてのプロセスは pid 1 のプロセスから fork で作られたのでした。そうなると、すべてのプロセスは pid 1 のプロセスと同じプロセスグループに属することになってしまいます。すべてのプロセスが同じグループに属すなら、グループの意味がないですね。だから、forkしたあと、プロセスグループをいじる仕組みが必要になってきます。それが setpgrp システムコールです。では例を見てみましょう。

1296 | 1297 |
#fork_setpgrp.pl
1298 | use strict;
1299 | use warnings;
1300 | 
1301 | my $pid = fork;
1302 | 
1303 | die "fork failed" unless defined $pid;
1304 | 
1305 | if ($pid) {
1306 |     # 親プロセス
1307 |     sleep;
1308 | }
1309 | else {
1310 |     # 子プロセス
1311 | 
1312 |     # setpgidシステムコールを
1313 |     # 引数なしで呼び出すと、
1314 |     # 自分のプロセスグループを作ってそこのリーダーになる
1315 |     setpgrp;
1316 |     sleep;
1317 | }
1318 | 
1319 | 1320 |

上記のようなスクリプトを fork_setpgrp.pl という名前で保存して、バックグラウンドで実行、ps で確認してみましょう

1321 | 1322 |
$ ps o pid,pgid,command f
1323 |  PID  PGID COMMAND
1324 | 1620  1620 -bash
1325 | 1666  1666  \_ perl fork_setpgrp.pl
1326 | 1667  1667  |   \_ perl fork_setpgrp.pl
1327 | 1673  1673  \_ ps o pid,pgid,command f
1328 | 
1329 | 1330 |

今度は、子プロセスは親プロセスと同じ PGID ではなくなりました。setpgrp システムコールを引数なしで呼び出したことにより、今までグループ内で子分役をやっていた子プロセスが、新しく自分のグループを作り、リーダーになっていることが見て取れるかと思います。なんだかベンチャー界隈でよく聴く独立譚みたいな話ですね。

1331 | 1332 |

ちなみに、PGID は、親の側からいじることもできます。

1333 | 1334 |
use strict;
1335 | use warnings;
1336 | 
1337 | my $pid = fork;
1338 | 
1339 | die "fork failed" unless defined $pid;
1340 | 
1341 | if ($pid) {
1342 |     # 親プロセス
1343 | 
1344 |     # setpgrp を引数付きで呼び出す
1345 |     my $pgid = $pid;
1346 |     setpgrp $pid, $pgid;
1347 |     sleep;
1348 | }
1349 | else {
1350 |     # 子プロセス
1351 |     sleep;
1352 | }
1353 | 
1354 | 1355 |

こういう感じで親プロセスのほうで引数付きで setpgrp を呼び出すことで、子プロセスの PGID を設定することもできます。

1356 | 1357 |

いったんまとめ

1358 | 1359 |

こんな感じで、プロセスが fork で子プロセスを作ったとき、その時点ではその子プロセスは親プロセスと同じプロセスグループに属しています。プロセスグループを変更したいときには、この子プロセスの PGID を setpgrp システムコールでいじってあげれば良いわけですね。

1360 | 1361 |

ちなみに、シェルから起動されたプロセスは、シェルが勝手に setpgrp を呼んでくれるので、それぞれがプロセスグループのリーダーとなっています。

1362 | 1363 |

プロセスグループ全体に kill をしてみよう

1364 | 1365 |

さて、いままでの話だけでは、「プロセスグループってのがあるのはわかったけど、そんなもんがあってなにがうれしいの」という感じがしますね。うれしいことのひとつとして、kill でプロセスグループに属する全てのプロセスに一気にシグナルを送れる、というものがあります。kill で pid を指定する部分に、"-" を付けてあげると、pid ではなくて pgid を指定したことになります。やってみましょう。

1366 | 1367 |
$ perl fork.pl &
1368 | 
1369 | $ ps o pid,pgid,command f
1370 |  PID  PGID COMMAND
1371 | 1678  1678 -bash
1372 | 1699  1699  \_ perl fork.pl
1373 | 1700  1699  |   \_ perl fork.pl
1374 | 1701  1701  \_ ps o pid,pgid,command f
1375 | 
1376 | $ kill -INT -1699 # 1699 ではなくて -1699 としている
1377 | 
1378 | $ ps o pid,pgid,command f # 一気にふたつのプロセスが消えている
1379 |  PID  PGID COMMAND
1380 | 1678  1678 -bash
1381 | 1702  1702  \_ ps o pid,pgid,command f
1382 | 
1383 | 1384 |

前回の謎に回答する

1385 | 1386 |

ではここで、前回の謎に回答しましょう。前回謎だった挙動は、「fg でプロセスをフォアグラウンドにしてから Ctrl+C で SIGINT を送信したときは子プロセスごと殺されたのに、 kill -INT でバックグラウンドのプロセスに SIGINT を送信したら親プロセスだけが殺される」という挙動でしたね。

1387 | 1388 |

勘のいいひとはすでにお気づきかもしれないですが、実は、「フォアグラウンド」とされる範囲は、プロセス単位ではなくて、プロセスグループ単位で決まっているのです。いくつか、例を見てみましょう。

1389 | 1390 |
# fork_and_trap_sigint.pl
1391 | use strict;
1392 | use warnings;
1393 | 
1394 | $SIG{INT} = sub {
1395 |     die "got SIGINT!";
1396 | };
1397 | 
1398 | fork;
1399 | 
1400 | sleep;
1401 | 
1402 | 1403 |

上記のようなスクリプトをフォアグランドで実行して、Ctrl+C でSIGINTを送ってみましょう。すると、"got SIGINT!" がふたつ標準エラーに出力されるはずです。これは、子プロセスと親プロセスが同じプロセスグループに属しているため、このふたつのプロセスがフォラグランドで実行されているからですね。

1404 | 1405 |

では今度は、プロセスグループが別の場合を見てみましょう。

1406 | 1407 |
# fork_and_setpgrp_and_trap_sigint.pl
1408 | use strict;
1409 | use warnings;
1410 | 
1411 | $SIG{INT} = sub {
1412 |     die "got SIGINT!";
1413 | };
1414 | 
1415 | my $pid = fork;
1416 | die "fork failed" unless defined $pid;
1417 | 
1418 | if ($pid) {
1419 |     # 親プロセス
1420 |     sleep;
1421 | }
1422 | else {
1423 |     # 子プロセス
1424 | 
1425 |     # setpgrpを呼び出して新しいプロセスグループのリーダーになる
1426 |     # これにより、子プロセスは親プロセスと異なるプロセスグループに
1427 |     # 属すことになり、フォアグランドで実行されている
1428 |     # プロセスグループから抜ける
1429 |     setpgrp;
1430 |     sleep;
1431 | }
1432 | 
1433 | 1434 |

上記のようなスクリプトをフォアグランドで実行して、Ctrl+C でSIGINTを送ってみましょう。すると、"got SIGINT!" が今度はひとつだけ出力されるはずです。これは、子プロセスが親プロセスのプロセスグループを抜けて別のプロセスグループになったため、フォアグランドから抜けてしまったためです。

1435 | 1436 |

別の例も見てみましょう。

1437 | 1438 |
# read_stdin_in_child.pl
1439 | use strict;
1440 | use warnings;
1441 | 
1442 | my $pid = fork;
1443 | die "fork failed" unless defined $pid;
1444 | 
1445 | if ($pid) {
1446 |     # 親ではstdin閉じる
1447 |     close(STDIN);
1448 | 
1449 |     # 子の終了待つ
1450 |     waitpid($pid,0);
1451 | }
1452 | else {
1453 |     # STDINからの入力をエコーする
1454 |     while(my $line = <STDIN>) {
1455 |         print $line;
1456 |     }
1457 | }
1458 | 
1459 | 1460 |

上記のようなスクリプトを作成し、フォアグラウンドで実行してみましょう。親プロセスは子プロセスが終わるまで待ってるのでそこでブロックしています。子プロセスは標準入力からの入力を受け取ろうとそこでブロックしています。

1461 | 1462 |

ここでターミナルになんか文字を打ち込めば、子プロセスがその入力を受け取ってエコーしてくれます。

1463 | 1464 |

ではこれをsetpgrpとの合わせ技でやるとどうなるでしょう?

1465 | 1466 |
# setpgrp_and_read_stdin_in_child.pl
1467 | use strict;
1468 | use warnings;
1469 | 
1470 | my $pid = fork;
1471 | die "fork failed" unless defined $pid;
1472 | 
1473 | if ($pid) {
1474 |     # 親ではstdin閉じる
1475 |     close(STDIN);
1476 | 
1477 |     # 子の終了待つ
1478 |     waitpid($pid,0);
1479 | }
1480 | else {
1481 |     # setpgrpを呼び出して新しいプロセスグループのリーダーになる
1482 |     # これにより、子プロセスは親プロセスと異なるプロセスグループに
1483 |     # 属すことになり、フォアグランドで実行されている
1484 |     # プロセスグループから抜ける
1485 |     setpgrp;
1486 | 
1487 | 
1488 |     # STDINからの入力をエコーする
1489 |     while(my $line = <STDIN>) {
1490 |         print $line;
1491 |     }
1492 | }
1493 | 
1494 | 1495 |

上記のようなスクリプトをフォアグラウンドで実行してみましょう。さっきとは異なり、ターミナルになにかを打ち込んでもおうむがえししてこないのが見て取れると思います。これは子プロセスが親プロセスとは別のPGIDに属したことによって、フォアグランドで実行されているプロセスグループから抜けたためですね。

1496 | 1497 |

さて、これで前回謎だった挙動にも説明がつきましたね。これで、プロセスグループの解説はおしまいにします。

1498 | 1499 |

おわりに

1500 | 1501 |

これにてこのシリーズはおしまいです。いかがだったでしょうか? 一度プロセスまわりについてまとめておきたいという動機で書き始めたのですが、これを書きながらわたしも理解があやふやなところが洗い出せたりして、なにかと有意義でした。

1502 | 1503 |

もしもこのドキュメントが役に立つと思っていただけたなら、勉強会とかそういうのであなたが属すコミュニティや会社に役立ててもらえたらとても嬉しいです。そのとき、「使ったよ!」とコメント欄とかメールとかで知らせてくれると、単純にわたしが喜びます(言わなくても自由に使っていただいてかまわないですけど)。

1504 | 1505 | --------------------------------------------------------------------------------