进程间通信方式——管道

进程间通信

  • 1.进程间通信概念及方式
  • 2.管道
    • 2.1 管道概念
    • 2.2 管道的原理
    • 2.3 管道的局限性
    • 2.4 管道的优缺点
  • 3.管道创建与应用
    • 3.1 pipe函数创建并打开
    • 3.2 程序实现
    • 3.3 管道的读写行为
      • 3.3.1 读管道
      • 3.3.2 写管道
      • 3.3.3 程序实例1
      • 3.3.4 程序实例2
  • 4.管道缓冲区大小
  • 5.命名管道FIFO
  • 6.命名管道与匿名管道的区别
  • 参考

1.进程间通信概念及方式

  ​linux系统下,进程空间地址相互独立,每个进程各自有不同的用户地址。任何一个进程的全局变量在另有一个进程中都看不到,所以进程和进程间不能互相访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1的数据从用户空间拷贝至内核缓冲区,进程2再从缓冲区把数据读取,内核提供的这种机制称为进程间通信(IPC)。

进程间通信方式——管道_第1张图片
​  在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。

​  主要方式有:

​     ① 管道 (使用最简单)

​     ② 信号 (开销最小)

​     ③ 共享映射区 (无血缘关系)

​     ④ 本地套接字 (最稳定)

  本文以下内容主要针对的是管道进行展开,其它进程间通信方式将在以后的文章中讲解。

2.管道

2.1 管道概念

​  管道是一种最基本的 IPC 机制,作用于有血缘关系进程之间,完成数据传递。调用 pipe 系统函数(或者用mkfifo命令)即可创建一个管道。有如下特质:

  1. 其本质是一个伪文件(实为内核缓冲区)

  2. 由两个文件描述符引用,一个表示读端,一个表示写端。

  3. 规定数据从管道的写端流入管道,从读端流出。

2.2 管道的原理

  管道实为内核使用环形队列机制(FIFO),借助内核缓冲区(4k)实现。

2.3 管道的局限性

​   1. 数据不能进程自己写,自己读。

​   2. 管道中数据不可反复读取。一旦读走,管道中不再存在。

​   3. 采用半双工通信方式,数据只能在单方向上流动。

   4.只能在有公共祖先的进程间使用管道。

2.4 管道的优缺点

优点:

  简单,相比信号,套接字实现进程间通信,简单很多。

缺点:

  1. 只能单向通信,双向通信需建立两个管道。

  2. 只能用于父子、兄弟进程(有共同祖先)间通信。该问题后来使用 fifo 有名管道解决。

3.管道创建与应用

3.1 pipe函数创建并打开

函数原型:

int pipe(int pipefd[2]); 

参数:
  fd[0] : r 读端

  fd[1] : w 写端

返回值:

  成功:0;失败:-1,设置 errno

注:函数调用成功返回 r/w 两个文件描述符。无需 open,但需手动 close。

  管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。实现父子进程间通信通常可以采用如下步骤:
进程间通信方式——管道_第2张图片

3.2 程序实现

int main (int argc,char *argv[])
{
    int ret;
    int fd[2];
    char buf [1024];
    pid_t pid;
    ret =pipe(fd);
    if(ret==-1)
    {
        sys_err("pipe error");
    }
    pid =fork();
    if(pid>0)
    {   //父进程写  父进程关闭管道读端
        close(fd[0]);
        write((fd[1]),"hello pipe",strlen("hello pipe"));
        sleep(1);
        close(fd[1]);
    }
    else if (pid ==0)
    {
        //子进程读   子进程关闭管道写端
        close(fd[1]);
        ret=read(fd[0],buf,sizeof(buf));
        write(STDOUT_FILENO,buf,ret);
        close(fd[0]);
    }
    return 0;
}

3.3 管道的读写行为

