使用管道在两个程序之间进行通信

Using pipes to communicate between two programs

我需要主程序从用户获取两个字符串和另一个程序的参数,调用 fork() 然后在子进程中我需要将字符串写入管道并将它们发送到另一个程序returns 一个我想传递给父级的整数,所以我试图为它使用另一个管道,但每次它在插入字符串后立即停止。

所以主程序:(已编辑)

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

#define LINELEN (80)

char *mygets(char *buf, int len);
int mygeti();

int main(int argc, char *argv[])
{
    char *cmpstr[] = {"lexcmp", "lencmp"};
    int veclen = sizeof(cmpstr)/sizeof(char *);
    char str1[LINELEN + 1];
    char str2[LINELEN + 1];
    int index;
    int pid[2];
    int pfd[4][2];
    
    for(int i = 0; i < 4; i++)
    {
        if(pipe(pfd[i]) < 0)
        {
            perror("pipe");
            return -2;
        }
    }
    
    pid[0] = fork();
    
    if(pid[0] == 0) // child a
    {
        close(pfd[0][1]);
        close(pfd[2][0]);
        
        dup2(pfd[0][0], STDIN_FILENO);
        dup2(pfd[2][1], STDOUT_FILENO);

        char *myargs[3];
        myargs[0] = "./loopcmp";
        myargs[1] = "lexcmp";
        myargs[2] = NULL;
        if(execvp(myargs[0], myargs) == -1)
        {
            perror("exec");
            return -2;
        }
        close(pfd[0][0]);
        close(pfd[2][1]);
    }
    else
    {
        pid[1] = fork();
        if(pid[1] == 0) //child b
        {
            close(pfd[1][1]);
            close(pfd[3][0]);
            
            dup2(pfd[1][0], STDIN_FILENO);
            dup2(pfd[3][1], STDOUT_FILENO);
            
            char *myargs[3];
            myargs[0] = "./loopcmp";
            myargs[1] = "lencmp";
            myargs[2] = NULL;
            if(execvp(myargs[0], myargs) == -1)
            {
                perror("exec");
                return -2;
            }
            close(pfd[1][0]);
            close(pfd[3][1]);
        }
        else // parent
        {
            while (1)
            {
                printf("Please enter first string:\n");
                if (mygets(str1, LINELEN) == NULL)
                    break;
                printf("Please enter second string:\n");
                if (mygets(str2, LINELEN) == NULL)
                    break;
                do {
                    printf("Please choose:\n");
                    for (int i=0 ; i < veclen ; i++)
                        printf("%d - %s\n", i, cmpstr[i]);
                    index = mygeti();
                } while ((index < 0) || (index >= veclen));
                
                close(pfd[index][0]);

                if(write(pfd[index][1], str1, strlen(str1)) == -1)
                {
                    perror("writeToPipe");
                    return -2;
                }
                if(write(pfd[index][1], str2, strlen(str2)) == -1)
                {
                    perror("writeToPipe");
                    return -2;
                }
                
                if(index == 0)
                {
                    close(pfd[2][1]);
                    char rbuf[1];                    
                    while(read(pfd[2][0], &rbuf, 1) > 0)
                    {
                        write(STDOUT_FILENO, &rbuf, 1);
                    }
                }
                
                if(index == 1)
                {
                    close(pfd[3][1]);
                    char rbuf[1];                    
                    while(read(pfd[3][0], &rbuf, 1) > 0)
                    {
                        write(STDOUT_FILENO, &rbuf, 1);
                    }
                }
            }
        }
    }
    
    
    return 0;
}

char *mygets(char *buf, int len)
{
    char *retval;

    retval = fgets(buf, len, stdin);
    buf[len] = '[=10=]';
    if (buf[strlen(buf) - 1] == 10) /* trim \r */
        buf[strlen(buf) - 1] = '[=10=]';
    else if (retval) 
        while (getchar() != '\n'); /* get to eol */

    return retval;
}

int mygeti()
{
    int ch;
    int retval=0;

    while(isspace(ch=getchar()));
    while(isdigit(ch))
    {
        retval = retval * 10 + ch - '0';
        ch = getchar();
    }
    while (ch != '\n')
        ch = getchar();
    return retval;
}

另一个程序-loopcmp:(这里我不应该改变任何东西)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define LINELEN (80)

int lencmp(const char *str1, const char *str2);
int lexcmp(const char *str1, const char *str2);
char *mygets(char *buf, int len);

