zoukankan      html  css  js  c++  java
  • 第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
    ", 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
    ", 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("
    write-end of pipe closed.
    ");
                    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
    ");
        }
    }
    
    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
    ", 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("-----------------------------------------
    ");
        //为wc命令提供统计的数据
        fp = popen("wc -l", "w");
        //向fp指向的缓存写入数据(因为type为"w",所以这些数据会被写入管道中)
        fprintf(fp, "line1
    line2
    line3
    ");//提供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
    ", argv[0]);
            exit(1);
        }
    
        printf("open fifo write...
    ");
        //打开命名管道
        int fd = open(argv[1], O_WRONLY);
        if( fd < 0 ){
            perror("open error");
            exit(1);
        }else{
            printf("open fifo success: %d
    ", 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
    ", argv[0]);
            exit(1);
        }
    
        printf("open fifo read...
    ");
        //打开命名管道
        int fd = open(argv[1], O_RDONLY);
        if(fd < 0){
            perror("open error");
            exit(1);
        }else{
            printf("open file sucess: %d
    ", fd);
        }
    
        //从命名管道中读取数据
        char buf[512];
        memset(buf, 0, sizeof(buf));
        while(read(fd, buf, sizeof(buf)) < 0){
            perror("read error");
        }
        printf("%s
    ", 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函数来设置非阻塞性读写。

  • 相关阅读:
    单例
    淘宝在数据处理领域的项目及开源产品介绍 | 岭南六少
    数据库垂直拆分,水平拆分利器,cobar升级版mycat
    基于Gtid的mysql主从复制 和 mysql-proxy 读写分离
    几行lua代码计算http包总长度_指甲锉_新浪博客
    使用Lua和OpenResty搭建验证码服务器
    测试比json更快更小的二进制数据传输格式Msgpack [pythono MessagePack 版本]
    使用 HAProxy, PHP, Redis 和 MySQL 轻松构建每周上亿请求Web站点
    nginx+lua+redis实现验证码防采集
    OpenResty(Nginx)+Lua+GraphicsMagick实现缩略图功能 | 琥珀志
  • 原文地址:https://www.cnblogs.com/5iedu/p/6579907.html
Copyright © 2011-2022 走看看