😺

「なるほどUnixプロセス」を通じてプロセスの基本をおさらいした

2022/11/06に公開

はじめに

プロセスとはプログラムの実行単位である、という事以外をほとんど知らずにコードを書いて過ごしてきました。
プロセスと仲良くなるために、プロセスとは何なのか、その動きについてなるほどUnixプロセスという書籍を通じて学んだことを、ここに整理していきたいと思います。
https://tatsu-zine.com/books/naruhounix

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を参照できます。
https://docs.ruby-lang.org/ja/latest/method/Process/m/pid.html

以下の例は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で参照できます。
https://docs.ruby-lang.org/ja/latest/method/Process/m/ppid.html

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のクラスタモードのコードリーディング

について、まとめていきたいと思います。

参考情報

https://tatsu-zine.com/books/naruhounix

GitHubで編集を提案

Discussion