プロセス、軽量プロセス、およびスレッドの概念
プロセスは、プログラムが実行される際のインスタンスであり、カーネルの観点ではCPU時間やメモリなどのシステムリソースを割り当てるための基本単位です。新しいプロセスが生成されると、そのアドレス空間は親プロセスのコピーとして作成され、次の命令から実行を開始します。
現代のUnix系システムでは、マルチスレッドアプリケーションが一般的です。このような環境では、1つのプロセスが複数のスレッドから構成され、それぞれが独立した実行パスを持ちます。Linuxでは「軽量プロセス(Lightweight Process)」という仕組みを使ってスレッドを実装しており、これらはアドレス空間やオープンファイルなどのリソースを共有できます。この設計により、スレッド間でのデータ共有が容易になり、かつ各スレッドは個別にカーネルによってスケジューリングされます。
POSIX準拠のスレッドAPIであるpthreadライブラリを使用するアプリケーションでは、複数の軽量プロセスが「スレッドグループ」として統合的に管理されます。これにより、同一アプリケーション内のすべてのスレッドが共通の識別子で扱われ、信号送信などの操作もグループ単位で行えるようになります。
プロセス記述子の構造
すべてのプロセスはtask_struct型の構造体によって記述されます。この構造体にはプロセスに関するすべての属性が含まれており、プロセス状態、スケジューリング情報、メモリ管理データ、ファイルディスクリプタ、親子関係など多岐にわたります。
プロセスの状態遷移
プロセスはその実行状況に応じて以下の状態を持ちます:
- TASK_RUNNING:実行中または実行可能状態。CPUの割り当て待ちを含む。
- TASK_INTERRUPTIBLE:中断可能な待機状態。信号受信で起床可能。
- TASK_UNINTERRUPTIBLE:非中断待機状態。明示的なイベントでのみ起床。
- TASK_STOPPED:SIGSTOPなどで一時停止された状態。
- TASK_TRACED:デバッガによって一時停止されている状態。
- EXIT_ZOMBIE:終了済みだが親プロセスがwaitを呼び出していない状態。
- EXIT_DEAD:完全に削除される直前の最終状態。
プロセス識別子の仕組み
各プロセスは固有のPID(Process ID)を持ち、これはtask_struct内のpidフィールドに格納されます。既定では最大32767までですが、64ビットシステムでは最大4194303まで拡張可能です。カーネルは/proc/sys/kernel/pid_maxを通じて上限値を動的に変更できます。
さらに、スレッドグループに対応するTGID(Thread Group ID)という概念があり、これはグループ内の最初のスレッドのPIDと一致します。getpid()システムコールはこのtgidを返すため、マルチスレッドアプリケーション内のすべてのスレッドが同じPIDとして認識されます。
重複を避けるため、カーネルはpidmap_arrayというビットマップ構造を使って未使用のPIDを管理しています。32ビット環境では1ページ(8KB)で十分ですが、64ビット環境では必要に応じて追加のページが確保されます。
スレッド情報とカーネルスタックの配置
Linuxでは、thread_info構造体とカーネルスタックを連続したメモリ領域に配置しています。通常は8KB(2ページ)の領域を使用し、アラインメントのために先頭アドレスが8192バイト境界に揃えられます。
x86アーキテクチャでは、スタックポインタespの下位13ビットをマスクすることでthread_infoの先頭アドレスを高速に取得できます。この設計により、現在実行中のプロセスを即座に特定することが可能となり、特にSMP環境でのパフォーマンス向上に寄与しています。
union thread_union {
struct thread_info info;
unsigned long stack[2048]; // 8KB
};
実行可能プロセスの管理:ランキュー
Linux 2.6以降では、O(1)スケジューラが導入されており、実行可能なプロセスの選出にかかる時間がプロセス数に依存しなくなりました。その中心にあるのがprio_array_t構造体で、優先度ごとに分類されたプロセスリストとビットマップで構成されています。
| フィールド | 説明 |
|---|---|
| nr_active | アクティブなプロセス数 |
| bitmap | 空でないキューがある優先度を示すビットマップ |
| queue[140] | 140段階の優先度ごとのプロセスキュー |
SMPシステムではCPUごとに独立したランキューが存在し、各CPUは自身のキューから次に実行するプロセスを選択します。
プロセス間の関係性
プロセスはツリー構造で関連付けられており、主な関係フィールドは以下の通りです:
| フィールド | 意味 |
|---|---|
| real_parent | 実際に生成したプロセス(存在しない場合はinit) |
| parent | 現在の親プロセス(ptraceなどで変更される場合あり) |
| children | 生成した子プロセスのリンクリスト |
| sibling | 兄弟プロセスへのリンク |
その他の関係としては、プロセスグループ、セッション、スレッドグループのリーダー指定や、デバッグ対象の追跡情報などがあります。
PIDハッシュ機構
PIDからtask_structへの高速検索を行うために、カーネルは複数のハッシュテーブルを使用しています。種類別に以下のようなテーブルが存在します:
- PIDTYPE_PID:個々のプロセスID
- PIDTYPE_TGID:スレッドグループID
- PIDTYPE_PGID:プロセスグループID
- PIDTYPE_SID:セッションID
衝突が発生した場合はチェイン法(双方向リスト)で解決します。また、pid_listフィールドを使うことで、特定のスレッドグループに属するすべてのプロセスを効率的に列挙できます。
ウェイティングキューによるブロッキング操作
条件待ちを行うプロセスは「ウェイティングキュー」に登録されます。これは特定のイベントが発生するまで睡眠状態に入るために使われます。
struct wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
struct wait_queue_entry {
unsigned int flags;
struct task_struct *task;
int (*func)(struct wait_queue_entry *, unsigned int, int, void *);
struct list_head entry;
};
主な操作関数:
prepare_to_wait():待機状態に移行finish_wait():待機解除wake_up():対象キューの全プロセスを起床
排他待ち(mutex)の場合はキュー末尾に追加され、非排他(イベント通知)の場合は先頭に挿入されます。
リソース制限の管理
各プロセスはrlimit構造体を通じてリソース使用量の上限を設定できます。
struct rlimit {
unsigned long rlim_cur; // 現在の制限値
unsigned long rlim_max; // 最大許容値
};
代表的な制限項目:
- RLIMIT_AS:仮想メモリサイズ
- RLIMIT_CORE:コアダンプサイズ
- RLIMIT_CPU:CPU使用時間
- RLIMIT_NOFILE:オープン可能なファイル数
- RLIMIT_NPROC:ユーザーが所有できるプロセス数
スーパーユーザーのみがrlim_maxを変更できます。
コンテキストスイッチの実装
プロセス切り替えはschedule()関数内で行われ、以下の手順で実施されます:
- ページグローバルディレクトリの切り替え(アドレス空間の変更)
- カーネルスタックとハードウェアコンテキストの復元
ハードウェアコンテキストは主にthread_structに保存され、汎用レジスタはカーネルスタック上に退避されます。TSS(Task State Segment)はx86の互換性維持のために存在しますが、実際のコンテキストスイッチはソフトウェアで実装されています。
FPU/MMX/SSEレジスタの遅延保存
FPUやSSEなどの特殊レジスタは、実際に使用されるまで保存されません。これはcr0.TS(Task Switched)フラグと例外処理を組み合わせた「遅延保存(lazy restore)」機構によって実現されています。
プロセス切り替え時にTSビットがセットされ、次にFPU命令を実行すると#NM(Device Not Available)例外が発生します。この例外ハンドラ内で初めてレジスタが復元されます。
void save_fpu_state(struct task_struct *tsk)
{
if (tsk->status & TS_FPU_USED) {
if (use_fxsave)
asm("fxsave %0" : "=m"(tsk->fpu.fxsave));
else
asm("fnsave %0" : "=m"(tsk->fpu.fsave));
tsk->status &= ~TS_FPU_USED;
set_cr0(get_cr0() | X86_CR0_TS);
}
}
カーネル自身がFPUを使用する場合はkernel_fpu_begin()/kernel_fpu_end()で保護します。
プロセス生成の最適化技術
Linuxはプロセス生成を高速化するために以下の技術を採用しています:
- COW(Copy-on-Write):物理ページの書き込み時にのみ複製
- 軽量プロセス:メモリ空間やファイルテーブルを共有
- vfork():親プロセスをブロックし、アドレス空間を一時的に共有
これらの機能はclone()システムコールのフラグで制御されます:
| フラグ | 効果 |
|---|---|
| CLONE_VM | 仮想メモリ空間を共有 |
| CLONE_FS | ルートディレクトリやumaskを共有 |
| CLONE_FILES | ファイルディスクリプタテーブルを共有 |
| CLONE_SIGHAND | シグナルハンドラを共有 |
| CLONE_THREAD | 同じスレッドグループに所属 |
カーネルスレッドの特徴
カーネルスレッドはユーザー空間を持たず、純粋にカーネル内でのみ動作します。主な用途:
kswapd:ページ再利用のトリガーpdflush:Dirtyバッファのディスク書き戻しksoftirqd:ソフト割り込みの処理keventd:カーネルタスクキューの実行
kernel_thread()関数を使って生成され、通常は無限ループで特定の監視タスクを担当します。
初期プロセスの起動フロー
システム起動時のプロセス生成順序:
- プロセス0(swapper/idle):静的データ構造で定義。全てのCPUに1つずつ存在。
- プロセス1(init):
kernel_thread(init, ...)で生成。その後execve("/sbin/init")でユーザー空間へ移行。 - 他のカーネルスレッドが順次生成される。
idleプロセスは実行可能プロセスが存在しないときにのみ稼働し、HLT命令で省電力状態に入ります。
プロセス終了の流れ
プロセス終了は以下の関数で処理されます:
do_group_exit():スレッドグループ全体を終了do_exit():単一スレッドの終了処理
主な処理ステップ:
- 各種リソース(メモリ、ファイル、シグナル)の解放
- 子プロセスの親をinitに再設定(孤児プロセスの回収)
- 終了コードを
exit_codeに設定 - 親プロセスにSIGCHLDを送信
- 状態をEXIT_ZOMBIEに遷移
- スケジューラを呼び出し、新たなプロセスを選出
ゾンビプロセスの回収
終了したプロセスは、親がwait4()またはwaitpid()を呼び出すまでゾンビ状態で残ります。この期間中に終了コードやリソース使用量を取得できるようにするためです。
release_task()関数が最終的な解放を行い、以下の処理を実施:
- PIDハッシュからの削除
- プロセスリンクの切断
- task_structとスタック領域の解放
- 所有者プロセスの参照カウンタ更新
親プロセスが先行して終了した場合は、initプロセスが子プロセスを継承し、定期的にwaitを呼び出してゾンビを回収します。