【Linux系统】理解管道通信,匿名管道实现进程池+命名管道实现服务端客户端通信模型(附源码)

【Linux系统】理解管道通信,匿名管道实现进程池+命名管道实现服务端客户端通信模型(附源码)

文章目录

一、进程间通信是什么

进程间通信(IPC),顾名思义,进程之间需要进行信息交换。
如:数据传输、资源共享、通知事件、进程控制。

进程间通信的方式有:管道、System V IPC、POSIX IPC。

由于进程具有独立性,进程间通信的前提就是,不同的进程能看到同一份资源。

二、管道

1. 什么是管道

管道是类Unix系统中最古老的进程间通信的方式。我们把从一个进程连接到另一个进程的数据流称为一个“管道”。

在这里插入图片描述

管道是单向通信的,称为单工通信。

管道分为匿名管道和命名管道。

2. 匿名管道

匿名管道(pipe)是亲缘进程间单向通信机制,本质是内核管理的一份文件,两个进程通过一对文件描述符实现一端读一端写,随进程退出自动销毁。匿名管道只能用在有血缘关系的进程之间!

系统调用pipe,用于创建一个匿名管道。

在这里插入图片描述

参数是一个文件描述符数组,管道创建后, fd[0]表示读端,fd[1]表示写端。
成功创建返回0,失败则返回错误码。

在这里插入图片描述

匿名管道是一个纯内存级的文件,不需要打开磁盘文件,没有路径,所以称为匿名管道。匿名管道没有名字、没有文件实体,只靠文件描述符来传递。这就是为什么它只能用在有血缘的进程之间,因为这些进程能拷贝文件描述符表,才能拿到同一根管道的读写端。

匿名管道通信有以下几种情况:

  • 子进程写得慢,父进程就要阻塞等待。等到管道有数据,父进程才能读。
  • 子进程写得快,父进程不读,管道一旦写满,子进程必须阻塞了。
  • 读端一直读,写端关闭,读端读完管道中的数据时,read返回0,表明读到文件末尾。
  • 写端一直写,读端关闭,操作系统会杀掉写端进程,进程异常终止,终止信号为13!

管道还有以下特点:

  • 管道只能单向通信。如果想要两个进程间互相通信,需要创建两个管道。
  • 匿名管道只能用在有血缘关系的进程之间,因为必须继承文件描述符表。
  • 管道是面向字节流的。多次写入的字节流,在读取时可能被一次读取完,也可能被拆分成多次。
  • 管道的生命周期随进程。管道是内核中的临时对象,没有持久化到磁盘。当所有持有管道文件描述符的进程都关闭后,管道会被内核自动销毁,数据也随之丢失。
  • 管道通信,对于多进程,自带同步与互斥机制。读空管道时,读进程会阻塞等待数据;写满管道时,写进程会阻塞等待空间。

使用演示:

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<sys/types.h>#include<unistd.h>#include<sys/wait.h>intmain(){int pipefd[2]={0};if(pipe(pipefd)!=0){exit(1);}// 根据文件描述符分配规则,这里pipefd内容应该是3 4,3为读端,4为写端printf("pipefd[0]: %d, pipefd[1]: %d\n", pipefd[0], pipefd[1]);// 下面测试子进程向父进程通信pid_t id =fork();if(id ==0){// 子进程中,要向管道写,所以要关闭读端,也就是关闭文件描述符pipefd[0]close(pipefd[0]);char* msg ="hello pipe";int cnt =5;char outbuffer[256];while(cnt){snprintf(outbuffer,sizeof(outbuffer),"子->父# %s %d", msg, cnt--);// 向管道中写write(pipefd[1], outbuffer,strlen(outbuffer));sleep(1);}close(pipefd[1]);exit(0);}// 父进程中,要从管道读,所以要关闭写端,也就是关闭文件描述符pipefd[1]close(pipefd[1]);char inbuffer[1024];while(1){ inbuffer[0]=0;// 从管道中读ssize_t n =read(pipefd[0], inbuffer,sizeof(inbuffer)-1);// -1为了给\0预留一个位置,避免缓冲区溢出。if(n >0){ inbuffer[n]=0;// 管道也是文件,结尾不会自动加\0,需要手动设置printf("%s\n", inbuffer);}elseif(n ==0){printf("管道读取结束\n");close(pipefd[0]);break;}else{perror("read");break;}}pid_t rid =waitpid(id,NULL,0);return0;}
在这里插入图片描述

