首页 > 代码库 > 第11章进程间通信(1)_管道

第11章进程间通信(1)_管道

1. 进程间通信概述

(1)概述

  ①数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。

  ②共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。

  ③通知事件:一个进程需要向另一个(组)进程发送消息,通知它们发生了某种事件(如进程终止时要通知父进程)。

  ④资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供锁和同步机制。

  ⑤进程控制:有些进程希望完全控制另一个进程的执行(如Degub进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

(2)现代的进程间通信方式

  ①管道(pipe)和命名管理(FIFO)    ②信号(signal)    ③消息队列    ④共享内存    ⑤信号量    ⑥套接字(socket)

2. 管道通信

2.1 概述

(1)管道是针对本地计算机的两个进程之间的通信而设计的通信方法,管道建立后,实际获得的是两个文件描述符一个用于读取,另一个用于写入

(2)最常见的IPC机制,通过pipe系统调用

(3)管道是单工的,数据只能向一个方向流动,需要双向通信时,需要建立起两个管道。

(4)数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据(即读取的顺序应与写入的顺序一致

2.2 管道的分类和读写

(1)管道的分类

  ①匿名管道:

    A.在关系进程中进程(父进程和子进程,兄弟进程之间)

    B.由pipe系统调用,管道由父进程建立

    C.管道位于内核空间,其实是一块缓存

  ②命名管道(FIFO):

    A.两个没有任何关系的进程之间通信可通过命名管道进行数据传输,本质上是内核中一块缓存,另在文件系统中以一个特殊的设计文件(管道文件)存在

    B.通过系统调用mkfifo创建。

(2)管道的创建: 

技术分享

头文件

#include <unistd.h>

函数

int pipe(int fd[2]);

功能

等待一个或者多个指定信号发生

返回值

成功返回0,否则返回-1

备注

①fd[0]:为pipe的读端,用于读取管道。

②fd[1]:为pipe的写端,用于写入管道。

(2)管道的读写

  ①管道主要用于不同进程间通信。实际上,通常先创建一个管道,再通过fork函数创建另一个子进程

技术分享 

  ②注意管道是单工的,所以要关闭父子进程中其中的一些fd(如上图所示)。如果需要双向通信,则需要创建2个管道。

【编程实验】父进程写入,子进程读取

//cal_pipe.c

技术分享
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

/*
* 父进程通过管道传输两个数据给子进程
* 由子进程负再从管道中读取并输出
*/

int main(void)
{
    int fd[2];
    
    //创建管道
    if(pipe(fd) < 0){
        perror("fork error");
        exit(1);
    }

    pid_t pid;
    if((pid = fork()) < 0){
        perror("fork error");
        exit(1);
    }else if(pid >0){ //parent process
        close(fd[0]); //父进程关闭读端,保留写端。
        
        int start = 1, end = 100;
        //往管道中写入数据
        if(write(fd[1], &start, sizeof(int)) != sizeof(int)){
            perror("write error");
            exit(1);
        }
        if(write(fd[1], &end, sizeof(int)) != sizeof(int)){
            perror("write error");
            exit(1);
        }
        close(fd[1]);
        wait(pid);
    }else{ //child process
        close(fd[1]); //子进程用来读取数据
        int start, end;
        //从管道中读取数据(注意与写入的顺序相同)
        if(read(fd[0], &start, sizeof(int)) < 0){
            perror("read error");
            exit(1);
        }
        if(read(fd[0], &end, sizeof(int)) < 0){
            perror("read error");
            exit(1);
        }
        close(fd[0]);

        printf("child process read start: %d, end: %d\n", start, end);
    }

    return 0;
}
/*输出结果:
 child process read start: 1, end: 100
*/
View Code

【编程实验】模拟管道命令

  ①一个子进程执行命令,将命令执行结果写入管道

  ②另一个子进程从管道中读取命令执行的结果,然后根据关键字过滤(grep)

  ③分析命令:cat /etc/passwd | grep root。执行该命令时,实际上有3进程:父进程shell,执行cat的子进程和执行grep的子进程!这与本例模拟的情景是一样的,由cat的子进程执行结果写入管道,grep的子进程从管道中读取出来!

 //cmd_pipe.c

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

char* cmd1[3] = {"/bin/cat", "/etc/passwd", NULL};
char* cmd2[3] = {"/bin/grep", "root", NULL};
//char* cmd2[3] = {"wc", "-l", NULL}; //统计多少用户
int main(void)
{
    int fd[2];

    if(pipe(fd) < 0){
        perror("pipe error");
        exit(1);
    }

    int i = 0;
    pid_t pid;
    //创建进程扇:1个父进程和2个子进程
    for(; i<2; i++){
        pid = fork();
        if(pid < 0){
            perror("fork error");
            exit(1);
        }else if(pid == 0){ //child process
            if(i == 0){ //第1个子进程,负责往管道写入数据
                close(fd[0]); //关闭读端   
                
                //注意cat命令默认输出是标准输出(屏幕),因此需输重定向到管道写端
                //将标准输出重定向到管道写端,cat执行结果会写入管道
                if(dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO){ //将fd[1]重向定为标准输出
                    perror("dup2 error");
                }
                
                close(fd[1]);//标准输出己重定向到管道写端,fd[1]可以关闭

                //调用exec函数执行cat命令
                if(execvp(cmd1[0], cmd1) < 0){//v数组,p绝对或相对路径
                    perror("execvp error");
                    exit(1);
                }
            }

            if(i == 1){ //第2个子进程,负责从管道读取数据
                close(fd[1]); //关闭写端
                //将标准输入重定向到管道读端,这样grep将从管道读入而不是从标准输入读取
                if(dup2(fd[0], STDIN_FILENO) != STDIN_FILENO){
                    perror("dup2 error");
                }

                close(fd[0]);//标准输入己重定向到管道读端,fd[0]可以关闭

                //调用exec函数执行grep命令
                if(execvp(cmd2[0], cmd2) < 0){
                    perror("execvp error");
                    exit(1);
                }
            }

            break;
        }else{ //parent process
            if( i== 1){ //须等第2个子进程创建完毕
                //父进程要等到子进程全部创建完毕才去回收
                close(fd[0]);
                close(fd[1]);
                wait(0); //回收两个子进程
                wait(0);
            }
        }
    }
    if(pid = fork() < 0){
    
    }

    return 0;
}
/*输出结果:
 root:x:0:0:root:/root:/bin/bash
 operator:x:11:0:operator:/root:/sbin/nologin
*/

【编程实验】协同进程(两个进程通过两个管道进行双向通信)

  ①父进程向第1个管道写入x和y。

  ②子进程从第1个管道读取x和y。并调用add进行相加。

  ③子进程将计算结果写入第2个管道。

  ④父进程从第2个管道中读取计算结果,并输出。

//add.c ==> 需单独编译成add.o的可执行文件

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

int main(int argc, char* argv[])
{
    int x = 0, y = 0;
    if(read(STDIN_FILENO, &x, sizeof(int)) < 0){
        perror("read error");
    }
    if(read(STDIN_FILENO, &y, sizeof(int)) < 0){
        perror("read error");
    }

    int result = x + y;

    if(write(STDOUT_FILENO, &result, sizeof(int)) != sizeof(int)){
        perror("write error");
    }

    return 0;
}

//co_process.c ==> 编译成可执行文件

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

int main(void)
{
    int fda[2], fdb[2];

    //创建两个管道,以实现双工操作
    if( (pipe(fda) < 0) || (pipe(fdb) <0) ){
        perror("pipe error");
        exit(1);
    }

    pid_t pid;
    pid = fork();
    if(pid < 0){
    }else if(pid == 0){//child process
        /*
         *(1)子进程负责从管道a中读取父进程写入的参数x和y
         *(2)通过exec函数去调用bin/add程序进行累加
         *(3)将累加的结果写入到管道b。
         */
        close(fda[1]);//只能从a管道读取
        close(fdb[0]);//只能向b管道写入

        //将标准输入重定向到管道a的读端,则
        //(add程序将从管道a的读端读取累加参数x和y)
        if(dup2(fda[0], STDIN_FILENO) != STDIN_FILENO){
            perror("dup2 error");
        }
        //将标准输出重定向到管道b的写端,则
        //(add程序累加后的结果会写入管道b中)
        if(dup2(fdb[1], STDOUT_FILENO) != STDOUT_FILENO){
            perror("dup2 error");
        }

        close(fda[0]); //重定向完毕,可以关闭
        close(fdb[1]);

        if(execlp("bin/add", "bin/add", NULL) < 0){
            perror("execlp error");
            exit(1);
        }
    }else{ //parent process
        /*
         *(1)从标准输入读取参数x和y
         *(2)将x和y写入管道a
         *(3)从管道b中读取累加结果并输出
         */
        close(fda[0]);
        close(fdb[1]);

        int x, y;
        //(1)读取累加参数x和y
        printf("please input x and y: ");
        scanf("%d %d", &x, &y);
        //(2)将x和y写入管道a
        if(write(fda[1], &x, sizeof(int)) != sizeof(int)){
            perror("write error");
        }
        if(write(fda[1], &y, sizeof(int)) != sizeof(int)){
            perror("write error");
        }

        //(3)从管道b中读取结果(注意管道中无数据时会阻塞!)
        int result;
        if(read(fdb[0], &result, sizeof(int)) < 0){
            perror("read error");
        }else{
            printf("add result is %d\n", result);
        }

        close(fda[1]);
        close(fdb[0]);
        wait(0);
    }
    return 0;
}

2.3 管道的特性

(1)通过打开两个管道来创建一个双向的管道

(2)管道是阻塞性的,当进程从管道中读取数据,若没有数据进程会阻塞。

(3)当一个进程往管道中不断地写入数据,但是没有进程去读取数据,此时只要管道没有满是可以了,但若管道放满数据时则会报错。

(4)不完整管道

  ①当一个写端己关闭的管道时,在所有数据被读取后,read返回0,以表示到达了文件的尾部。

  ②如果一个读端己被关闭的管道,则产生信号SIGPIPE,如果忽略该信号或捕捉该信号并从处理程序返回则write返回-1,同时errno设置为EPIPE。

【编程实验】读一个写端己关闭的管道

//broken_pipe_r.c

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

/*
 *不完整管道:读取一个写端己经关闭的管道
 */
int main()
{
    int fd[2];
    if(pipe(fd) < 0){
        perror("pipe error");
        exit(1);
    }

    pid_t pid;
    if((pid = fork()) < 0){
        perror("fork error");
        exit(1);
    }else if (pid > 0){ //parent process
        //父进程从不完整管道中读取数据
        
        close(fd[1]);
 
        sleep(5);//等待子进程将管道的写端关闭
        
        while(1){
            char c;
            if(read(fd[0], &c, 1) == 0){
                printf("\nwrite-end of pipe closed.\n");
                break;
            }else{
                printf("%c", c);
            }
        }

        close(fd[0]);
        wait(0);
    }else{ //child process
        //子进程负再将数据写入管道
        close(fd[0]);
        char* s = "12345";
        write(fd[1], s, strlen(s)*sizeof(char));

        //写入数据后关闭管道的写端-->变成不完整
        //管道,但要确保在父进程读管道之前管道的写端被关闭!
        close(fd[1]);
    }
    return 0;
}
/*输出结果
 12345
 write-end of pipe closed.
 */

【编程实验】写一个读端己关闭的管道

//broken_pipe_w.c

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

/*
 *不完整管道:写一个读端己被关闭的管道
 */

//信号处理函数
void sig_handler(int signo)
{
    if(signo == SIGPIPE){
        printf("SIGPIEP occured\n");
    }
}

int main(void)
{
    //注册信号处理函数
    if(signal(SIGPIPE, sig_handler) == SIG_ERR){
        perror("signal sigpipe error");
        exit(1);
    }
    int fd[2];
    if(pipe(fd) < 0){
        perror("pipe error");
        exit(1);
    }

    pid_t pid;
    if((pid = fork()) < 0){
        perror("fork error");
        exit(1);
    }else if(pid > 0){ //parent process       
        //父进程注册信号处理函数
        if(signal(SIGPIPE, sig_handler) == SIG_ERR){
            perror("signal sigpipe error");
            exit(1);
        }

        //父进程负责将数据写入不完整管道(读端关闭)中
        sleep(5);//让子进程先运行,以保证读端关闭

        close(fd[0]);

        char* s = "12345";
        int len = strlen(s) * sizeof(char);
        if(write(fd[1], s, len ) != len){
            fprintf(stderr, "%s, %s\n", strerror(errno),
                   (errno == EPIPE) ? "EPIPE": ", unknow");
        }

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

    }else{ //child process
        close(fd[0]);
        close(fd[1]);
    }
    return 0;
}
/*输出结果:
 SIGPIEP occured
 Broken pipe, EPIPE
 */

2.4 标准库中的管道操作

头文件

#include <stdio.h>

函数

FILE* popen(const char* cmdstring, const char* type);

参数

cmdstring:要执行的命令行参数。

type:r或w。

  ①如果type为r,则表示由子进程exec(cmdstring),结果写入管道(子进程内部会将标准输出重定向到管道写端),父进程从管道中读取命令的执行结果。

  ②如果type为w,则表示父进程将数据写入管道,子进程从管道中读取数据作为命令执行的输入(内部将标准输入重定到到管道的读端)。

返回值

成功返回文件指针,出错返回NULL

功能

通过创建一个管道,调用fork()产生一个子进程,然后由子进程执行cmdstring命令。

函数

int pclose(FILE* fp);

返回值

cmdstring的终止状态,出错返回-1

功能

关闭管道

备注

  ①使用popen()创建的管道必须使用pclose()关闭。其实popen/pclose和标准文件输入/输出流中的fopen/fclose十分相似。

  ②封装管道的常用操作。

【编程实验】利用标准库操作管道的读写

//popen_rw.c

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>

/*利用管道操作文件*/

int main(void)
{
    FILE* fp;
    //命令执行的结果放置在fp指向的结构体缓存中
    fp = popen("cat /etc/passwd", "r");
    
    char buf[512];
    memset(buf, 0, sizeof(buf));
    while(fgets(buf, sizeof(buf), fp) != NULL){
        printf("%s", buf);
    }

    pclose(fp);

    printf("-----------------------------------------\n");
    //为wc命令提供统计的数据
    fp = popen("wc -l", "w");
    //向fp指向的缓存写入数据(因为type为"w",所以这些数据会被写入管道中)
    fprintf(fp, "line1\nline2\nline3\n");//提供3行的数据,作为wc要统计的数据来源!
    pclose(fp);

    return 0;
}

2.5 命令管道(FIFO)

(1)FIFO的创建

头文件

#include <sys/types.h>

#include <sys/stat.h>

函数

int mkfifo(const char* pathname, mode_t mode);

参数

①pathname:要创建的管道文件名

②mode:权限(mode % ~umask)

返回值

(1)成功返回0,出错返回-1

(2)FIFO相关出错信息

  ①EACCES(无存取权限)            ②EEXIST(指定文件不存在)

  ③ENAMETOOLONG(路径名太长)      ④ENOENT(包含的目录不存在)

  ⑤ENOSPC(文件系统剩余空间不足)  ⑥ENOTDIR(文件路径无效)

  ⑦EROFS(指定的文件存在于只读文件系统中)

备注

 

(2)注意事项

  ①只要对FIFO有适当访问权限,FIFO可用在任何两个没有任何关系的进程之间通信。

  ②本质上内核中的一块缓存,其在文件系统中以一个特殊的设备文件(管道文件)存在

  ③在文件系统中只有一个索引块存放文件的路径,没有数据块,所有数据存放在内核中。

  ④命名管道必须读或写同时打开否则单独读或单独写会引发阻塞

  ⑤命令mkfifo创建命名管道(命令内部调用mkfifo函数)

  ⑥对FIFO的操作与操作普通文件一样。

  ⑦一旦己经用mkfifo创建一个FIFO,就可以用open打开它,一般的文件I/O函数(close、read、write和unlink等)都可用于FIFO。

【编程实验】读写管道文件

(1)运行本例子中的两个进程之前,必须先mkfifo创建一个命名管道文件(如s.pipe)

(2)不管先运行读还是写的进程。如果命名管道只被打开一端(读或写),则另一个进程会被阻塞。可以通过先运行读或写进程来观察进程被阻塞的现象

//fifo_write.c ==>编译成单独的可执行文件

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

/*向命名管道写入数据*/

int main(int argc, char* argv[])
{
    if(argc < 2){
        printf("usage: %s fifo\n", argv[0]);
        exit(1);
    }

    printf("open fifo write...\n");
    //打开命名管道
    int fd = open(argv[1], O_WRONLY);
    if( fd < 0 ){
        perror("open error");
        exit(1);
    }else{
        printf("open fifo success: %d\n", fd);
    }

    char* s = "1234567890";
    size_t size = strlen(s);
    if(write(fd, s, size) != size){
        perror("write error");
    }

    close(fd);

    return 0;
}
/*输出结果:
 [root@localhost]# bin/fifo_write s.pipe //要先mkfifo s.pipe创建命名管道文件
 open fifo write...
 open fifo success: 3
 */

//fifo_read.c  ==>编译成单独的可执行文件

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <memory.h>

/*从命名管道中读取数据*/

int main(int argc, char* argv[])
{
    if(argc < 2){
        printf("usage: %s fifo\n", argv[0]);
        exit(1);
    }

    printf("open fifo read...\n");
    //打开命名管道
    int fd = open(argv[1], O_RDONLY);
    if(fd < 0){
        perror("open error");
        exit(1);
    }else{
        printf("open file sucess: %d\n", fd);
    }

    //从命名管道中读取数据
    char buf[512];
    memset(buf, 0, sizeof(buf));
    while(read(fd, buf, sizeof(buf)) < 0){
        perror("read error");
    }
    printf("%s\n", buf);

    close(fd);

    return 0;
}
/*输出结果:
 [root@localhost]# bin/fifo_read s.pipe
 open fifo read...
 open file sucess: 3
 1234567890
 */

2.6 匿名和命令管道的读写

(1)匿名管道和命名管道读写的相同点

相同点

说明

阻塞特性

默认都是阻塞性读写

网络通信

都适用于socket的网络通信

阻塞不完整管道

①单纯读时,在所有数据被读取后,read返回0,以表示到达了文件尾部。

②单纯写时,则产生信号SIGPIPE,如果忽略该信号或捕捉该信号并从处理程序返回,则write返回-1,同时errno设置为EPIPE。

阻塞完整管道

①单纯读时,要么阻塞,要么读取到数据

②单纯写时,写到管道满时会出错

非阻塞不完整管道

①单纯读时直接报错

②单纯写时,则产生信号SIGPIPE,如果忽略该信号或捕捉该信号并从处理程序返回,则write返回-1,同时errno设置为EPIPE。

非阻塞完整管道

①单纯读时直接报错。

②单纯写时,写到管道满时会出错。

(2)匿名管道和命名管道读写的不同点

不同点

说明

打开方式

打开方式不一致

设置阻塞特性

①pipe通过fcntl系统调用来设置O_NONBLOCK来设置非阻塞性读写。

②FIFO通过fcntl系统调用或者open函数来设置非阻塞性读写。

 

第11章进程间通信(1)_管道