Linux プロセス管理とシステムコールの実装詳細

プロセス識別子と階層構造

Linux 環境において、プロセスは固有の識別子(PID)によって管理されます。カーネルは各プロセスに一意な PID を割り当てますが、これはシステム起動からの通し番号であり、長時間稼働すると再利用される可能性があります。デフォルトの最大値は 32768 程度に設定されていることが多いです。

プロセス間には親子関係が存在します。新しいプロセスを生成した側を「親プロセス」、生成された側を「子プロセス」と呼びます。子プロセスは親プロセス的用户 ID やグループ ID などの属性を継承します。また、プロセスグループという概念があり、関連するプロセス群をまとめて信号を送信などを扱う単位として利用されます。

現在のプロセス ID および親プロセス ID を取得するには、以下のシステムコールを使用します。

#include <unistd.h>
#include <sys/types.h>

pid_t getpid(void);   // 自プロセスの ID を取得
pid_t getppid(void);  // 親プロセスの ID を取得

プロセス生成:fork システムコール

新しいプロセスを生成するには fork() 関数を用います。この呼び出し成功后、プロセスが複製され、親プロセスと子プロセスの 2 つの実行フローが存在することになります。

#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);

戻り値は以下の通りです。

  • 親プロセス:生成された子プロセスの PID(正の値)が返る。
  • 子プロセス:0 が返る。
  • 失敗:-1 が返る。

子プロセスは親プロセスのメモリ空間、スタック、ヒープなどを複製しますが、カーネルはパフォーマンス向上のため「コピーオンライト(Copy-on-Write)」機制を採用しています。これは、実際にメモリ内容が変更されるまで物理的な複製を行わず、ページテーブルを共有する仕組みです。

注意点として、fork 後の実行順序はスケジューラに依存するため、親と子のどちらが先に実行されるかは保証されません。また、ループ内で fork を呼び出す場合、生成された子プロセスもループを継続するため、プロセス数が指数関数的に増加する可能性があります。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    int count = 0;
    pid_t process_id;

    // 3 回 fork を実行する例
    for (int i = 0; i < 3; i++) {
        process_id = fork();
        if (process_id < 0) {
            perror("fork failed");
            return 1;
        } else if (process_id == 0) {
            // 子プロセスはループを抜ける
            count = i;
            break; 
        }
    }

    if (process_id > 0) {
        // 親プロセスのみループを完了
        printf("Parent process finished spawning.\n");
    } else {
        printf("Child process %d created. PID: %d\n", count, getpid());
    }
    return 0;
}

プログラム実行の置き換え:exec 関数族

子プロセス内で別のプログラムを実行するには、exec 関数族を使用します。この呼び出しが成功すると、現在のプロセスイメージが新しいプログラムに完全に置き換わります。プロセス ID は変更されませんが、コード領域、データ領域などが新しいものに書き換わります。

関数名の末尾に p が付くもの(例:execlp)は、PATH 環境変数から実行ファイルを検索します。

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

int main() {
    pid_t pid = fork();

    if (pid > 0) {
        // 親プロセス:子プロセスの終了を待つ
        printf("Parent waiting for child...\n");
        wait(NULL);
        printf("Child process terminated.\n");
    } else if (pid == 0) {
        // 子プロセス:別のコマンドを実行
        printf("Child executing new program...\n");
        // ps コマンドを実行し、現在のプロセス一覧を表示
        execlp("ps", "ps", "-ef", NULL);
        
        // exec が成功した場合、ここには到達しない
        perror("exec failed");
        return 1;
    }
    return 0;
}

引数の設定には注意が必要です。最初の引数はパスまたはファイル名、二番目以降は argv として渡される値であり、慣例として二番目の引数には実行ファイル名自体を指定します。末尾は必ず NULL で終える必要があります。

プロセスの終了と待機機制

プロセスは exit() を呼び出すか、main 関数から戻ることで終了します。親プロセスが子プロセスよりも先に終了した場合、子プロセスは init プロセス(または systemd)に引き取られ「オーファンプロセス」となります。逆に、子プロセスが終了しても親が回収しない場合、プロセス管理構造(PCB)がカーネルに残り「ゾンビプロセス」となります。

