fork() と execve()

May 04, 2018

fork() と execve()

Linux システムプログラミングでよく用いられる fork() と execve() について、実装も交えて書いていこうと思います。
実際は fork() -> execve() という流れで、組み合わせて用いられることが多いですが、まずはそれぞれの簡単な説明から。

fork()

「同様のプログラムの処理を複数のプロセスに分割して処理」という目的には fork() を用いる。
fork() は次のような流れで新しいプロセスを作成する。

  1. 子プロセス用のメモリ領域を確保し、親プロセスのメモリをコピーする
  2. 親プロセスと子ppプロセスは異なるコードを実行するように分岐するが、これは fork() の戻り値がそれぞれのプロセスで異なることを利用する

execve()

「現在のプロセスを、全く異なるプログラムを実行させる別のプロセスに変化させる」という目的には execve() を用いる。
execve() は以下の流れでプロセスの上書きを行う。

  1. 実行ファイルを読み出し、プロセスのメモリマップに必要な情報を読み取る
  2. 現在のプロセスのメモリを新しいプロセスのデータで上書きする
  3. 新しいプロセスの最初の命令から実行を開始する

なお、実行ファイルから読み出す情報については 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() される。
実際にこの流れを実装してみる。

fork-and-exec.c
#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 を出力していることがわかる。


 © 2023, Dealing with Ambiguity