【Linux】进程间通信(二)命名管道(FIFO)实战指南:从指令操作到面向对象封装的进程间通信实现

【Linux】进程间通信(二)命名管道(FIFO)实战指南:从指令操作到面向对象封装的进程间通信实现

文章目录


命名管道

(基于文件+inode的进程间通信方案)
首先我们要清楚,多个进程可以同时打开多个普通文件,OS会为每一个进程都创建一个struct file,但是多个进程共享同一份inode、文件缓冲区和操作方法集,所以只会加载一次文件的属性和内容到struct file的inode和文件缓冲区中,这和父进程打开一个匿名管道并fork一个子进程后父子进程的行为类似。
命名管道就是从上面的普通文件改造而来,最直观的区别就是进程对命名管道的文件缓冲区写数据时,数据不会刷新到磁盘中。

命名管道的操作

指令操作

下面是创建命名管道的指令:
在这里插入图片描述


为什么命管道叫做fifo呢?其实管道本质就是一个队列,因为它有先进先出的特性。

在这里插入图片描述
我们可以看到,mkfifo创建出来的文件的类型是p,也就是管道文件。
下面我们来尝试用管道来传输数据:
在这里插入图片描述

代码操作

Makefile

我们创建两个独立的文件:client.cpp,server.cpp分别表示客户端和服务端。接下来写Makefile:
在这里插入图片描述
这里Makefile其实不能实现我们想要的——client.cpp,server.cpp分别编译并生成两个可执行程序,最后只会生成一个可执行程序。
这是因为Makefile本身一次只会形成一个可执行程序,运行时会从上往下扫描,把遇到的第一个目标文件形成可执行程序。
所以我们需要先创建一个只有依赖关系没有依赖方法的伪目标all,它依赖两个可执行程序:client,server,这样Makefile从上往下扫描时遇到的第一个目标文件就是all,然后就会执行all依赖关系中的client,server,这样就能一次创建两个可执行程序了。
在这里插入图片描述

创建命名管道

下面是代码层面场景命名管道的库函数调用接口:
在这里插入图片描述
因为进程间通信需要不同的进程看到同一份资源,所以我们再创建一个common.hpp文件,把客户端和服务端共享的内容都放到common.hpp中。
#ifndef__COMMON_HPP__#define__COMMON_HPP__#include<iostream>#include<string>#include<sys/types.h>#include<sys/stat.h>#include<unistd.h> std::string fifoname ="fifo"; mode_t mode =0666;#endif
下面就需要创建管道了,我们让服务端创建。但是需要注意,创建管道文件是可能失败的,比如管道文件已存在时。
程序结束时还需要删除管道文件,删除文件需要用到unlink,它不仅是系统调用,也是一个指令,可以用unlink指令在命令行删除管道文件。

#include"common.hpp"intmain(){int n =mkfifo(fifoname.c_str(), mode);if(n ==0){ std::cout <<"mkfifo suceessful"<< std::endl;}else{ std::cout <<"mkfifo failed"<< std::endl;}sleep(5);int m =unlink(fifoname.c_str());(void)m;return0;}

实现通信

我们准备实现让client发送数据然后server接受数据。下面先实现server端,我们已经创建好管道文件了,下面就需要调用文件的各种系统调用接口打开文件、读取文件、关闭文件,和我们在文件系统介绍的一摸一样,小编就不过多赘述了。
这里小编要补充几点:
1、有关命名管道的操作特点,在打开管道一端,但另一端未打开的时候,open操作会被阻塞,因为如果不阻塞直接打开就有可能读到0。
2、读到的数据我们用字符串数组暂存,并且读取时要预留一个位置给\0,所以read的第三个参数需要sizeof(buffer) - 1,读取完毕后自己手动在字符串末尾添加\0。
3、当read读到0时就意味着client退出了,这时我们server端也需要退出,所以需要对read的返回值进行特殊处理。
//server.cpp#include"common.hpp"intmain(){// 1、创建管道文件int n =mkfifo(fifoname.c_str(), mode);if(n ==0){ std::cout <<"mkfifo suceessful"<< std::endl;}else{// std::cout << "mkfifo failed" << std::endl;perror("mkfifo");exit(1);}// 2、打开管道文件// 命名管道特点,在打开一端,但另一端未打开的时候,open操作会阻塞int fd =open(fifoname.c_str(), O_RDONLY);if(fd <0){perror("open");exit(2);} std::cout <<"open file success"<< std::endl;// 3、读取管道数据char buffer[SIZE]={0};while(true){ buffer[0]=0;// 清空字符串 ssize_t num =read(fd, buffer,sizeof(buffer)-1);if(num >0)// read失败返回-1{ buffer[num]=0;// 保持C风格字符串,末尾加0 std::cout <<"client say# "<< buffer << std::endl;}elseif(num ==0){ std::cout <<"clent quit, me too!"<< std::endl;break;}else{// read错误break;} std::cout <<"num: "<< num << std::endl;}// 4、归还资源close(fd);int m =unlink(fifoname.c_str());(void)m;return0;}
然后实现client端,还是平常打开文件的逻辑,唯一需要注意是处理输入的时候不用cin,而用getline,因为getline可以读入空格。
//client.cppintmain(){int fd =open(fifoname.c_str(), O_WRONLY);if(fd <0){perror("open");exit(1);} std::string message;while(true){ std::cout <<"please enter# ";getline(std::cin, message);// getline可以读取空格write(fd, message.c_str(), message.size());}close(fd);return0;}
现在我们来总结一下:
1、client和server是如何看到同一份资源的?因为命名管道不同于匿名管道,它有文件系统路径标识,所以当server和client通过路径+文件名打开的文件时就能通过路径解析找到唯一的文件inode,进而保证不同的进程打开的是同一个文件。
2、为什么fifo叫命名管道?因为命名管道本身就有名字,并且也有inode,open打开文件时如果打开的是命名管道就会对其做特殊处理,我们作为程序员不用操心。