ゾンビプロセスを回避し、子プロセスのリソースを回収するには、親プロセスで wait() または waitpid() を呼び出す必要があります。

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

int main() {
    pid_t pid;
    int stat_val;

    pid = fork();
    if (pid == 0) {
        // 子プロセス:適当な時間を_sleep して終了
        sleep(2);
        printf("Child exiting with status 5.\n");
        exit(5);
    } else if (pid > 0) {
        // 親プロセス:子プロセスの終了を待つ
        wait(&stat_val);
        
        if (WIFEXITED(stat_val)) {
            printf("Child exited normally. Status: %d\n", WEXITSTATUS(stat_val));
        } else if (WIFSIGNALED(stat_val)) {
            printf("Child terminated by signal.\n");
        }
    }
    return 0;
}

waitpid() を使用すると、特定のプロセスを待機したり、ノンブロッキングで状態を確認したりできます。

#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>

// 特定の子プロセスをノンブロッキングで監視
pid_t reaped_pid = waitpid(target_pid, &stat_val, WNOHANG);
if (reaped_pid == 0) {
    // 子プロセスはまだ終了していない
} else if (reaped_pid > 0) {
    // 子プロセスが終了し、回収された
}

プロセス間通信:パイプ

親子プロセス間でデータを送受信するには、パイプ(pipe)が利用されます。パイプはカーネル内のバッファであり、半二重通信を行います。データは一度読み出すと消滅します。

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

int main() {
    int pipe_fd[2];
    pid_t pid;
    char buffer[1024];
    const char *message = "Data from parent to child";

    if (pipe(pipe_fd) == -1) {
        perror("pipe");
        exit(1);
    }

    pid = fork();
    if (pid > 0) {
        // 親プロセス:書き込み
        close(pipe_fd[0]); // 読み込み側を閉じる
        write(pipe_fd[1], message, strlen(message));
        close(pipe_fd[1]);
    } else if (pid == 0) {
        // 子プロセス:読み込み
        close(pipe_fd[1]); // 書き込み側を閉じる
        ssize_t bytes = read(pipe_fd[0], buffer, sizeof(buffer));
        if (bytes > 0) {
            write(STDOUT_FILENO, buffer, bytes);
        }
        close(pipe_fd[0]);
    }
    return 0;
}

不要なファイルディスクリプタを閉じることで、パイプの EOF 条件を正しく処理できます。

デーモンプロセスの作成

バックグラウンドで継続的に動作するサービスプロセスをデーモンと呼びます。デーモンを作成するには、端末から独立し、セッションリーダーになるなどの手順が必要です。

  1. fork() して親プロセスを終了し、シェルに戻す。
  2. setsid() で新しいセッションを作成し、制御端末から分離する。
  3. 作業ディレクトリをルートディレクトリに変更する(chdir("/"))。
  4. 不要なファイルディスクリプタ(stdin, stdout, stderr)を閉じる。
  5. 標準入出力を /dev/null などにリダイレクトする。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int create_daemon() {
    pid_t pid = fork();
    if (pid < 0) exit(1);
    if (pid > 0) exit(0); // 親プロセス終了

    if (setsid() < 0) exit(1); // 新しいセッション

    chdir("/"); // ルートディレクトリへ移動

    // 標準ファイルディスクリプタを閉じる
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    // 必要に応じて /dev/null へリダイレクト
    int null_fd = open("/dev/null", O_RDWR);
    if (null_fd >= 0) {
        dup2(null_fd, STDIN_FILENO);
        dup2(null_fd, STDOUT_FILENO);
        dup2(null_fd, STDERR_FILENO);
    }

    return 0;
}

int main() {
    create_daemon();
    while (1) {
        // デーモンとしての処理
        sleep(10);
    }
    return 0;
}

これにより、プロセスはターミナルに依存せず、システム起動時から稼働し続けるバックグラウンドタスクとして機能します。

タグ: linux-kernel process-management inter-process-communication daemon-process system-calls

5月28日 04:09 投稿