Linux——进程间通信——命名管道

       

目录

一.命名管道

        1.1定义与区别

        1.2命名管道的原理

1.3命名管道的创建

1.4命名管道的实现

Comm.hpp 头文件:

Sever.cc代码(该进程是读取数据的):

Client.cc(该进程是写数据的):

1.4.2案例测试:

重新执行Sever:

1.4.3案例执行:

这里需要注意:

二.总结:


         在上一篇博客中,我讲述了有关进程间通信的三种方式,有System V、POSIX和管道。重点讲述了管道的来源,匿名管道的代码实现,以及在匿名管道中出现的多种情况案例分析。

(132条消息) Linux——进程通信_橙予清的zzz~的博客-CSDN博客icon-default.png?t=N5K3https://blog.csdn.net/weixin_69283129/article/details/131434925?spm=1001.2014.3001.5501

        匿名管道大多应用于父子进程间的数据通信,接下来我将介绍一下管道的另一种形式——命名管道。

一.命名管道

        1.1定义与区别

        命名管道,物如其名,那么这个管道是有名字的,它的作用是通过调用mkfifo系统调用函数的方式创建一个管道文件,在该管道文件内让两个毫无血缘关系的进程之间进行数据通信;而匿名管道的作用是通过使用pipe函数创建一个公共的没有名字的管道,然后通过使用fork函数创建出有血缘关系的子进程,让父子进程进行数据间的通信。

        命名管道(FIFO)区别于匿名管道的,它可以用于任意两个进程之间的通信,原因在于它提供了一个路径与特定文件相关联,以FIFO的文件形式存在于文件系统之中,这样即使是任意进程,只要访问这个路径,就能通过FIFO文件相互通信。

        FIFO即First In First Out ,意为先进先出,在计算机科学,它通常指一种特殊的数据结构——队列。在命名管道中,数据的读取顺序和写入顺序是一样的,即先写入的数据会被先读取。

        

        1.2命名管道的原理

        命名管道的原理还是像本质说的那样:想让不同进程之间实现通信,就必须让不同的进程看到同一份公共的资源。

        首先,不同的进程打开同一个文件,一定是指向同一个struct file结构体(进程属性:文件描述符表,它也是一个结构体),而不是各自拷贝一份struct file,这个容易理解,否则当第二个进程按照同一个路径打开这个文件时,文件就已经被打开过了。当然也不能让文件为了每个进程都加载到内存中,否则会出现很多重复的文件,效率也会因此而降低。

        而操作系统对于通信也是追求效率的,让磁盘和内存进行IO,速度太慢,所以科学家们在磁盘中定义了一种叫做命名管道文件,它是一种特殊文件,被多个进程打开的同时,还能保证不会将内存的数据刷新到磁盘,所以它不会被保存数据,大小永远为0。说白了它就像是一个临时的文件一样。该文件在系统路径中,因此这个路径具有唯一性,不同的进程通过同一条路径看到同一个管道文件从而共享同一份公共资源。

        

命名管道就是让两个不同的独立进程,看到同一份资源!那它是怎么做到的?

答:命名管道可以让两个进程打开指定名称的(路径+文件名称)同一个文件

        这也是OS采用相对路径和绝对路径不冲突的原因,而且使用绝对路径来标定某些文件的位置,这是绝对路径的唯一性决定的。实际上,文件路径展开后就是一颗树,从根结点到任意节点是绝对路径,它不会和其他任意路径相交,因此它具有唯一性。


        结合匿名管道的特性,不同进程通过同一条路径找到的文件是同一个文件,这个文件是管道文件。在本质上命名管道和匿名管道都是文件,而且都是内存文件,但前者有磁盘实例,后者无磁盘实例。


        井且还有一个重要的特点:当我们向命名管道中写入数据时候,该数据并不会刷新到磁盘中的命名管道文件,当我们从命名管道中读取数据时候,也不会是从磁盘文件的命名管道读取,而是从打开的命名管道读取数据;这么做就是为了效率!所以往磁盘文件中写入内容的效率远不如通过管道向文件写入内容。


1.3命名管道的创建

        命名管道被创建的方式有两种:一是在程序内通过调用mkfifo函数生成、另一种是使用mkfifo指令生成。

方式1:

         通过mkfifo函数的两个参数可知:第一个参数是需要填文件的路径的,第二个参数表示文件的权限,这个和umask掩码权限关联,在Linux中,umask默认是002,若不想用系统默认的umask,可以重新设置。

        mkfifo函数,成功则在磁盘中创建出一个管道文件,返回0;创建失败则返回-1

方式2:

1.4命名管道的实现

        案例说明:该案例需要用到两个进程,一个进程进行管道文件的创建和删除,且采用只读的方式打开管道文件——即从管道文件中读取数据;另一个进程只管采用write的方式去open管道文件——既向管道文件中写入数据。

        这就达到了一个进程写,一个进程读的目的,实现了数据通信。

Comm.hpp 头文件:

        在显示代码前,我来介绍一个系统调用函数:

       unlink()会删除参数pathname 指定的文件. 如果该文件名为最后连接点, 但有其他进程打开了此文件, 则在所有关于此文件的文件描述词皆关闭后才会删除. 如果参数pathname 为一符号连接, 则此连接会被删除。

返回值:成功则返回0, 失败返回-1, 错误原因存于errno

