如何以有限的叉数运行类似 shell 的管道任务?

Posted

技术标签:

【中文标题】如何以有限的叉数运行类似 shell 的管道任务?【英文标题】:How to run shell-liked pipe tasks with limited fork number? 【发布时间】:2019-11-04 01:35:33 【问题描述】:

我有一个简单的程序,想模拟我没有足够的fork容量的情况,所以我在做pipe任务的时候限制了fork个数。

让用 C++ 编写的类似 shell 的管道作业:

ls | cat | cat | cat | cat | cat | cat | cat | cat

我有运行pipe()fork() 的代码:

#include <errno.h>
#include <fcntl.h>
#include <iostream>
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

const int fork_limit = 3;
int fork_counter = 0;

static void sig_chld_handler(int signo) 
  int status;
  pid_t pid;
  while ((pid = waitpid(-1, &status, WNOHANG)) > 0) 
    printf("received SIGCHLD from child process %d\n", pid);
    fork_counter -= 1;
    fprintf(stdout, "counter --, %d\n", fork_counter);
  


int main(int argc, char **argv) 

  signal(SIGCHLD, sig_chld_handler);

  char **cmds[9];

  char *p1_args[] = "ls", NULL;
  char *p2_args[] = "cat", NULL;

  cmds[0] = p1_args;
  cmds[1] = p2_args;
  cmds[2] = p2_args;
  cmds[3] = p2_args;
  cmds[4] = p2_args;
  cmds[5] = p2_args;
  cmds[6] = p2_args;
  cmds[7] = p2_args;
  cmds[8] = p2_args;


  int pipes[16];
  pipe(pipes);     // sets up 1st pipe
  pipe(pipes + 2); // sets up 2nd pipe
  pipe(pipes + 4);
  pipe(pipes + 6);
  pipe(pipes + 8);
  pipe(pipes + 10);
  pipe(pipes + 12);
  pipe(pipes + 14);


  pid_t pid;

  for (int i = 0; i < 9; i++) 

    // === comment this part to run correctly ===
    while (fork_limit < fork_counter) 
      usleep(10000);
    
    // ===

    pid = fork();
    if (pid == 0) 
      fprintf(stdout, "fork p%d\n", i);

      // read
      if (i != 0) 
        if (dup2(pipes[(i - 1) * 2], 0) < 0) 
          fprintf(stderr, "dup2 error\n");
          exit(EXIT_FAILURE);
        
      

      // write
      if (i != 8) 
        if (dup2(pipes[i * 2 + 1], 1) < 0) 
          fprintf(stderr, "dup2 error\n");
          exit(EXIT_FAILURE);
        
      

      for (int j = 0; j < 16; j++) 
        close(pipes[j]);
      

      execvp(*cmds[i], cmds[i]);
     else 
      fork_counter += 1;
      fprintf(stdout, "counter ++, %d \n", fork_counter);
    
  

  for (int j = 0; j < 16; j++) 
    close(pipes[j]);
  

  waitpid(pid, NULL, 0); // wait the last one.

  std::cout << "Parent done." << std::endl;

while (fork_limit &lt; fork_counter) 这行是我限制子号的地方。 如果我删除 while 块,代码运行良好,但如果我添加它会挂起。

我想以前的孩子会死,所以fork_counter -= 1,新的孩子可以被分叉,但行为不是,我不知道为什么。


没有while的结果。

counter ++, 1 
counter ++, 2 
fork p0
fork p1
counter ++, 3 
fork p2
counter ++, 4 
counter ++, 5 
fork p3
fork p4
counter ++, 6 
fork p5
counter ++, 7 
counter ++, 8 
fork p6
fork p7
counter ++, 9 
fork p8
received SIGCHLD from child process 13316
counter --, 8
Applications
Desktop
Documents
Downloads
Library
Movies
Music
Pictures
received SIGCHLD from child process 13319
counter --, 7
received SIGCHLD from child process 13318
counter --, 6
received SIGCHLD from child process 13317
counter --, 5
received SIGCHLD from child process 13320
counter --, 4
received SIGCHLD from child process 13322
counter --, 3
received SIGCHLD from child process 13321
counter --, 2
received SIGCHLD from child process 13323
counter --, 1
received SIGCHLD from child process 13324
counter --, 0
Parent done.

拥有while的结果,这意味着我限制了分叉数。

counter ++, 1 
counter ++, 2 
fork p0
fork p1
counter ++, 3 
counter ++, 4 
fork p2
fork p3
received SIGCHLD from child process 13291
counter --, 3
counter ++, 4 
fork p4