int main(int argc, char *argv[])
{
    int(*cmpfunc)(const char *, const char *) = NULL;
    char str1[LINELEN + 1];
    char str2[LINELEN + 1];

    if (argc != 2)
        return -1;
    
    if (!strcmp(argv[1], "lexcmp"))
        cmpfunc = lexcmp;
    else if (!strcmp(argv[1], "lencmp"))
        cmpfunc = lencmp;
    else
        return -1;

    while (1)
    {
        if (mygets(str1, LINELEN) == NULL)
            break;
        if (mygets(str2, LINELEN) == NULL)
            break;
        printf("%d\n", cmpfunc(str1, str2));
        fflush(stdout);
    }
    return 0;
}

int lencmp(const char *str1, const char *str2)
{
    int val;
    val = strlen(str1) - strlen(str2);
    if (val < 0)
        return 1;
    if (val > 0)
        return 2;
    return 0;
}

int lexcmp(const char *str1, const char *str2)
{
    int val;

    val = strcmp(str1, str2);
    if (val < 0)
        return 1;
    if (val > 0)
        return 2;
    return 0;
}

char *mygets(char *buf, int len)
{
    char *retval;

    retval = fgets(buf, len, stdin);
    buf[len] = '[=11=]';
    if (buf[strlen(buf) - 1] == 10) /* trim \r */
        buf[strlen(buf) - 1] = '[=11=]';
    else if (retval) while (getchar() != '\n'); /* get to eol */

    return retval;
}

这是我得到的: Picture

以及我实际需要它来打印从子进程返回的整数,然后重新开始并获取新的两个字符串,依此类推,直到用户退出。我究竟做错了什么?我只能修改主程序(第一个)

首先要做的是确保关闭每个进程中所有不必要的文件描述符。

这意味着任何与 lexcmp child 流程相关的内容都应在 lencmp child 流程中关闭,反之亦然。 parent 需要关闭两个“TO”管道的读取端和两个“FROM”管道的写入端。

在适当的情况下,这些关闭中的每一个都应该恰好发生一次。

照原样,在 parent 中,您正在循环调用 close(pfd[index][0]);close(pfd[2][1]);close(pfd[3][1]);

调用dup2后,应立即关闭第一个参数(原管道端)。事实上,在 children 中,您正试图在调用 execvp 关闭它们,这将导致下一期...

如果 execvp 成功,它永远不会 returns,因为它将完全替换过程映像。任何在运行 之后预期的东西实际上都在故障状态下运行。所以

if(execvp(myargs[0], myargs) == -1)
{
    perror("exec");
    return -2;
}

可以写成

execvp(myargs[0], myargs)
perror("exec");
return -2;

效果相同。


旁白:main 的大型 if .. else if .. else 结构有点难以阅读,并且不需要,因为每个 if 语句的主体都会导致child 个进程被替换,或因错误退出。


下一个问题与死锁有关,这通常发生在两个相互通信的进程试图同时阻止彼此读取时。

您的 child 进程期望以非常具体的方式输入:一次 2 ,创建一对字符串。两次写调用,形式为

write(pfd[index][1], strX, strlen(strX))

不写任何换行符,因此children永远等待,永远不会发送回任何数据,parent将永远等待,永远不会收到任何数据。


旁白:mygets 严重 flawed,在某些方面,包括无法检测 EOF或 I/O 失败(此函数是等待中的 SIGSEGV)。更令人讨厌的失败之一是此处的评论

if (buf[strlen(buf) - 1] == 10) /* trim \r */

完全错误。 ASCII 十进制 10 是 '\n'、换行符或换行符。 '\r' 或回车符 return 将是十进制的 13。这就是为什么强烈建议使用字符常量 'A' 而不是整数常量 65 的原因。

一般来说,这里的副作用是您的字符串被删除了尾随换行符。


第二个死锁发生在您去读取 child 进程的响应时。

首先,这个例子

char rbuf[1];                    
while(read(pfd[N][0], &rbuf, 1) > 0)
{
    write(STDOUT_FILENO, &rbuf, 1);
}

格式错误。删除 & 运算符,char rbuf[1]; 更改为 char rbuf;。解决这个问题,以及上面的换行问题,将导致 parent 进程从 child.

读取数据

然后问题就变成了 while (read(...) > 0) 循环会不断地 阻塞 调用进程的执行,等待更多数据可用。

当 child 进程已经开始尝试从 parent 进程读取另一对行时,这意味着另一个死锁。

一个简单的解决方案是尝试在 parent 中进行一次相当大的读取,并依靠 child 中 fflush(stdout); 的行为将管道刷新到 parent.

这里是一个功能性的 -ish 示例,只做了很少的改动。这个程序还是有一些问题,比如:parent进程一般不知道child进程的状态,依赖信号传播(^C ) 从终端优雅地结束进程树,因为 loopcmp 不处理 EOF (真的应该和写 loopcmp.c / mygets 的人讨论这个)。

