Debian 9에서 하위 프로세스가 종료될 때까지 posix_spawnp가 중단됩니다.

Debian 9에서 하위 프로세스가 종료될 때까지 posix_spawnp가 중단됩니다.

우리는 최근 posix_spawnp가 다음과 같은 흥미로운 사례를 발견했습니다.걸다생성된 자식 프로세스가 종료될 때까지데비안 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

데비안 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일 것임).

관련 정보