Linux - 进程间通信方式之管道

概述

进程间通信(IPC)有多种方式,管道是进程间通信最基本的方式。
管道是"半双工"的,即单向的。
管道是先进先出(FIFO)的。

单进程中的管道

int fd[2];
使用文件描述符fd[1],向管道写数据
使用文件描述符fd[0],从管道读数据
管道读写示意图

注:单进程中的管道无实际用处。管道用于多进程通信。

一、管道的创建

使用pipe系统调用

/*************************************************************************
 * 函数:int pipe(int pipefd[2]);
 * 功能:创建一个管道
 * 参数:
 * 		pipefd[2] - 用于返回引用管道末端的两个文件描述符。
 * 					pipefd[0]: 管道的读取端
 * 					pipefd[1]: 管道的写入端
 * 返回:
 * 		成功 - 返回0
 * 		失败 - 返回-1,并设置errno
 * 描述:执行失败时,不会修改pipefd的值
 * 		如果对pipefd[0]进行写操作或对pipefd[1]进行读操作将造成不可预期的错误。
**************************************************************************/

二、管道的使用

1、单进程使用管道通信

注意:创建管道后,获得该管道的两个文件描述符,不需要普通文件操作中的open操作。
示例代码 - 单个进程使用管道通信:

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

int main(int argc, char *argv[]) {
    int fd[2] = { 0 };
    char buffer1[1024], buffer2[1024];

    int ret = pipe(fd);
    if(ret != 0) {
        fprintf(stderr, "pipe() - failed! reason: %s\n", strerror(errno));
        exit(1);
    }

    // 写数据
    strcpy(buffer1, "Sample code: a single process uses pipes to communicate!");
    if( write(fd[1], buffer1, strlen(buffer1)) == -1 ) {
        fprintf(stderr, "write() - failed! reason: %s\n", strerror(errno));
        exit(2);
    }
    printf("write data success!\n");

    // 读数据
    bzero(buffer2, sizeof(buffer2));
    if( read(fd[0], buffer2, sizeof(buffer2)) == -1 ) {
        printf("read() failed! reason: %s\n", strerror(errno));
        exit(3);
    }
    printf("read success! data: %s\n", buffer2);

    close(fd[0]);
    close(fd[1]);

    return 0;
}

2、多进程使用管道通信

创建管道之后,再创建子进程,此时共有4个文件描述符。
4个端口,父子进程分别持有1个读端口和1个写端口。既可向任一写端口写入数据,也可从任一读端口读入数据。

示例代码 - 多进程进程使用管道通信:

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

int main(int argc, char *argv[]) {
    int fd[2] = { 0 };
    char pbuffer[1024] = { 0 };  // 父进程
    char cbuffer[1024] = { 0 };  // 子进程

    int ret = pipe(fd);
    if(ret < 0) {
        fprintf(stderr, "pipe() - failed! reason: %s\n", strerror(errno));
        exit(1);
    }

    pid_t pid = fork();
    if (pid < 0) {
        fprintf(stderr, "fork() - failed! reason: %s\n", strerror(errno));
        exit(2);
    } else if (pid == 0) {
        bzero(cbuffer, sizeof(cbuffer));
        if( read(fd[0], cbuffer, sizeof(cbuffer)) < 0) {
            fprintf(stderr, "process[%d]: read() - failed! reason: %s\n", getpid(), strerror(errno));
            exit(3);
        }
        printf("process[%d]: read - success! data: %s\n", getpid(), cbuffer);
    } else {
        bzero(pbuffer, sizeof(pbuffer));
        strcpy(pbuffer,"Sample code: multi-process processes use pipes to communicate!");
        if(write(fd[1], pbuffer, strlen(pbuffer)) < 0) {
            fprintf(stderr, "parent process[%d]: write - failed! reason: %s\n", getpid(), strerror(errno));
            exit(4);
        }
        printf("parent process[%d]: write - success!\n", getpid());
    }

    if(pid > 0) {
        int wstatus = 0;
        wait(&wstatus);
    }

    return 0;
}

3、子进程使用exec启动新程序时管道的使用

有程序P1, P2。他们使用管道进行通信。P1由用户输入一段字符串后把这段字符串发送给P2。P2收到以后把该字符串打印出来。

  1. P1:
    创建管道,创建子线程,使用exec函数将子线程替换成P2(在使用exec时,把管道的读端作为exec的参数)。在父进程中获取用户的输入,输入完成后把输入好的字符串发给P2(即父进程把字符串写入管道)。
  2. P2:
    从参数中获取管道的读端 (参数为P2的main函数的参数),读管道,把独到的字符串打印出来。
  3. 难点:
    子进程使用exec启动新程序运行后,新进程能够使用原来子进程的管道(因为exec能共享原来的文件描述符),但问题是新进程并不知道原来的文件描述符是多少!
  4. 解决方案
    把子进程的管道文件描述符,用exec的参数传递给子进程

