「なるほどUnixプロセス」を通じてプロセスの基本をおさらいした
はじめに
プロセスとはプログラムの実行単位
である、という事以外をほとんど知らずにコードを書いて過ごしてきました。
プロセスと仲良くなるために、プロセスとは何なのか、その動きについてなるほどUnixプロセス
という書籍を通じて学んだことを、ここに整理していきたいと思います。
Unix プロセスとは
はじめに述べた通り、Unixにおけるプログラムの最小の実行単位です。
すべてのプログラムはこのプロセス上で実行されます。
現在起動しているプロセスはps
コマンドで確認することができます。
以下の例はシンプルなubuntuのDockerコンテナ内部でps
を実行した結果で、ps
コマンドだけでなくbash
自体もプロセス上で実行されていることがわかります。
user@dccca362012b:~$ ps
PID TTY TIME CMD
1 pts/0 00:00:00 bash
13 pts/0 00:00:00 ps
では次に、このプロセスにはどのような情報が含まれているのかを見ていきたいと思います。
プロセスが持つ情報
プロセス ID
プロセスには識別子としてIDがカーネルにより割り当てられます。このIDのことをプロセスID
と呼びます。
先ほどのps
の結果における、PID
がプロセスIDの事を指しています。
ここでは、bashが動いているプロセスのプロセスIDは1
、psが動いているプロセスのプロセスIDは13
ということが読み取れます。
user@dccca362012b:~$ ps
PID TTY TIME CMD
1 pts/0 00:00:00 bash
13 pts/0 00:00:00 ps
Rubyでは Process.pid
でプロセスIDを参照できます。
以下の例はirb
で実行した例です。この場合のPIDは81085
と確認できます。
❯ irb
irb(main):001:0> Process.pid
=> 81085
ps
で確認してみましょう。81085
がirbのPIDとなっているので、一致してそうですね。
❯ ps -p 81085
PID TTY TIME CMD
81085 ttys007 0:01.17 irb
親プロセス
プロセスには親となるプロセス、親プロセス
が存在します。
親プロセスは、そのプロセスを起動したプロセスとなります。
先ほどの例でいうと、irb
を起動したプロセスは(私の環境のログインシェルがzshなので)zshプロンプトとなります。
この親プロセスはRubyにおいてProcess.ppid
で参照できます。
irb(main):002:0> Process.ppid
=> 70330
70330
が親プロセスIDで、そのIDで起動しているプロセスを確認してみると、zshプロンプトであることが確認できます。
❯ ps -p 70330
PID TTY TIME CMD
70330 ttys007 0:13.67 /opt/homebrew/bin/zsh
ファイルディスクリプタと標準ストリーム
Unixにおいて、すべてはファイルとして扱われます。テキストファイルなどのファイルは当然ですが、デバイスや後述するソケット・パイプなどの情報もファイルとして扱うことができます。
つまり、あらゆる入出力をテキストファイルを開くのと同じ感覚で扱えるように、Unixがインタフェースを用意してくれているのです。
ここでは、このファイルのことをテキストファイルなどのファイルと区別するためにリソース
と呼ぶことにします。
プロセスにプロセスIDがあるように、リソースにもIDが割り当てられています。それがファイルディスクリプタ
です。
Rubyでは fileno
でファイルディスクリプタにアクセスできます。
試しに、以下のようなtest.txt
ファイルをRubyで開いて確認してみます。
hello!
hello!
hello!
hello!
hello!
ここではファイルディスクリプタが11
であると確認できました。
irb(main):016:0> file = File.open('./test.txt')
=> #<File:./test.txt>
irb(main):017:0> file.fileno
=> 11
ファイルディスクリプタはカーネルによって、整数値の3
から順に割り当てられていきます。
また、プロセスが閉じられると、そこで開いていたファイルディスクリプタも一緒に閉じられます。
0~2
の番号はいかなるプロセスにおいても、特別なリソースのために割り当てられています。
それが標準ストリーム
です。
標準ストリームは以下の3つから構成されています。
- STDIN(標準入力)→ 0
- STDOUT(標準出力)→ 1
- STDERR(標準エラー出力)→ 2
irb(main):027:0> STDIN.fileno
=> 0
irb(main):028:0> STDOUT.fileno
=> 1
irb(main):029:0> STDERR.fileno
=> 2
この標準ストリームを利用して、コンソール経由で情報を入力・出力したり、プロセス間で情報をやり取りしていたりします。
fork と execuve
zshプロンプトでirbコマンドを実行すると、zshプロンプトが親プロセスとなり、irbがその子プロセスとなることは先ほど説明した通りです。
このirbコマンドを実行した時に、どのような過程で子プロセスが作られているのかを見ていきたいと思います。
fork
というシステムコールを利用することで、プロセスは自身のすべての情報をコピーした
プロセスを作ることができます。このコピーされたプロセスが子プロセスです。
この時、メモリ空間を丸ごとコピーします。コピーするので、子プロセスで何か操作(新しい値の代入・新しいインスタンスを作成するなど)したことが親プロセスに影響を及ぼすことはありませんし、fork後の親プロセスの操作も同様に影響はありません。
ここで1つ疑念が湧くと思います。「プロセスは親プロセスのコピーということなら、一番親となるようなプロセスって何なの?」
Unixにおいてはカーネルが実行させる最初のプロセスとして、initプロセス
が呼ばれます。initプロセスにはPIDが1
として割り当てられています。
このinitプロセスを先祖として、各プロセスが枝分かれのように生成されていきます。これは親子関係の木構造となることから、プロセスツリー
と呼ばれています。
すべてのプロセスはinitプロセスから始まり、initプロセスをコピーした子プロセスが作られるということがわかりました。
しかし、これだけではzsh → irbのプロセスが作られるまでの条件を満たせていません。
それは、forkは子プロセスをコピーする
だけなので、このままでは親プロセスと同じことしかできません。
そこで、exec
というシステムコールを利用することで、現在のプロセスの内容を書き換えることができます。
例えばプロンプトに通常のirb
を起動 → 終了すると、irbのプロセスだけが終了してzshプロンプトに制御が戻ります。
しかし、$ exec 'irb'
を起動 → 終了するとzshプロンプトに制御が戻らず、ターミナル自体が閉じてしまいます。
これは、execによってzshプロセスがirbプロセスに上書きされたため発生した事象です。
このforkとexecをセットで利用することで、親プロセスとは異なる子プロセスを生成できる仕組みになっているわけです。
zshプロンプトでirbコマンドを実行した例では
- zshプロセスをforkする
- 標準入力で受け取ったコマンドである
irb
をforkした子プロセスでexecする
という仕組みになっていたということですね。
次回へつづく
慣れない執筆で力尽きたので続きは次回へ回したいと思います。
- プロセスへ命令を伝える
シグナル
- プロセス間で通信するためのパイプ・ソケット
- デーモンプロセスについて
-
Puma
のクラスタモードのコードリーディング
について、まとめていきたいと思います。
参考情報
Discussion