3.3.1 读管道

  1. 管道中有数据,read 返回实际读到的字节数。

  2. 管道中无数据:

    (1) 管道写端被全部关闭,read 返回 0 (好像读到 文件结尾

    (2) 写端没有全部被关闭,read 阻塞等待(不久的 将来可能有数据递达,此时会让出 cpu)

3.3.2 写管道

  1. 管道读端全部被关闭, 进程异常终止(也可使用捕捉 SIGPIPE 信号,使进程不终止)

  2. 管道读端没有全部关闭:

    (1) 管道已满,write 阻塞。

    (2) 管道未满,write 将数据写入,并返回实际写入的字节数。

3.3.3 程序实例1

  使用管道实现父子进程间通信,完成:ls | wc –l。假定父进程实现 ls,子进程实现 wc。ls 命令正常会将结果集写出到 stdout,但现在会写入管道的写端;wc –l 正常应该从 stdin 读取数据,但此时会从管道的读端读。

代码:

#include
#include
#include
#include 
#include 
using namespace std;
void sys_err(const char *str)
{
    perror(str);
    exit(1);
}
int main (int argc,char *argv[])
{
    int ret;
    int fd[2];
    char buf [1024];
    pid_t pid;
    //创建管道
    ret =pipe(fd);
    //判断是否创建成功
    if(ret==-1)
    {
        sys_err("pipe error");
    }
    //创建子进程
    pid=fork();
    if(pid==-1)//创建子进程失败
    {
        perror("fork error");
        exit(1);
    }
    //实现从父进程向子进程通信,父进程写管道打开,读管道关闭; 子进程读管道打开,写管道关闭
    else if(pid>0)//父进程
    {
        //父进程关闭读端
       close(fd[0]);
       //将标准输出文件描述符指向父进程管道写端
       dup2(fd[1],STDOUT_FILENO);
       //利用execlp函数执行指定程序命令
       execlp("ls","ls",NULL) ;
    }
    else if (pid==0)//子进程
    {
        //子进程关闭写端
        close(fd[1]);
        //将标准输入文件描述符指向子进程管道读端
        dup2(fd[0],STDIN_FILENO);
        //利用execlp函数执行指定程序命令
        execlp("wc","wc","-l",NULL);
       
    }
    return 0;
}

运行结果:
进程间通信方式——管道_第3张图片

3.3.4 程序实例2

  使用管道实现兄弟进程间通信。 兄:ls 弟: wc -l 父:等待回收子进程。

  要求:使用“循环创建 N 个子进程”模型创建兄弟进程,使用循环因子 i 标示。注意管道读写行为。

代码:

#include
#include
#include
#include 
#include 
using namespace std;
void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main (int argc,char *argv[])
{
    int i,ret;
    int fd[2];
    char buf [1024];
    pid_t pid;
    //创建管道
    ret =pipe(fd);
    //判断是否创建成功
    if(ret==-1)
    {
        sys_err("pipe error");
    }
    //创建子进程   循环创建
    for(i=0;i<2;i++)   //表达式2出口   父进程使用
    {
        pid =fork();
        if(pid==-1)//创建子进程失败
         {
        perror("fork error");
        exit(1);
         }
        if (pid==0)  //子进程出口
            break;
    }
    if(i==2)//父进程
    {
       close(fd[0]);
       close(fd[1]);
       wait (NULL);
       wait (NULL);
    }
    else if (i==0)//兄进程
    {
        close(fd[0]);
       //将标准输出文件描述符指向兄进程管道写端
       dup2(fd[1],STDOUT_FILENO);
       //利用execlp函数执行指定程序命令
       execlp("ls","ls",NULL) ;
    }
     else if (i==1)//弟进程
    {
        close(fd[1]);
        //将标准输入文件描述符指向弟进程管道读端
        dup2(fd[0],STDIN_FILENO);
        //利用execlp函数执行指定程序命令
        execlp("wc","wc","-l",NULL);
    }
    return 0;
}

运行结果:

进程间通信方式——管道_第4张图片

4.管道缓冲区大小

  可以使用 ulimit –a 命令来查看当前系统中创建管道文件所对应的内核缓冲区大小。通常为:

pipe size (512 bytes, -p) 8

  也可以使用 fpathconf 函数,借助参数 选项来查看。使用该宏应引入头文件

<unistd.h>long fpathconf(int fd, int name);

成功:返回管道的大小

失败:-1,设置 errno

5.命名管道FIFO

  FIFO 常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间。但通过 FIFO,不相关的进程也能交换数据。

  FIFO 是 Linux 基础文件类型中的一种。但FIFO 文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行 read/write,实际上是在读写内核通道,这样就实现了进程间通信。

创建:

  1. ​命令行:mkfifo 管道名

  2. 库函数:int mkfifo(const char *pathname, mode_t mode); 成功:0; 失败:-1

6.命名管道与匿名管道的区别

  1. 在通信上,命名管道提供了一个路径名与之关联,以FIFO文件的形式存储于文件系统中,能够实现任何两个进程之间通信。而匿名管道对于文件系统是不可见的,它仅限于在父子进程之间的通信。原因在于:匿名管道内核中的缓冲区没有标识符,(没有管道标识符)所以其他进程无法使用。而命名管道内核中的缓冲区有标识符
  2. 在使用上,命名管道创建完成后就可以使用,其使用方法与管道一样,区别在于:命名管道使用之前需要使用open()打开。这是因为:命名管道是设备文件,它是存储在硬盘上的,而管道是存在内存中的特殊文件。但是需要注意的是,命名管道调用open()打开有可能会阻塞,但是如果以读写方式(O_RDWR)打开则一定不会阻塞;以只读(O_RDONLY)方式打开时,调用open()的函数会被阻塞直到有数据可读;如果以只写方式(O_WRONLY)打开时同样也会被阻塞,知道有以读方式打开该管道。

参考

https://blog.csdn.net/qq_33951180/article/details/68959819

你可能感兴趣的:(Linux系统,linux)