FreeBSDの posix_spawnの実装

http://blog.kazuhooku.com/2015/05/how-to-properly-spawn-external-command.html


を見て, 再現コードFreeBSDで実行してみたとき, 期待される挙動になったので FreeBSDでの実装を確認してみました.

コード

FreeBSD r282608のソースコードを対象にしています.


posix_spawnは lib/libc/gen/posix_spawn.cで定義される.

int
posix_spawn(pid_t *pid, const char *path,
    const posix_spawn_file_actions_t *fa,
    const posix_spawnattr_t *sa,
    char * const argv[], char * const envp[])
{
        return do_posix_spawn(pid, path, fa, sa, argv, envp, 0);
}

do_posix_spawn関数を呼び出すだけ.

static int
do_posix_spawn(pid_t *pid, const char *path,
    const posix_spawn_file_actions_t *fa,
    const posix_spawnattr_t *sa,
    char * const argv[], char * const envp[], int use_env_path)
{
        pid_t p;
        volatile int error = 0;

        p = vfork();
        switch (p) {
        case -1:
                return (errno);
        case 0:
                if (sa != NULL) {
                        error = process_spawnattr(*sa);
                        if (error)
                                _exit(127);
                }
                if (fa != NULL) {
                        error = process_file_actions(*fa);
                        if (error)
                                _exit(127);
                }
                if (use_env_path)
                        _execvpe(path, argv, envp != NULL ? envp : environ);
                else
                        _execve(path, argv, envp != NULL ? envp : environ);
                error = errno;
                _exit(127);
        default:
                if (error != 0)
                        _waitpid(p, NULL, WNOHANG);
                else if (pid != NULL)
                        *pid = p;
                return (error);
        }
}

初めに vforkを呼び出しています. vforkは forkと違いメモリ空間がコピーされない,かつ親プロセスは子プロセスが execするか exitするまで待つという性質があります.このため子プロセスが先に動作し, _execveが呼ばれます(posix_spawnpの場合は _execvpe). このとき実在しないコマンドが指定されている等で execが失敗したとき, error変数にerrnoを書き込み _exitします.


execが成功 or execが失敗+exitの後, 親プロセスが動き出します. switchの defaultの部分が実行され, はじめに errorがチェックされます. 前述の通り vforkではメモリ空間がコピーされないので, 親プロセスから子プロセスでの変更が確認できます. (vforkでなく, forkを使う場合, h2oの実装のように pipe等を使う必要があります)
で, errorが発生していれば waitpidをして, errorを返します. このような実装になっているので, 子プロセスで execが失敗した場合エラーを返すことができるということになります.
(字面だけ見ると, default節の ifが真になることはないので, コンパイラに削除される可能性がある. そうならないように volatileがつけられている. コンパイラはfork/vforkの挙動など知らない)

glibcでの実装

sysdeps/posix/spawni.cの __spawni関数です. 親プロセスが戻るところだけ示します.

  /* Generate the new process.  */
  if ((flags & POSIX_SPAWN_USEVFORK) != 0
      /* If no major work is done, allow using vfork.  Note that we
         might perform the path searching.  But this would be done by
         a call to execvp(), too, and such a call must be OK according
         to POSIX.  */
      || ((flags & (POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF
                    | POSIX_SPAWN_SETSCHEDPARAM | POSIX_SPAWN_SETSCHEDULER
                    | POSIX_SPAWN_SETPGROUP | POSIX_SPAWN_RESETIDS)) == 0
          && file_actions == NULL))
    new_pid = __vfork ();
  else
    new_pid = __fork ();

  if (new_pid != 0)
    {
      if (new_pid < 0)
        return errno;

      /* The call was successful.  Store the PID if necessary.  */
      if (pid != NULL)
        *pid = new_pid;

      return 0;
    }

vfork/forkが失敗した場合は errnoを返しますが, それ以外は 0を返しています.
なので子プロセスが execに失敗した場合でも 0(成功)が返っていたわけです.

vfork

vforkを使っている理由はわかりませんが, マルチスレッド下で呼ばれる可能性がある場合を考慮しているためと思われます(すぐに execすることが確定しているのでパフォーマンスを考えてのことかもしれませんが...)


マルチスレッド + forkの問題についてはこちらを参照してください


追記

  • POSIXでは posix_spawnは MMUなしの環境でも動作するように実装しないといけないとなっていました. そのための vforkかも.