使用带有缓冲流的轮询功能

时间:2013-12-14 09:07:20

标签: c exec fork polling

我正在尝试使用C中的poll函数实现客户端 - 服务器类型的通信系统。流程如下:

  1. 主程序分叉子流程
  2. 子进程调用exec函数来执行some_binary
  3. 父母和孩子互相发送消息,发送的每条消息都取决于收到的最后一条消息。
  4. 我尝试使用poll实现此功能,但遇到了问题,因为子进程缓冲了其输出,导致poll调用超时。这是我的代码:

    int main() {
    char *buffer = (char *) malloc(1000);
    int n;
    
    pid_t pid; /* pid of child process */
    
    int rpipe[2]; /* pipe used to read from child process */
    int wpipe[2]; /* pipe used to write to child process */
    pipe(rpipe);
    pipe(wpipe);
    
    pid = fork();
    if (pid == (pid_t) 0)
    {
        /* child */
    
        dup2(wpipe[0], STDIN_FILENO);
        dup2(rpipe[1], STDOUT_FILENO);
        close(wpipe[0]); close(rpipe[0]);
        close(wpipe[1]); close(rpipe[1]);
        if (execl("./server", "./server", (char *) NULL) == -1)
        {
            fprintf(stderr, "exec failed\n");
            return EXIT_FAILURE;
        }       
        return EXIT_SUCCESS;
    }
    else
    {
        /* parent */
    
        /* close the other ends */
        close(wpipe[0]);
        close(rpipe[1]);
    
        /* 
          poll to check if write is good to go 
                    This poll succeeds, write goes through
            */
        struct pollfd pfds[1];
        pfds[0].fd = wpipe[1];
        pfds[0].events = POLLIN | POLLOUT;
        int pres = poll(pfds, (nfds_t) 1, 1000);
        if (pres > 0)
        {
            if (pfds[0].revents & POLLOUT)
            {
                printf("Writing data...\n");
                write(wpipe[1], "hello\n", 6);
            }
        }
    
        /* 
            poll to check if there's something to read.
            This poll times out because the child buffers its stdout stream.
        */
        pfds[0].fd = rpipe[0];
        pfds[0].events = POLLIN | POLLOUT;
        pres = poll(pfds, (nfds_t) 1, 1000);
        if (pres > 0)
        {
            if (pfds[0].revents & POLLIN)
            {
                printf("Reading data...\n");
                int n = read(rpipe[0], buffer, 1000);
                buffer[n] = '\0';
                printf("child says:\n%s\n", buffer);
            }
        }
    
        kill(pid, SIGTERM);
        return EXIT_SUCCESS;
    }
    }
    

    服务器代码简单:

    int main() {
        char *buffer = (char *) malloc(1000);
    
        while (scanf("%s", buffer) != EOF)
        {
            printf("I received %s\n", buffer);
        }   
        return 0;
    }
    

    如何阻止poll次呼叫因缓冲而超时?

    编辑:

    我希望该程序即使在exec ed二进制文件是外部的时候也能工作,即我无法控制代码 - 比如unix命令,例如cat或{{1} }。

2 个答案:

答案 0 :(得分:2)

您的代码中似乎存在两个问题。 “stdout”默认为缓冲, 所以服务器应该明确地刷新它:

printf("I received %s\n", buffer);
fflush(stdout);

主程序在尝试阅读时不应注册POLLOUT (但您可能需要注册POLLERR):

pfds[0].fd = rpipe[0];
pfds[0].events = POLLIN | POLLERR;

通过这些修改,您可以获得预期的输出:

$ ./main
Writing data...
Reading data...
child says:
I received hello

通常,您还应检查poll()的返回值,并重复调用if 必要的(例如,在中断的系统调用或超时的情况下)。

答案 1 :(得分:1)

当你在前一个问题的related answer中回答时,您需要实施event loop;顾名思义,它是循环,所以你应该在父进程中编码:

while (1) { // simplistic event loop!
   int status=0;
   if (waitpid(pid, &status, WNOHANG) == pid)
      { // clean up, child process has ended
        handle_process_end(status);
        break;
      };
   struct pollpfd pfd[2];
   memset (&pfd, 0, sizeof(pfd)); // probably useless but dont harm
   pfd[0].fd = rpipe[0];
   pfd[0].events = POLL_IN;
   pfd[1].fd = wpipe[1];
   pfd[0].event = POLL_OUT;
   #define DELAY 5000 /* 5 seconds */
   if (poll(pfd, 2, DELAY)>0) {
      if (pfd[0].revents & POLL_IN) {
         /* read something from rpipe[0]; detect end of file; 
            you probably need to do some buffering, because you may 
            e.g. read some partial line chunk written by the child, and 
            you could only handle full lines. */
      };
      if (pfd[1].revents & POLL_OUT) {
         /* write something on wpipe[1] */
      };
   }
   fflush(NULL);
} /* end while(1) */

您无法预测管道的可读性或可写性,这可能会多次发生。当然,涉及很多缓冲(在父进程中),我把细节留给你....你对子进程中的缓冲没有影响(某些程序检测到它们的输出是否是一个终端isatty)。

上面提到的事件轮询循环是什么让你避免因为stdout管道已满而父进程被阻塞而导致子进程被阻塞的死锁情况(对于孩子的stdin管道) )因为管道已满:使用事件循环,您可以在输入管道(即子进程的stdout)上轮询某些数据时立即读取,并在输出管道被轮询写入后立即写入一些数据(即不满)。您无法事先预测这些事件“孩子的输出是父母可读的”和“孩子的输入是父母可写的”发生的顺序。

我建议阅读Advanced Linux Programming,其中有几章解释这些问题!

BTW我的简单事件循环有点不对:如果子进程终止并且某些数据仍保留在其stdout管道中,则其读取不会完成。您可以在waitpid

之后移动poll测试

此外,不要指望单个write(来自子进程)进入管道会在父进程中触发一个read。换句话说,没有消息长度的概念。但是,POSIX知道PIPE_MAX ....请参阅其write文档。您传递给readwrite的缓冲区可能大小为PIPE_MAX

我再说一遍:您需要在事件循环中调用poll ,很可能poll将被多次调用 < / em>(因为您的循环将重复多次!),并将以不可预测(且不可重现)的顺序报告可读或可写的管道末端!第一次运行程序可能会报告“rpipe[0]可读”,您read 324个字节,重复事件循环,poll说“wpipe[1]可写”,您可以write 10个字节,重复事件循环,poll告诉“rpipe[0]可读”,你read 110个字节,你重复事件循环,poll再次告诉“rpipe[0]可读”,你read 4096个字节等等......同一个环境中同一个程序的第二次运行会给出不同的事件,比如:poll说“wpipe[1]可写”,你write 1000个字节,你重复循环,poll说“rpipe[0]可读,等等....

注意:您的问题不是孩子(“客户”)计划中的缓冲,我们假设您无法更改。所以重要的不是它中的缓冲数据,而是真正的输入和输出(这是你的父进程可以观察到的唯一内容;内部子缓冲与父进程无关),即数据您的子计划已经真正 read(2)write(2)。如果通过pipe(7),此类数据将变为poll(2) - 能够在父进程中执行(并且您的父进程可以在{{1}之后readwrite POLL_IN之后更新的POLL_OUT字段中的{}或revents。顺便说一句,如果您对孩子进行了编码,请不要忘记在其中的适当位置拨打poll