[Linux] 进程间通信——匿名管道&&命名管道
标题:[Linux] 进程间通信——匿名管道&&命名管道
(图片来源于网络)
目录
正文开始:
一、进程间通信
当你参加场竞赛,你一定需要与你的队友密切配合,这样才能高效的完成任务。然而,配合的前提是你们知道彼此现在的状况,就是说,你们需要尽可能的共享信息,而这一过程不就是通信吗?于是,得出结论:
配合的前提是通信。
一台计算机,是被操作系统管理着(操作系统是软硬件资源的管理者),而进程间由于具有独立性,体现为每个进程都有自己的进程地址空间,这就意味着多个进程之间相互的数据不可见(所有的数据,不论局部或者全局),于是想要实现进程间的相互通信,通常是比较困难的(成本比较高)。
图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)!