管道

管道是Linux中实现进程间通信(IPC)的方式之一,分为无名管道和命名管道(FIFO)。无名管道是匿名的,适用于父子进程间通信,而命名管道允许非亲缘进程间的通信。管道具有单向通信特性,且数据读取后即被移除。在使用管道时,需要考虑其读写端的管理以及阻塞与非阻塞模式的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

进程之间通讯(IPC,interprocess communication)

管道

管道是内核维护的一种特殊的文件,主要用来作为进程间传递数据的主要方式,分为无名管道和命名管道(FIFO)。在shell里面的一些过滤程序,就是管道,比如|grep

管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

无名管道

  1. 创建匿名管道的函数:

        #include <unistd.h>
        int pipe(int fd[2]);
    • 功能:创建一个匿名管道

    • 参数:fd[2]用来返回管道两端的文件描述符;

      • fd[0]:指向无名管道读端;

      • fd[1]:指向无名管道的写端。

    • 返回值:

      • 成功:返回0;

      • 失败:返回-1,将错误记录在errno中。

  2. 管道的实现过程:

    • 父进程使用函数pipe创建一个管道,得到两个文件描述符,fd[0],fd[1]前者用来读,后者用来写。分别代表了管道的读端和写端。如下图所示:

      pipe1

    • 父进程fork一个子进程,由于子进程复制了父进程的文件描述符,因此同一个管道同时有两个读端和写端,如下图所示:

      pipe2

    • 关闭不需要的读端和写端,这个的根据子进程和父进程谁读谁写来决定。最终,管道只有一个读端,一个写端,数据从写端流到读端。父进程写子进程读的情况如下:

      pipe3

  3. 匿名管道特点:

    • 管道通信是单向的,有固定的读端和写端。

    • 数据被进程从管道读出后,在管道中该数据就不存在了。

    • 当进程去读取空管道的时候,进程会阻塞。

    • 当进程往满管道写入数据时,进程会阻塞。

    • 管道容量为64KB(#define PIPE_BUFFERS 16 include/linux/pipe_fs_i.h)

  4. 对匿名管道的读写:

    • 创建好的管道,如果写端被关闭,则读端在读完数据之后,read返回0,表示文件结束。

    • 写一个读端已经被关闭的管道,会产生SIGPIPE信号,如果自己使用signal定义了对该信号的处理函数,或者忽略,则write会返回-1,并且将errno设置为EPIPE

    • 常量PIPE_BUF规定了内核缓冲区的大小,当读写的数据小于这个值,则读写操作都是原子的,不会被其他同时对这个管道读写的操作交叉。

  5. 匿名管道的局限性:

    • 一般情况下是半双工的(数据只能在一个方向上流动),有的系统也提供全双工的管道。

    • 只能是由共同祖先的两个进程之间通信,一般用于父子进程之间。

  6. 标准I/O库提供的创建方法:

    
    #include <stdio.h>
    
    FILE* popen(const char* cmdstring, const char* type);
    • 功能:父进程以type的权限创建一个管道,并在子进程中执行cmdstring指令,并将子进程的管道连接到标准输入(type = w),标准输出(type = r)。popen先调用fork创建子进程,在子进程中调用exec函数执行cmdstring

      • cmdstringshell执行shell -c cmdstring

      popen1

      popen2

    • 参数:

      • cmdstring:实际上调用的是excel("/bin/sh","sh", "-c","cmdstring",NULL);

      • type:只能是读或者写,针对父进程创建管道返回的FILE*指针对应文件的权限。

    • 返回值:

      • 成功:一个文件指针。

      • 失败: 返回NULL.

        
        #include <stdio.h>
        
        int pclose(FILE *fp);
    • 功能:关闭管道,等待子进程结束的waitpid()就在这里实现。

    • 返回值:

      • 成功:返回cmdstring的终止向状态

      • 失败:返回-1

协同进程

  1. 协同进程就是他的输入输出都被管道连接到同一个进程的过滤程序。
    coprocess
  2. 实例
    //copress.c
    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <string.h>
    #include <signal.h>
    #include <errno.h>
    #define LINE_MAX 100
    #define info() {                                                              \
        FILE* fp = fopen("add2.log","a+");                                        \
        if(fp == NULL)                                                            \
            exit(-1);                                                             \
        fprintf(fp, "%s func  %s, line  %d: ",__FILE__, __FUNCTION__, __LINE__ ); \
        fprintf(fp, ": %s\n", strerror(errno));                                   \
        fprintf(fp, "fp: %d\n",stdin->_fileno);                                   \
        fflush(fp);                                                               \
    }
    void handler(int signo){
        printf("caught sigpipe\n");
    }
    int main(){
        int fd1[2],fd2[2];
        pid_t pid;
        char line[LINE_MAX +1];
        if(signal(SIGPIPE, handler) == SIG_ERR){
            perror("signal error");
        }
        if(pipe(fd1) < 0 || pipe(fd2)){
            perror("Create Pipe Error");
        }
        if((pid = fork()) < 0){
            perror("Fork Error");
        }
        else if(pid > 0){
            // parent  
            close(fd1[0]);
            close(fd2[1]);
            while(fgets(line, LINE_MAX,stdin) != NULL){
                info();
                int n = strlen(line);
                if(write(fd1[1],line,n) != n){
                    perror("Parent Write Error");
                }
                // sleep(2);
                info();
                if((n = read(fd2[0], line, LINE_MAX)) < 0){
                    perror("Parent Read Error");
                }
                if(n == 0){
                    printf("child closed pipe\n");
                    break;
                }
                info();
                line[n] = 0;
                if(fputs(line, stdout) == EOF){
                    perror("Parent Stdout Error");
                }
                info();
            }
            if(ferror(stdin)){
                printf("fets error\n");
            }
            exit(0);
        }
        else{
            //child
            close(fd1[1]);
            close(fd2[0]);
            if(fd1[0] != STDIN_FILENO){
                if(dup2(fd1[0],STDIN_FILENO) != STDIN_FILENO){
                    exit(-1);
                }
                close(fd1[0]);
            }
            if(fd2[1] != STDOUT_FILENO){
                if(dup2(fd2[1],STDOUT_FILENO) != STDOUT_FILENO){
                    exit(-1);
                }
                close(fd2[1]);
            }
            execl("./add2","add2",(char*)0);
            exit(-1);
        }
        return 0;
    }
    //add2.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <errno.h>
    #include <syslog.h>
    #define LINE_MAX 100
    #define info() {                                                              \
        FILE* fp = fopen("add2.log","a+");                                        \
        if(fp == NULL)                                                            \
            exit(-1);                                                             \
        fprintf(fp, "%s func  %s, line  %d: ",__FILE__, __FUNCTION__, __LINE__ ); \
        fprintf(fp, ": %s\n", strerror(errno));                                   \
        fprintf(fp, "fp: %d\n",stdin->_fileno);                                   \
        fflush(fp);                                                               \
    }
    // 自己写的例子
    int main(){
        char line[LINE_MAX+1];
        int int1,int2;
        info();
        while(fgets(line,LINE_MAX,stdin) != NULL){
            info();
            if(sscanf(line,"%d%d",&int1,&int2) == 2){
                info()
                sprintf(line,"%d\n", int1 + int2);
                info();
                line[strlen(line)] = '\0';
                if(fputs(line,stdout) == EOF){ 
                    perror("Fputs Error");
                }
                //由于管道是全缓冲,如果不刷新,则有不会立即写在管道缓冲区里面,导致父进程读不到数据而阻塞,同时,本程序下一次获取数据时候也得不到数据而阻塞,导致死锁
                fflush(stdout); 
                info();
            }
            else{
                info();
                printf("invalid parameter.\n");
                fflush(stdout);
                // write(STDOUT_FILENO, "invalid parameter\n",18);
                info();
            }
        }
        info();
        exit(0);
    }
    //apue书上例子
    // int main(){
    //     char line[LINE_MAX+1];
    //     int int1,int2,n;
    //     info();
    //     while((n = read(STDIN_FILENO, line, LINE_MAX)) > 0){
    //         line[n] = 0;
    //         info();
    //         if(sscanf(line,"%d%d",&int1,&int2) == 2){
    //             sprintf(line,"%d\n", int1 + int2);
    //             n = strlen(line);
    //             if(write(STDOUT_FILENO, line,n) != n){
    //                 perror("Fputs Error");
    //             }
    //             info();
    //         }
    //         else{
    //             info();
    //             perror("invalid parameter.");
    //         }
    //     }
    //     exit(0);
    // }

