Linuxのプロセス管理
Linux上の全てのプロセスはtask_struct構造体で管理されます。
また、Linuxカーネルは、常に全てのプロセスの全ての管理情報を
参照することができます。
Linuxプロセスの生成
forkシステムコールはプロセスのtask_struct構造体をコピーして新しい
プロセスを生成します。
do_fork(フラグ, プロセスコンテキスト)
空きtask_structを一つ確保(alloc_task_struct関数)
プロセスIDを付ける(get_pid関数)
task_structの各メンバの初期化
ファイルディスクリプタテーブルのコピー(copy_files関数)
カレントディレクトリ、umask等のコピー(copy_fs関数)
シグナル情報のコピー(copy_sighand関数)
親プロセスコンテキストのコピー(copy_thread関数)
仮想空間をCopy-On-Writeで複製コピー(copy_mm関数)
生成した子プロセスをRUNQに繋ぐ(wake_up_process関数)
Linuxプロセスの実行
生成されたプロセスはexecシステムイコールにより、新しいコマンドを実行することが
できます。execシステムイコールは一度全ての仮想空間を解放し、その後
新しい空間を生成しマップします。
do_execve(ファイルパス, 引数・環境)
ファイルのオープン(open_namei関数)
exec後のユーザID/グループID計算、ファイルヘッダの読み込み(prepare_binprm関数)
コマンド名、環境変数、起動引数を読み込む(copy_strings関数)
各バイナリ種別毎のハンドラ呼び出し(search_binary_handler関数)
execシステムコールによって実行に移されたプロセスは次のテーブルにある状態を
ある時点で取ることになります。
状態 | 説明 |
TASK_RUNNING | 実行可能状態 |
TASK_INTERRUPTIBLE | 待ち状態。シグナル受信可能 |
TASK_UNINTERRUPTIBLE | 待ち状態。シグナル受信不可 |
TASK_ZOMBIE | ゾンビ状態。exit後の状態 |
TASK_STOPPED | サスペンド状態 |
CPU上で実行可能なプロセスはTASK_RUNNING状態になっています。
複数あるTASK_RUNNING状態のプロセスのうち最も高い
プライオリティを持つタスクにCPUが与えられます。
TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLEは、共にある条件の
成立を待って実行を中断している状態です(ディスクへのI/O要求を
発行した後にI/Oが完了するのを待っている状態,TTY端末からの
入力を待っている状態など)。
この待ち状態に二つの状態が用意されているのは、待っている間に
シグナルを送られたとき、この待ちを解除するか否かを制御するためです。
TASK_INTERRUPTIBLE状態で待ちに入っている場合は、強制的に
起床されます。(ディスクへのI/O完了待ちでは、I/O処理完了まで
シグナルのハンドリングを遅らせるためにTASK_UNINTERRUPTIBLEで
待つようにしますが、起床する時が保証されないTTY端末I/O待ちの
場合は、TASK_INTERRUPTIBLEで待ちに入ります)
TASK_STOPPED状態は、サスペンドシグナル(SIGSTOPやSIGTTINなど〕
を送られて実行中断状態になった状態を示します。この状態のプロセスは
スケジューリングの対象となりません。リジュームシグナル(SIGCONT)
が送られると、TASK_RUNNING状態に戻され、スケージューリングの
対象となります。
TASK_ZOMBIE状態は、exitしてから消滅するまでの間のプロセス状態で
す。プロセスは死んだ後も親プロセスにwaitを実行してもらうまでは、
TASK_ZOMBIE状態としてシステム内に存在し続けます。
Linuxプロセスの終了
最後に、プロセスの終了はdo_exit関数で行われます。明示的にexitシステムコールを
呼び出したとき以外にも、シグナルを受けて死ぬときなどにも呼び出されます。
do_exit関数ではtask_structを除く全ての資源の解放を行います。
do_exitはexit_notify関数で親プロセスにSIGCHLDを送出します。
SIGCHLDを受け取った親プロセスは、ZOMBIE状態になった子プロセスを
探し出しtask_structの解放を行います。
do_exit(終了コード)
{
このプロセス用のタイマを止める(del_timer_sync関数)
IPCセマフォの解放(sem_exit関数)
仮想空間の解放(__exit_mm関数)
ファイルのクローズと管理域の解放(__exit_files関数)
カレントディレクトリ、umask情報の解放(__exit_fs関数)
シグナルの破棄と管理領域の解放(__exit_sighand関数)
プロセス状態をTASK_ZOMBIEに変更する
親プロセスへの通知(exit_notify関数)
CPUの放棄(schedule関数)
}
Linuxのプロセススケジューリング
Linuxスケジューラ
Linuxスケジューラはプロセスとスレッドを全く区別せずに扱います。
実行可能なプロセス(スレッド)はRUNキューにリンク されてます。
スケジューラはこのRUNキューに継っているプロセスのうち 最も高い優先
度を持つプロセスを選び出しCPU(実行権)を与えます。
現在実行中のプロセスはcurrentというポインタによ
り指されています。
何も実行するプロセスがなくなると、スケジューラはidleと呼ばれる
何もしないプロセスに実行権を渡します。
プロセスの切り替え
プロセスの切替えとは、現在走行中のプロセスのコンテキストを保存し、
次に走行するプロセスのコンテキストを CPU上にロードする作業です。
再び走行を開始するときは、先程メモリ上にセーブしたコンテキストを
CPU上にロードしなおせば、中断地点から処理を再開することができます。
Linuxではswitch_to関数がその作業を担っています。
コンテキストセーブ域としては、プロセスのカーネルスタックと
struct_task内にとられた領域(tss域)を利用しいます。
プロセスの同期
走行中のプロセスが待ちに入る場合、待ち対象毎に
用意されているwaitキューヘッドに自分自身を繋ぎCPUを放棄します。
(自分自身をRUNキューからはずし、スケジューラを呼び出す) sleep_on関数、
interruptible_sleep_on関数が用意されています。
この二つの関数の違いは、WAIT状態になったときのプロセスが
シグナルにより起床するかしないかという点です。
このプロセスはイベントの発生により起床されます。
wake_up関数、wake_up_interruptible関数などによって
起床されたばかりのプロセスはRUNキュー継っていますが、
waitキューヘッドの方にも 継ったままになっています。
このプロセスは再度実行権が与えられた時に、 まず最初に自分自身を
waitキューヘッドから外す処理を行います。
v2.2までは、wake_up関数は対象となるWAITキューヘッドで待ちに入っている
プロセスを全てRUN状態にしていましたが、性能改善のためv2.4からは
WAITキューヘッドで待ちに入っているプロセスのうち先頭のプロセスだけを
RUN状態にすることができるようになりました。プロセスの属性にTASK_EXCLUSIVEを
持たせると、起床処理時に先頭のプロセスのみ起床するようになります。
- wake_up(WAITキューヘッド)
- TASK_UNINTERRUPTIBLE、TASK_INTERRUPTIBLE両方で待ちに入っている
プロセスを起こします。ただし起こすプロセスは先頭の一つだけ
(TASK_EXCLUSIVE)とします。(TASK_EXCLUSIVE属性で待ちに入っている
プロセスを起こしたら、あとのプロセスは起こしません)
- wake_up_all(WAITキューヘッド)
- TASK_UNINTERRUPTIBLE、TASK_INTERRUPTIBLE両方で待ちに入っている
プロセスを全て起こします。(TASK_EXCLUSIVE属性は無視します)
- wake_up_interruptible(WAITキューヘッド)
- TASK_INTERRUPTIBLE両方で待ちに入っている
プロセスを起こします。ただし起こすプロセスは先頭の一つだけ
(TASK_EXCLUSIVE)とします。(TASK_EXCLUSIVE属性で待ちに入っている
プロセスを起こしたら、あとのプロセスは起こしません)
- wake_up_interruptible_all(WAITキューヘッド)
- TASK_INTERRUPTIBLE両方で待ちに入っているプロセスを全て起こします。
プリエンプション処理
プロセスがwake_up_process関数などにより走行可能となったとき、
RUNキューにリンクされますが、RUNキューにリンクしただけでは、
そのプロセスのプライオリティが幾ら高くても
CPUの実行権を与えられることはありません。
このプロセスが現在走行中のプロセスよりプライオリティが高い時、
スケジューラに対してCPUの明け渡し要求(プリエンプト要求)を
出さねばなりません(reschedule_idle関数)。 プリエンプト要求は、
カレントプロセスのtask_structのneed_reschedメンバに印を付けることで
実現しています。
プリエンプト要求を受けたスケジューラは、
Linuxカーネルの処理が一区切りついたところで再スケジューリングを行います
(schedule関数)。 再スケジューリングを行うのは、以下のポイントです。
- システムコール終了時
- 割り込みハンドラ終了時
- idle処理
また、これはLinuxカーネルのコード実行中にはプリエンプションが
発生しないことを意味しています。
Linuxカーネル内走行中のプロセスは明示的にスケジューラを 呼び出さない限り、
他のプロセスにCPUを奪われることはありません。
これはLinuxカーネル内の資源排他を単純化することに役立っています。
スレッド
Linuxプロセスの生成で触れたように、
forkシステムコールはプロセスのtask_struct構造体を
コピーして新しいプロセスを生成します。
一方、cloneによるスレッド生成の場合は、それら資源の コピーを全く行いません。
代わりに両方のコンテキストから全くおなじ資源が参照できるように共有します。
しかし、それ以外の点では まったくプロセスと同等です。
当然スケジューラからもプロセスとスレッドを全く区別せずに扱われます。
おなじ空間を共有するスレッド(同一のプロセス 内のスレッド)であっても、
マルチプロセッサシステムの場合、 別々のCPU上で同時実行
されることもありえます。