👋

プロセスとフォークについて

2024/10/30に公開

プロセスとは?

プロセスとは、簡単に言うと、「実行中のプログラム」のこと。例えば、スマホでブラウザアプリを開いているとき、そのブラウザアプリは1つのプロセスとして動いている。それと同時に、メールアプリも開いていれば、それは別のプロセスとして動作している。
各プロセスは、それぞれ独立して動作していて、プロセスごとにメモリやCPUなどのコンピュータリソースを使用している。

具体例を挙げると、

  • ウェブブラウザを起動する → 1つのプロセスが作られる
  • 音楽プレイヤーを起動する → もう1つのプロセスが作られる
  • テキストエディタを起動する → さらに別のプロセスが作られる
    といった感じ。

1. プロセスの特徴

  • 独立したメモリ空間[1]を持つ
  • OSによってスケジューリングされる[2]
  • 他のプロセスと並行して実行される
  • 固有のプロセスID(PID)を持つ
  • CPUの時間を使用する[3]

フォークとは?

既存のプロセス(親プロセス)から新しいプロセス(子プロセス)を作成する操作のこと。親プロセスが子プロセスを生み出すイメージ。子プロセスは親プロセスのほぼ完全なコピーとして作成される。

1. プロセスとフォークの関係

フォークはプロセスの生成方法の一つ。

2. フォークの概要

  • 親プロセスの実行状態をコピーして新しいプロセスを作成する
  • 子プロセスは親プロセスとは別のPIDを持つ
  • 子プロセスは親プロセスのメモリイメージのコピーを受け取るが、独立したメモリ空間を持つ

3. 親プロセスと子プロセスの関係

  • メモリ内容: 子プロセスは親プロセスのメモリ内容を完全にコピーする。これには、コード、データ、スタック、ヒープが含まれる。
  • 実行位置: 子プロセスは、親プロセスがフォークを呼び出した直後の位置から実行を開始する。
  • ファイルディスクリプタ[4] 開いているファイルやソケットなども子プロセスに引き継がれる。

4. 親プロセスと子プロセス間で、独立しているものと共有しているもの

  • 独立:

    • 子プロセスは独自のPIDを持つ。
    • 子プロセスは独自のメモリ空間を持ち、親プロセスのメモリに直接アクセスできない。
  • 共有:

    • ファイルディスクリプタ:親子プロセス間でファイルポインタ[5]が共有される。
    • プロセス属性[6]:ユーザーID、グループID、カレントディレクトリなどが共有される。

5. Copy-on-Write (CoW) メカニズム

「子プロセスが必要になるまで親プロセスのデータをコピーしない」という方式。システムのパフォーマンスと効率性を向上させる重要な技術。これにより、親プロセスと子プロセスは必要最小限のメモリ使用で効率的に動作できる。フォーク時にメモリを効率的に扱うことができるので、多くのOSでこの方式を採用している。

  • フォーク直後、子プロセスは親プロセスのメモリ内容への参照だけを持つ。
  • 子プロセスがメモリページを変更しようとする時だけ、そのページのコピーが作成される。
  • 親プロセスの元のデータは変更されず、子プロセスだけが新しいコピーを使用する。

メリット

  • 子プロセス生成時の不必要なメモリコピーを避け、システムリソースを節約する。
  • 親プロセスの大量のデータを子プロセスが扱う際の効率が向上する。
  • 子プロセスが変更を加えても、親プロセスの元のデータは影響を受けない。

6. プロセス間通信 (IPC)

フォークによって作成された親子プロセスの間でも、他の独立したプロセス間と同様に以下のようなIPCメカニズムを利用して通信ができる。

  • パイプ[7]
  • 共有メモリ[8]
  • メッセージキュー[9]
  • セマフォ[10]
  • ソケット[11]

7. 親プロセスと子プロセスの終了

  • 子プロセスが終了すると、その終了状態は親プロセスが回収するまでカーネル[12]内に保持される。
  • 親プロセスが子プロセスより先に終了した場合、子プロセスは「孤児プロセス」[13]となり、init プロセス(PID 1)[14]に引き取られる。