示例代码 - P1:

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

int main(int argc, char *argv[]) {
    int fd[2] = { 0 };

    if (pipe(fd) < 0) {
        fprintf(stderr, "pipe() - failed! reason: %s\n", strerror(errno));
        exit(1);
    }

    int pid = fork();
    if(pid < 0) {
        fprintf(stderr, "fork() - failed! reason: %s\n", strerror(errno));
        exit(2);
    } else if (pid == 0) {
        char arg[1024] = { 0 };
        sprintf(arg, "%d", fd[0]);
        if (execl("P2.exe", "P2.exe", arg, NULL) < 0) {
            printf("execl() - failed! reason: %s\n", strerror(errno));
            exit(3);
        }
    } else {
        char buffer[1024] = { 0 };
        strcpy(buffer, "The use of pipes when a child process starts a new program using exec.");
        if(write(fd[1], buffer, strlen(buffer)) < 0) {
            fprintf(stderr, "write() - failed! reason: %s\n", strerror(errno));
            exit(4);
        }
        printf("process[%d] send data success!\n", getpid());

    }

    if(pid > 0) {
        int wstatus = 0;
        wait(&wstatus);
    }

    return 0;
}

示例代码 - P2:

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

int main(int argc, char *argv[]) {
    if(argc < 2) {
        fprintf(stderr, "Insufficient number of parameters!\n");
        exit(1);
    }

    int fd = 0;
    char buffer[1024] = { 0 };

    sscanf(argv[1], "%d", &fd);
    if(read(fd, buffer, sizeof(buffer)) < 0) {
        fprintf(stderr, "read() - failed! reason: %s\n", strerror(errno));
        exit(2);
    }

    printf("read success! data: %s\n", buffer);

    return 0;
}

4、关闭管道的读端/写端

对管道执行读(read)操作时,如果管道中已经没有数据了,此时read将被"阻塞"。如果此时管道的写端已被关闭(close),则读操作将可能被一直阻塞!而此时的阻塞没有任何意义了(因为管道的写端已被关闭,即不会在写入数据了)。
因此,如果不准备再向管道中写入数据,则把该管道的所有写端关闭。如果此时在对该管道执行读(read)操作时,就会返回0,而不再阻塞该读(read)操作。这是管道的特性。如果有多个写端口,而只关闭了一个写端口,那么无数据时读操作将仍被阻塞。
实际实现方式:父子进程各有一个管道的读端和写端;把父进程的读端(或写端)关闭;把子进程的写端(或读端)关闭;使这个"4端口"管道变成单向的"2端口"管道。

示例代码 - 将父子进程的双读写端口改造成单读写端口:

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

int main(int argc, char *argv[]) {
    int fd[2] = { 0 };

    if( pipe(fd) < 0) {
        fprintf(stderr, "pipe() - failed! reason: %s\n", strerror(errno));
        exit(1);
    }

    int pid = fork();
    if(pid < 0) {
        fprintf(stderr, "fork() - failed! reason: %s\n", strerror(errno));
        exit(2);
    } else if(pid == 0) {
        close(fd[1]);

        char buffer[1024] = { 0 };
        if(read(fd[0], buffer, sizeof(buffer)) < 0) {
            fprintf(stderr, "process[%d]: read - failed! reason: %s\n", getpid(), strerror(errno));
            exit(3);
        }
        printf("process[%d]: read success! data: %s\n", getpid(), buffer);

		// 休眠5秒后再次尝试读取数据,此时read返回0
        sleep(5);
        bzero(buffer, sizeof(buffer));
        read(fd[0], buffer, sizeof(buffer));
        printf("process[%d]: The pipe is closed! data length: %lu\n", getpid(), strlen(buffer));
    } else {
        close(fd[0]);

        char buffer[1024] = { 0 };
        strcpy(buffer, "Transform the dual read-write port of the parent-child process into a single read-write port.\n");
        if(write(fd[1], buffer, strlen(buffer)) < 0) {
            fprintf(stderr, "process[%d]: write failed! reason: %s\n", getpid(), strerror(errno));
            exit(4);
        }
        printf("process[%d]: write success!\n", getpid());

		// 子进程写完数据后关闭写文件描述符
        close(fd[1]);
    }

    if(pid > 0) {
        int wstatus;
        wait(&wstatus);
    }


    return 0;
}

