恐竜本第3章です。この章ではプロセスの状態やOSがプロセス管理に用いるデータ構造、プロセス間通信の概要に関して述べています。
プロセスの構造
プロセスはプログラムを実行する際の単位であり、プログラム自体の他に様々な情報が付随する。
- text section: プロセスで実行されているプログラム
- program counter: プログラムのどの部分を実行しているか
- stack: 関数の引数、戻り先アドレス、ローカル変数
- data section: グローバル変数
- heap: 動的に割り当てたメモリ
data section, text section, heapは物理的にメモリの後ろのアドレス、stackは前のアドレスに配置されており、サイズ増加にともなってheapはアドレスを下から上へ、stackは上から下へと伸びる。
プロセスの状態
- New 作成中のプロセスの状態
- Running 命令が実行されている
- Waiting I/Oの終了やシグナルを受け取るのを待っている
- Ready プロセッサが命令を実行するのを待っている
- Terminated 終了したプロセス
Process Control Block (PCB)
プロセスの管理情報はPCBというデータ構造でまとめられている。プロセスごとに異なるであろう情報を保持している。
- プロセスの状態: New, Ready, Running, Waiting, Terminated
- プログラムカウンター
- CPUレジスタ: 割り込み時の退避用
- CPUスケジューリング情報: プロセスの優先度、スケジューリング用キューのポインタなど
- メモリ管理情報: ベース・リミットレジスタ1、ページテーブル
- アカウント情報
- I/Oの状態
スレッド
複数の処理を並行して行うための実行単位。
スケジューリング
すべてのプロセスはjob queueに入れられる。メインメモリ上にロードされて実行できる状態になったプロセスはready queueに入る。ready queueは連結リストで実装されており、各連結リストはPCBをデータとして持っている。ready queueのqueue headerは先頭のPCBのポインタと末尾のPCBのポインタを保持している。OS上には他にも、I/Oのためのdevice queueなどが存在する。PCBはいろいろなqueue上に並ぶことができる。
ready queueの先頭に来たプロセスは次のいずれかが起こるまで実行できる。
- I/Oを要求し、I/O queueに並ぶ
- サブプロセスを生成し、その終了まで待つ
- 割り込みが発生してCPUの割当を剥がされ、ready queueの末尾に並ぶ
様々なqueue間をプロセスが遷移する様子を表した図をqueuing-diagramと呼び、可視化に便利である。
スケジューラ
キューに並んだプロセスをどのように実行していくかを決めるのがスケジューラであり、long-termなスケジューラとshort-termなスケジューラが存在している。short-termなスケジューラは100 msなどでプロセスをスイッチするのに対し、long-termなスケジューラは数分単位でプロセスを作る。long-termなスケジューラは、I/OバウンドなプロセスとCPUバウンドなプロセスをうまく組み合わせることで効率的にプロセスを実行するよう設計するのが重要である。
タイムシェアリングシステムなどではlong-termスケジューラが存在しないこともある。その代わり、中間的な存在としてmedium-termスケジューラがある。このスケジューラは時々プロセスをメモリから追い出すことで、同時実行数を減らし、処理の効率化を図ることがある。このことをswappingという。
コンテキストスイッチ
プロセスの実行を切り替える際には今のCPUの状態を保存し、別のプロセスに移って、そのプロセスの前回実行時の状態を復元して実行開始する。このことをコンテキストスイッチという。オーバーヘッドは数msである(執筆当時)。速度はハードウェアに依存し、CPUにコンテキストスイッチの退避用レジスタが用意されている場合などではメモリに状態書き込みを行わず済むため速い。
プロセスの生成・停止
プロセスは子プロセスを作ることができる。それぞれのプロセスはプロセス識別子pidを持つ。プロセス作成時の親プロセスの動き方としては、子が終わるまで待つ、子と同時に動くという二種類がある。また、メモリアドレス空間の使い方も、子が親のアドレス空間をコピーする場合と、子は新規に割り当てられた空間にプログラムを読み込む場合とがある。
Unixでは子プロセスを作る際にfork()システムコールを用いる。プログラム実行時にfork()呼び出しに到達すると、オリジナルのコピーのアドレス空間が作られ、子プロセスが生成される。子プロセスは同じプログラムのfork呼び出しの行から実行がスタートするが、このときfork呼び出しの返り値が親と異なっている。プログラム中では返り値を使って自分が親プロセスであるか、子プロセスであるかを判別する。子プロセスが別のプログラムをロードしたい場合には、exec()システムコールを次に実行し、メモリ上に読み込んで実行する。また、この間親プロセスが子の終了を待ちたい場合はwait()システムコールを呼ぶ。プロセスが終了する際はexit()システムコールが呼ばれる。
WindowsでもCreateProcessで似たようなことができるが、forkとは異なり同じアドレス空間のコピーはつくられない。また、呼び出し引数が多く煩わしい。
子プロセスを終了する際は強制終了する方法が用意されていたり(WindowsのTerminateProcess)、親を終了すると子をすべて終了させるようなOSの仕様になっていたりする。
プロセス間通信
プロセス間で通信したい状況が発生することがある。例えば以下。
- 情報の共有
- 処理の高速化(CPUやI/Oチャンネルが複数ある場合に限る)
- モジュラリティ
- 利便性
プロセス間通信はshared memoryとmessage passingの二種類の方法に分けることができる。message passingはシステムコールを介するためカーネルに処理を移したりするオーバーヘッドがある一方で、shared memoryはセットアップができればカーネルの助けなく通信ができ、高速である。
message passingではコミュニケーションリンクを通信したいプロセス間で作成し、そのリンクに対してsend()とreceive()の操作を行う。2つのプロセスがPとQであるとする。直接的な通信では、send()とreceive()の仕様として、send(P,message)/receive(Q,message)などとするシンメトリックなものと、send(P,message)/receive(id,message)とする非対称なものが存在する。後者は受信先のプロセスを明示しないという点で非対称となっている。いずれにしても通信相手を直接コード中に記載しなければならないため、片方のプロセスのIDを変えたりすると、変更箇所が多くなるデメリットが有る。間接的な通信では、この問題に対処できる。この手法ではメールボックス(もしくはポート)に対してメッセージ送信や受信する。この手法だと同じメールボックスに対して複数のプロセスが送信できたり受信できたりする。メールボックスはプロセスが所有者になっていたりOSが所有者になっていたりする。前者の場合はプロセスが終了したらメールボックスも消える。
メッセージの送信や受信の際に、プロセスをブロックする通信手法としない通信手法がある。送信時にブロックする場合、受信者が受信するまで、ブロックが継続する。受信時にブロックする場合は、送信者がメッセージを送るまでブロックされる。
メッセージのバッファリングに関しても、バッファされない・有限サイズのバッファリング・無限サイズのバッファリングが考えられる。バッファされない場合、メッセージを受信者が受け取るまで送信者はブロックされる。有限サイズの場合、バッファ溢れが起きるまではブロックされないが、一杯になったタイミングで送信しようとするとブロックされる。無限サイズの場合決してブロックされない。
遠隔手続き呼び出し
TCPやUDPなどの通信を行うための概念をsocketと呼ぶ。socket上のメッセージ送信・受信によって外部システムの機能を呼び出すことをRPCと呼ぶ。データの形式(ビッグエンディアンかリトルエンディアンかなど)はexternal data representation(XDR)で定めておき、齟齬がないようにする。
RPCで問題となるのが、ネットワーク上の問題で同じパケットが複数回到達したりパケットが到達しないことがよくあるということである。複数回同じ命令を実行しないためには、サーバー側でメッセージをためておき、重複検出する必要がある。更に、必ず1回ある命令を実行することを保証するには、サーバー側は命令を実行したらACKを返すようにする必要がある(パケットロス時の再送のため)。
また、通信のためのポート番号をサーバーとクライアントとで合意する必要がある。このためにはmatch-makerを使って動的に決めたりする。match-makerは固定のポートで動作しており、クライアントはまずmatch-makerにどのポートと通信すればよいか問い合わせたあと、所望のプログラムとそのポートで通信する。
パイプ
パイプは双方向の通信をするための概念で、ファイルとして扱われる。
UNIXではpipe()で2個のファイルディスクリプタがオープンされる。片方をwrite-end,もう片方をread-endと呼ぶ。UNIXではforkした子プロセスはオープン中のファイルを受け継ぐので、pipe後にforkしたfdにアクセスできる。2個のfdのうち、親はwrite-endかread-endの片方を使用し、使わない方をcloseする。子は親と反対のfdをcloseする。fdには通常のread()とwrite()が使用でき、通信が可能となる。Windowsでは匿名パイプという名前で同様の機能を使うことができる。
通常のパイプはプロセス終了時に消滅するが、名前付きパイプ(UNIXではFIFO)は終了後も残る。また、親子関係にないプロセスからもアクセスできる。
所感
プロセスの状態の話やPCB、キューの話は意識したことがなかったため、今回学べて良かったです。こういう知識は、開発で似たような問題に取り組むときアーキテクチャの参考にしたり、使おうとするフレームワークやライブラリが似たような概念を取り入れているようなときに役立つんじゃないでしょうか。
コメント