前置き
最近、「なるほどUnixプロセス ― Rubyで学ぶUnixの基礎」という本を読んでいる。
僕の所属している会社・はてなでは「メンター制度」というものがある。はてなに所属する全てのエンジニアに、誰かしら(グレード的に自分より上となるエンジニア)をメンターとしてつける、という制度。メンターとメンティーは月に一度、1on1を実施する。
自分はセールスエンジニアという特殊な職種ではあるが、メンターとしてシスプラ(インフラ)部門のエンジニアの方に付いて頂くことになった。このことは僕にとって、とても有り難く、心強い。
というのも、入社から2ヶ月ちょっと、実際にセールスエンジニアとして業務を進めてみて、インフラ周りの基礎知識・ローレイヤーに関しての基礎知識が強く求められるなぁと...、、というか、それを知っているといないとでは、問題や課題についての理解の仕方も違ってくるし、そのアプローチ(問題の切り分け方とか)もより本質的なものにすることができるな、と、日々骨身に染みるように感じているから。ましてや僕は、これまでずっとアプリケーションエンジニアとして生きてきたので、その基礎固めの必要性はなおさらだ。
そういったところの必要性や課題感(あと、今の自分のレベルも)はメンターもよく理解してくれていて、その上で、ひとつずつ身に付けていきましょう!と言っていただいているのもまた非常に有り難いところ。この本を読んでいるのも、その一環。
今年に入ってから、読書メモを付けながら本を読むというのがすっかり癖になってしまったので、今回も同じようにやっている。それがこれ。以上が前置き。
Unixシステムのコンポーネント
Unixシステムは、おおきく「ユーザーランド」と「カーネル」に分けられる。「ユーザーランド」はともかくとして、「カーネル」って確かによく聞くワード。
「カーネル」というのは、そのUnixシステムが載っているハードウェアを制御するための中間層。何と何との中間かというと、片方はハードウェアだけど、もう片方が「ユーザーランド」になる。以下のようなイメージ。
【ユーザーランド】<->【カーネル】<->【ハードウェア】
ハードウェアに対する全ての制御は、カーネルを通じて行う必要がある。
「ユーザーランド」とは、「プログラムが実行される場所」のこと。算術演算とか文字列操作とかはユーザーランドだけで完結させられるものだけど、前述の通り、ハードウェアに関する操作を行おうとするとカーネルを介する必要がある。
なるほど、ならば介そうじゃないか、と思うんだけど、プログラムは直接カーネルを操作することはできない。じゃあもうどうしたらいいのよって話なんだけど、「システムコール」(これもまたよく耳にするワード)というものもまた用意されていて、カーネルとのやりとりはすべてシステムコール経由で行うことになる。
システムコールがカーネルとユーザーランドとを取り次いでくれるわけだ。さっきも使った↓のイメージでいうと、
【ユーザーランド】<->【カーネル】<->【ハードウェア】
左側の矢印 <->
がシステムコールに当たる。システムコールを使うことで、プログラムはカーネルを経由してハードウェアと相互作用できるようになる。
Unixの原子・プロセス
プロセスはUnixシステムの肝。なぜなら、あらゆるコードはプロセス上で実行されるから。
例えば、コマンドラインから ruby を起動すると、コードを実行するための新しいプロセスが生成される。コードの実行が終わると、そのプロセスは終了する。
DBサーバとかは、起動し続けているイメージがあるけど、それはつまりそのサーバのプロセスが動き続けている、ということになる。
そんなプロセスだけど、Unixシステムで動作する全てのプロセスには、固有の識別子・プロセスIDが振られている。pid
と呼ばれる。あくまで識別子で、pid
はプロセスの中身とは一切関連づいていない。
手元でも実際に pid
を確認してみる。ターミナルから irb を開き、以下のような Ruby のコードで irb プロセスの pid を確認。
puts Process.pid # => 65120
このプロセスについての情報を、ps(1)
コマンドを使って確認することもできる。
$ ps -p 65120 PID TTY TIME CMD 65120 ttys008 0:00.22 irb
(2016/10/06 追記) ちなみに、プロセス名が [ ]
で囲われているものがカーネルランドのプロセス/スレッド、ということらしい。会社の分報で教えて頂いた!
ここで使用した Ruby のコード Process.pid
はユーザーランド上だけで完結できる処理・命令ではなく、カーネルに対してプロセスについての情報を問い合わせる必要があるものになる。つまり Process.pid
によってその背後でシステムコールが使われることになるが、そのシステムコールは getpid(2)
である。
ps(1)
とか getpid(2)
とかってなんなの
ここでちょっと本題とは横道に逸れる。
man
コマンドというものがある。これは、あるコマンドについてのマニュアルを参照するためのコマンド。例えば man ps
とすると以下のような結果が得られる。
$ man ps PS(1) BSD General Commands Manual PS(1) NAME ps -- process status SYNOPSIS ps [-AaCcEefhjlMmrSTvwXx] [-O fmt | -o fmt] [-G gid[,gid...]] [-g grp[,grp...]] [-u uid[,uid...]] [-p pid[,pid...]] [-t tty[,tty...]] [-U user[,user...]] ps [-L] DESCRIPTION The ps utility displays a header line, followed by lines containing information about all of your processes that have controlling terminals. (略)
ここでも PS(1)
と書かれているけど、この (1)
は「セクション」を表している。man ps
は、正式には man 1 ps
。セクション1はユーザーコマンドのマニュアルで、セクション2はシステムコールのマニュアル。あるセクションについてのものしか無いものもあるし、複数のセクションが存在するコマンドもある(同名のユーザーコマンド・システムコールがある、とか)。
なので、仮に find(1)
と表記された場合は「manページのセクション1に find コマンドについてのマニュアルがある」ということを示しているし、「ここで stat(1)
コマンドを実行してみよう」などと書かれていた場合、それはユーザーコマンドの方の stat
だな、ということがわかる。
以上、横道終わり。
プロセスの「親」
すべてのプロセスには親となるプロセスがいる。たいていの場合、「あるプロセスを起動したプロセス」が親プロセスとなる。親プロセスの参照は親プロセスID(ppid)で参照できる。これも実際にやってみる。
ターミナルから irb を開き、以下のような Ruby のコードで irb プロセスの ppid を確認することで、「irb プロセスの親プロセスID」を確認できる。
puts Process.ppid # => 12204
このプロセスについての情報を、ps(1)
コマンドを使って確認してみる。
$ ps -p 12204 PID TTY TIME CMD 12204 ttys008 0:05.19 -zsh
自分は zsh を使っていて、そこから irb を起動した。なので、表示されているのも zsh プロセスの情報。
親プロセスIDを取得するための Ruby コード Process.ppid
も当然システムコールが使われている。getppid(2)
が対応している。
プロセスと「ファイルディスクリプタ」
Unix の世界では「すべてがファイル」。「すべて」、本当に全てで、デバイスやソケット、パイプなどのような「リソース」的なものも、全てファイルとして扱われる。
実行中のプロセスを pid
で表すのと同じく、「あるプロセスの中で開かれたファイル」は「ファイルディスクリプタ」として表す。実行中のプロセスでリソースを開くと、「ファイルディスクリプタ番号」が割り当てられる。
リソースを開いたプロセスが終了すると、ファイルディスクリプタも閉じられる。ファイルディスクリプタとプロセスは、共に生き、共に死ぬ。
これも Ruby のコードで確認してみる。Ruby では、開いたリソースは IO クラスで参照される。全ての IO オブジェクトは、自身に割り当てられたファイルディスクリプタ番号を把握している。
passwd = File.open('/etc/passwd') puts passwd.fileno # => 3
この 3
がファイルディスクリプタ番号。カーネルはこの番号を使って、プロセスが使用しているリソースを追跡する。
続いて、ひとつのプロセス内で複数のリソースを開いてみた場合。
passwd = File.open('/etc/passwd') puts passwd.fileno # => 3 hosts = File.open('/etc/hosts') puts hosts.fileno # => 4 # 開いていた passwd ファイルを閉じる。 passwd.close null = File.open('/dev/null') puts null.fileno # => 3
この実行結果により、以下のようなことが言える。
ファイルディスクリプタは、あくまで「開いているリソースだけ」追跡する。閉じられたリソースについては追跡されないので、ファイルディスクリプタ番号も割り当てられない。
これは、カーネル視点から考えても、「リソースが閉じられる=そのリソースはハードウェアレイヤーとはやりとりする必要がなくなる」となるので、カーネルでのリソース管理の必要性がないからだ。
ユーザーが扱うファイルディスクリプタ番号が 3
から始まる理由
すべての Unix プロセスには3つの開かれたリソースがもれなくついてくる。
- 標準入力(STDIN)
- キーボードデバイスやパイプなどの入力からの読み込み全般のための方法を提供
- 標準出力(STDOUT)
- モニタやファイル、プリンタといって出力先への書き込み全般のための方法を提供
- 標準エラー出力(STDERR)
- 基本的には標準出力と同様
「プロセスにもれなくついてくる」3つのリソースのファイルディスクリプタ番号を確認してみる。
puts STDIN.fileno # => 0 puts STDOUT.fileno # => 1 puts STDERR.fileno # => 2
これが、ユーザーがやりとりするリソースのためのファイルディスクリプタ番号が 3
から採番される理由。標準入力・標準出力・標準エラー出力、の3つのリソースが常にファイルディスクリプタ番号の 0〜2を専有する。
ここまでのリソース周りを取り扱うために使用されたシステムコールは、Ruby の IO クラスに定義されているメソッドを使うことで、以下のようなものが使われていたことになる。
open(2)
close(2)
read(2)
write(2)
fsync(2)
- メモリ上にあるファイルの内容をストレージデバイス上のものと同期・転送(フラッシュ)させるためのもの
stat(2)
- ファイルの状態を取得するためのもの
リソース数の制限
上述のとおり、リソースを閉じることなくリソースを開き続けていくと、ファイルディスクリプタの番号も増え続けていく。
ひとつのプロセスあたり、どれくらいまでのファイルディスクリプタを持つことができるのか?は、システムの設定による。カーネルによって1プロセスごとにリソースの制限が設定されている。
例えば、ファイルディスクリプタの最大数は以下のような Ruby コードで知ることが出来る。
p Process.getrlimit(:NOFILE) # => [256, 9223372036854775807]
( NOFILE
はファイルディスクリプタ番号のことっぽい。参考:Module: Process (Ruby 1.9.3) )
256
はファイルディスクリプタ数のソフトリミット、9223372036854775807
はハードリミット。ソフトリミットを超えた場合には Errno::EMFILE
例外が送出される。
ソフトリミットは本当の制限・上限ではなく、必要に応じて引き上げることもできる。(ハードリミットも、本当にこの数値分だけリソースをオープンできる、というわけではなく、単にその値として Process::RLIM_INFINITY が設定されているだけ。)
以下のような Ruby コードで、そのプロセスにおけるソフトリミットの引き上げが可能。
Process.setrlimit(:NOFILE, 4096) p Process.getrlimit(:NOFILE) # => => [4096, 4096]
権限さえあれば、Process.setrlimit
の第3引数にも値を設定することで、ハードリミットの指定を行うことも可能。
ファイルディスクリプタ以外のリソースについても、同じく制限の確認・変更が可能。
# プロセスのユーザーが作成できる最大プロセス数 Process.getrlimit(:NPROC) # => [709, 1064] # プロセスが作成できるファイルサイズの最大値 Process.getrlimit(:FSIZE) # => [9223372036854775807, 9223372036854775807] # プロセススタックの最大サイズ Process.getrlimit(:STACK) # => [8388608, 67104768]
実用例としては、単に大量のリソースを必要とするプログラム・ツール内で制限を拡張するという用途もあるし、逆に、第三者のコードの実行に対して制約をつけるといった目的での使い方も可能。
ここで用いられたシステムコールは、Ruby の Process.getrlimit と Process.setrlimit がそれぞれ getrlimit(2) と setrlimit(2) に対応している。
以上
以上、Unixプロセスとリソースの基礎の再確認。
次は、プロセスとの情報のやりとりについて。