Skip to content
Published on

[オペレーティングシステム] 03. プロセス:概念、生成、通信

Authors

プロセスの概念

プロセス(Process)は実行中のプログラムである。プログラムがディスクに保存された受動的な存在であるのに対し、プロセスはメモリにロードされて実行される能動的な存在である。

プロセスのメモリ構造

[プロセスのメモリレイアウト]

高アドレス
+------------------+
|   スタック(Stack)  |  ローカル変数、関数パラメータ、リターンアドレス
|        |         |  (下方向に成長)
|        v         |
|                  |
|        ^         |
|        |         |
|    ヒープ(Heap)   |  動的割り当てメモリ (malloc, new)
|                  |  (上方向に成長)
+------------------+
|  データ(Data)     |  グローバル変数、静的変数
+------------------+
|  テキスト(Text)   |  プログラムコード(機械語命令)
+------------------+
低アドレス

プロセスの状態

プロセスは実行中に以下の状態を経る。

[プロセス状態遷移図]

                    ディスパッチ
         +--------+-------->+---------+
         |  準備   |         |  実行    |
         | (Ready) |<--------| (Running)|
         +--------+ タイマー  +---------+
           ^    ^   割り込み    |     |
           |    |              |     |
 admitted  |    | I/O完了      | I/O | exit
           |    |   イベント   |要求  |
           |    |              |     |
         +---+  +--------+    |   +--------+
         |新規|  | 待機    |<---+   | 終了    |
         |生成|  |(Waiting)|        |(Terminated)
         +---+  +--------+        +--------+
  • 新規生成(New): プロセスが生成中
  • 準備(Ready): CPU割り当てを待っている状態
  • 実行(Running): CPUで命令を実行中
  • 待機(Waiting): I/Oまたはイベントの完了を待っている状態
  • 終了(Terminated): 実行が完了した状態

プロセス制御ブロック(PCB)

各プロセスはOS内でPCB(Process Control Block)で表現される。

// Linuxカーネルのtask_struct構造体(簡略化)
struct task_struct {
    volatile long state;          // プロセス状態
    pid_t pid;                    // プロセスID
    pid_t tgid;                   // スレッドグループID

    struct mm_struct *mm;         // メモリ管理情報
    struct files_struct *files;   // オープンファイルテーブル
    struct thread_struct thread;  // CPUレジスタ状態

    unsigned int policy;          // スケジューリングポリシー
    int prio;                     // 優先度
    cpumask_t cpus_allowed;       // 実行可能なCPU

    struct task_struct *parent;   // 親プロセス
    struct list_head children;    // 子プロセスリスト
    // ... 数百の追加フィールド
};

PCBにはプロセス状態、プログラムカウンタ、CPUレジスタ、スケジューリング情報、メモリ管理情報、I/O状態情報などが含まれる。


プロセススケジューリング

スケジューリングキュー

OSは複数のキューを使用してプロセスを管理する。

[プロセススケジューリングキュー]

  レディキュー (Ready Queue)
  +-----+  +-----+  +-----+  +-----+
  | PCB |->| PCB |->| PCB |->| PCB |---> CPU
  +-----+  +-----+  +-----+  +-----+

  ディスク待ちキュー
  +-----+  +-----+
  | PCB |->| PCB |---> ディスクコントローラ
  +-----+  +-----+

  ネットワーク待ちキュー
  +-----+
  | PCB |---> ネットワークインターフェース
  +-----+

コンテキストスイッチ

CPUが別のプロセスに切り替える際、現在のプロセスの状態を保存し、新しいプロセスの状態を復元する必要がある。

[コンテキストスイッチ]

プロセス P0       オペレーティングシステム  プロセス P1
  実行中                                  アイドル
    |                                       |
    |--割り込み/システムコール-->            |
    |              PCB0に状態保存            |
    |              PCB1から状態復元           |
    |                           ---------->|
    アイドル                       実行中    |
    |                                       |
    |              <--割り込み/システムコール--|
    |              PCB1に状態保存            |
    |              PCB0から状態復元           |
    |<---------                             |
  実行中                                  アイドル

コンテキストスイッチ時間は純粋なオーバーヘッドである。ハードウェアによって約1〜1000マイクロ秒かかる。


