UNIXシステムにおけるfork関数の仕組み

UNIXまたは類似OSにおいて、forkはプロセスを2つのほぼ同一のプロセスに分けるシステムコールである。この関数により、実行中のプログラムが2つのプロセスに分割され、それぞれが同じコード位置から実行を開始する。両方のプロセスは、fork()の次の命令から継続して動作する。

ソースコードをコピーし、独自に開発することで異なるソフトウェアを作成する行為は、バージョン管理の意味だけでなく、開発者コミュニティの分裂を意味することもある。

forkシステムコールは新しいプロセス(子プロセス)を作成し、元のプロセス(親プロセス)と同時に実行される。子プロセスはfork()以降の命令を実行し始め、親プロセスと同じPC、CPUレジスター、およびオープンされたファイルを使用する。

引数を必要とせず、整数値を返す:

負の値:子プロセスの作成に失敗した

0:新しく作成された子プロセスに返される

正の値:親プロセスまたは呼び出し元に返される。この値は新しく作成された子プロセスのプロセスIDを示す。

fork関数のヘッダーファイル:

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

関数プロトタイプ:

pid_t fork(void);

(pid_tはマクロであり、#include<sys/types.h>でintとして定義されている)

戻り値:成功時は2つの値を返し、子プロセスでは0、親プロセスでは子プロセスIDを返す。エラー時は-1を返す。

関数説明

子プロセスは親プロセスのコピーであり、データ空間、ヒープ、スタックなどのリソースのコピーを得る。これらの領域は親子間で共有されず、独立したアドレス空間を持つ。

UNIXは親プロセスのアドレス空間を子プロセスにコピーするため、子プロセスには独自のアドレス空間がある。異なるUNIXシステムでは、fork後の実行順序が子プロセスか親プロセスかはシステム依存であり、移植性を考慮する際にはこれに依存しないことが重要である。

なぜforkは2回返すのか?

親プロセスとヒープセグメントをコピーするため、2つのプロセスがfork関数内で待機し、戻り値が2回出力される。つまり、親プロセスと子プロセスそれぞれで返される値が異なる。

fork関数の特徴は、一度呼び出されるが、2回戻り値が得られる点である。この関数には3つの異なる戻り値があり、それによって現在のプロセスが子プロセスか親プロセスかを判断できる。

(1)親プロセスでは、新規に作成された子プロセスのIDが返される;

(2)子プロセスでは、0が返される;

(3)エラーの場合、負の値が返される。

親子プロセスでの戻り値が異なる理由は、リスト構造のように、親プロセスの戻り値が子プロセスのIDを指し、子プロセスには子プロセスがないため、戻り値は0になる。

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
 
int main(int argc,char *argv[]){
    pid_t pid=fork();
    if ( pid < 0 ) {
        fprintf(stderr,"エラー!");
    } else if( pid == 0 ) {
        printf("子プロセス領域");
        exit(0);
    } else {
        printf("親プロセス領域、子プロセスPIDは%d",pid);
    }
    // 子プロセス終了を待つためにwaitまたはwaitpid関数を使用する必要がある
    exit(0);
}

上の例では、親プロセスが子プロセスが終了する前に終了する可能性がある。必要な場合、waitまたはwaitpid関数を使って親プロセスが子プロセスの終了を待つようにする。

Linuxシステムにおけるfork()の戻り値にはNULLは存在しない。

エラーコード

エラー時の戻り値には以下が含まれる:

EAGAIN

プロセス数の上限に達した

ENOMEM

新しいプロセスにメモリを割当てられない

forkのもう一つの特徴は、親プロセスで開いている記述子がすべて子プロセスにコピーされることである。親プロセスと子プロセスで同じ番号のファイル記述子はカーネル内の同一のfile構造体を指し、file構造体の参照カウンタが増加する。

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

void sys_err(const char *str)
{
    perror(str);//前回の関数でエラーが発生した原因を標準デバイスに出力し、パラメータsに続くエラー原因文字列を追加する。
    // このエラー原因はグローバル変数errnoの値に基づいて決定される。
    exit(1);
}

int main(void)
{
    pid_t pid;//pid_t型でプロセス番号を定義し、実際にはint型である
    char buf[1024];
    int fd[2];//ファイル記述子
    char *p = "パイプテスト\n";

   if (pipe(fd) == -1)//パイプの作成に失敗した
       sys_err("パイプエラー");

   pid = fork();//子プロセスを作成
   if (pid < 0) {
       sys_err("フォークエラー");
   } else if (pid == 0) {
        close(fd[1]);//子プロセスの書き込み側を閉じる
        int len = read(fd[0], buf, sizeof(buf));//データを読み込む
        write(STDOUT_FILENO, buf, len);
        close(fd[0]);//読み込み側を閉じる
   } else {
       close(fd[0]);//親プロセスは読み込み側を閉じる
       write(fd[1], p, strlen(p));//書き込み側にデータを送る
       wait(NULL);//子プロセスの終了を待つ
       close(fd[1]);//書き込み側を閉じる
   }

    return 0;
}

タグ: fork Unix プロセス管理 システムコール プログラミング

6月8日 20:44 投稿