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

Shl*_*hen 5 c fork pipe process dup2

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

所以主程序:(编辑)

#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] = '\0';
    if (buf[strlen(buf) - 1] == 10) /* trim \r */
        buf[strlen(buf) - 1] = '\0';
    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;
}

Run Code Online (Sandbox Code Playgroud)

另一个程序 - 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] = '\0';
    if (buf[strlen(buf) - 1] == 10) /* trim \r */
        buf[strlen(buf) - 1] = '\0';
    else if (retval) while (getchar() != '\n'); /* get to eol */

    return retval;
}
Run Code Online (Sandbox Code Playgroud)

这是我得到的: 图片

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

Oka*_*Oka 4

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

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

在适当的情况下,每个关闭都应该只发生一次。

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

调用后dup2,您应该立即关闭第一个参数(原始管道末端)。事实上,在孩子们中,您试图在被调用 execvp关闭它们,这会导致下一个问题......

如果execvp成功,它永远不会返回,因为它将完全替换过程映像。任何期望在其真正运行之后运行的东西都处于故障状态。所以

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

可以写成

execvp(myargs[0], myargs)
perror("exec");
return -2;
Run Code Online (Sandbox Code Playgroud)

达到同样的效果。


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


下一个问题与死锁有关,当两个相互通信的进程试图同时阻止彼此读取时,最常见的是死锁。

您的子进程以非常特定的方式期望输入:一次2行,创建一对字符串。两个 write 调用的形式为:

write(pfd[index][1], strX, strlen(strX))
Run Code Online (Sandbox Code Playgroud)

不写入任何换行符,因此子级将永远等待,永远不会发回任何数据,而父级将永远等待,永远不会接收任何数据。


旁白:在某些方面mygets存在严重 缺陷,包括无法检测EOF或 I/O 故障(此功能是等待中的 SIGSEGV)。更令人讨厌的失败之一是这里的评论

if (buf[strlen(buf) - 1] == 10) /* trim \r */
Run Code Online (Sandbox Code Playgroud)

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

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


当您去读取子进程的响应时,会发生第二个死锁。

首先这个例子

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

是畸形的。删除&运算符,更改char rbuf[1];char rbuf;. 解决这个问题以及上面的换行问题将导致父进程从子进程读回数据。

那么问题就变成了while (read(...) > 0)循环将持续阻塞调用进程的执行,等待更多数据可用。

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

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

这是一个功能性示例,做了很少的更改。这个程序还存在一些问题,比如:父进程一般不知道子进程的状态,依靠^C来自终端的信号传播()来优雅地结束进程树,因为loopcmp不处理EOF(确实应该讨论这个)与写loopcmp.c/的人一起mygets)。

此外,mygeti也存在缺陷,因为无法区分无效输入和有效输入0。它也不处理EOF或防止有符号整数溢出

围绕创建子进程的一些更强大的抽象(函数和结构)将有助于进一步清理这个问题。

不过,这应该会帮助你进步。

#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] = '\0';
    if (buf[strlen(buf) - 1] == 10) /* trim \r */
        buf[strlen(buf) - 1] = '\0';
    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;
}
Run Code Online (Sandbox Code Playgroud)

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


最后一点:按照这种方式fgets+ 1字符串缓冲区的大小是相当没有意义的(尽管由于mygets执行其自己的设计不佳而 间接需要它们buf[len] = '\0')。fgets最多写入len - 1非空字节,始终为其放置的空终止字节留有空间。