8. プロセスとフォークの概念を、レストランの運営に例える

  1. レストラン全体が親プロセスに当たるとする。レストランでは、以下を行う。
    • 注文を受け付ける
    • 全体の管理を行う
    • リソース(調理器具、食材など)の割り当てを管理する

  2. 注文を受けたら、注文ごとに新しい料理人を割り当てる(フォーク操作に当たる)
    • 各注文に対して新しいプロセス(料理人)を作成する
    • 親プロセスのリソースと状態を子プロセスにコピーする

  3. 各料理人は独立して料理を作る(子プロセス)
    • それぞれの子プロセス(料理人)は独立して作業を行う
    • 必要に応じて親プロセス(レストラン)とコミュニケーションを取る

コード例

以下は、Pythonを使用したフォークの基本的な例。

import os

print(f"レストラン(親プロセス)が開店しました。PID: {os.getpid()}")

# 新しい注文(子プロセス)を作成
pid = os.fork()

if pid > 0:
    # 親プロセス(レストラン全体)の処理
    print(f"新しい注文を受けました。料理人(子プロセス)のPID: {pid}")
elif pid == 0:
    # 子プロセス(個々の料理人)の処理
    print(f"料理人が調理を始めました。私のPID: {os.getpid()}")
    print(f"私の親(レストラン)のPID: {os.getppid()}")
else:
    print("フォークに失敗しました")

# 両方のプロセスがこの行を実行します
print(f"処理を終了します。PID: {os.getpid()}")

このコードを実行すると、以下のような出力が得られる(PIDは実行ごとに異なるので注意)。

レストラン(親プロセス)が開店しました。PID: 1234
新しい注文を受けました。料理人(子プロセス)のPID: 1235
処理を終了します。PID: 1234
料理人が調理を始めました。私のPID: 1235
私の親(レストラン)のPID: 1234
処理を終了します。PID: 1235

上記のコードの解説:

# 新しい注文(子プロセス)を作成
pid = os.fork()

os.fork()を呼び出した瞬間、親プロセスと全く同じ状態の子プロセスが作成され、プログラムが2つのプロセスで並行して動作し始める。

親プロセス(レストラン)のos.fork()は新しく作られた子プロセスのProcess ID (PID)を返す。親プロセスで返ってくるPIDの値は、正の整数 (pid > 0)。
子プロセス(料理人)では、os.fork()は0を返す (pid = 0)。

if pid > 0:
    # 親プロセス(レストラン全体)の処理
    print(f"新しい注文を受けました。料理人(子プロセス)のPID: {pid}")
elif pid == 0:
    # 子プロセス(個々の料理人)の処理
    print(f"料理人が調理を始めました。私のPID: {os.getpid()}")
    print(f"私の親(レストラン)のPID: {os.getppid()}")
else:
    print("フォークに失敗しました")

両方のプロセスが同じコードの残りの部分を実行するが、親プロセスと子プロセスでpidの値が異なるため、異なる処理を行う。
親プロセスでは、if pid > 0:のブロックを、子プロセスでは、elif pid == 0:のブロックを実行する。
fork()が失敗した場合、-1が返される。この場合は、else:ブロックが実行される。

現在実行中のプロセスのPIDを知るには:

os.getpid()で、それを呼び出したプロセス自身の PID を返す。
os.getppid()で、それを呼び出したプロセスの親プロセスの PID を取得する。

9. フォークが使われる場面の例

フォークは、複数の作業を並行して行う必要がある場面で特に役立つ。
以下に、フォークが使われる場面の例を挙げる。

例1: バックグラウンドジョブ処理:

親プロセスがメインの処理を続ける間、子プロセスが時間のかかる処理を行う。

例2: シェル[15]:

ユーザーがコマンドを入力すると、シェル(親プロセス)がそのコマンドを実行するための子プロセスを作成する。

