自学内容网 自学内容网

[Linux] 进程间通信——匿名管道&&命名管道

标题:[Linux] 进程间通信——匿名管道&&命名管道

@水墨不写bug

(图片来源于网络)

目录

一、进程间通信

 二、进程间通信的方案——匿名管道

(1)匿名管道的原理

(2)使用匿名管道

三、进程间通信的方案——命名管道

(1)认识管道文件

(2)使用命名管道


 正文开始:

一、进程间通信

        当你参加场竞赛,你一定需要与你的队友密切配合,这样才能高效的完成任务。然而,配合的前提是你们知道彼此现在的状况,就是说,你们需要尽可能的共享信息,而这一过程不就是通信吗?于是,得出结论:

        配合的前提是通信。

        一台计算机,是被操作系统管理着(操作系统是软硬件资源的管理者),而进程间由于具有独立性,体现为每个进程都有自己的进程地址空间,这就意味着多个进程之间相互的数据不可见(所有的数据,不论局部或者全局),于是想要实现进程间的相互通信,通常是比较困难的(成本比较高)。

 图1(进程间具有独立性)

        进程间通信的前提是让不同的进程看到同一份资源,这份资源(称为 "共享资源"),不是属于某一个进程,而是操作系统这个中间人提供的资源(本质就是一段内存)。

        由于操作系统不相信任何人,于是操作系统提供了一系列的系统调用,用来创建共享资源。这同时也意味着:操作系统提供了很多接口,调用不同的接口,会创建不同类型的共享资源。这些不同的共享资源就有:

        管道(包括匿名管道、命名管道)SystemV IPC的共享内存、消息队列、信号量

本文要讲解的就是头一种进程间通信的方式:管道

 二、进程间通信的方案——匿名管道

(1)匿名管道的原理

进程打开文件时,发生了什么? 

        当进程以读或者的方式打开一个文件的时候,操作系统会在内存中创建一个结构体——struct file,这个结构体用来维护被打开的文件:

        标准输入,标准输出,标准错误被默认打开(文件结构体数组——fd数组的0,1,2被默认的三个“文件”占用):

图2(进程与被打开的文件对应创建的结构体)

这时,如果进程B以读的方式打开一个文件:比如调用了C的fopen函数以r的方式打开,具体结果就会成为这样:

此时,再以w方式打开同一个文件:

(以读方式打开文件后,再次以写方式打开,会创建一个专门用于write的structfile,但文件的内核级数据会沿用read structfile的同一份)——这是匿名管道的基本原理条件。

        此时fork创建子进程,子进程会“共享”父进程的代码和数据(包括fd_array),于是,可以表示为:

如果让父进程关闭r,子进程关闭w,那么: 这样,不酒满足两个进程看到同一份内核级缓冲区了吗?

这就是匿名管道的原理。


(2)使用匿名管道

头文件:<unistd.h>

函数原型:

 使用:

        调用pipe时,传入一个数组类型int [2],将带出两个文件fd,一个是读fd ,下标为[0],一个是写fd,下标为[1]。这两个fd就是匿名管道内核级缓冲区的fd,由于匿名管道没有文件路径和文件名,所以称为“匿名管道”。

 注意:

        匿名管道只能用于进行具有血缘关系的进程之间进行通信,常用于父子进程之间进行通信。

        管道内部,自带进程之间的同步机制。

        管道文件的生命周期随进程。

        管道文件在通信的时候,是面向字节流的,write次数和read次数不是一一匹配的。

        管道的通信模式,是特殊的半双工模式。

使用时,四种特殊情况:

        管道文件为空 && write fd没有关闭,读条件不具备,读进程被阻塞,直到pipe内有数据。

        管道文件被写满 && read fd 不读但是没有关闭,管道已满,写进程被阻塞,直到pipe内有空间。

        管道一直在读 && 写 fd被关闭,读fd读到0,表示读到了文件结尾。

        read fd关闭 ,write fd没有关闭,若再写入数据,write会被OS以SIGPIPE信号终止。


三、进程间通信的方案——命名管道

         由于匿名管道的缺陷是只能让两个具有血缘关系的进程通信,命名管道就是为了解决这样的问题而设计的。

(1)认识管道文件

通过指令 mkfifo + 文件名 可以创建管道文件,文件类型为p:

        这就是管道文件,这样的文件具有确定的路径和文件名,所以就称为“命名管道”。这个文件对应匿名管道的内核级缓冲区。这就意味着,原理虽然和匿名管道不完全相同,但是思路完全一致,这里不再赘述。

(2)使用命名管道

创建管道文件,两种方法:

        1.指令mkfifo + 管道文件名称

        2.使用系统封装后的接口:

头文件:

函数原型: 

返回值就是命名管道的fd;如果创建失败,返回-1,错误吗被设置。 

删除管道文件,两种方法:

        1.指令方法

        rm + 文件名 / unlink + 文件名

        2.使用系统封装的接口

        


        到这里,你或许对管道的使用还有一些迷惑,通过下面的这一个小项目,或许你会对进程间通信的方案——管道有一个更深入的理解:

        项目:简单的sever和client之间的通信,通给C++的封装来尽量简化代码的逻辑,尽可能规范:

namedPipe.h:

​
#pragma once

#include<iostream>
#include<unistd.h>
#include<string>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

using std::cout;
using std::endl;

const std::string comPath = "./myNamedPipe";

enum WHO
{
    //文件操作符的默认值
    defaultFd = -1,

    //标识调用者的身份
    sever = 0,
    client = 1,

    //读取namedPipe的一次读入数据的大小:bytes
    BaseSize = 4096
};

//管理命名管道的类,不同的身份的人调用会产生不同的效果
//serer管理命名管道的生命周期,client只负责使用管道
class NamedPipe
{
private:
    bool OpenNamedPipe(int openstyle)
    {
        //把得到的fd交给成员变量_fd即可
        _fd = open(comPath.c_str(),openstyle);
        if(_fd < 0)
        {
            perror("open namedpipe fail!");
            exit(1);
        }
        return true;
    }
public:

    NamedPipe(const std::string& path,int who)
        :_pipePath(path)
        ,_who(who)
        ,_fd(defaultFd)
    {
        //sever需要创建命名管道
        //cilent则什么都不需要做
        if(who == sever)
        {
            int res = mkfifo(comPath.c_str(),0666);
            if(res < 0)
            {
                perror("sever mkfifo fail!");
                exit(1);
            }
            cout<<"sever mkfifo success!"<<endl;
        }
    }

    bool OpenForRead()
    {
        return OpenNamedPipe(O_RDONLY);
    }

    bool OpenForWrite()
    {
        return OpenNamedPipe(O_WRONLY);
    }

    //通过stl的string为载体来写入,s为输入型参数
    int Write(const std::string &s)
    {
        int res = write(_fd,s.c_str(),s.size());
        //如果出错,打印错误信息,终止进程
        //如果正确,返回写入数据的字节数
        if(res < 0)
        {
            perror("read fail!");
            exit(1);
        }
        return res;
    }

    //通过stl的string为载体来输出,s为输出型参数
    int Read(std::string *s)
    {
        char buf[BaseSize] = {0};
        int res = read(_fd,buf,BaseSize);

        *s = buf;

        //如果出错,打印错误信息,终止进程
        //如果正确,返回读取的数据的字节数
        if(res < 0)
        {
            perror("read fail!");
            exit(1);
        }
        return res;
    }

    ~NamedPipe()
    {
        if(_who == sever)
        {
            int res = unlink(comPath.c_str());
            if(res < 0)
            {
                perror("unlink fail!");
                exit(1);
            }

            if(_fd != defaultFd)
            {
                close(_fd);
            }
        }
    }

private:
    const std::string _pipePath;//命名管道的路径,便于不同进程找到命名管道
    int _who;//身份
    int _fd;//namedpipe的文件描述符
};

​

sever.cc:

#include "namePipe.hpp"

// 一般可以看着namePipe的头文件来使用头文件内部的接口
// 也就是说
// sever等的enum常量的声明是可以被看到的,所以sever的直接使用并不突兀
// 但是namePipe被写为  hpp = .h + .cc

// 服务端,读取数据,管理namedPipe
int main()
{
    // 通过类来管理namePipe,出作用域自动析构
    NamedPipe fifo(comPath.c_str(), sever);
    if (fifo.OpenForRead())
    {

        while (true)
        {
            std::string s;
            int n = fifo.Read(&s);
            cout << n << ":" << s.c_str() << endl;
        }
    }

    return 0;
}

client.cc:

#include "namePipe.hpp"

// 客户端,写入数据
int main()
{
    NamedPipe fifo(comPath.c_str(), client);

    if (fifo.OpenForWrite())
    {
        std::string s("I am process A");
        while (true)
        {
            sleep(1);
            int n = fifo.Write(s);
            cout<<n<<" bytes writen "<<endl;
        }
    }

    return 0;
}

makefile:

.PHONY:all
all:sever client
sever:sever.cc
g++ -g -o $@ $^ -std=c++11
client:client.cc
g++ -g -o $@ $^ -std=c++11

.PHONY:clean
clean:
rm -rf sever client

 完~

未经作者同意禁止转载


原文地址:https://blog.csdn.net/2301_79465388/article/details/144066839

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!