此外,mygeti 也有缺陷,因为无法将无效输入与 0 的有效输入区分开来。它不处理EOF,或阻止signed integer overflow

围绕创建 child 进程的一些更强大的抽象(功能和结构)将有助于进一步清理它。

不过,这应该可以帮助您取得进步。

#define _POSIX_C_SOURCE 200809L
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#define LINELEN (80)

char *mygets(char *buf, int len);
int mygeti();

void close_pipe(int fd[2])
{
    close(fd[0]);
    close(fd[1]);
}

int main(void)
{
    char *cmpstr[] = {"lexcmp", "lencmp"};
    int veclen = sizeof(cmpstr)/sizeof(char *);
    char str1[LINELEN + 1];
    char str2[LINELEN + 1];
    int index;
    int pid[2];
    int pfd[4][2];

    /* pfd[0] is TO lexcmp
     * pfd[1] is TO lencmp
     * pfd[2] is FROM lexcmp
     * pfd[3] is FROM lencmp
     */

    for(int i = 0; i < 4; i++)
        if(pipe(pfd[i]) < 0) {
            perror("pipe");
            return -2;
        }

    pid[0] = fork();

    if (pid[0] == 0) {
        /* child lexcmp */
        close_pipe(pfd[1]);
        close_pipe(pfd[3]);

        close(pfd[0][1]);
        close(pfd[2][0]);

        dup2(pfd[0][0], STDIN_FILENO);
        dup2(pfd[2][1], STDOUT_FILENO);
        close(pfd[0][0]);
        close(pfd[2][1]);

        char *args[] = { "./loopcmp", "lexcmp", NULL };
        execvp(*args, args);
        perror("exec");
        return -2; /* This only returns from the child */

    }

    pid[1] = fork();

    if (pid[1] == 0) {
        /* child lencmp */
        close_pipe(pfd[0]);
        close_pipe(pfd[2]);

        close(pfd[1][1]);
        close(pfd[3][0]);

        dup2(pfd[1][0], STDIN_FILENO);
        dup2(pfd[3][1], STDOUT_FILENO);

        close(pfd[1][0]);
        close(pfd[3][1]);

        char *args[] = { "./loopcmp", "lencmp", NULL };
        execvp(*args, args);
        perror("exec");
        return -2; /* This only returns from the child */
    }

    /* parent */

    close(pfd[0][0]);
    close(pfd[1][0]);
    close(pfd[2][1]);
    close(pfd[3][1]);

    while (1) {
        printf("Please enter first string: ");
        if (mygets(str1, LINELEN) == NULL)
            break;
        printf("Please enter second string: ");
        if (mygets(str2, LINELEN) == NULL)
            break;

        do {
            printf("Please choose (");
            for (int i=0 ; i < veclen ; i++)
                printf(" [%d] %s", i, cmpstr[i]);
            printf(" ): ");
            index = mygeti();
        } while ((index < 0) || (index >= veclen));

        if (0 >= dprintf(pfd[index][1], "%s\n%s\n", str1, str2)) {
            fprintf(stderr, "Failed to write to child %d\n", index);
            perror("dprintf");
            return -2;
        }

        char buf[64];
        ssize_t bytes = read(pfd[index + 2][0], buf, sizeof buf - 1);

        if (-1 == bytes) {
            perror("read from child");
            return -2;
        }

        buf[bytes] = 0;
        printf("Result: %s", buf);
    }
}

char *mygets(char *buf, int len)
{
    char *retval;

    retval = fgets(buf, len, stdin);
    buf[len] = '[=15=]';
    if (buf[strlen(buf) - 1] == 10) /* trim \r */
        buf[strlen(buf) - 1] = '[=15=]';
    else if (retval)
        while (getchar() != '\n'); /* get to eol */

    return retval;
}

int mygeti()
{
    int ch;
    int retval=0;

    while(isspace(ch=getchar()));
    while(isdigit(ch))
    {
        retval = retval * 10 + ch - '0';
        ch = getchar();
    }
    while (ch != '\n')
        ch = getchar();
    return retval;
}

注意 dprintf 的用法。如果由于某种原因不可用,请确保在每个字符串后写一个换行符。


最后:根据 fgets 的工作方式,字符串缓冲区大小的 + 1 是毫无意义的(尽管由于 mygets 执行它自己的,设计不佳 buf[len] = '[=49=]')。 fgets 最多写入 len - 1 non-null 字节,总是 为它放置的空终止字节留出空间。