プロセス操作

プロセス生成:fork()

Unix/Linuxではfork()システムコールでプロセスを生成する。fork()は呼び出したプロセスのコピーを作成する。

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

int main() {
    pid_t pid;

    printf("fork()呼び出し前 - PID: %d\n", getpid());

    pid = fork();  // 子プロセス生成

    if (pid < 0) {
        // fork失敗
        perror("fork失敗");
        return 1;
    } else if (pid == 0) {
        // 子プロセス:fork()が0を返す
        printf("子プロセス - PID: %d, 親PID: %d\n",
               getpid(), getppid());
    } else {
        // 親プロセス:fork()が子のPIDを返す
        printf("親プロセス - PID: %d, 子PID: %d\n",
               getpid(), pid);
        wait(NULL);  // 子の終了を待つ
        printf("子プロセス終了\n");
    }

    return 0;
}
[fork()の動作]

 (PID 100)              fork()
     |                      |
     |                      |--->  (PID 101)
     |                      |      親のアドレス空間をコピー
     |                      |      fork()戻り値 = 0
     | fork()戻り値 = 101   |
     |                      |

fork()とexec()の組み合わせ

fork()後にexec()を呼び出すと、子プロセスが新しいプログラムを実行する。

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

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

    if (pid == 0) {
        // 子:exec()で新しいプログラムを実行
        // exec()は現在のプロセスイメージを新しいプログラムに置き換える
        printf("子:lsコマンドを実行\n");
        execlp("ls", "ls", "-la", NULL);

        // exec()成功時、以下のコードは実行されない
        perror("exec失敗");
    } else if (pid > 0) {
        // 親:子の終了を待つ
        int status;
        waitpid(pid, &status, 0);

        if (WIFEXITED(status)) {
            printf("子の終了コード:%d\n", WEXITSTATUS(status));
        }
    }

    return 0;
}

プロセスの終了

プロセスはexit()システムコールで終了を要求する。親プロセスはwait()で子の終了ステータスを取得する。

  • ゾンビプロセス(Zombie): 終了したが親がwait()を呼び出していないプロセス
  • 孤児プロセス(Orphan): 親が先に終了したプロセス。init(PID 1)が新しい親になる

プロセス間通信(IPC)

プロセスは独立的(independent)または協調的(cooperating)である。協調的プロセスにはIPCが必要である。

共有メモリ(Shared Memory)

複数のプロセスが同じメモリ領域にアクセスしてデータを交換する。

// POSIX共有メモリ - 生産者
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>

#define SHM_NAME "/my_shm"
#define SHM_SIZE 4096

int main() {
    // 共有メモリオブジェクトの作成
    int shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    ftruncate(shm_fd, SHM_SIZE);

    // メモリマッピング
    char *ptr = mmap(NULL, SHM_SIZE,
                     PROT_READ | PROT_WRITE,
                     MAP_SHARED, shm_fd, 0);

    // データの書き込み
    const char *message = "Hello from producer!";
    memcpy(ptr, message, strlen(message) + 1);

    printf("生産者:メッセージ書き込み完了\n");
    munmap(ptr, SHM_SIZE);
    return 0;
}
// POSIX共有メモリ - 消費者
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

#define SHM_NAME "/my_shm"
#define SHM_SIZE 4096

int main() {
    // 既存の共有メモリを開く
    int shm_fd = shm_open(SHM_NAME, O_RDONLY, 0666);

    // メモリマッピング
    char *ptr = mmap(NULL, SHM_SIZE,
                     PROT_READ,
                     MAP_SHARED, shm_fd, 0);

    // データの読み取り
    printf("消費者:%s\n", ptr);

    munmap(ptr, SHM_SIZE);
    shm_unlink(SHM_NAME);  // 共有メモリの削除
    return 0;
}

メッセージパッシング(Message Passing)

OSカーネルを通じてプロセス間でメッセージをやり取りする。共有メモリより遅いが、同期が内蔵されている。

[IPCモデルの比較]

共有メモリモデル:              メッセージパッシングモデル:

プロセスA   プロセスB          プロセスA   プロセスB
  |    \   /    |                |           |
  |     \ /     |                | send(M)   |
  |  共有メモリ  |                |---> カーネル|
  |     / \     |                |     |     |
  |    /   \    |                |     v     |
  |           |                  | receive(M)|
                                 |     ----->|
