fork()、vfork()、exec()、clone()の違い
1. 概要
システムコールは、オペレーティングシステムによって利用可能になるサービスへのインターフェイスを提供します。 システムコールfork()、 vfork()、 exec()、および clone()はすべて、作成と操作に使用されますプロセス。
このチュートリアルでは、これらの各システムコールとそれらの違いについて説明します。
2. fork()
プロセスはfork()システムコールを実行して、新しい子プロセスを作成します。
子プロセスには、親プロセスと同じデータがあります。 ただし、両方のプロセスには別々のアドレス空間があります。
子プロセスの作成後、親プロセスと子プロセスの両方が同時に実行されます。 fork()システムコールの後に次のステップを実行します。
親プロセスと子プロセスのアドレス空間が異なるため、一方のプロセスに加えられた変更は、もう一方のプロセスには反映されません。
後の改善により、親プロセスと子プロセスが同じアドレス空間を共有できるようにするコピーオンライトメカニズムが導入されました。これにより、データを子プロセスにコピーする必要がなくなりました。 いずれかのプロセスが共有アドレス空間のページを変更した場合、システムは新しいアドレス空間を割り当てて、両方のプロセスを独立して実行できるようにします。
2.1. fork()を実行しています
fork()システムコールがどのように機能するかを示す簡単なCプログラムを作成しましょう。
まず、 Nano エディターを使用して、fork_test.cという名前のファイルを作成します。
$ nano fork_test.c
次に、このコンテンツを追加します。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
int main(int argc, char **argv) {
pid_t pid = fork();
if (pid==0) {
printf("This is the Child process and pid is: %d\n",getpid());
exit(0);
} else if (pid > 0) {
printf("This is the Parent process and pid is: %d\n",getpid());
} else {
printf("Error while forking\n");
exit(EXIT_FAILURE);
}
return 0;
}
ここでは、新しいプロセスを開始し、変数 pid を使用して、 fork()呼び出しによって作成された子プロセスのプロセス識別子を格納しています。 次に、 fork()呼び出しによって返されるpidの値がゼロに等しいかどうかの確認に進みます。
変更を保存した後、ccコマンドを使用してfork_test.cをコンパイルします。
$ cc fork_test.c
これにより、作業ディレクトリにa.outという実行可能ファイルが作成されます。
最後に、a.outファイルを実行できます。
$ ./a.out
This is the Parent process and pid is: 69032
This is the Child process and pid is: 69033
ここで、親プロセスと子プロセスのプロセス識別子が異なることがわかります。
上記の出力では、 fork()呼び出しは、出力を2回返します。1回は親プロセスで、もう1回は子プロセスです。
getpid()関数呼び出しを使用して、if…elseブロック内の親プロセスと子プロセスの実際のPIDを取得しています。
3. vfork()
vfork()は、 clone()システムコールの特殊なケースとして機能します。 親プロセスのアドレス空間をコピーせずに、新しいプロセスを作成します。 これは、パフォーマンス指向のアプリケーションで役立ちます。
子プロセスが作成されると、親プロセスは常に中断されます。 子プロセスが正常に終了するか、異常に終了するか、 exec システムコールを実行して新しいプロセスを開始するまで、中断されたままになります。
vfork()システムコールによって作成された子プロセスは、その親の属性を継承します。 これらには、ファイル記述子、現在の作業ディレクトリ、信号処理などが含まれます。
3.1. vfork()を実行しています
vfork()システムコールがどのように機能するかを示す簡単なCプログラムを作成してみましょう。
まず、vfork_test.cという名前のファイルを作成します。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
pid_t pid = vfork(); //creating the child process
printf("parent process pid before if...else block: %d\n", getpid());
if (pid == 0) { //checking if this is the a child process
printf("This is the child process and pid is: %d\n\n", getpid());
exit(0);
} else if (pid > 0) { //parent process execution
printf("This is the parent process and pid is: %d\n", getpid());
} else {
printf("Error while forking\n");
exit(EXIT_FAILURE);
}
return 0;
}
ここでは、変数 pid を使用して、 vfork()呼び出しによって作成された子プロセスのPIDを格納しています。 次に、if…elseブロックの前にある親のPIDの値を確認します。
変更を保存したら、vfork_test.cをコンパイルしましょう。
$ cc vfork_test.c
最後に、作成したa.outファイルを実行できます。
$ ./a.out
parent process pid before if...else block: 117117
This is the child process and pid is: 117117
parent process pid before if...else block: 117116
This is the parent process and pid is: 117116
vfork()システムコールは、最初に子プロセスで、次に親プロセスで、出力を2回返します。
両方のプロセスが同じアドレス空間を共有しているため、最初の出力で一致するPID値があります。 if else ブロックでは、子プロセスが最初に実行されます。実行中。
4. exec()
exec()システム関数は、既存のプロセスのコンテキストで新しいプロセスを実行し、それを置き換えます。これはオーバーレイとも呼ばれます。
このシステム関数は、 execl()、 execlp()、 execv()、 execvp()、 execle()、および execve()。
4.1. exec()を実行しています
簡単なテストとして、 exec()システムコールがどのように機能するかを示す2つのCプログラムを作成します。 exec()呼び出しを使用して、最初のプログラムから2番目のプログラムを実行します。
exec_test1.cという名前の最初のプログラムを作成しましょう。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
printf("PID of exec_test1.c = %d\n", getpid());
char *args[] = {"Hello", "From", "Parent", NULL};
execv("./exec_test2", args);
printf("Back to exec_test1.c");
return 0;
}
ここでは、 main()という関数を作成し、引数を渡します。 関数内では、 getpid()関数を使用して取得した後にPIDを出力しています。 次に、3つの文字列を引数として渡す文字配列を宣言します。 execv()システムコールを呼び出してから、2番目のプログラムの実行結果を引数として渡します。
次に、 exec_test2.c:という2番目のプログラムを作成しましょう。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
printf("Hello from exec_test2.c\n");
printf("PID of exec_test2.c process is: %d\n", getpid());
return 0;
}
ここでは、最初のプログラムから exec()によって起動されたプロセスのメッセージとPIDを出力しています。
cc コマンドを使用して、exec_test1.cを実行可能ファイルにコンパイルします。
$ cc exec_test1.c -o exec_test1
これにより、作業ディレクトリにexec_test1という実行可能ファイルが作成されます。
次に、2番目のプログラムをコンパイルします。
$ cc exec_test2.c -o exec_test2
これにより、作業ディレクトリにexec_test1という実行可能ファイルが作成されます。
最後に、exec_test1ファイルを実行できます。
$ ./exec_test1
PID of exec_test1.c = 171939
Hello from exec_test2.c
PID of exec_test2.c process is: 171939
上記の出力から、2番目のプログラムのプロセスでPIDが変更されていないことがわかります。 さらに、exec_test1.cファイルからの最後の印刷ステートメントは印刷されませんでした。 これは、 execv()システムコールを実行すると、現在実行中のプロセスが置き換えられ、最初のプロセスに戻る方法が含まれていないためです。
5. clone()
clone()システムコールを使用すると、子プロセスを異なる名前空間に配置できます。 clone()システムコールの使用に伴う柔軟性により、アドレス空間の共有を選択できます。親プロセスを使用して、 vfork()システムコールをエミュレートします。 また、使用可能なさまざまなフラグを使用して、ファイルシステム情報、開いているファイル、およびシグナルハンドラーを共有することもできます。
これは、 clone()システムコールの署名です。
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
/* pid_t *parent_tid, void *tls, pid_t *child_tid */ );
詳細を理解するために、いくつかの部分を分解してみましょう。
- * fn :関数を指すポインター
- * stack :スタックの最小バイトを指します
- pid_t :プロセス識別子(PID)
- * parent_tid :親プロセスメモリ内の子プロセススレッド識別子( TID )の保存場所を指します
- * child_tid :子プロセスメモリ内の子プロセススレッド識別子(TID)の保存場所を指します
5.1. clone()の実行
clone()システムコールがどのように機能するかを確認するために、簡単なCプログラムを作成してみましょう。
まず、clone_test.cという名前のファイルを作成します。
// We have to define the _GNU_SOURCE to get access to clone(2) and the CLONE_*
#define _GNU_SOURCE
#include <sched.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int child_func(void* arg) {
char* buffer = (char*)arg;
printf("Child sees buffer = \"%s\"\n", buffer);
strcpy(buffer, "hello from child");
return 0;
}
int main(int argc, char** argv) {
// Allocate stack for child task.
const int STACK_SIZE = 65536;
char* stack = malloc(STACK_SIZE);
if (!stack) {
perror("malloc");
exit(1);
}
// When called with the command-line argument "vm", set the CLONE_VM flag on.
unsigned long flags = 0;
if (argc > 1 && !strcmp(argv[1], "vm")) {
flags |= CLONE_VM;
}
char buffer[100];
strcpy(buffer, "hello from parent");
if (clone(child_func, stack + STACK_SIZE, flags | SIGCHLD, buffer) == -1) {
perror("clone");
exit(1);
}
int status;
if (wait(&status) == -1) {
perror("wait");
exit(1);
}
printf("Child exited with status %d. buffer = \"%s\"\n", status, buffer);
return 0;
}
ここでは、 clone()を2つの方法で使用しています。1つは CLONE_VM フラグあり、もう1つはフラグなしです。 子プロセスにバッファを渡し、子プロセスがそれに文字列を書き込みます。 次に、子プロセスにスタックサイズを割り当て、 CLONE_VM(vm)オプションを使用してファイルを実行しているかどうかを確認する関数を作成します。 さらに、親プロセスで100バイトのバッファーを作成し、それに文字列をコピーしてから、 clone()システムコールを実行してエラーをチェックしています。
cc コマンドを使用して、exec_test.cを実行可能ファイルにコンパイルします。
$ cc clone_test.c
これにより、作業ディレクトリにa.outという実行可能ファイルが作成されます。
最後に、a.outファイルを実行できます。
./a.out
Child sees buffer = "hello from parent"
Child exited with status 0. buffer = "hello from parent"
vm 引数なしで実行すると、 CLONE_VM フラグがアクティブにならず、親プロセスの仮想メモリが子プロセスにクローンされます。 子プロセスは、 buffer 内の親プロセスによって渡されたメッセージにアクセスできますが、子によって buffer に書き込まれたものは、親プロセスにアクセスできません。
ただし、 vm 引数を渡すと、 CLONE_VM がアクティブになり、子プロセスが親のプロセスメモリを共有します。 バッファに書き込んでいることがわかります。
$ ./a.out vm
Child sees buf = "hello from parent"
Child exited with status 0. buf = "hello from child"
今回はメッセージが異なり、子プロセスから渡されたメッセージを確認できます。
6. 比較
これらの各システムコールの概要と比較を示すこの表を見てみましょう。
比較係数 | フォーク() | vfork() | exec() | クローン() |
---|---|---|---|---|
呼び出す | fork()、は呼び出しプロセスの子プロセスを作成します | vfork()は、親といくつかの属性を共有する子プロセスを作成します | exec()、呼び出しプロセスを置き換えます | clone()は、子プロセスを作成し、共有されるデータをより詳細に制御します |
プロセスID | 親プロセスと子プロセスには一意のIDがあります | 親プロセスと子プロセスは同じIDを持っています | 実行中のプロセスとそれを置き換えるプロセスは、同じPIDを持っています | 親プロセスと子プロセスには一意のIDがありますが、指定すると共有できます |
実行 | 親と子のプロセスが同時に開始されます | 子プロセスの実行中、親プロセスは一時的に中断されます | 親プロセスは終了し、新しいプロセスはエントリポイントで開始されます | 親と子のプロセスが同時に開始されます |
7. 結論
この記事では、 fork()、 vfork()、 exec()、および clone( )システムコール。
fork()、 vfork()、および clone()は同様に機能しますが、データの処理方法にわずかな違いがあります。 また、各システムコールがどのように動作するかを示すいくつかの簡単なCプログラムを作成して実行しました。
vfork()システムコールは廃止されたと見なされ、特にコピーオンライト機能があるため、 fork()システムコールを使用することをお勧めします。