以面向对象封装命名管道

1、构造函数中不写创建管道逻辑,析构函数中不写关闭管道逻辑,而是将创建管道和关闭管道和关闭文件描述符单独写成三个方法,因为客户端和服务端都会使用命名管道,服务端既要读取数据又要打开管道、打开文件、关闭管道、关闭文件描述符,而客户端只打开文件、关闭文件,这样解耦合方便服务端、客户端各自调用自己需要的接口。
2、封装Close时添加一个文件描述符默认值判断defaultfd,defaultfd默认为-1,打开管道成功了将defaultfd改为管道的fd,关闭管道后将defaultfd重新置为-1,当Close的参数为-1时表示程序没有打开管道文件或者已经将管道删除了,这时直接return,避免对无效 fd 执行 close 导致的系统错误、资源污染。 3、实现面向对象代码时对于参数传递的最佳实践如下:
输入参数:const+&
输出参数:*
输入输出参数:&
在这里插入图片描述

源码

//common.hpp#ifndef__COMMON_HPP__#define__COMMON_HPP__#include<stdio.h>#include<iostream>#include<string>#include<sys/types.h>#include<sys/stat.h>#include<unistd.h>#include<fcntl.h> std::string fifoname ="fifo"; mode_t mode =0666;#defineSIZE128//缓冲区大小#endif
//NamedPipe.hpp#pragmaonce#include"common.hpp"constint defaultfd =-1;classNamedPipe{public:NamedPipe(const std::string name):_name(name),_fd(defaultfd){}// 创建管道boolCreate(){int n =mkfifo(_name.c_str(), mode);if(n ==0){ std::cout <<"mkfifo suceessful"<< std::endl;}else{// std::cout << "mkfifo failed" << std::endl;perror("mkfifo");returnfalse;}returntrue;}boolOpenForRead(){ _fd =open(_name.c_str(), O_RDONLY);if(_fd <0){perror("open");returnfalse;} std::cout <<"open file success"<< std::endl;returntrue;}boolOpenForWrite(){ _fd =open(_name.c_str(), O_WRONLY);if(_fd <0){perror("open");returnfalse;}returntrue;}// 输出型参数boolRead(std::string *out){char buffer[SIZE]={0}; ssize_t num =read(_fd, buffer,sizeof(buffer)-1);if(num >0)// read失败返回-1{ buffer[num]=0;// 保持C风格字符串,末尾加0*out = buffer;}elseif(num ==0){returnfalse;}else{returnfalse;}returntrue;}// 输入型参数voidWrite(const std::string &in){write(_fd, in.c_str(), in.size());}// 关闭管道文件描述符(本代码示例中服务端、客户端都需要关闭)voidClose(){if(_fd == defaultfd){return;// 直接return,避免执行无效操作}int n =close(_fd);if(n <0)perror("close"); _fd =-1;}// 归还管道文件voidRemove(){int m =unlink(_name.c_str());(void)m;}~NamedPipe(){}private: std::string _name;// 管道文件名int _fd;// 管道文件描述符};
//server.cpp#include"NamedPipe.hpp"intmain(){ std::string fifoname ="fifo"; NamedPipe np(fifoname);// 1、创建管道文件 np.Create();// 2、打开管道文件 np.OpenForRead();// 3、读取管道数据 std::string message;while(true){bool res = np.Read(&message);if(res){ std::cout <<"client say# "<< message << std::endl;}else{break;}}// 4、归还资源 np.Close(); np.Remove();return0;}
//client.cpp#include"NamedPipe.hpp"intmain(){ NamedPipe np(fifoname); np.OpenForWrite(); std::string message;while(true){ std::cout <<"please enter# ";getline(std::cin, message);// getline可以读取空格 np.Write(message);} np.Close();return0;}

