Linuxプロセス状態とプロセス待機の詳細

コンピュータ上の多くのプログラムは時間を分けて実行される

プログラムが実行される際、メモリにロードされ、オペレーティングシステムがプロセスを生成する。Linuxでは、プロセス情報を保持するtask_structを何らかのデータ構造で管理する。その後、そのプロセスがスケジューリングされるのを待つ。しかし、プログラムが起動されても、プロセスが常に実行状態にあるわけではない。
プロセスの状態遷移図
プロセスがscanf関数でキーボードなどの外部デバイスにアクセスする場合、ユーザーが入力を行うまでプロセスは待機状態となり、実行状態ではない。つまり、プロセスは作成時、CPUにスケジューリングされる時、CPUを待つ時、終了時などで状態が異なる。task_struct構造体の最初のメンバがプロセスの状態である。

struct task_struct {
    volatile long state;    /* -1: 実行不可, 0: 実行可能, >0: 停止 */
    // その他のプロセス属性
};

プロセス作成後の主な4つの状態

プロセスの状態
Linuxシステムにおけるプロセスの状態一覧(psコマンドの出力など)
状態名

実行可能状態 (R)

実行可能状態のプロセスは、いつでもCPUにスケジューリング可能である。CPUは1つしかなく、コンピュータは並行実行能力を持つため、多数のプロセスが実行可能状態で双方向の実行キュー(runqueue)にリンクされる。あるプロセスがCPUで実行を終えたり、タイムスライスを使い切った場合、そのプロセスは実行キューに戻り再び実行可能状態となる。ただし、プロセスが何らかのリソースを必要としているがまだ取得できていない場合、CPUを離れてブロックキューで待機する。
実行キューとブロックキュー

ブロック状態 (S, D, T)

プロセスがブロックされるのは、リソースを取得してから次の処理を進める必要があるためである。リソースが準備できていない場合、OSは効率を考慮して該当プロセスをCPUから待機キューに移動し、状態を実行状態から待機状態(スリープ状態)に変更する。Linuxではブロック状態をスリープ状態と呼び、"S (sleeping)"と表示される。S状態のプロセスは直接終了可能である。一方、D状態(uninterruptible sleep)は深いスリープ状態であり、直接終了できない。

ブロックの仕組み

実行中のプロセスがリソースを要求し、それがまだ到着していない場合、OSはprepare_to_wait()関数を呼び出して現在のプロセスを待機キューに入れ、状態をTASK_UNINTERRUPTIBLEに設定する。その後schedule()を呼び出してCPUを明け渡し、他のプロセスを実行させる。条件が満たされたらfinish_wait()関数で待機キューからプロセスを削除する。また、OSはプロセスに停止シグナル(SIGSTOP)を送信してプロセスを一時停止させることができ、この状態はT(stopped)と表示される。
待機キューのイメージ

ブロックされたプロセスが再スケジュールされない場合:スワップアウト

ブロックされたプロセスが多すぎると占有メモリが増大する。メモリが逼迫した場合、OSはブロックされたプロセスの一部をディスク上のスワップ領域に退避させる(スワップアウト)。
スワップの概念図
Linuxカーネルでは、S、D、Tはいずれもプロセスが待機している状態である。SIGSTOPで停止(T)したプロセスは、SIGCONTで再び実行を再開できる。
ブロックされていたプロセスがリソースを取得し実行条件が整うと、待機キューから実行キューに再リンクされ、保存されていたコンテキストをもとに後続のコードを実行する。

プロセス終了状態

プロセスはタスク実行後にリソースを解放する必要がある。終了状態には以下の3パターンがある。

  1. 正常終了、実行結果は正しい。
  2. 正常終了、実行結果は誤り。
  3. 異常終了(シグナルなどによる強制終了)。

プロセス終了時には終了情報が記録される。プロセスは親プロセスによって生成され、親は子を管理するため、子の終了情報を知る必要がある。Linuxでは、終了時に終了情報を自身のtask_structに記録し、コードやデータは解放するが、終了情報を含むtask_structは親プロセスまたはOSが読み取るまで保持する。

ゾンビ状態 (Z)