カーネル介入最小化              カーネルが毎転送ごとに介入

パイプ(Pipes)

パイプは2つのプロセス間の通信チャネルである。

通常パイプ(Ordinary Pipe)

親子関係のプロセス間でのみ使用可能で、単方向である。

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

int main() {
    int fd[2];  // fd[0]: 読み取り端、fd[1]: 書き込み端
    pid_t pid;
    char buffer[256];

    // パイプの作成
    if (pipe(fd) == -1) {
        perror("パイプ作成失敗");
        return 1;
    }

    pid = fork();

    if (pid == 0) {
        // 子:パイプから読み取り
        close(fd[1]);  // 書き込み端を閉じる

        int bytes = read(fd[0], buffer, sizeof(buffer));
        printf("子が受信したメッセージ:%s (%dバイト)\n", buffer, bytes);

        close(fd[0]);
    } else {
        // 親:パイプに書き込み
        close(fd[0]);  // 読み取り端を閉じる

        const char *msg = "こんにちは、子プロセス!";
        write(fd[1], msg, strlen(msg) + 1);
        printf("親が送信したメッセージ:%s\n", msg);

        close(fd[1]);
        wait(NULL);
    }

    return 0;
}

名前付きパイプ(Named Pipe / FIFO)

親子関係でない任意のプロセス間でも使用可能である。

# シェルでの名前付きパイプの使用
mkfifo /tmp/my_pipe

# ターミナル 1: 書き込み
echo "Hello via named pipe" > /tmp/my_pipe

# ターミナル 2: 読み取り
cat /tmp/my_pipe

クライアント・サーバー通信

ソケット(Sockets)

ソケットは通信のエンドポイント(endpoint)である。IPアドレスとポート番号の組み合わせで識別される。

// TCPサーバーの例
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);
    char buffer[1024];

    // ソケット作成
    server_fd = socket(AF_INET, SOCK_STREAM, 0);

    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8080);

    // バインドとリッスン
    bind(server_fd, (struct sockaddr *)&server_addr,
         sizeof(server_addr));
    listen(server_fd, 5);

    printf("サーバー待機中(ポート 8080)...\n");

    // クライアント接続の受け入れ
    client_fd = accept(server_fd,
                       (struct sockaddr *)&client_addr,
                       &addr_len);

    // データ受信と応答
    int bytes = recv(client_fd, buffer, sizeof(buffer), 0);
    buffer[bytes] = '\0';
    printf("受信:%s\n", buffer);

    const char *response = "サーバー応答:メッセージ受信完了";
    send(client_fd, response, strlen(response), 0);

    close(client_fd);
    close(server_fd);
    return 0;
}

リモートプロシージャコール(RPC)

RPCはネットワークを通じてリモートシステムの関数をあたかもローカル関数のように呼び出すメカニズムである。

[RPC動作過程]

クライアント                            サーバー
    |                                    |
    |-- 1. ローカル関数呼び出し形式 ---   |
    |      (スタブがパラメータをマーシャリング) |
    |                                    |
    |-- 2. ネットワーク経由で送信 ------->|
    |                                    |-- 3. サーバースタブが
    |                                    |      アンマーシャリング
    |                                    |
    |                                    |-- 4. 実際の関数実行
    |                                    |
    |<-- 5. 結果返却 --------------------|
    |      (スタブが結果をアンマーシャリング)  |
    |                                    |

RPCで注意すべき点は以下の通りである。

  • データ表現: クライアントとサーバーのデータ形式が異なる場合があるため、XDR(External Data Representation)などの標準形式を使用
  • バインディング: クライアントがサーバーを見つける方法。固定ポートまたはランデブーデーモン(ポートマッパー)を使用
  • 実行セマンティクス: 「最大1回(at most once)」または「正確に1回(exactly once)」の保証

まとめ

プロセスはOSの核心的な抽象化単位である。fork()とexec()でプロセスを生成し、新しいプログラムを実行する。プロセス間通信には共有メモリ、メッセージパッシング、パイプ、ソケット、RPCなど様々なメカニズムがあり、それぞれ性能と利便性の面でトレードオフがある。