总结

命名管道主要用于在毫无关系的进程之间进行文件级进程通信。其他特点匿名、命名管道相同。

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的赞和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

在这里插入图片描述

Read more

Linux:网络编程套接字及UDP

Linux:网络编程套接字及UDP

一、预备知识 1.1 理解网络通信的本质  问题1:在进行网络通信的时候,是不是我们的两台机器在进行通信呢?? ——>思考一下我们打开qq软件,他属于应用层,完成了数据的发送和接受…… 1、用户使用应用层软件,完成数据的发送和接受 2、网络协议的中下三层,主要解决的是将数据安全可靠的送到远端机器         而要使用软件进行通信,就得先把这个软件启动起来,也就是进程,所以网络通信的本质就是进程间通信!!只不过是不同主机下的进程!! 1.2 理解IP地址和端口号         既然我们要进行两个主机之间的进程间通信,那么我们就要知道如何找到对方吧! 所以我们需要有IP地址来作为寻找主机的唯一标识。      可是光有IP地址就能完成通信了呢??想象一下发qq消息的例子,有了IP地址能够把消息发送到对方的机器上,可是对方机器有那么多进程,我怎么知道我要发给哪个进程呢??因此我们还需要有一个标识来区分出信息要交给哪个进程,所以我们需要有端口号!! 端口号(port)是传输层协议的内容. 端口号是一个2字节16位的整数; 端口号用来标识一个进程, 告诉操作系统

By Ne0inhk
Flutter for OpenHarmony: Flutter 三方库 mongo_dart 助力鸿蒙应用直连 NoSQL 数据库构建高效的数据流转系统(纯 Dart 驱动方案)

Flutter for OpenHarmony: Flutter 三方库 mongo_dart 助力鸿蒙应用直连 NoSQL 数据库构建高效的数据流转系统(纯 Dart 驱动方案)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在进行 OpenHarmony 的工业巡检、内部管理系统或边缘计算(Edge Computing)应用开发时,有时我们需要鸿蒙前端应用直接与后端的 MongoDB 数据库进行交互,而不仅仅是通过传统的 Web API 转发。 mongo_dart 是一个极其强大的、全功能、纯 Dart 实现的 MongoDB 驱动程序。它不依赖任何原生底层驱动(如 C 驱动),通过 Dart 的 Socket 机制直接实现 BSON 协议封装。这意味着你在鸿蒙设备上无需配置复杂的 NDK 动态库,即可拥有连接、查询、甚至是实时聚合分析 MongoDB 数据的能力。 一、网络直连架构模型

By Ne0inhk
OpenClaw:一只“小龙虾”如何用三个月掀翻AI圈,让黄仁勋惊呼“超越Linux”?

OpenClaw:一只“小龙虾”如何用三个月掀翻AI圈,让黄仁勋惊呼“超越Linux”?

目录 一、发展历史:一个“退休”程序员的10天“玩票”,如何引爆全球? 1. 故事的起点:奥地利“闲人”的10天代码狂欢 2. 改名风波:被Anthropic“追杀”的龙虾 3. 封神时刻:25万星标,超越Linux 4. 大佬“接盘”:OpenAI的橄榄枝 二、OpenClaw是什么?——给AI装上“手”和“眼睛” 核心定义:从“嘴”到“手”的进化 四层架构:一只龙虾的解剖图 它能做什么?——那些让人惊叹的实战案例 三、竞品分析:当“龙虾”火了,模仿者们来了 1. OpenClaw:

By Ne0inhk
Flutter for OpenHarmony:Flutter 三方库 forge2d 赋予鸿蒙应用真实的物理动态(基于 Box2D 的高性能物理引擎)

Flutter for OpenHarmony:Flutter 三方库 forge2d 赋予鸿蒙应用真实的物理动态(基于 Box2D 的高性能物理引擎)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在进行 OpenHarmony 游戏开发或构建具有极致动画交互的应用时,传统的补间动画(Tween Animation)往往显得生硬平直。如果你想实现物体的碰撞、反弹、重力坠落或者是复杂的绳索摆动,你需要一套成熟的物理模型。 forge2d 是著名物理引擎 Box2D 的纯 Dart 移植版。它不仅高度优化了性能,且深度集成了 Flutter 的渲染循环,是鸿蒙平台上构建 2D 物理世界的基石。 一、核心物理概念解析 forge2d 模拟了一个虚拟的物理世界,包含刚体(Body)、形状(Shape)和夹具(Fixture)。 Collision World (物理世界: 重力, 时间步) Body A (刚体: 位置, 线速度)

By Ne0inhk