【问题标题】:Do I understand how Unix file descriptors work in C?我了解 Unix 文件描述符如何在 C 中工作吗?
【发布时间】:2012-02-13 00:40:22
【问题描述】:

下面的简短程序旨在遍历从命令行传递的 argv 并执行每个参数。这不是我的作业,而是我正在做的准备做作业的事情。

第一个参数从 STDIN 和 STDOUT 获取输入,并写入管道。在每次迭代结束时(最后一次除外),文件描述符被交换,以便下一个 exec 写入的管道将被下一个读取。例如,我打算以这种方式为

./a.out /bin/pwd /usr/bin/wc 

只打印工作目录的长度。代码如下

#include <stdio.h>                                                              
#include <unistd.h>                                                             
#include <sys/types.h>                                                          
#include <stdlib.h>                                                             
#include <string.h>                                                             

main(int argc, char * argv[]) {                                                 

  int i;
  int left[2], right[2], nbytes; /* arrays for file descriptors */

  /* pointers for swapping */
  int (* temp);
  int (* leftPipe) = left;                 
  int (* rightPipe) = right;

  pid_t childpid;                                                               
  char readbuffer[80];                                                          

  /* for the first iteration, leftPipe is STDIN */
  leftPipe[0] = STDIN_FILENO;
  leftPipe[1] = STDOUT_FILENO;

  for (i = 1; i < argc; i++) {                                                  

    /* reopen the right pipe (is this necessary?) */
    pipe(rightPipe);                                                            
    fprintf(stderr, "%d: %s\n", i, argv[i]);
    fprintf(stderr, "%d %d %d %d\n", leftPipe[0], leftPipe[1], rightPipe[0], rightPipe[1]);                                                                                    
    if ((childpid = fork()) == -1) {                                            
      perror("fork");                                                           
      exit(1);                                                                  
    }                                                                           

    if (childpid == 0) {                                                        

      /* read input from the left */                                            
      close(leftPipe[1]); /* close output */                                    
      dup2(leftPipe[0], STDIN_FILENO);                                          
      close(leftPipe[0]); /* is this necessary? A tutorial seemed to be doing this */ 

      /* write output to the right */                                           
      close(rightPipe[0]); /* close input */                                    
      dup2(rightPipe[1], STDOUT_FILENO);                                        
      close(rightPipe[1]);                                                      

      execl(argv[i], argv[i], NULL);                                            
      exit(0);                                                                  
    }                                                                           

    wait();                                                                     

    /* on all but the last iteration, swap the pipes */
    if (i + 1 < argc) {              

      /* swap the pipes */                                                      
      fprintf(stderr, "%d %d %d %d\n", leftPipe[0], leftPipe[1], rightPipe[0], rightPipe[1]);
      temp = leftPipe;                                                          
      leftPipe = rightPipe;                                                     
      rightPipe = temp;                                                         
      fprintf(stderr, "%d %d %d %d\n", leftPipe[0], leftPipe[1], rightPipe[0], rightPipe[1]);
    }                                                                           
  }                                                                             

    /* read what was last written to the right pipe */                          
    close(rightPipe[1]); /* the receiving process closes 1 */                  

    nbytes = read(rightPipe[0], readbuffer, sizeof(readbuffer));       
    readbuffer[nbytes] = 0;
    fprintf(stderr, "Received string: %s\n", readbuffer);                                

  return 0;                                                                     
}

更新:在以下所有测试用例中,我最初都使用了 /bin/wc,但哪个 wc 发现抽水马桶根本不在我想的地方。我正在修改结果。

普通情况下的输出(./a.out /bin/pwd)与预期的一样:

1: /bin/pwd
Received string: /home/zeigfreid/Works/programmatical/Langara/spring_2012/OS/labs/lab02/play

使用第一个示例运行此程序的输出 (./a.out /bin/pwd /usr/bin/wc):

1: /bin/pwd
0 1 3 4
3 4 0 1
2: /bin/wc

此时,终端挂起(可能正在等待输入)。

如您所见,没有收到字符串。我想象的是我在上面做错了什么,或者在交换指针时,或者我不理解 unix 文件描述符。最后,我的任务将是解释任意长的管道,这是我解决问题的想法之一。我很难判断我是否走在了树上的正确轨道上。我了解 unix 文件描述符吗?

更新:

使用 /bin/ls 作为第二个参数运行它,我得到以下结果(数字是不同点的文件描述符):

1: /bin/pwd
0 1 3 4
0 1 3 4
3 4 0 1
2: /bin/ls
3 4 5 6
Received string: a.out
log
pipe2.c
play.c
@

最后还是有一些垃圾,但是我现在更担心我看不懂指针!这两个命令虽然相互独立,但它们并没有真正使用管道。

UPDATE:垃圾字符来自未关闭字符串。现在我关闭它,没有垃圾。

【问题讨论】:

  • 我想建议将您所有的 printf(...) 呼叫更改为 fprintf(stderr,...)。将标准 IO (printf(3)) 与较低级别的例程 (pipe(2), dup2(2), close(2)) 混合使用起来麻烦而不值得。
  • 注意!我想夹板会同意的。
  • 在打印之前不要终止字符串,这解释了垃圾。在read 之后尝试readbytes[nbytes] = 0
  • 所以,在我看来,在交换之后,正在执行的进程无法从交换的管道中读取。如果没有交换,我们可以从管道中读取。如果第二个进程没有从管道读取数据,那么它运行良好并将输出放入管道。

