Webサーバーが同時に多くの要求へ応答するとき、すべての要求がCPU上で同時に命令を実行しているとは限りません。I/Oを待つ要求とCPUを使う要求を切り替えるだけでも、仕事は重なって進みます。まず『資源を持つまとまり』と『命令を実行する流れ』を区別しましょう。
この単元でできるようになること
プロセスとスレッドが持つ情報を分類し、実行可能・実行中・待機中の違いを説明します。さらに、スレッドの切替えに何を保存する必要があり、なぜスレッド数を増やすだけでは速くならないかを考えます。
プロセスは実行環境、スレッドは実行の流れ
プロセスは、アドレス空間や開いているファイルなどを伴う実行環境です。同じ実行ファイルを2回起動しても、通常は別のプロセスになり、それぞれの変数を持ちます。同じ仮想アドレスの数値が見えていても、別の物理メモリを指すことがあります。
スレッドは、次にどの命令を実行するかという位置やレジスタ、スタックなどの実行状態を持ちます。同じプロセス内のスレッドは通常、ヒープやグローバル変数、開いているファイルを共有します。一方、関数の呼出し履歴を保持するスタックは各スレッドにあります。
『スタックが別だから、ほかのスレッドのスタックへ絶対にアクセスできない』とはいえません。同じアドレス空間を共有する以上、有効なアドレスを受け渡せばアクセスできる場合があります。スタックの寿命が終わった後に参照したり、同期せず書き換えたりすると不具合になります。所有することと、ハードウェアで隔離されることは別です。
forkでは変数の変更先が分かれる
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
int value = 10;
pid_t pid = fork();
if (pid == -1) return 1;
if (pid == 0) {
value = 20;
printf("child: %d\n", value);
return 0;
}
waitpid(pid, 0, 0);
printf("parent: %d\n", value);
return 0;
}子は20、親は10を表示します。fork直後には変数の内容が同じでも、通常の変数への書込みはそれぞれのプロセスの状態を変えます。実装ではコピーオンライトにより物理ページのコピーを遅らせられますが、プログラムから見た独立性は維持されます。共有メモリとして明示した領域などは別に考えます。
CPUを使える状態と、使っている状態
実行可能は、CPUが割り当てられれば進められる状態です。実行中は、実際にCPUで命令を実行している状態です。待機中は、データの到着やロックの解放などを待ち、その条件が満たされるまで進めない状態です。OSによって状態名や細分化は異なりますが、CPU不足とI/O待ちを分けるための基本になります。
スケジューラは実行可能なスレッドから次の実行対象を選びます。待っているスレッドへCPUを渡しても、欲しいデータが届くわけではありません。CPU使用率が低いのに応答が遅いなら、ディスク、ネットワーク、ロックなど、待っている理由を調べる必要があります。
中断した場所から再開するための保存
コンテキストスイッチでは、中断された側の実行を後で再開できるように、レジスタや命令位置などを保存し、次の実行対象の状態を復元します。異なるアドレス空間への切替えでは、その管理情報も扱います。PCBやTCBという呼び名はこの管理を説明するモデルで、実際のOSの構造体と一対一に一致するとは限りません。
切替えの負担には保存・復元の処理に加え、CPUキャッシュやアドレス変換の状態が変わる影響もあります。CPUを使い切る仕事でスレッドだけを大量に増やすと、切替えや競合の割合が増えることがあります。逆にI/O待ちの多い仕事では、その間に別の仕事を進める余地があります。仕事の性質を見て並行度を決めます。
理解を確かめる
問1:同じプロセスの2スレッドが同じグローバル変数を増やします。変数は別々でしょうか。通常は同じ変数を共有するので、更新の同期が必要です。スレッドごとのレジスタやスタックが別でも、この共有はなくなりません。
問2:1コア上の2スレッドが交互に進んでいます。これは並行実行でしょうか。はい。同時刻に1本しか命令を実行しなくても、複数の仕事が重なって進む並行性はあります。同時刻に複数の命令列を進める並列性とは区別します。
仮想記憶・ページテーブル・TLBで、プロセスごとのアドレス空間を支える仕組みを学びます。