fork() と execve()
Linux システムプログラミングでよく用いられる fork() と execve() について、実装も交えて書いていこうと思います。
実際は fork() -> execve() という流れで、組み合わせて用いられることが多いですが、まずはそれぞれの簡単な説明から。
fork()
「同様のプログラムの処理を複数のプロセスに分割して処理」という目的には fork() を用いる。
fork() は次のような流れで新しいプロセスを作成する。
- 子プロセス用のメモリ領域を確保し、親プロセスのメモリをコピーする
- 親プロセスと子ppプロセスは異なるコードを実行するように分岐するが、これは fork() の戻り値がそれぞれのプロセスで異なることを利用する
execve()
「現在のプロセスを、全く異なるプログラムを実行させる別のプロセスに変化させる」という目的には execve() を用いる。
execve() は以下の流れでプロセスの上書きを行う。
- 実行ファイルを読み出し、プロセスのメモリマップに必要な情報を読み取る
- 現在のプロセスのメモリを新しいプロセスのデータで上書きする
- 新しいプロセスの最初の命令から実行を開始する
なお、実行ファイルから読み出す情報については readelf コマンドで確認が可能。
$ readelf -h /bin/sleep
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x401770
Start of program headers: 64 (bytes into file)
Start of section headers: 26176 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 8
Size of section headers: 64 (bytes)
Number of section headers: 29
Section header string table index: 28
$ readelf -S /bin/sleep
There are 29 section headers, starting at offset 0x6640:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
...
[13] .text PROGBITS 00000000004014e0 000014e0
0000000000002f3c 0000000000000000 AX 0 0 16
...
[25] .data PROGBITS 00000000006064a0 000064a0
0000000000000080 0000000000000000 WA 0 0 32
.text がコード領域で .data がデータ領域。
sleep コマンドの場合はそれぞれの情報が以下であることがわかる。
コード領域のファイル内オフセット | 0x14e0 |
---|---|
コード領域のサイズ | 0x2f3c |
コード領域のメモリマップ開始アドレス | 0x4014e0 |
データ領域のサイズ | 0x80 |
データ領域のメモリマップ開始アドレス | 0x6064a0 |
エントリポイント | 0x401770 |
プロセスのメモリマップは /proc/[pid]/maps から確認することができる。
$ /bin/sleep 10000 &
[1] 24087
$ cat /proc/24087/maps
00400000-00406000 r-xp 00000000 ca:01 262881 /bin/sleep
00606000-00607000 rw-p 00006000 ca:01 262881 /bin/sleep
...
1 行目がコード領域で 2 行目がデータ領域だが、どちらも表で示したメモリマップに収まっていることがわかる。
実際に使ってみる
先ほども述べたように、実際は fork() で生成した子プロセスが exec() の流れになることが多い。
例えば bash が echo プロセスを生成する際は、まず fork() が実行され、子プロセスが echo となり execev() される。
実際にこの流れを実装してみる。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <err.h>
static void child() {
char *args[] = { "/bin/echo", "hello", NULL };
printf("This is a child process. pid is %d.\n", getpid());
fflush(stdout);
execve("/bin/echo", args, NULL);
err(EXIT_FAILURE, "execev() failed.");
}
static void parent(pid_t pid_c) {
printf("This is a parent process. pid is %d, and a child process is %d.\n", getpid(), pid_c);
exit(EXIT_SUCCESS);
}
int main(void) {
pid_t ret;
ret = fork();
if (ret == -1)
err(EXIT_FAILURE, "fork() failed.");
if (ret == 0) {
child();
} else {
parent(ret);
}
}
実装としては、fork() の戻り値を調べ、子プロセスだった場合 (ret が 0 の場合) は child() を実行し、それ以外の場合は parent() を実行する流れとなる。
実行結果は以下。
$ ./fork-and-exec
This is a parent process. pid is 24113, and a child process is 24114.
This is a child process. pid is 24114.
$ hello
無事に子プロセスが execve() により echo へと変化し、hello を出力していることがわかる。