例3: Webサーバー:

Webサーバーが複数のリクエストを同時に処理する際に、フォークが使われている。親プロセスがリクエストを受け付け、子プロセスが各リクエストを処理している。
この例において、プロセスとフォークの関係は以下のようになる。

  1. メインサーバープロセス(親)が起動し[16]、ポートをリッスンする[17]
  2. クライアントからリクエストが来ると、親プロセスがフォークを実行。
  3. 子プロセスが作成され、そのリクエストの処理を担当。
  4. 親プロセスは新たなリクエストの受付を続ける。
  5. 子プロセスはリクエスト処理が完了すると終了する。

フォークを使用することで、サーバーは複数のリクエストを並行して処理できる。各リクエストは独立したプロセスで処理されるため、1つのリクエスト処理が問題を起こしても、他のリクエスト処理や親プロセスには影響しにくい。

以下は、この動作を模したPythonの簡易的なコード例。
このコードでは、Webサーバーがフォークを使用して複数のリクエストを並行処理する様子を簡略化してシミュレートしている。各子プロセスが独立してリクエストを処理し、親プロセスが全体を管理する様子を示している。

import os
import time

def handle_request(request_number):
    print(f"子プロセス {os.getpid()} がリクエスト {request_number} を処理中...")
    time.sleep(2)  # リクエスト処理をシミュレート
    print(f"子プロセス {os.getpid()} がリクエスト {request_number} の処理を完了")

def simple_server():
    print(f"サーバー(親プロセス {os.getpid()})が起動しました")
    
    for request in range(5):  # 5つのリクエストをシミュレート
        pid = os.fork()
        
        if pid == 0:  # 子プロセス
            handle_request(request)
            os._exit(0)  # 子プロセスを終了
        else:  # 親プロセス
            print(f"親プロセスがリクエスト {request} のための子プロセス(PID: {pid})を作成")
    
    # 全ての子プロセスの終了を待つ
    for _ in range(5):
        os.wait()
    
    print("全てのリクエストが処理されました。サーバーを終了します。")

if __name__ == "__main__":
    simple_server()

上記のコードの解説:

def simple_server():
    print(f"サーバー(親プロセス {os.getpid()})が起動しました")
    
    for request in range(5):  # 5つのリクエストをシミュレート
        pid = os.fork()

simple_server()関数内で、5回のループを回す(=5つのリクエストが来ることをシミュレートする)。各ループでos.fork()を呼び出し、新しい子プロセスを作成する。

        if pid == 0:  # 子プロセス
            handle_request(request)
            os._exit(0)  # 子プロセスを終了
        else:  # 親プロセス
            print(f"親プロセスがリクエスト {request} のための子プロセス(PID: {pid})を作成")

fork()の後、子プロセス(pid == 0)ではhandle_request()関数を呼び出してリクエスト処理をシミュレートし、処理後にos._exit(0)で終了する。
os._exit(0)は、現在のプロセスを即座に終了させるシステムコールで、引数の0は正常終了を意味する。(os._exit()はプロセスを即座に終了させ、クリーンアップ処理を行わないので、子プロセスの終了には適しているが、メインプログラムの終了には通常sys.exit()を使用する。)

親プロセス(pid > 0)では、新しく作成した子プロセスのPIDを表示し、次のリクエストの処理に進む。

    # 全ての子プロセスの終了を待つ
    for _ in range(5):
        os.wait()

親プロセスによってos.wait()が呼び出される。
os.wait()関数は、いずれかの子プロセスが終了するまで待機する。
子プロセスが終了すると、os.wait()は即座に制御を親プロセス(呼び出し元)に戻す。
親プロセスは、終了した子プロセスの情報(PIDと終了ステータス)を受け取る。
このコードでは、5回のループでos.wait()を呼び出しているので、全ての子プロセス(この場合は5つ)が終了するまで、親プロセスは待機する。

これにより、全ての子プロセスが処理を完了するまで親プロセス(メインのサーバープロセス)が終了しないようにしている。このため、サーバーアプリケーション全体が全てのリクエスト処理を確実に完了してから終了することを保証している。