3. 命名管道

匿名管道只能用在有血缘关系的进程之间。
如果我们想用在不相关的进程之间通信,可以使用命名管道(FIFO)完成!
无关的进程之间想要通信,必须看到同一份资源,所以命名管道必须有路径(名字),双方才都能看到他。

命名管道本质是一种特殊类型的文件——管道文件。

命名管道可以从命令行上创建,使用命令mkfifo 文件名

也可以在程序中创建,使用函数mkfifo,第一个参数是文件名,第二个参数是文件权限。成功创建返回0,失败返回-1:

在这里插入图片描述

命名管道使用完需要我们手动删除,可以使用函数unlink删除文件的方式:

在这里插入图片描述

匿名管道由pipe函数创建并打开;命名管道由mkfifo函数创建,用open打开。它们的唯一区别在于创建与打开的方式不同,这些工作完成时候,它们具有相同的语义。

三、实例:匿名管道实现进程池

#include<cassert>#include<cstdio>#include<cstdlib>#include<ctime>#include<iostream>#include<string>#include<sys/types.h>#include<sys/wait.h>#include<unistd.h>#include<vector>// 进程池,是指提前创建好多个子进程,在需要使用时直接分配任务。省去了创建子进程的开销// 父进程需要管理“通道”,组织管理子进程enum{ OK =0, PIPE_ERR, FORK_ERR, READ_ERR, WRITE_ERR, WAIT_ERR };// 全局定义好子进程数量,任务数量constint gprocessnum =7;voidtask1(){ std::cout <<"这是下载数据任务"<< std::endl;}voidtask2(){ std::cout <<"这是打印日志任务"<< std::endl;}voidtask3(){ std::cout <<"这是刷新磁盘任务"<< std::endl;}voidtask4(){ std::cout <<"这是更新用户状态任务"<< std::endl;}typedefvoid(*task_t)();// 任务表constint gtasknum =4; task_t tasks[gtasknum]={task1, task2, task3, task4};classProcessPool{private:// 内部类维护子进程通道classChannel{private:int _wfd;// 当前子进程通道的管道写端fd pid_t _id;// 子进程id std::string channel_name;// 自定义通道的名字public:Channel(int wfd, pid_t id):_wfd(wfd),_id(id){ channel_name ="channel-"+ std::to_string(id);}voidClosePipe(){close(_wfd);}voidPrintInfo(){printf("管道wfd: %d, 通道名: %s\n", _wfd, channel_name.c_str());}intgetfd(){return _wfd;}constchar*getname(){return channel_name.c_str();}voidWait(){ pid_t rid =waitpid(_id,nullptr,0);if(rid <0){// wait出错exit(WAIT_ERR);} std::cout <<"回收子进程: "<< _id << std::endl;}};public:ProcessPool(){// 种下随机数种子srand(time(NULL));}// 初始化进程池,创建好若干个子进程通道voidInit(){CreateProcessChannels();}// 打印通道信息验证voidDebug(){for(auto& channel : channels){ channel.PrintInfo();}}// 随机分配任务执行voidRun(){int cnt =10;while(cnt--){int itask =SelectTask();int ichannel =SelectChannel();printf("向通道%s发送任务task%d...\n", channels[ichannel].getname(), itask +1);SendTask2Channel(itask, ichannel);sleep(1);}}voidQuit(){for(auto& channel : channels){ channel.ClosePipe(); channel.Wait();}}private:voidCreateProcessChannels(){for(int i =0; i < gprocessnum; i++){int pipefd[2]={0};int n =pipe(pipefd);if(n <0){// 管道创建出错exit(PIPE_ERR);} pid_t id =fork();if(id <0){// 子进程创建出错exit(FORK_ERR);}elseif(id ==0){// 子进程 read// 子进程的文件描述符表是拷贝父进程的。父进程fd表中有指向其他管道的wfd,子进程必须关闭指向其他管道的wfd!if(!channels.empty()){for(auto& channel : channels) channel.ClosePipe();}// 子进程从管道中读数据,关闭写端close(pipefd[1]);// 子进程进入待执行任务状态,将来执行完成后回来退出DoTask(pipefd[0]);exit(OK);}else{// 父进程 write// 关闭管道读端close(pipefd[0]); channels.emplace_back(pipefd[1], id);printf("创建子进程%d成功\n", id);}}}voidDoTask(int fd){// 子进程需要持续监听管道,等待父进程下发任务,直到父进程主动关闭管道写端。while(1){int task_code; ssize_t n =read(fd,&task_code,sizeof(task_code));if(n ==sizeof(task_code)){// 根据读取到的task_code从任务表中选择函数执行if(task_code >=0&& task_code < gtasknum){ tasks[task_code]();}}elseif(n ==0){// 读到了文件尾,说明管道写端关闭了printf("%d任务退出\n",getpid());break;}else{// read出错exit(READ_ERR);}}}intSelectTask(){// 随机选一个任务returnrand()% gtasknum;}intSelectChannel(){// 依次选择子进程staticint i =0;int selected = i; i++; i %= gprocessnum;return selected;}voidSendTask2Channel(int itask,int ichannel){assert(0<= itask && itask < gtasknum && ichannel >=0&& ichannel < gprocessnum); ssize_t n =write(channels[ichannel].getfd(),&itask,sizeof(itask));if(n <0){// write出错exit(WRITE_ERR);}}private:// 组织所有的子进程通道 std::vector<Channel> channels;};intmain(){ ProcessPool pp;// 初始化进程池,创建好若干个子进程通道 pp.Init();// 打印通道信息验证 pp.Debug();// 随机分配任务执行 pp.Run();// 释放管道,回收子进程 pp.Quit();return0;}