#pragma once
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string>
#include<cstring>
#include<cerrno>
#include<cstdlib>
using namespace std;
#define NAME_ "./tmp/pipe.106"    //采用宏定义的方式确定管道的路径
#define NUM 5

//创建管道函数
bool Create_(const std::string& path){
    umask(0);    //设置文件权限的掩码为0

    int n=mkfifo(path.c_str(),0666);
    if(n<0){
        cerr<<"error: "<<strerror(errno)<<endl;
        return false;
    }

    else{   
            cout<<"命名管道开辟成功!欢迎使用! "<<endl;
            return true;
    }
}

 //删除管道文件函数
void Remove(const std::string& path){
    int n=unlink(path.c_str());     //等到该文件的文件描述符都被关闭后才会被删除
    assert(n==0);                    //确保unlink调用成功
    (void)n;
}

Sever.cc代码(该进程是读取数据的):

#include "Comm.hpp"


int main(){
    //创建管道文件
    Create_(NAME_);

    //用读方式访问管道文件
    int rfd=open(NAME_,O_RDONLY);
    if(rfd<0){
        cerr<<"errno:"<<strerror(errno)<<endl;  //访问失败返回错误
        exit(-1);
    }

    //访问成功
    else{

        char buffer[1024];
        int cnt=0;
        //采用循环的方式不断的从管道中读取数据,并打印数据
        while(true){
            ssize_t r=read(rfd,buffer,sizeof(buffer)-1);

            //判断read的返回值,若大于0,表明读取成功
            if(r>0){
                buffer[r]=0;
                buffer[strlen(buffer)-1]=0;
                cout<<" Had received:#  "<<buffer<<endl;
            }

            //若等于0,表明管道中已经没有数据可读了,上一次已经读完了,直接退出循环
            else if(r==0){
                cout<<"Client had exited! ,me too!"<<endl;
                break;
            }
            //若小于0,表明管道中,数据读取错误,也直接跳出循环
            else{
                cout<<"Client exception, I quit right now! "<<endl;
                break;
            }
        }

        cout<<"Sever 正常退出!"<<endl;
    }

    //删除管道文件
    Remove(NAME_);
}

Sever.cc文件就是负责管道文件的创建和销毁的。 

Client.cc(该进程是写数据的):

#include "Comm.hpp"

int main(){
    //以写的方式打开管道文件
    int wfd=open(NAME_,O_WRONLY);
    if(wfd<0){
        cerr<<"error: "<<strerror(errno)<<endl;
        exit(-1);
    }

    //打开成功
    else{
        char buffer[1024];
        int cnt=0;
        while(true){
            cout<<"请输入您想输入的内容:"<<endl;
            fgets(buffer,sizeof(buffer),stdin);     //使用fgets函数可以将键盘中输入的内容输入C缓冲区
            write(wfd,buffer,strlen(buffer));       //将缓冲区内容写入管道中
        }
    }
    return 0;
}

1.4.2案例测试:

        首先先来运行一下Sever的可执行文件,先确保一下管道文件是否能被成功创建! 

 

 运行后Sever.cc的可执行文件后发现,管道文件被创建成功!

 但是第二次运行该可执行文件时,发现运行失败,原因是管道文件已存在!

        这是mkfifo函数的弊端,mkfifo是创建一个管道文件,成功则返回0,且会在指定路径下创建出一个管道文件。而第二次运行server可执行文件时,第一次的server运行已经在指定路径中创建出了管道文件,而第二次server又进行了一次管道文件的创建,同名文件不能在一个路径下创建两次,所以第二次的server运行失败,就是因为该文件在同一路径下创建两次导致的,若还想运行server,就需要先删除管道文件。

重新执行Sever:

1.4.3案例执行:

        我使用了两个端口进行进程通信的演示,左端口为读取进程,右端口为写进程:

1.首先运行Sever可执行文件,因为需要先创建出管道文件;

2.其次再运行Client可执行文件 ;

3.然后在Client那一端口就输入想要输入的内容到管道,回车确定;

4.紧接着Sever端口就会从管道中读取数据到显示屏;

       

这里需要注意:

        1.当Client不输入内容时,Server会处于阻塞状态,它将一直等待Client输入内容,Server才会继续进行读取。

        2.当Client写进程停止写入一一即关闭写端时,读端Server在读取完管道的所有内容后,系统也会随着写端关闭而正常结束while循环,进而正常关闭Server进程,这就是rfd返回值为0的情况。

        3.当Client写进程在写入的过程中,我们强制关闭了Server读进程,那么写端就会被强制关闭——异常终止Client写进程,读端被提前关闭,写端再写入就是非法的,所以会报异常退出!如下:


二.总结:

        命名管道(FIFO)最大的特性就是每个FIFO都有一个路径名与之相关联,从而允许无亲缘关系的任意两个进程间通过FIFO进行通信。所以FIFO有以下特性:

1.和管道一样,FIFO仅提供半双工的数据通信,即只支持单向的数据流;
2.和管道不同的是,FIFO可以支持任意两个进程间的通信;
3.FIFO 在文件系统中作为一个特殊的文件而存在,但 FIFO 中的内容却存放在内存中;

4.当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用FIFO 有名字,不相关的进程可以通过打开命名管道进行通信。

猜你喜欢

转载自blog.csdn.net/weixin_69283129/article/details/131442422