def handle_request(request_number):
    print(f"子プロセス {os.getpid()} がリクエスト {request_number} を処理中...")
    time.sleep(2)  # リクエスト処理をシミュレート
    print(f"子プロセス {os.getpid()} がリクエスト {request_number} の処理を完了")

handle_request()関数は、それぞれの子プロセスで呼び出され、リクエスト処理をシミュレートする。time.sleep(2)で2秒間の処理時間をシミュレートしている。

プロセスやフォークについて知ることがどのようなことに役に立つのか?

1. コンピュータの基本的な仕組みの理解に役立つ

プロセスとフォークを理解することで、コンピュータの内部動作が分かるようになる。

なぜ重要か?

プロセスは実行中のプログラムの単位であり、フォークはプロセスの複製を作る操作なので、これらの概念を理解することで、コンピュータがどのようにプログラムを実行し、管理しているかが分かる。例えば、スマートフォンで複数のアプリを同時に開いても遅くならない理由は、各アプリが別々のプロセスとして実行され、OSがそれらを効率的に管理しているためである。

どのように役立つか?

プロセスとフォークの仕組みを理解することで、複雑なシステムの設計が容易になる。例えば、オンラインゲームのサーバーでは、各プレイヤーの接続を別々のプロセスとして管理し、必要に応じてフォークを使って新しいプロセスを作成することで、多数のプレイヤーを効率的に処理できる。

2. 並列処理の理解に役立つ

プロセスとフォークの概念は、コンピュータが複数の仕事を同時に処理する方法を理解するのに役立つ。

なぜ重要か?

現代のアプリケーションの多くは、複数の作業を同時に行う必要がある。プロセスを使うことで、これらの作業を並列に実行できる。フォークを使えば、一つのプログラムから複数の独立したプロセスを作成し、それぞれが異なるタスクを同時に実行できる。例えば、ビデオ会議アプリでは、カメラからの映像取り込み、音声の録音と再生、画面の共有などの処理を別々のプロセスで行うことで、スムーズな動作を実現している。

どのように役立つか?

並列処理の理解は、応答性の高いアプリケーションや、多くのユーザーを同時に扱えるウェブサイトの開発に役立つ。例えば、大規模なeコマースサイトでは、商品の閲覧、検索、購入処理などを別々のプロセスで処理することで、数千人が同時にサイトを利用しても遅くならない仕組みを実現できる。

3. コンピュータのリソース管理の理解に役立つ

プロセスを理解することで、メモリやCPU時間などのコンピュータリソースの使用方法が分かるようになる。

なぜ重要か?

各プロセスは独自のメモリ空間を持ち、CPUやその他のリソースを共有して使用する。これらのリソースを効率的に管理することで、プログラムの性能が向上する。例えば、メモリ使用量を最適化したプロセス設計により、同じハードウェアでもより多くのタスクを同時に処理できるようになる。

どのように役立つか?

効率的なアプリケーションを開発するには、リソース管理を理解する必要がある。例えば、バックグラウンドで動作しながらもバッテリー消費の少ない位置情報トラッキングアプリを開発する際、プロセスの動作とリソース使用を最適化することで、長時間の使用が可能になる。

4. セキュリティを考慮したプログラム設計に役立つ

プロセス間の分離を理解することで、セキュリティを考慮したプログラム設計ができるようになる。

なぜ重要か?

各プロセスは独立したメモリ空間を持つため、あるプロセスの問題が他のプロセスに影響を与えにくい。この特性は、特に個人情報を扱うアプリケーションなど、セキュリティを重要視するシステムで重要となってくる。例えば、オンラインバンキングアプリでは、重要な金融取引を別プロセスで実行することで、他の部分に問題が生じても取引の安全性を確保できる。

どのように役立つか?

