setpgid 的竞争条件

Race condition with setpgid

在为我的 OS class 编写程序时,我发现了一个有趣的案例,其中涉及一个似乎涉及 setpgid 的竞争条件。

分别编译下面的各个程序。在执行 ./test 3(或任何大于 2 的数字)后,ps jx 将显示所有 infy 进程已被放置在同一组中。 ./test 2 将显示 setpgid 尝试移动最后一个进程失败的错误。取消注释 "fix me" 行将导致 ./test 2 按预期工作。

任何人都可以提供解释或解决方案吗?

// test.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

char* args[] = {
  "./infy",
  NULL
};

int main(int argc, char* argv[])
{
  if (argc != 2)
  {
    fprintf(stderr, "Usage: %s [num]\n", argv[0]);
    return 1;
  }
  int num = strtol(argv[1], NULL, 10);
  if (num < 2)
  {
    fprintf(stderr, "Invalid number of processes\n");
    return 1;
  }

  pid_t pid = fork();
  if (pid > 0)
  {
    int s;
    waitpid(pid, &s, 0);
    fprintf(stderr, "Children done\n");
  }
  else
  {
    pid_t pgid = -1;
    int i;
    for (i = 1; i < num; i++)
    {
      pid_t pid2 = fork();
      if (pid2 > 0)
      {
        if (pgid == -1)
        {
          pgid = pid2;
        }
      }
      else
      {
        if (setpgid(0, pgid == -1 ? 0 : pgid) != 0)
        {
          perror("setpgid failed in non-last process");
        }
        execve(args[0], args, NULL);
        perror("exec failed");
        exit(1);
      }
    }

    // uncomment me to fix
    //fprintf(stderr, "pgid %d\n", pgid);
    if (setpgid(0, pgid) != 0)
    {
      perror("setpgid failed in last process");
    }
    execve(args[0], args, NULL);
    perror("exec failed");
    exit(1);
  }
} 

其中 "infy" 是一个单独的程序:

// infy.c
#include <unistd.h>

int main()
{
  while (1)
  {
    sleep(1);
  }
} 

您的问题的答案似乎在 setpgid(2) 的手册页中给出:

ERRORS
       EACCES An attempt was made to change the process group ID of
              one of the children of the calling process and the child
              had  already  performed an execve(2) (setpgid(),
              setpgrp()).

这是一个竞争条件。如果您的原始父进程,即在您的评论下方以 运行 setpgid() 调用结束的进程,设法在其子进程 execve() 的另一个可执行文件之前执行它,它将成功。如果子进程设法在父进程进入 setpgid() 之前执行 execve(),则父进程 setpgid() 将失败。

如果您的 fprintf() 调用不存在,最终会改变父进程的执行配置文件,并对其进行足够大的更改,从而最终影响父进程赢得或输掉比赛的可能性。

我发现额外的 fprintf() 调用似乎实际上使您的父进程 赢得比赛 非常有趣!但就是这样。

我终于明白了。当 setpgid 失败时,errno 被设置为 EPERMEPERM 的手册页上可能出现的错误之一是:

The value of the pgid argument is valid but does not match the process ID of the process indicated by the pid argument and there is no process with a process group ID that matches the value of the pgid argument in the same session as the calling process.

这种情况下的竞争条件是 child 进程是否可以在 parent 之前设置它的 pgid。如果 child 赢得比赛,一切都很好。如果 parent 赢得比赛,它试图设置的进程组尚不存在,并且 setpgid 失败。

parent 进程的解决方案是在第一个分支之后立即设置 child 的组 ID,方法是在 if (pgid == -1) 块中调用 setpgid(pid2, pid2)

也相关,来自手册页:

To provide tighter security, setpgid() only allows the calling process to join a process group already in use inside its session or create a new process group whose process group ID was equal to its process ID.

山姆是对的。我做了一个测试,发现即使child不调用setpgid(),只要不execvp,parent调用setpgid() 成功。这是演示代码。

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
using namespace std;

int main(int argc, char* argv[]) {
  pid_t pid = fork();
  if(pid == 0) {
    char* argv[3];
    argv[0] = strdup("sleep");
    argv[1] = strdup("10");
    argv[2] = NULL;

    execvp(argv[0], argv);
    cout << "execvp failed: " << strerror(errno) << endl;
    exit(0);
  }

  sleep(5);

  int result = setpgid(pid, pid);
  cout << "setpgid return value: " << result << endl;
  if(result == -1) {
    cout << "setpgid failed: " << strerror(errno) << endl;
    cout << "Errno: " << errno << endl;
    if(errno == EACCES) {
      cout << "yellow" << endl;
    }
  }
  return 0;
}

如果按原样放置睡眠调用,parent 对 setpgid() 的调用会在 child 执行任何操作并成功之前完成。如果 sleep 调用移动到替代位置,则 execvp() 首先通过并且 parents 调用失败并显示错误号 13,并打印 "yellow"。