(hang)

【问题讨论】:

我认为cat 进程并没有死亡,因此一旦其中三个进程运行,父进程就会永远等待。我不知道为什么他们没有死…… 这很奇怪。我认为cat 应该通过管道连接到下一个cat 并死掉。如果我把第一个命令改成cat file,第一个命令也会死掉,但是左边还是不行。 【参考方案1】:

main 程序(按顺序)执行以下操作:

    预先创建所有管道 使用管道分叉子项(每个子项关闭所有继承的管道) 关闭所有管道

问题在于“关闭所有管道”的时机。因为main 正在等待第一个孩子完成 (while (fork_limit &lt; fork_counter)),然后才能完成第 2 步。

但是,cat 子代(例如,第一个 cat)在其输入管道被所有进程关闭之前无法完成,包括等待它们完成的 main。实际上是一个僵局。

考虑对main 进程进行小修改,一旦子代被分叉,它将关闭到每个子代的管道:

if ( fork() ) 
   // Children
   ...

 else 
   // Main - close pipes ASAP.
      close(pipes[(i-1)*2]) ;
      close(pipes[(i-1)*2+1]);
      fork_counter += 1;
      fprintf(stdout, "counter ++, %d \n", fork_counter);

可能还需要对子项中的管道关闭进行一些修改。

【讨论】:

【参考方案2】:

感谢@dash-o 的回答

它的作用是:

#include <errno.h>
#include <fcntl.h>
#include <iostream>
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

const int fork_limit = 4;
int fork_counter = 0;

static void sig_chld_handler(int signo) 
  int status;
  pid_t pid;
  while ((pid = waitpid(-1, &status, WNOHANG)) > 0) 
    printf("received SIGCHLD from child process %d\n", pid);
    fork_counter -= 1;
    fprintf(stdout, "counter --, %d\n", fork_counter);
  


int main(int argc, char **argv) 

  signal(SIGCHLD, sig_chld_handler);

  char **cmds[9];

  char *p1_args[] = "ls", NULL;
  char *p2_args[] = "cat", NULL;

  cmds[0] = p1_args;
  cmds[1] = p2_args;
  cmds[2] = p2_args;
  cmds[3] = p2_args;
  cmds[4] = p2_args;
  cmds[5] = p2_args;
  cmds[6] = p2_args;
  cmds[7] = p2_args;
  cmds[8] = p2_args;

  int pipes[16];
  pipe(pipes);     // sets up 1st pipe
  pipe(pipes + 2); // sets up 2nd pipe
  pipe(pipes + 4);
  pipe(pipes + 6);
  pipe(pipes + 8);
  pipe(pipes + 10);
  pipe(pipes + 12);
  pipe(pipes + 14);

  pid_t pid;

  for (int i = 0; i < 9; i++) 

    while (fork_limit < fork_counter) 
      usleep(10000);
    

    pid = fork();
    if (pid == 0) 
      fprintf(stdout, "fork p%d\n", i);

      // read
      if (i != 0) 
        if (dup2(pipes[(i - 1) * 2], 0) < 0) 
          fprintf(stderr, "dup2 error\n");
          exit(EXIT_FAILURE);
        
      

      // write
      if (i != 8) 
        if (dup2(pipes[i * 2 + 1], 1) < 0) 
          fprintf(stderr, "dup2 error\n");
          exit(EXIT_FAILURE);
        
      

      for (int j = 0; j < 16; j++) 
        close(pipes[j]);
      

      execvp(*cmds[i], cmds[i]);
     else 

      if (i != 0) 
        close(pipes[(i - 1) * 2]);
        close(pipes[(i - 1) * 2 + 1]);
      

      fork_counter += 1;
      fprintf(stdout, "counter ++, %d \n", fork_counter);
    
  

  for (int j = 0; j < 16; j++) 
    close(pipes[j]);
  

  waitpid(pid, NULL, 0); // wait the last one.

  std::cout << "Parent done." << std::endl;

【讨论】:

以上是关于如何以有限的叉数运行类似 shell 的管道任务?的主要内容,如果未能解决你的问题,请参考以下文章

管道符和作业控制shell变量和环境变量配置文件

shell基础之管道符和变量

在Linux系统中使用Shell实现多线程运行任务(多任务并发执行) 2022-05-30

管道符和作业控制shell变量环境变量配置文件

八管道符和作业控制shell变量环境变量配置文件

Linux学习笔记(二十四)管道符和作业控制shell变量环境变量配置文件