C++ で fork を使ってプロセスを作成する

胡金庫 2023年10月12日
  1. fork() を用いて C++ でプログラム内に 2つのプロセスを作成する
  2. fork()execve を用いて C++ で複数のプロセスを作成する
  3. fork()execve を用いて C++ で子プロセスの自動クリーンアップ関数を用いて複数のプロセスを作成する
C++ で fork を使ってプロセスを作成する

この記事では、C++ で fork() システムコールを使ってプロセスを作成する方法をいくつか説明します。

fork() を用いて C++ でプログラム内に 2つのプロセスを作成する

fork 関数はほとんどの Unix ベースのオペレーティングシステムで利用できる POSIX 準拠のシステムコールです。この関数は元の呼び出しプログラムの複製である新しいプロセスを生成します。後者のプロセスは parent と呼ばれ、新たに作成されたプロセスは child と呼ばれる。これら 2つのプロセスは、別々のメモリ空間で実行されている 2つのスレッドと見ることができます。現在の Linux の実装では内部的にスレッドの概念がないため、スレッドはメモリ領域を共有している点を除けばプロセスと似たような構造になっていることに注意してください。関数 fork は、同じプログラム内での同時実行や、ファイルシステムから新しい実行ファイルを実行することができます(後の例で説明します)。

以下の例では、一つのプログラム内でのマルチプロセッシングを示すために fork を利用します。fork は引数を取らず、両方のプロセスで戻り値を返します。戻り値は親プロセスの子プロセスの PID であり、子プロセスでは 0 が返されています。呼び出しに失敗した場合、親プロセスでは -1 が返されています。したがって、戻り値の評価に基づいて if 文を構築することができ、それぞれの if ブロックは対応するプロセスで実行され、結果として同時実行されることになります。

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

#include <iostream>

using std::cout;
using std::endl;

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

  if (c_pid == -1) {
    perror("fork");
    exit(EXIT_FAILURE);
  } else if (c_pid > 0) {
    cout << "printed from parent process " << getpid() << endl;
    wait(nullptr);
  } else {
    cout << "printed from child process " << getpid() << endl;
    exit(EXIT_SUCCESS);
  }

  return EXIT_SUCCESS;
}

出力:

printed from parent process 27295
printed from child process 27297

fork()execve を用いて C++ で複数のプロセスを作成する

fork 関数呼び出しのより実用的な使い方は、複数のプロセスを作成し、それらのプロセス内で異なるプログラムを実行することです。この例では、親プロセス用と子プロセス用の 2つのソースコードファイルが必要であることに注意してください。子プロセスのコードは、単一の整数に加算する単純な無限ループであり、SIGTERM シグナルを送ることで停止することができます。

親プロセスはフォークされた子プロセスが実行する必要のあるファイル名を宣言してから spawnChild 関数を 6 回呼び出す。関数 spawnChildfork/execve の呼び出しをラップし、新たに作成されたプロセス ID を返します。execve は子プロセス内で新しいプログラムコードを起動するための引数としてプログラム名と引数リストを必要とすることに注意してください。6つの子プロセスが作成されると、親プロセスは while ループを続けて wait 関数を呼び出します。wait は親プロセスを停止し、子プロセスのいずれかが終了するまで待ちます。

親プロセスを正常に終了させるには、各子プロセスを終了させる必要があることに注意してください。親プロセスを中断すると、子プロセスは実行を続け、親プロセスはシステムプロセスになります。

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

#include <atomic>
#include <filesystem>
#include <iostream>
#include <vector>

using std::cout;
using std::endl;
using std::string;
using std::vector;
using std::filesystem::exists;

constexpr int FORK_NUM = 6;

pid_t spawnChild(const char* program, char** arg_list) {
  pid_t ch_pid = fork();
  if (ch_pid == -1) {
    perror("fork");
    exit(EXIT_FAILURE);
  } else if (ch_pid > 0) {
    cout << "spawn child with pid - " << ch_pid << endl;
    return ch_pid;
  } else {
    execve(program, arg_list, nullptr);
    perror("execve");
    exit(EXIT_FAILURE);
  }
}