ゾンビ状態の図

  1. プロセスが終了しても、親プロセスがその終了ステータスを取得しない(waitしない)場合、OSは終了したプロセスのtask_structを維持し、状態はZ(zombie)となる。
    子プロセスが終了したにもかかわらず親が回収しないとゾンビ状態が続く。ゾンビプロセスに対してkill -9を実行しても強制終了できない。ゾンビが多発するとメモリリークにつながる。task_structが解放されないためである。親がwaitすると状態がX(dead)に変わり、ようやくプロセスが完全に解放される。X状態はユーザーからは見えない。

ゾンビ状態のイメージ

  1. 親プロセスが先に終了した場合、子プロセスは孤児(orphan)となり、PID 1のinitプロセス(または現代のシステムではsystemd)に引き取られ管理される。これを「プロセスの托孤」という。

プロセス終了の詳細

プロセス終了時には戻り情報がある。正常終了したプロセスは終了コード0を返す(main関数の戻り値)。Linuxではecho $?で直前のプロセスの終了コードを確認できる。終了コードが0以外の場合は異常を示し、値によって異なるエラー原因を表す。また、異常終了はシグナル(数値)によっても引き起こされる。プロセス終了の結果は、終了コードと終了シグナルの2つの情報で判断する。
シグナルの一覧はkill -lで確認可能。エラーコードに対応する文字列はstrerror()関数で取得できる(0~133のエラーコードが存在する)。

kill -l
// よくある異常: 0除算による浮動小数点例外、ヌルポインタ参照によるセグメンテーション違反
for (int i = 0; i < 134; i++) {
    printf("エラーコード:%d, エラーメッセージ:%s\n", i, strerror(i));
}

親プロセスは子プロセスの終了情報(終了コードと終了シグナル)を受け取るためにプロセス待機を行う。

プロセス待機 (wait)

プロセス待機は、親プロセスが子プロセスのリソースを回収し、ゾンビ状態によるメモリリークを防ぐために行う。また、親は子のタスク実行結果を知るために待機する。待機には以下のシステムコールを使用する。

pid_t waitpid(pid_t pid, int *status, int options);
pid_t wait(int *status);

これらはゾンビ状態の子プロセスを回収するために使う。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdbool.h>

void child_work() {
    int cnt = 10;
    while (cnt--) {
        printf("子プロセス: pid=%d, ppid=%d\n", getpid(), getppid());
        sleep(1);
    }
    printf("子プロセス終了\n");
}

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        child_work();
        exit(0);
    }

    // 親プロセス
    int count = 15;
    while (count--) {
        printf("親プロセス: pid=%d, ppid=%d\n", getpid(), getppid());
        sleep(1);
    }
    printf("親プロセス終了\n");

    int status = 0;
    int n = waitpid(-1, &status, 0);  // 任意の子を待つ
    if (n == pid) {
        printf("wait成功\n");
    }
    printf("終了コード: %d, エラーメッセージ: %s\n",
           (status >> 8) & 0xFF, strerror((status >> 8) & 0xFF));
    printf("終了シグナル: %d\n", status & 0x7F);
    sleep(5);
    return 0;
}

wait実行例
子プロセスがタスクを完了しても親がwaitするまで子はZ状態となる。親がwaitに成功すると子のリソースは完全に解放され、Z状態は消える。

プロセス終了情報の取得

waitpid()の第1引数pid:

  • 正の値: そのPIDと一致する子を待つ。
  • -1: 任意の子を待つ。

第2引数statusは出力用のint型ポインタで、終了コードと終了シグナルを下位16ビットに詰めて返す。ビット構成: bit15~8が終了コード、bit7~0のうち下位7ビットが終了シグナル。
statusビット構成

異常終了の場合、終了コードは無意味。終了コードは(status >> 8) & 0xFF、終了シグナルはstatus & 0x7Fで取得する。

waitpid()の戻り値が0以上の場合はwait成功(0の場合は子がまだ終了していない)、負の値は失敗。

第3引数optionsで待機方法を指定:

  • 0: ブロッキング待機 (子が終了するまで親は停止)
  • WNOHANG: ノンブロッキング待機 (子が終了していなくても即座に戻り、ポーリングで繰り返し確認する必要がある)。

ノンブロッキング待機を使う場合、親は子より後に終了するか、子の終了を確認するループを回す必要がある。親が先に終了すると子が孤児になり、PID 1に管理が移る。

タグ: linux プロセス状態 タスク構造体 ゾンビプロセス waitpid

8月7日 09:08 投稿