【OS入門#3】Operating Systemを学習:プロセスについて知ろうぜ
はじめに
2本くらいosについて記事を書きましたが、今回はそれに続けてosのプロセスについて書こうと思います!プロセスは、ソフトウェアのパフォーマンスを最適化したい、セキュアなシステム設計をしたい、あるいは新しい技術を深く理解したいと思っているなら、「プロセスの理解」は確実にその足がかりになると思っています。今回もOperating System Concepts 10th Editionを参考に書いたので、ぜひよければ読んで頂けると幸いです!
ちなみに前回はファイルシステムについても書いており、こちらもosの理解が深まるかなと思うのでこちらも目を通していただけると嬉しい!
プロセスとカーネル
プロセスとは
本では以下のように紹介されている:
’A process is a program in execution. A process is more than the program code, which is sometimes known as the text section. It also includes the current activity...’
このように、本では、「プロセスとは実行中のプログラムであり、それは単なるコード以上のものだ」と定義されている。プロセスが持つ要素として、text section(実行するプログラムコード)、program counter(現在の命令位置)、stack(関数呼び出しや戻り先)、data section(グローバル変数)、heap(動的に確保されたメモリ) などがあり、これらをOSは Process Control Block (PCB) として保持し、切り替え時に保存・復元する。また、PCBはカーネル(kernel)内のメモリ領域に保存されている。
カーネルとは
’The operating system must provide mechanisms for process creation, scheduling, and termination. It must also provide for communication and synchronization between processes.’
プロセスを管理するために存在するのが kernel(カーネル) である。カーネルは、オペレーティングシステム(OS)の中核を成す部分であり、コンピュータのすべての資源(CPU、メモリ、入出力デバイスなど)を直接制御・管理する特権的なソフトウェア層と言える。
以前もどこかに書いたと思うが、アプリケーションプログラムは、プロセスという単位でCPU上で実行されるが、それらのプロセスは、カーネルが提供する機能を通じてのみ、ハードウェアにアクセスできる。たとえばファイルの読み書き、プロセスの生成、ネットワーク通信といった操作は、システムコール(system call) というインターフェースを通じて、カーネルに依頼する形で実行される。
プロセスとカーネルの関係
"A process needs certain resources, including CPU time, memory, files, and I/O devices, to accomplish its task. The operating system is responsible for allocating these resources to processes."
つまり、プロセスは実行中のプログラムであり、カーネルはそれの管理者である。プロセスは実行されるためにさまざまなリソースを必要とするが、それらはすべてカーネルを通じてしか取得できない。
ここで、上記で説明した通り、プロセスもアプリケーションプログラム同様、自力でハードウェアにアクセスすることは禁止されている。そこで、ここでもカーネルに「お願い」する手段という形でシステムコールという手段を用いて資源にアクセスする。
カーネルは、各プロセスを管理するために、先ほども出てきた、 PCB(Process Control Block) を保持している。
ここからわかるように、ユーザプロセスはそれぞれ独立した仮想アドレス空間を持ち、互いに干渉できない。
"Each process is executed within its own address space, and the operating system provides mechanisms to isolate and protect processes from one another."
そもそも空間とは、メモリの中での“権限の異なる領域”であり、OSは、CPUやメモリなどのリソースを、信頼できるカーネルコードと、信頼できないユーザアプリケーションで分けて管理するために、「モードと空間の分離」をしている。
CPUは、“モードビット”というフラグで、現在の実行が「ユーザモード」か「カーネルモード」かを制御しており、プロセスがsystem callを行うと、モードを一時的にユーザ→カーネルに切り替え、要求を処理したら、またカーネル→ユーザモードに戻る仕組みとなっている。
これらを分けるのには理由として:
-
保護(Protection):
ユーザプログラムが他のプロセスやカーネルを壊すのを防ぐ -
安定性(Stability):
バグったアプリが OS 全体をクラッシュさせるのを防ぐ -
抽象化(Abstraction):
複雑なハードウェア操作はすべてカーネルが代行する
こういった仕組みにより、OSは安全かつ安定に運用される。
プロセスの状態とその遷移
"The operating system must track the state of each process as it executes. A process may be in one of several states."
プロセスは常に何らかの「状態」にあり、それに応じてOSが適切に処理を行う。
主な状態として、new
(プロセスが作成されたばかり(まだ実行されていない))、ready
(実行待ち。CPUの割り当てを待っている状態)、running
(実際にCPU上で動いている状態(1つのCPUには1つだけ))、waiting(blocked)
(I/O待ちなどのために一時停止中)、terminated(exit)
(プロセスが終了してOSから消去される直前)などがある。
また、この状態管理の裏では、スケジューラ(scheduler)というカーネルの機構が「どのプロセスにCPUを割り当てるか?」を決定しているが、これはまたどこかの記事で説明したい。
サブルーチンとプロセスの違い
"A function call is a control transfer within the same address space. In contrast, a process creation results in a separate address space and execution context."
そもそもサブルーチン(関数)とは、ある手続き、コードの一部を呼び出すことである。ここで、サブルーチン呼び出しとプロセスの最大の違いはメモリ空間の分離である。サブルーチンでは、同じプロセスの中で動作し、スタックを使って制御を一時的に移すだけである。メモリ空間(コード、データ、ヒープ、スタック)は共有、終了後は呼び出し元に戻る(戻り先アドレスを持つ)。
例えば、以下のコード例があったとする。
#include <stdio.h>
void greet() {
printf("Hello\n");
}
int main() {
greet();
printf("Back to main\n");
return 0;
実行の流れとして、greet() を呼び出す(同じプロセスの中)→ printf("Hello") → greet() がreturnしてmainに戻る
という流れである。greet() は一時的に実行を引き受けるけど、必ずmainに戻り、これがサブルーチンは戻り先を持っているということである。
一方でプロセスは、完全に独立した実行単位を作り、独立したアドレス空間(自分だけのコード、スタック、ヒープ)を持つ。親子プロセスはお互いに戻ることはできず、OSがPCB(Process Control Block)を新しく作って管理される。
例としてfork()について考えてみる。fork()はプロセスを複製し、新しいプロセス(子プロセス)システムコールである。
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子プロセス
printf("Child\n");
} else {
// 親プロセス
printf("Parent\n");
}
return 0;
}
流れとして、folk()のシステムコールが呼ばれると、OSがプロセスを複製する。そこで、pid == 0
の分岐に入るのが子プロセス、pid > 0
の分岐に入るのが親プロセスであり、この2つの実行は、完全に独立して走る。どちらが先に進むかはスケジューラ次第であり、子が先にprintf()
を実行することもあれば、親が先のこともある。これら二つの親子プロセスは同じプログラムから分かれたが、もう交わることはない。OSはプロセス終了時に「ゾンビ状態(Zombie)」として一時保存しておき、親が wait()
を呼ぶことで、子の終了ステータスを受け取り、完全にプロセスを破棄できる。
つまり、
一つのプログラムが走っていた。ある時、それは自分と全く同じ“もう一つの存在”を生み出す決断をした(fork())。
それは、姿も記憶も同じだが、違う道を歩むもう一つのプロセス。
片方は親となり、もう片方は子として、それぞれの分岐点(if(pid == 0))を迎え、それぞれの処理を進めていく。
彼らは同じ場所から出発したが、もう二度と同じ未来は共有しない。
そして、親が wait() を呼ぶとき、ようやく子の旅の終わりを見届けることができる。
ちょっとエモい。
ここで、wait()
だが、コードに自体に明記されてないのは、関数は戻ってくるのが前提だからwaitは必要ないが正解。
まとめ
これまで見てきたように、プロセスとは「実行中のプログラム」をOSが安全・効率的に管理するための仮想的な実行単位であり、それぞれが独立したメモリ空間やコンテキストを持ち、カーネルによって生成・実行・終了が制御されている。
しかし、現実のコンピュータには通常CPUコアの数よりも多くのプロセスが同時に存在する。
つまり、OSはどのプロセスにいつCPUを割り当てるかを巧みに決定しなければならない。
CPUという有限資源を複数プロセスでどう公平かつ効率的に分配するか」という「スケジューリング(Scheduling)」を次に扱う。つまり、プロセスの“存在”を支えるのがカーネルだとすれば、プロセスの“順序”を支配するのがスケジューラである。
気が向いたら書きたい。
参考文献
- Abraham Silberschatz, Peter B. Galvin, Greg Gagne, Operating System Concepts, 10th Edition, Wiley, 2018.: https://amzn.to/44zHODM
Discussion