效果演示:

在这里插入图片描述

四、实例:命名管道实现服务端客户端通信模型

// Fifo.hpp#pragmaonce#include<cstdio>#include<cstring>#include<fcntl.h>#include<iostream>#include<string>#include<sys/stat.h>#include<sys/types.h>#include<unistd.h>#defineFORREAD1#defineFORWRITE2const std::string myfifo ="./fifo";classFifo{public:Fifo(const std::string& filename = myfifo):_filename(filename),_mode(0666),_fd(-1){}// 创建管道voidBuild(){// 如果管道文件已存在,就returnif(IsExist())return;int n =mkfifo(_filename.c_str(), _mode);if(n <0){ std::cerr <<"mkfifo error: "<<strerror(errno)<< std::endl;exit(1);} std::cout <<"mkfifo success"<< std::endl;}// 打开管道voidOpen(int mode){if(mode == FORREAD){ _fd =open(_filename.c_str(), O_RDONLY);}elseif(mode == FORWRITE){ _fd =open(_filename.c_str(), O_WRONLY);}if(_fd <0){ std::cerr <<"open error: "<<strerror(errno)<< std::endl;exit(2);} std::cout <<"open success"<< std::endl;}// 删除管道voidDelete(){if(!IsExist()){return;}int n =unlink(_filename.c_str());if(n <0){ std::cerr <<"delete error: "<<strerror(errno)<< std::endl;exit(3);} std::cout <<"delete success"<< std::endl;}// 发送消息voidSend(std::string& msgin){ ssize_t n =write(_fd, msgin.c_str(), msgin.size());}// 接受消息intReceive(std::string& msgout){char buffer[128]; ssize_t n =read(_fd, buffer,sizeof(buffer)-1);if(n >0){ buffer[n]='\0'; msgout = buffer;return n;}elseif(n ==0){return0;}else{return-1;}}private:boolIsExist(){structstat st;// stat函数用于查询一个文件的属性,如果查到了返回0// 利用这一点判断管道文件是否存在int n =stat(_filename.c_str(),&st);if(n ==0){returntrue;}else{ errno =0;// 消除这次失败对后面代码的影响returnfalse;}}private: std::string _filename; mode_t _mode;int _fd;};
// Server.cc#include"Fifo.hpp"intmain(){// 服务端 创建并打开管道 Fifo pipefile; pipefile.Build(); pipefile.Open(FORREAD); std::string msg;while(1){int n = pipefile.Receive(msg);if(n >0){ std::cout <<"客户端说: "<< msg << std::endl;}else{break;}} pipefile.Delete();return0;}
// Client.cc#include"Fifo.hpp"intmain(){// 客户端 写入信息 Fifo fileclient; fileclient.Open(FORWRITE);while(1){ std::cout <<"请输入:"<< std::endl; std::string msg; std::getline(std::cin, msg); fileclient.Send(msg);}return0;}