プロセスの分離を活用することで、安全性の高いアプリケーションを設計できる。例えば、ウェブブラウザでは、各タブを別々のプロセスで実行することで、悪意のあるスクリプトを含むウェブサイトを開いても、他のタブやシステム全体に影響を与えない設計が可能になる。

5. 大規模なシステムの仕組みの理解に役立つ

Webサーバーなどの大規模システムでは、プロセスとフォークが頻繁に使用される。

なぜ重要か:

大規模システムでは、多数の並行処理が必要となる。プロセスとフォークを使用することで、これらの処理を効率的に管理できる。例えば、ソーシャルメディアプラットフォームでは、投稿の作成、タイムラインの更新、通知の送信など、様々な処理を別々のプロセスで実行することで、システム全体の応答性と安定性を向上させている。

どのように役立つか?

スケーラブルなシステムを設計するには、プロセスとフォークの理解を理解する必要がある。例えば、動画配信サービスでは、ユーザーの接続ごとに新しいプロセスをフォークすることで、数百万人のユーザーが同時にストリーミングを楽しめるシステムを構築できる。

6. デバッグスキルの向上に役立つ

プロセス関連の問題(デッドロックやレースコンディションなど)を理解し、デバッグする能力が向上する。

なぜ重要か:

複数のプロセスが並行して動作する環境では、タイミングに依存する複雑な問題が発生することがある。信頼性の高いソフトウェアを開発するには、これらの問題を理解し、解決する能力が必要である。例えば、複数のプロセスが同時にデータベースを更新しようとした際に起こるデータの整合性の問題を理解し、適切に対処できるようになる。

どのように役立つか?

プロセスとフォークを理解することで、複雑な並行処理の問題をデバッグするスキルが向上する。例えば、複数のユーザーが同時にチケットを予約しようとした際に発生する予約の重複問題を、プロセス間の競合状態として理解し、適切な同期機構を実装することで解決できるようになる。

7. OSの仕組みの理解に役立つ

プロセスとフォークはオペレーティングシステム(OS)における重要な概念である。

なぜ重要か:

OSはプロセスの作成、実行、終了を管理し、リソースを割り当てる。そのため、これらの概念を理解することで、OSがどのようにコンピュータを制御しているかが分かるようになる。例えば、アプリケーションの起動時にOSがどのようにプロセスを作成し、必要なリソースを割り当てているかが理解できるようになる。

どのように役立つか:

OSの仕組みを理解することで、より効率的なプログラムを作れるようになる。また、新しいOSの機能も理解しやすくなる。例えば、スマートフォンOSの新機能であるバックグラウンド処理の最適化やバッテリー節約モードが、プロセスの優先度制御やリソース割り当ての調整によって実現されていることが理解でき、それを活用したアプリケーションを開発できるようになる。

まとめ

プロセスとは

プロセスは、コンピュータで動作中のプログラムのこと。各プロセスは自分だけの作業領域(メモリ空間)と識別番号(PID)を持つ。複数のプロセスが同時に動き、それぞれが独立してコンピュータの資源を使う。これにより、複数のタスクを効率的かつ安全に実行できる。

フォークとは

フォークとは、既存のプロセス(親プロセス)から新しいプロセス(子プロセス)を作る操作のこと。フォークは、プロセスの生成方法の一つである。子プロセスは親のほぼ完全なコピーとして作られるが、独自の識別番号(PID)と作業領域(メモリ空間)を持つ。この方法により、1つのプログラムから複数の作業を同時に行うことができる。Webサーバーなどで並列処理を実現し、複数のユーザーからのリクエストを同時に処理することができる。

プロセスとフォークの概念を理解することで、以下のように役に立つ。

  • より効率的なプログラムを書けるようになる。
  • 複雑なシステムの動作原理を把握しやすくなる。
  • 並行処理や分散システムの設計に役立つ。