int main() {
  string program_name("child");
  char* arg_list[] = {program_name.data(), nullptr};
  vector<int> children;
  children.reserve(FORK_NUM);

  if (!exists(program_name)) {
    cout << "Program file 'child' does not exist in current directory!\n";
    exit(EXIT_FAILURE);
  }

  for (int i = 0; i < FORK_NUM; ++i)
    children[i] = spawnChild(program_name.c_str(), arg_list);
  cout << endl;

  pid_t child_pid;
  while ((child_pid = wait(nullptr)) > 0)
    cout << "child " << child_pid << " terminated" << endl;

  return EXIT_SUCCESS;
}

子プロセスのソースコード(別ファイル)。

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

#include <iostream>

volatile sig_atomic_t shutdown_flag = 1;

void GracefulExit(int signal_number) { shutdown_flag = 0; }

int main() {
  // Register SIGTERM handler
  signal(SIGTERM, GracefulExit);

  unsigned int tmp = 0;
  while (shutdown_flag) {
    tmp += 1;
    usleep(100);
  }

  exit(EXIT_SUCCESS);
}

fork()execve を用いて C++ で子プロセスの自動クリーンアップ関数を用いて複数のプロセスを作成する

前の例のコードでは、すべての子プロセスが終了する前に親プロセスが終了してしまった場合に不器用な動作をしていました。この場合、シグナルハンドラ関数を親プロセスに追加して、SIGQUIT シグナルを受信すると自動的にすべての子プロセスを終了させます。シグナルを送信するには kill -SIGQUIT pid_num_of_parent コマンドを用います。

シグナルハンドラでアクセスする必要があるグローバル変数の一部は std::atomic 型として宣言されていることに注意してください。

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

#include <atomic>
#include <filesystem>
#include <iostream>

using std::cout;
using std::endl;
using std::string;
using std::filesystem::exists;

constexpr std::atomic<int> FORK_NUM = 6;
constexpr std::atomic<int> handler_exit_code = 103;

std::atomic<int> child_pid;
std::atomic<int> *children;

void sigquitHandler(int signal_number) {
  for (int i = 0; i < FORK_NUM; ++i) {
    kill(children[i], SIGTERM);
  }
  while ((child_pid = wait(nullptr)) > 0)
    ;
  _exit(handler_exit_code);
}

pid_t spawnChild(const char *program, char **arg_list) {
  pid_t ch_pid = fork();
  if (ch_pid == -1) {
    perror("fork");
    exit(EXIT_FAILURE);
  } else if (ch_pid > 0) {
    cout << "spawn child with pid - " << ch_pid << endl;
    return ch_pid;
  } else {
    execve(program, arg_list, nullptr);
    perror("execve");
    exit(EXIT_FAILURE);
  }
}

int main() {
  string program_name("child");
  char *arg_list[] = {program_name.data(), nullptr};

  if (!exists(program_name)) {
    cout << "Program file 'child' does not exist in current directory!\n";
    exit(EXIT_FAILURE);
  }

  children = reinterpret_cast<std::atomic<int> *>(new int[FORK_NUM]);
  signal(SIGQUIT, sigquitHandler);

  for (int i = 0; i < FORK_NUM; ++i) {
    children[i] = spawnChild(program_name.c_str(), arg_list);
  }
  cout << endl;

  while ((child_pid = wait(nullptr)) > 0)
    cout << "child " << child_pid << " terminated" << endl;

  return EXIT_SUCCESS;
}
著者: 胡金庫
胡金庫 avatar 胡金庫 avatar

DelftStack.comの創設者です。Jinku はロボティクスと自動車産業で8年以上働いています。自動テスト、リモートサーバーからのデータ収集、耐久テストからのレポート作成が必要となったとき、彼はコーディングスキルを磨きました。彼は電気/電子工学のバックグラウンドを持っていますが、組み込みエレクトロニクス、組み込みプログラミング、フロントエンド/バックエンドプログラミングへの関心を広げています。

LinkedIn Facebook