🍴

【C言語】fork()とは 【関数解説】

に公開

みなさんこんにちは、ウタです。

現在、42Tokyoに通っておりせっせと課題をやっているのですが、その中で概念を深く理解すべきと判断した関数があったので一つずつ記事にしていこうと思います。

今回はfork()についてお話しします。


マルジェラのフォークリングかっこいいですよね。いつか欲しい、、、

まずはサンプルコード

まずは、サンプルコードを見て行きましょう。
関数の定義としては下記です。

pid_t fork(void);

引数:

  • なし(void)

返り値:

  • 成功時:*同時に二つの返り値を返します
    • 親プロセスには子プロセスのPID(正の整数)
    • 子プロセスには0
  • 失敗時:-1

この場では、pid_tintとほぼ同義と考えていきます。

シンプルなサンプルはこちら

int main() {
    pid_t pid;
    
    printf("fork()を実行する前\n");
    
    pid = fork();
    if (pid == 0)
        printf("子プロセスです!PID: %d\n", getpid()); // 子プロセス
    else if (pid > 0)
        printf("親プロセスです!子のPID: %d\n", pid); // 親プロセス
    else 
        printf("fork()に失敗しました\n"); // エラー
    
    return 0;
}

実行結果例:

fork()を実行する前
親プロセスです!子のPID: 1234
子プロセスです!PID: 1235

何をやっている関数なのか

fork()親プロセスから子プロセスを分岐させて、並行に複数の処理を行うことができる関数です。
コピーはfork()が実行されたタイミングから発生します。つまり、必ずしも親プロセスを「最初から」コピーするわけではないということは頭に入れておきましょう。

こちらのサンプルがわかりやすいかと思います。

int main()
{
    printf("Line 1\n");           // プロセス分岐前
    printf("Line 2\n");           // プロセス分岐前
    
    if (fork() == 0)              // ここで分岐!
        printf("Child: Line 3\n");    // 子プロセスのみ実行
    else
        printf("Parent: Line 4\n");   // 親プロセス(=オリジナル)のみ実行
    
    printf("Line 5\n");           // 親子両方で実行
}

実行結果

Line 1 // プロセス分岐前
Line 2 // プロセス分岐前
Child: Line 3 // 子プロセス
Parent: Line 4 // 親プロセス
Line 5 // 子プロセス
Line 5 // 親プロセス

このサンプルコードでは親(=オリジナル)プロセスは、Line 1Line 2Line 4Line 5を出力しています。対して子プロセスは、Line 3Line 5のみを出力しています。

どこで使うの?

実際の使用用途についてを解説していきます。
例として、立て続けにwc -lcatコマンドを実行してみます。
すると下記のようなコードになります。

int main(int ac, char **av, char **envp) {
    // 1つ目のコマンド用に子プロセス作成
    if (fork() == 0) {
        char *wc_args[] = {"wc", "-l", "infile", NULL};
        execve("/usr/bin/wc", wc_args, envp);
    }
    
    // 2つ目のコマンド用に子プロセス作成  
    if (fork() == 0) {
        char *cat_args[] = {"cat", "infile", NULL};
        execve("/bin/cat", cat_args, envp);
    }
    
    wait(NULL); // 1つ目の子プロセス(wc)の終了を待つ
    wait(NULL); // 2つ目の子プロセス(cat)の終了を待つ
    return 0;
}
infile
Hello
World
Test

出力結果

$ ./program       // 実行
3 infile          // 1つ目の子プロセス wc -l の結果(行数とファイル名)
Hello             // 2つ目の子プロセスcat infile の結果(infileの中身)
World
Test

この中でexecve()を使用するのですが、execve()は現在のプロセスを完全に置き換えて終了してしまいます。そのため、2つのexecve()を続けて実行することは不可能です。fork()で子プロセスを作ることで、親プロセスを残しながら複数のコマンドを実行できるのです。

int main() {
    execve("/usr/bin/wc", wc_args, envp);  // ここでプロセス終了
    execve("/bin/cat", cat_args, envp);    // 到達不可能!
    return 0;  // 到達不可能!
}

上記では、一つ目のexecve()が実行された後、オリジナルプロセスが終了してしまうため二つ目のexecve()まで到達することが出来ません。

注意点

1. 親子プロセスの実行順序は保証されない

if (fork() == 0) {
   printf("子プロセス\n");
} else {
   printf("親プロセス\n");
}

前述しましたが、出力順序は実行環境によって変わる可能性があります。
確実な順序制御が必要な場合はwait()を適切に使用しましょう。

2. fork()の失敗チェックを忘れずに

pid_t pid = fork();
if (pid == -1) {
    perror("fork failed");
    exit(1);
}

メモリ不足やプロセス数制限により、fork()が失敗する場合があります。

3. 子プロセスの回収(ゾンビプロセス対策)

// 親プロセスで必ず実行
wait(NULL); 

wait()を呼ばないと、子プロセスがゾンビプロセスとして残り続ける可能性があります。

4. 変数の独立性を理解する

int counter = 0;
if (fork() == 0) {
    counter = 10;  // 子プロセスでの変更
} else {
    counter = 20;  // 親プロセスでの変更
}
// 親子それぞれで独立した値を持つ

fork()後は親子で独立したメモリ空間を持つため、変数の変更は互いに影響しません。

まとめ

ここまで読んでいただきありがとうございます🤩

この記事では、fork()について以下のポイントを説明しました:

  • fork()親プロセスから子プロセスを分岐させる関数
  • 返り値で親子を区別:親には子のPID、子には0が返る
  • fork()実行時点の状態がコピーされる(プログラム開始からではない)
  • execve()との組み合わせで複数コマンドの並行実行が可能
  • 適切なエラーハンドリングと子プロセス回収が重要

fork()は一見複雑に見えますが、「1つのプロセスが2つに分岐する」というシンプルな概念です。
ただ、今までやってきた課題では全く出てこなかった概念だけに理解するのに少し時間がかかりました😅
次回は、fork()と組み合わせて使われることの多いpipe()について詳しく解説していきます!

https://zenn.dev/uta_san1012/articles/ff069e9756340f

Discussion