【问题标题】:Race condition with setpgid使用 setpgid 的竞争条件
【发布时间】:2015-04-28 06:11:44
【问题描述】:

在为我的操作系统类编写程序时,我发现了一个有趣的案例,其中涉及似乎是涉及setpgid 的竞争条件。

分别编译下面的每个程序。执行./test 3(或任何> 2的数字)后,ps jx将显示所有infy进程已被放置在同一个组中。 ./test 2 将显示setpgid 尝试移动最后一个进程失败的错误。取消注释“修复我”行将导致 ./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);
  }
} 

【问题讨论】:

    标签: c linux posix pid fork


    【解决方案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() 调用似乎实际上使您的父进程赢得了比赛,这很有趣!但事实就是如此。

    【讨论】:

    • 我认为情况并非如此。子进程更改自己的 pgid。父进程更改自己的 pgid。父级从不尝试更改任何子级的 pgid。此外,您可以将 fprintf() 替换为对 sleep() 的调用,它仍然会导致问题得到修复。 没有办法父母在睡了几秒钟后就赢得了比赛!
    【解决方案2】:

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

    pgid 参数的值有效但与 pid 参数指示的进程的进程 ID 不匹配,并且在同一会话中没有进程组 ID 与 pgid 参数的值匹配的进程调用过程。

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

    解决方案是父进程在第一次分叉后立即设置子进程的组 ID,方法是在 if (pgid == -1) 块中调用 setpgid(pid2, pid2)

    同样相关,来自手册页:

    为了提供更严格的安全性,setpgid() 只允许调用进程加入其会话中已在使用的进程组,或者创建一个进程组 ID 等于其进程 ID 的新进程组。

    【讨论】:

      【解决方案3】:

      山姆是对的。我做了一个测试,发现即使孩子不调用setpgid(),只要不是execvp,家长对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;
      }
      

      如果按原样放置睡眠呼叫,则父母对setpgid() 的呼叫会在孩子执行任何操作并成功之前通过。如果睡眠呼叫移动到替代位置,则execvp() 首先通过,父母呼叫失败并显示 errno 13,并打印“黄色”。

      【讨论】:

        猜你喜欢
        • 2011-12-12
        • 2019-02-10
        • 1970-01-01
        • 1970-01-01
        • 2021-10-10
        • 2015-12-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多