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

server/2024/12/2 20:58:01/

标题:[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的一次读入数据的大小:bytesBaseSize = 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.ccg++ -g -o $@ $^ -std=c++11
client:client.ccg++ -g -o $@ $^ -std=c++11.PHONY:clean
clean:rm -rf sever client

 完~

未经作者同意禁止转载


http://www.ppmy.cn/server/146836.html

相关文章

Qt中QSpinBox valueChanged 信号触发两次

Qt中QSpinBox valueChanged 信号触发两次 如果使用鼠标调整&#xff0c;这个信号则会被触发两次如果使用键盘输入&#xff0c;则会触发一次 connect(ui->spinBox_rows, SIGNAL(valueChanged(int)), this, SLOT(test()));https://blog.csdn.net/dododododoooo/article/deta…

【大数据学习 | Spark】Spark on hive与 hive on Spark的区别

1. Spark on hive Spark on hive指的是使用Hive的元数据&#xff08;Metastore&#xff09;和SQL解析器(HiveQL)。这种方式下&#xff0c;spark可以读取和写入hive表&#xff0c;利用hive的元数据信息来进行表结构的定义和管理。 具体特点为&#xff1a; 1.1 元数据共享 sp…

droppath

DropPath 是一种用于正则化深度学习模型的技术&#xff0c;它在训练过程中随机丢弃路径&#xff08;或者说随机让某些部分的输出变为零&#xff09;&#xff0c;从而增强模型的鲁棒性和泛化能力。 代码解释&#xff1a; import torch import torch.nn as nn # 定义 DropPath…

[python脚本处理文件入门]-17.Python如何操作Excel文件的读写

哈喽,大家好,我是木头左! 在Python中,处理Excel文件最常用的库之一是xlrd,它用于读取Excel文件。而当需要创建或写入Excel文件时,xlwt库则是一个不错的选择。这两个库虽然功能强大,但使用起来也非常简单直观。 安装与导入 确保你已经安装了这两个库。如果没有安装,可以…

单点登录深入详解之技术方案总结

技术方案之CAS认证 概述 CAS 是耶鲁大学的开源项目&#xff0c;宗旨是为 web 应用系统提供一种可靠的单点登录解决方案。 CAS 从安全性角度来考虑设计&#xff0c;用户在 CAS 输入用户名和密码之后通过ticket进行认证&#xff0c;能够有效防止密码泄露。 CAS 广泛使用于传统应…

sql分类

SQL&#xff08;Structured Query Language&#xff09;是一种用于管理和操作关系数据库管理系统&#xff08;RDBMS&#xff09;的编程语言。SQL 可以分为几个主要类别&#xff0c;每个类别都有其特定的用途和功能。以下是 SQL 的主要分类&#xff1a; 1. 数据定义语言&#x…

map用于leetcode

//第一种map方法 function groupAnagrams(strs) {let map new Map()for (let str of strs) {let key str ? : str.split().sort().join()if (!map.has(key)) {map.set(key, [])}map.get(key).push(str)} //此时map为Map(3) {aet > [ eat, tea, ate ],ant > [ tan,…

【LeetCode: 3232. 判断是否可以赢得数字游戏 + 模拟】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…