脚注
  1. メモリ空間: コンピュータの一時的な記憶領域で、プログラムが動作中に使用するデータやコードを保存する場所。各プログラムは自分専用のこの領域を持つ。 ↩︎

  2. OSによるスケジューリング: オペレーティングシステム(OS)が、複数のプログラムをいつ、どの順番で実行するかを管理し決定すること。これにより、コンピュータのリソースを効率的に使用できる。 ↩︎

  3. CPUの時間の使用: プログラムがコンピュータの中央処理装置(CPU)を使って計算や処理を行う時間のこと。各プログラムは、割り当てられた時間だけCPUを使用して動作する。 ↩︎

  4. ファイルディスクリプタ: オペレーティングシステムが開いているファイルや入出力リソース(ネットワーク接続、デバイスなど)を識別するための番号のこと。(=プログラムがファイルや他のリソースにアクセスする際に使う識別子。) ↩︎

  5. ファイルポインタ: ファイル内の現在の読み書き位置を示す印のようなもの。例えば、テキストファイルを読んでいる時、どこまで読んだかを記憶している。親子プロセス間で共有されるため、両方のプロセスが同じ位置からファイルの読み書きを続けられる。 ↩︎

  6. プロセスに関連付けられた様々な設定や情報のこと。
    ユーザーID:プロセスを実行しているユーザーの識別子
    グループID:プロセスが属するグループの識別子
    カレントディレクトリ:プロセスが現在作業している場所(フォルダ)
    これらの属性が共有されることで、子プロセスは親プロセスと同じ権限や環境で動作できる。 ↩︎

  7. パイプ:
    一方向のデータ流れを提供する通信チャネル。主に親子プロセス間での通信に使用される。 ↩︎

  8. 共有メモリ:
    複数のプロセスが直接アクセスできる共通のメモリ領域。高速なデータ共有が可能だが、同期に注意が必要。 ↩︎

  9. メッセージキュー:
    プロセス間でメッセージを送受信するための仕組み。メッセージは順序付けられ、優先度をつけることも可能。 ↩︎

  10. セマフォ:
    プロセス間の同期や共有リソースへのアクセス制御に使用。複数プロセスの協調動作を管理するのに役立つ。 ↩︎

  11. ソケット:
    ネットワーク通信にも使用される汎用的な通信インターフェース。同一マシン上のプロセス間通信にも使用可能。 ↩︎

  12. カーネル:
    オペレーティングシステム(OS)の中核部分。ハードウェアとソフトウェアの橋渡しをする役割を持つ。プロセス管理、メモリ管理、デバイス制御などの基本的な機能を提供する。 ↩︎

  13. 孤児プロセス:親プロセスが終了したにもかかわらず、実行を続けている子プロセスのこと。通常のプロセス管理の枠組みから外れた状態となる。 ↩︎

  14. init プロセス(PID 1):
    システム起動時に最初に作成されるプロセス。他のすべてのプロセスの先祖となる、最も基本的なプロセス。PID(プロセスID)が1であることから、PID 1とも呼ばれる。システムの初期化や、孤児プロセスの管理など、重要な役割を担う。 ↩︎

  15. シェルとは、コンピュータのオペレーティングシステム(OS)と対話するためのインターフェースプログラム。Windows の「コマンドプロンプト」や「PowerShell」、macOS や Linux の「Terminal」アプリケーション内で動作する「Bash」や「Zsh」などが代表的なシェルの例。
    シェルの基本的な役割は、以下。
    ・ユーザーの指示(コマンド)をコンピュータに伝える「通訳」のような役割を果たす。
    ・ユーザーが入力したコマンドを解釈し、OSに適切な指示を出す。 ↩︎

  16. メインサーバープロセス(親)が起動する:
    Webサーバーソフトウェアが実行され、主要な制御を行うプロセスが開始されることを指す。 ↩︎

  17. ポートをリッスンする:
    ポートとは、ネットワーク通信において、特定のプロセスと通信するために使用される論理的な接続点のこと。リッスンとは、サーバーが特定のポートで接続要求を待ち受けている状態を指す。ポートをリッスンするとは、クライアントからの接続要求を検出できる準備ができていることを意味する。 ↩︎

Discussion