效果演示:

命名管道实现服务端客户端通信演示

本篇完,感谢阅读

Read more

Java连接电科金仓数据库(KingbaseES)实战指南

Java连接电科金仓数据库(KingbaseES)实战指南

摘要:本文分享了KingbaseES V8.6数据库与SpringBoot 2.7.x框架的集成实战经验。内容包括:1. 环境准备(Ubuntu系统安装配置、驱动获取方式);2. JDBC基础操作(连接、查询、事务处理);3. SpringBoot项目完整配置(pom依赖、数据源配置);4. MyBatis-Plus集成(实体类、Mapper、Service层实现);5. RESTful接口开发示例。文章提供了详细的代码示例,涵盖从数据库安装到应用开发的完整流程,帮助开发者快速实现国产数据库适配。 目录 前言 一、环境准备与驱动获取 1.1 数据库安装与配置 1.2 JDBC驱动获取与配置 1.3 创建测试数据库 二、基础JDBC连接与操作 2.1 最基础的JDBC连接示例 2.

By Ne0inhk
【Java 开发日记】我们来说一下无锁队列 Disruptor 的原理

【Java 开发日记】我们来说一下无锁队列 Disruptor 的原理

目录 一、为什么需要 Disruptor?—— 背景与问题 二、核心设计思想 三、核心组件与原理 1. 环形缓冲区(Ring Buffer) 2. 序列(Sequence) 3. 序列屏障(Sequence Barrier) 4. 等待策略(Wait Strategy) 5. 事件处理器(EventProcessor) 6. 生产者(Producer) 四、工作流程示例(单生产者 -> 单消费者) 五、多消费者与依赖关系 六、总结:Disruptor 高性能的秘诀 一、为什么需要 Disruptor?—— 背景与问题 在高并发编程中,传统的队列(如 java.

By Ne0inhk
Java-Spring入门指南(十四)利用IDEA教你构建第一个SpringMVC系统

Java-Spring入门指南(十四)利用IDEA教你构建第一个SpringMVC系统

Java-Spring入门指南(十四)SpringMVC项目实战搭建 * 前言 * 一、首先导入我们的Maven * 二、接着导入SpringMVC相关的包 * 三、创建Servlet_web环境 * (1)配置springmvc.xml * (2)配置web.xml里面的中央处理器 * (3)为什么需要配置前端控制器? * 五、配置最新的tomcat 11 * 六、运行项目 前言 * 在上一篇博客中,我们系统学习了SpringMVC的核心流程与组件分工,明确了DispatcherServlet(前端控制器)、HandlerMapping(处理器映射器)等组件的协作逻辑。 * 理论之后更需实践,如何从0到1搭建一个可运行的SpringMVC项目,如何将核心组件配置落地,是本次实战的核心目标。 * 本文将基于Maven+IDEA+Tomcat 11环境,一步步完成SpringMVC项目的搭建、配置与运行,让你直观感受“理论”到“实战”的转化过程。 我的个人主页,欢迎来阅读我的其他文章 https:

By Ne0inhk
javaSE————网络原理

javaSE————网络原理

今天巨无聊,全是概念,重点记一下五元组,TCP/IP五层模型和OSI七层调用模型,大家这期就当看故事啦; 1,网络发展史 1)独立模式 我们刚开始使用计算机呢,客户端的数据不是共享的,如果有一个人想要办理业务,而这个业务所需的资源是在三台电脑上,那么这个人就需要在这三个电脑上不断的办理任务,而其他人想要办理业务,还需要等到前一个人办理完,效率非常低,那怎么办,我们就改进; 2)网络互联 接下来我们就使用网络进行计算机资源的共享,让多个计算机可以一起办理业务,达成数据共享,即网络通信,我们可以根据网络互联的规模分为局域网和广域网; 3)局域网LAN 局域网是本地,局部构建的一种私有网络,又被称为内网,局域网内的主机能够实现网络通信,局域网和局域网在没有连接的情况是不能进行通信的; 组件局域网等待方式也有很多种,可以通过网线直连,也可以通过交换机相连,还可以通过集线器相连,还可以通过路由器连接交换机在与多个主机相连; 4)广域网WAN 广域网就是多个局域网完成了连接,很多很多的局域网都能进行网络通信,我们其实可以把咱们中国的网络看成一个巨大的广域网,我们管内部

By Ne0inhk