标签: c pipe file-descriptor io-redirection


【解决方案1】:

挂起是由于“右”管道的写入端在分叉后没有在主进程中正确关闭造成的。因此,wc 将永远不会停止读取(毕竟,主进程仍然可以将内容写入管道!)。只有在写入端的文件描述符的所有副本都已关闭后才会停止读取。

这是一个固定版本:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char * argv[])
{
  int i;
  int left[2], right[2], nbytes; /* arrays for file descriptors */

  /* pointers for swapping */
  int (* temp);
  int (* leftPipe) = left;
  int (* rightPipe) = right;

  pid_t childpid;
  char readbuffer[80];

  leftPipe[0] = STDIN_FILENO;
  // no need to assign leftPipe[1] here, it will not be used

  for (i = 1; i < argc; i++) {
    pipe(rightPipe); // create new pipe

    fprintf(stderr, "%d: %s\n", i, argv[i]);
    fprintf(stderr, "%d %d %d %d\n", leftPipe[0], leftPipe[1], rightPipe[0], rightPipe[1]);
    if ((childpid = fork()) == -1) {
      perror("fork");
      exit(1);
    }

    if (childpid == 0) {
      // use the reading end of the left pipe as STDIN
      dup2(leftPipe[0], STDIN_FILENO);
      // use the writing end of the right pipe as STDOUT
      dup2(rightPipe[1], STDOUT_FILENO);
      // close reading end of the right pipe
      close(rightPipe[0]);
      execl(argv[i], argv[i], NULL);
      exit(0);
    }
    // IMPORTANT!! close writing end of the right pipe, otherwise
    // the program will hang (this is the main bug in your original
    // implementation)
    close(rightPipe[1]);

    // wait properly!
    waitpid(childpid, NULL, 0);

    /* on all but the last iteration, swap */
    if (i + 1 < argc) {
      fprintf(stderr, "%d %d %d %d\n", leftPipe[0], leftPipe[1], rightPipe[0], rightPipe[1]);
      temp = leftPipe;
      leftPipe = rightPipe;
      rightPipe = temp;
      fprintf(stderr, "%d %d %d %d\n", leftPipe[0], leftPipe[1], rightPipe[0], rightPipe[1]);
    }
  }

  nbytes = read(rightPipe[0], readbuffer, sizeof(readbuffer));
  readbuffer[nbytes] = 0;
  fprintf(stderr, "Received string: %s\n", readbuffer);

  return 0;
}

输出:

 >> ./a.out /bin/ls /bin/cat /usr/bin/wc
1: /bin/ls
0 32767 3 4
0 32767 3 4
3 4 0 32767
2: /bin/cat
3 4 4 5
3 4 4 5
4 5 3 4
3: /usr/bin/wc
4 5 5 6
Received string:     266     294    4280

如果您对此解决方案有具体问题,请告诉我 :) 您的原始代码还有一些其他小问题:

  • 不需要使用指针,我们可以在管道周围复制(性能肯定不会有问题;)
  • 使用int 代替size_t
  • 您没有修复在使用 -Wall 标志进行编译时会显示给您的所有警告

如果你有兴趣,我会这样写:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char **argv) {
  size_t i, nbytes;
  int left[2], right[2], tmp[2];
  pid_t childpid;
  char readbuffer[80];

  left[0] = STDIN_FILENO;

  for (i = 1; i < argc; ++i) {
    pipe(right);

    switch ((childpid = fork())) {
      case -1:
        perror("fork");
        exit(1);
      case 0:
        dup2(left[0], STDIN_FILENO);
        dup2(right[1], STDOUT_FILENO);
        close(right[0]);
        execl(argv[i], argv[i], NULL);
      default:
        close(right[1]);
        waitpid(childpid, NULL, 0);
    }

    if (i == argc - 1) break;
    memcpy(tmp,   left,  sizeof tmp);
    memcpy(left,  right, sizeof left);
    memcpy(right, tmp,   sizeof right);
  }

  nbytes = read(right[0], readbuffer, sizeof readbuffer);
  readbuffer[nbytes] = 0;
  fprintf(stderr, "Received string: %s\n", readbuffer);

  return 0;
}

【讨论】:

  • 不错!我没有用 -Wall 编译,你是对的。通常我会这样做,并且我也会修复夹板 -weak 警告,但这只是一个实验,所以我没有那么彻底。很高兴看到答案是相对较小的,而不是我担心的分类错误。所以答案是“是”,但我需要更多的细节练习。非常感谢您的解决方案非常好!
  • @Ziggy:如果对您有帮助,请您接受这个答案:)
  • 我一定会的!我倾向于:)
【解决方案2】:

要修复输出末尾的垃圾,请在最后的 printf 之前添加以下行。

readbuffer[nbytes] = 0;

至于悬挂问题 - 我需要多花点心思来解决这个问题。我猜这与管道和缓冲有关。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-30
    相关资源
    最近更新 更多