命名管道(FIFO)

  1. 命名管道是一种可以在非亲缘进程之间进行通信的一种方式。命名管道类似于匿名管道,都是有内核维护的一块缓冲区,不在文件系统之中,文件系统知识记录了命名管道的路劲,因此可以在任意两个进程之间使用。

  2. 命名管道的相关操作:

        #include <sys/types.h>
        #include<sys/stat.h>
        int mkfifo(const char *pathname, mode_t mode)
    • 函数功能:创建有名管道(FIFO文件)。创建命名管道之后要打开才能使用。

    • 参数说明:

      • const char *pathname:文件名(可以带路径);

      • mode_t mode:创建FIFO文件的访问权限。设置用户、组群、其他的权限(0666)

      • 返回值:

        • 创建成功:返回0;

        • 创建失败:返回-1,同时将错误记录在errno。

              #include <sys/stat.h>
              #include <fcntl.h> //定义了 AT_*常量
              mkfifoat(int fd, const char* pathname, mode_t mode);
    • 函数功能:创建有名管道(FIFO文件)。

    • 参数说明:

      • 待查看的文件名是由fdpathname共同决定的。

        • 如果pathname是个绝对路径,则忽略fd参数

        • 如果pathname是个相对路径路径,且 fd=AT_FDCWD,则在当前工作目录的路径下查找pathname

        • 如果pathname是个相对路径路径,且 fd!=AT_FDCWD,则在fd对应的打开目录下查找pathname

      • mode_t mode:创建FIFO文件的访问权限。一般为只读(O_RDONLY),只写(O_WR_ONLY),可以设置非租塞模式。

    • 返回值:

      + 创建成功:返回0;
      
      + 创建失败:返回-1,同时将错误记录在errno。
      
    • 命名管道的打开,读, 写就和正常的文件的读写是一样的。可以使用第三章的函数来操作。

      • int unlink(const char *pathname);

        • 头文件:#include <unistd.h>

        • 函数功能:从文件系统删除文件。

          • +
        • 参数说明:

        • 返回值:

          • 创建成功:返回0;

          • 创建失败:返回-1,同时将错误记录在errno。

  3. 命名管道打开规则:

    • open设置了非阻塞标志O_NONBLOCK

      • 读打开:若已经有相应进程写打开FIFO,则当前打开操作将成功返回,否则成功返回。

      • 写打开:若已经有相应进程读打开FIFO,则当前打开操作将成功返回,否则打开失败,返回错误ENXIO

    • open没有设置非阻塞标志O_NONBLOCK

      • 读打开:若已经有相应进程写打开FIFO,则当前打开操作将成功返回,否则阻塞到有进程写打开为止。

      • 写打开:若已经有相应进程读打开FIFO,则当前打开操作将成功返回,否则阻塞到有进程读打开为止。

  4. 命名管道的读写操作规则:

    • 命名管道只有读端和写端都打开的时候才能进行数据传输,否则一般情况下会阻塞直到另外一端打开为止。

    • 阻塞读打开FIFO成功:说明已经有进程写打开了FIFO

      • 有写进程打开FIFO,且FIFO中没有数据,则读操作阻塞。写进程结束,读操作认为读到了文件结束符。
    • 非阻塞读打开FIFO:无论是否有写进程打开了FIFO,都成功。

      • 有写进程打开FIFO,且FIFO中没有数据,读操作返回-1,当前errno值为EAGAIN,提醒以后再试。

      • 没有写进程打开FIFO,且FIFO中没有数据,

    • 阻塞写打开FIFO成功:说明已经有进程读打开了FIFO

      • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。

      • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。

    • 非阻塞写打开FIFO:

      • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。FIFO有剩余空间时,在写满所有FIFO空闲缓冲区后,写操作返回;FIFO没有剩余空间,则失败返回EAGAIN

      • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。

        • 如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;

        • 如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写;

  5. 其他:

    • 写一个没有读打开的FIFO, 会产生SIGPIPE信号,读一个最后一个写进程关闭了的FIFO会产生一个文件结束符。

    • 读打开的阻塞标志只对本进程第一个读操作施加作用,如果本进程内有多个读操作序列,则在第一个读操作被唤醒并完成读操作后,其它将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO中没有数据也一样(此时,读操作返回0)。

    • 造成阻塞的原因有两种:

      • 当前FIFO内有数据,但有其它进程在读这些数据;

      • FIFO有进程写打开,且FIFO内没有数据。(写打开进程结束则不再阻塞)

  6. 实例

        //fifo_server.c
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <fcntl.h>
    #include <errno.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <string.h>
    #define BUF_MAX 8192
    #define fifo "./fifo"
    int main(){
        int fd, nrev,bytes_wr;
        char buf_wr[BUF_MAX];
        int flag = O_WRONLY;
    #ifdef NBLOCK
        flag |= O_NONBLOCK;
    #endif
    #ifdef ATOMIC
        bytes_wr = 2048;
    #else
        bytes_wr = 4096 + 1024;
    #endif
        memset(buf_wr, 42, BUF_MAX);
        if(mkfifo(fifo, 0666) < 0){
            perror("Create FIFO Error");
        }
        if((fd = open(fifo,flag)) < 0){
            perror("Open FIFO Failed");
        }
        if((nrev = write(fd, buf_wr,bytes_wr)) < 0){
            if(errno == EAGAIN){
                perror("Try Later");
            }
            perror("Write FIFO Error");
        }
        else
            printf("Write %d Bytes To FIFO\n", nrev);
        // pause();
        unlink(fifo);
        return 0;
    }
    //fifo_client.c
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <fcntl.h>
    #include <errno.h>
    #include <stdio.h>
    #include <string.h>
    #include <stdio.h>
    #include <unistd.h>
    #define BUF_MAX 8192
    #define fifo "./fifo"
    int main(){
        int fd, nrev,bytes_rd;
        char buf_rd[BUF_MAX];
        int flag = O_RDONLY;
    #ifdef NBLOCK
        flag |= O_NONBLOCK;
    #endif
    #ifdef ATOMIC
        bytes_rd = 1000;
    #else
        bytes_rd = 4096 + 1024;
    #endif
        if((fd = open(fifo,flag)) < 0){
            perror("Open FIFO Failed");
        }
        while(1){
            memset(buf_rd, 0, sizeof(buf_rd));
            if((nrev = read(fd, buf_rd,bytes_rd)) < 0){
                if(errno == EAGAIN){
                    perror("NO DATA IN FIFO");
                }
                perror("Read FIFO Error");
            }
            else{
                printf("Read %d Bytes From FIFO: \n", nrev);
                printf("\t%s\n",buf_rd);
            }
            sleep(1);
        }
        return 0;
    }

参考链接:
Linux环境进程间通信(一)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值