示例代码 - 将父子进程的双读写端口改造成单读写端口 解析:

  1. 父进程的写操作注释,子进程的读操作会被阻塞。
  2. 父进程的写操作注释,并close父进程的写端,此时子进程的读操作将被阻塞。
  3. 父进程的写操作注释,并close父子进程的写端,此时子进程的读操作将直接返回0,而不再阻塞。
  4. 最终方案:关闭父进程的读端,关闭子进程的写端。当父进程不再发送数据时,就关闭本进程的写端。

实力代码 - 父进程将用户输入的数据发送给子进程,子进程显示出来,直到父进程发送exit为止:

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

int main(int argc, char* argv[]) {
    int fd[2] = { 0 };

    if (pipe(fd) < 0) {
        fprintf(stderr, "pipe() - failed! reason: %s\n", strerror(errno));
        exit(1);
    }

    pid_t pid = fork();
    if (pid < 0) {
        fprintf(stderr, "fork() - failed! reason: %s\n", strerror(errno));
        exit(2);
    }
    else if (pid == 0) {
        // 关闭写端
        close(fd[1]);

        int times = 0;
        char buffer[1024] = { 0 };
        while (1) {
            bzero(buffer, sizeof(buffer));
            if (read(fd[0], buffer, sizeof(buffer)) == 0) {
                fprintf(stdout, "child process[%d]: The writer has been closed.\n", getpid());
                break;
            }
            printf("child process[%d] - (No. %d)%s\n", getpid(), ++times, buffer);
        }
    }
    else {
        char buffer[1024] = { 0 };
        while (1) {
            bzero(buffer, sizeof(buffer));
            printf("parent process[%d] - please input:", getpid());
            scanf("%s", buffer);
            if (strcmp(buffer, "exit") == 0) {
                fprintf(stdout, "parent process[%d]: Type [input] to close the write side.\n", getpid());
                close(fd[1]);
                break;
            }
            if (write(fd[1], buffer, strlen(buffer)) < 0) {
                fprintf(stderr, "parent process[%d]: write() - failed! reason: %s\n", getpid(), strerror(errno));
            }
        }
    }
    return 0;
}

5、把管道作为标准输入和标准输出

  • 把管道作为标准输入和标准输出的优点:
  1. 子进程使用exec启动新程序时,就不需要再把管道的文件描述符传递给子程序了。
  2. 可以直接使用标准输入(或标准输出)的程序(例如:od-c(统计字符个数,结果为八进制))。
  • 实现原理:
  1. 使用dup复制文件描述符
  2. 用exec启动新程序后,原进程中已打开的文件描述符仍保持打开,即可以共享原进程的文件描述符。
  • 附 - dup函数的介绍
/***********************************************************************
 * 函数:int dup(int oldfd);
 * 功能:Dup()系统调用创建文件描述符oldfd的副本,对新描述符使用编号最小的未使用文件描述符。
 * 参数:
 			oldfd - 需要创建副本的文件描述符
 * 返回:
 			成功 - 返回新的文件描述符
 			失败 - 返回-1,并设置errno
 * 说明:函数返回的新文件描述符和被复制的文件描述符,指向同一个文件或管道。
************************************************************************/

示例代码 - 把管道作为子进程的标准输入、标准输出:

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

int main(int argc, char *argv[]) {
    int fd[2] = { 0 };

    if(pipe(fd) < 0) {
        fprintf(stderr, "pipe() - failed! reason: %s\n", strerror(errno));
        exit(1);
    }

    int pid = fork();
    if(pid < 0) {
        fprintf(stderr, "fork() - failed! reason: %s\n", strerror(errno));
        exit(2);
    } else if(pid == 0) {
        close(fd[1]);

        close(0);
        dup(fd[0]);
        close(fd[0]);

        execlp("./pipe_std.exe", "./pipe_std.exe", NULL);
        printf("execlp() - failed! reason: %s\n", strerror(errno));
    } else {
        close(fd[0]);

        char buffer[1024] = "Hello  World";
        if(write(fd[1], buffer, strlen(buffer)) < 0 ) {
            fprintf(stderr, "write() - failed! reason: %s\n", strerror(errno));
            exit(3);
        }
        close(fd[1]);
    }

    if(pid > 0) {
        int wstatus = 0;
        wait(&wstatus);
    }

    return 0;
}

实力代码 - 把管道作为进程的标准输入、标准输出:

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

int main(int argc, char *argv[]) {
    char buffer[1024] = { 0 };


    bzero(buffer,sizeof(buffer));
    scanf("%s", buffer);
    printf("%s\n", buffer);

    bzero(buffer,sizeof(buffer));
    scanf("%s", buffer);
    printf("%s\n", buffer);

    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值