Debian 9では、子プロセスが終了するまでposix_spawnpがハングします。

Debian 9では、子プロセスが終了するまでposix_spawnpがハングします。

私たちは最近、posix_spawnpが次のような興味深いケースを見つけました。歩く生成された子プロセスが終了するまでDebian 9。 Ubuntu(18.04)やCentOS(7.3)などの他のディストリビューションでは再現できません。最後のコードスニペットを使用して再現できます。./test_posix_spawnp sleep 30実行可能ファイルの名前をtest_posix_spawnpとし、コンパイルして実行します。sleep 30しばらくの間サブプロセスを実行するためにこれを渡します。PID of child: xxxインジケータがすぐに印刷されないことがわかります。

次のサンプルコードは実際のコードをシミュレートします。重要なのは、stdin/stdout/stderr とロギング用に開かれたファイル記述子を除く、子プロセスのすべてのファイル記述子を閉じ、stdout/stderr をログファイルにリダイレクトすることです。実際のケースとこのシミュレーションケースの両方で子プロセスが作成されたように見え、渡される実行可能ファイルの実行が開始されました。

私たちの質問:

以前この問題を経験した人はいますか?これはlibc(2.24)のバグのように聞こえますか?そうでない場合は、コードをどのように修正しますか?それでは、どうすればいいですか?

PS これが重要かどうかはわかりませんが、再現可能であるか、Debianでposix_spawnpの実行中に追加のパイプが作成され、親が読み取り側を持ち、子が書き込み側を持つことが観察されました。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <spawn.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <wait.h>
#include <errno.h>

#define errExit(msg)    do { perror(msg); \
                            exit(EXIT_FAILURE); } while (0)

#define errExitEN(en, msg) \
                       do { errno = en; perror(msg); \
                            exit(EXIT_FAILURE); } while (0)

char **environ;

int
main(int argc, char *argv[])
{
  pid_t child_pid;
  int s, status;
  sigset_t mask;
  posix_spawnattr_t attr;
  posix_spawnattr_t *attrp;
  posix_spawn_file_actions_t file_actions;
  posix_spawn_file_actions_t *file_actionsp;

  attrp = NULL;
  file_actionsp = NULL;
  long open_max = sysconf(_SC_OPEN_MAX);
  printf("sysconf says: max open file descriptor %ld\n", open_max);
  if (open_max > 32768) {
    open_max = 32768;
    printf("bump max open file desriptor to %ld\n", open_max);
  }
  int flags = O_WRONLY | O_CREAT | O_APPEND;
  mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IRGRP;
  int log_fd = open("test_posix_spawnp.log", flags, mode);

  printf("opened output file \"test_posix_spawnp.log\", fd=%d\n", log_fd);

  /* Close all fds except log_fd to which stdout and stderr are redirected */

  s = posix_spawn_file_actions_init(&file_actions);
  if (s != 0)
    errExitEN(s, "posix_spawn_file_actions_init");

  s = posix_spawn_file_actions_adddup2(&file_actions, log_fd, STDOUT_FILENO);
  if (s != 0)
    errExitEN(s, "posix_spawn_file_actions_adddup2");

  s = posix_spawn_file_actions_adddup2(&file_actions, log_fd, STDERR_FILENO);
  if (s != 0)
    errExitEN(s, "posix_spawn_file_actions_adddup2");

  for (int i = 3; i < open_max; ++i) {
    if (i == log_fd) continue;
    s = posix_spawn_file_actions_addclose(&file_actions, i);
    if (s != 0)
      errExitEN(s, "posix_spawn_file_actions_addclose");
  }

  file_actionsp = &file_actions;

  s = posix_spawnp(&child_pid, argv[optind], file_actionsp, attrp,
                   &argv[optind], environ);
  if (s != 0)
    errExitEN(s, "posix_spawn");

  printf("PID of child: %ld\n", (long) child_pid);

  /* Clean up after ourselves */

  if (file_actionsp != NULL) {
    s = posix_spawn_file_actions_destroy(file_actionsp);
    if (s != 0)
      errExitEN(s, "posix_spawn_file_actions_destroy");
  }

  exit(EXIT_SUCCESS);
}

ベストアンサー1

Debian 9に含まれているglibc 2.24を見ました。

posix_spawnp(およびposix_spawn)は、システムコールではなくユーザーモードCコードで実装されています。次の操作を行います。

  1. 旗でパイプを作りますO_CLOEXEC
  2. Cloneはフラグで呼び出されますCLONE_VFORK。 vfork は子プロセスと親プロセスの間の通信を制限します。ここでパイプが動作します。
  3. 親はパイプの書き込みの終わりを閉じ、読み取りの終わりから読み取ろうとします。
  4. 子プロセスはパイプの読み出し端を閉じ、すべてのファイル操作を実行します。
  5. 子はexecvpを呼び出します。成功すればパイプ閉じなければならない。失敗すると、子プロセスはパイプにエラーコードを書き込みます。
  6. 親の読み取りが返されます。子プロセスのexecvpが成功した場合は、以下をお読みください。失敗しなければならないパイプの書き込み終了のためドアを閉めたはずだった、親はこの変数をec0に設定します。読み取りに成功した場合、ec子が送信したエラーコードです。
  7. 親のposix_spawnpはを返しますec

エラーがあるため、この単語をイタリック体でマークしました。

posix_spawnpがこれをすべて実行している間、posix_spawn_file_actions_addcloseglibcコードはそのファイル記述子に影響を与えるファイル操作を見るたびにパイプの書き込み側で繰り返し操作を実行するのに十分スマートです。

int p = args->pipe[1];
...
/* Dup the pipe fd onto an unoccupied one to avoid any file
   operation to clobber it.  */
if ((action->action.close_action.fd == p)
    || (action->action.open_action.fd == p)
    || (action->action.dup2_action.fd == p))
  {
    if ((ret = __dup (p)) < 0)
      goto fail;
    p = ret;
  }

問題は、繰り返すフラグO_CLOEXECはコピーされないため、fdは子のすでに実行されているプロセスにリークされ、プロセスが終了するまで閉じられません。それまでは親項目の読み取りは返されません。

バグが修正されました。今回提出してください。子孫はパイプの代わりに共有変数を使用して、成功または失敗を親項目に渡します。

このバージョンのglibcの使用に固執する場合、posix_spawnpにパイプの書き込み側を閉じるように指示する以外にできることはあまりありません(例コードではlogfd + 2になります)。

おすすめ記事