技能培训中心网站建设,天津实体店网站建设,seo排名工具有哪些,制作网站平台目录
前言
一、进程间通信的基本概念
二、管道
1、管道的基本概念
2、匿名管道
#xff08;1#xff09;原理
#xff08;2#xff09;测试代码
#xff08;3#xff09;读写控制相关问题
a、读端关闭
b、写端关闭
c、读快写慢
d、读慢些快
#xff08;41原理
2测试代码
3读写控制相关问题
a、读端关闭
b、写端关闭
c、读快写慢
d、读慢些快
4进程池代码拓展
3、命名管道
1原理
2测试代码
三、共享内存
1、共享内存的原理
2、测试代码
3、共享内存的特性 前言 进程间通信的方式有很多如管道、共享内存、消息队列、信号量、socket套接字等等本文主要讲解其中管道和共享内存两种方式 一、进程间通信的基本概念 我们要理解进程间通信呢首先得知道以下三个问题 进程间通信是什么 我们为什么要进程间通信 怎么进行进程间通信 对于所有知识我们拥有上述三个问题得答案我们就可彻底弄清进程间通信得本质了 进程间通信是什么 进程间通信就是让两个进程看到同一块空间内存以达到我们通过这块共同空间来进程交互得过程 我们都知道我们可以通过fork创建子进程fork创建后得子进程与父进程共享同一块代码那么我们是否可以通过创建一个创建变量的资源缓冲区来使这两个父子进程看到同一块空间内存呢很不幸由于我们进程间的独立性所以我们无法通过全局变量来让两个进程看到同一块空间并且使用这个空间进行通信准确来说是可以看到但是不能进行通信因为当我们父进程或子进程对这块空间内容进行修改时会发生写时拷贝这也是保证进程间独立性而产生的机制也正是由于进程具有独立性所以我们的进程间通信便没有那么容易 为什么要进行进程间通信 在实际开发中我们可能会有并发编程的需求而我们的单进程是不具备并发能力的而有时我们的并发之间的多进程需要一起协同配合既然需要协同配合就可能需要进行通信因此我们需要进程间通信 怎么进行进程间通信 关于如何进行进程间通信我们主要有以下几种策略分别来自于不同的方 Linux原生方案管道匿名管道、命名管道 System V方案共享内存、消息队列不常用、信号量 POSIX 方案socket套接字 上述为一些主流方案本文主要讲解 管道方案 与 System V 提供的共享内存方案 二、管道
1、管道的基本概念 首先管道我们在前面学习指令的时候就已经接触过了只不过我们对其了解并不深刻当时我们只知道我们可以通过管道将一个指令进程的输出结果传输给另一个指令进程 1、管道是一个信息传输的媒介如上图所示进程A将信息写入管道而进程B从管道中读取数据 2、管道是一个文件我们之前提过Linux下一切皆文件的理念那么我们的管道也应该是一个文件只不过这个文件是属于内存级文件不会将数据刷新到缓冲区中也没有必要刷新到缓冲区中做持久化的动作 3、管道的文件的通信方式为半双工通信关于这我们需要补充以下几个概念 单工通信只有一个方向的通信且只有固定的一端作为接收端一端作为输入端 半双工通信通信的双方都可以作为接收端也可以作为输入端当某一时刻只能有一个端发送一端接收就好像两个人聊天你说一句我接收后再回复一句你再接收 全双工通信通信的双方既可以作为接收端也可以作为输入端某一个时刻既可以输入也可以接收就好像两个人吵架每个人都可以挺别人说话的同时对别人说话 2、匿名管道
1原理 首先我们来介绍匿名管道实现进程间通信的原理再使用匿名管道作为进程间通信的方法之前我们得确保通信双方进程为父子关系这是使用匿名管道实现进程间通信得基本前提 前面我们讲过进程的相关内核数据与我们创建一个进程会发生什么以及当我们调用fork会发生什么有了上述知识铺垫我们不难想到当我们使用fork创建子进程时我们的子进程会创建自己的内核数据如PCB控制块虚拟地址空间、页表、文件描述符结构体等内核数据其中我们还讲过子进程会拷贝父进程内核数据中的某些数据当我们对这些数据需要进行修改时会发生写时拷贝的现象那么问题来了我们的维护当前进程打开的文件的结构体 files_struct 是否也会发生拷贝呢当然这是肯定的因此我们使用fork时应该如下图所示 我们匿名管道实现进程间通信就是基于这一特性---- “子进程会继承父进程的文件描述符数组”因此我们不妨首先创建一个管道文件然后父进程分别以读和写的方式打开这个管道文件接着我们创建子进程子进程必然会对文件描述符数组的内容进行拷贝子进程也拥有对该文件读和写这两个文件描述符此时若我们想让父进程写子进程读我们只需要将父进程的读文件描述符关闭子进程写文件描述符关闭然后我们再调用系统调用read和write分别向对应文件描述符读和写即可这就是我们使用匿名管道的方式实现进程间通信的过程 2测试代码 在正是实现代码之前我们首先介绍几个与管道相关的接口 pipe创建匿名管道 参数该函数只有一个参数是一个整型数组当我们调用该函数时该函数会为我们创建一个匿名管道文件并打开这个管道文件其中数组的 0 号下标放的是以读的方式打开该文件的文件描述符1 号下标方式的以写的方式打开该管道文件的文件描述符记忆0想象成嘴巴代表读1想象成笔代表写 返回值若函数调用成功返回0调用失败返回-1错误码被设置 有了上面函数的学习我们就可以写一段简单的基于匿名管道的进程间通信代码了如下所示 #include iostream
#include cstring
#include cstdlib
#include unistd.h
#include sys/types.h
#include sys/wait.hint main()
{// 1、创建管道int pipefd[2] {0};int n pipe(pipefd);if(n -1){perror(pipe);exit(1);}// 2、创建子进程pid_t id fork();if(id 0){// 子进程(读)// 3、建立单向信道// 关闭子进程写端文件描述符close(pipefd[1]);// 4、进行通信char buf[1024]; // 接收缓冲区while(true){// 读取来自父进程的信息ssize_t sz read(pipefd[0], buf, sizeof(buf));// 这里就不对read进行差错检验了buf[sz] \0; // 我们想要读的是字符串,因此在语言层面得加 \0if(sz 0){std::cout 读数据前\n;std::cout child[ getpid() ], father: buf std::endl;}else if(sz 0){std::cout father stop write, me quit std::endl;break;}sleep(1);}exit(0);}// 父进程写// 3、建立单向信道close(pipefd[0]); // 关闭读端// 4、进行通信const char* msg 这是发给子进程的消息 ;char tmp[1024] {0};int count 0;while(true){memset(tmp, 0, sizeof(tmp));snprintf(tmp, sizeof(tmp), %s[%d], %d\n, msg, getpid(), count);ssize_t sz write(pipefd[1], tmp, strlen(tmp));sleep(1);}// 5、回收子进程waitpid(id, nullptr, 0);// 6、关闭文件描述符可关可不关因为程序快运行结束OS会自动释放close(pipefd[1]);return 0; 上述代码是实现了一个父进程写子进程不断的读的功能 3读写控制相关问题 基于上述代码我们还要进行更深层次的研究我们分别测试以下几种情况下会发生什么 a、读端关闭 我们给上述代码读端设置一个计数器设置成5秒后读端退出观察会发生什么 我们输入命令行监视脚本指令如下所示 while :; do ps -axj | head -1 ps -axj | grep ./main | grep -v grep; sleep 1; echo --------------------;done 我们发现当我们的读端关闭时写端进程被终止了也就是被杀掉了由于上述我们让子进程关闭读端文件描述符后休眠5秒因此我们的子进程没有退出我们不难得出结论 结论读端关闭时写端进程被操作系统杀死 b、写端关闭 同样的道理我们使用计数器的方式使写端到一定的时间后关闭我们观察读端会如何 运行结果如下所示 仔细观察打印了我们在read返回值为0的输出内容 结论当我们写端关闭时读端会读到文件的末尾返回0 c、读快写慢 我们将上述代码更改一下将我们的读端设置为一秒读一次将我们的写端设置为三秒写一次再运行代码看一看会发生什么 我们会发现我们的进程三秒才会打印一次我们的读进程不是一秒读一次吗那我们的读进程在干嘛呢 结论当读快写慢时读进程会阻塞等待写进程进行写入 d、读慢些快 我们将上述代码改一下读进程三秒读一次而写进程一秒写一次又会发生什么呢 我们发现第一次立刻打印了后面三秒打印一次且每次都打印了三条内容这时结果也显而易见了 结论读慢写快时我们的写进程会一直往管道文件里写直至写满写满后写进程会阻塞直至下一次读进程读取管道文件时写进程才会被唤醒 总结 综上所述我们使用匿名管道进行进程间通信的本质是我们通过子进程会继承父进程文件描述符数组的特性使我们的父进程和子进程看到同一个文件它们可以通过该文件进行通信在我看来进程间通信的本质是让两个进程看到同一块空间的方式而通信是根据上层定义的 匿名管道提供了访问控制所谓访问控制就是我们上述讨论a、b、c、d四种不同的方式并不是所有进程间通信具有访问控制如我们后面讲的共享内存 匿名管道的生命周期是随着进程的一旦进程结束我们的匿名管道文件也随之销毁 4进程池代码拓展 通过上述知识我们可以实现一个简单的进程池代码 具体代码如下所示 // task.h文件主要封装任务和任务管理器
#pragma once
#include iostream
#include vector
#include string
#include map
#include functional
#include mutex// 定义函数对象类型
using func_t std::functionvoid();void webRequest()
{std::cout 正在执行网络请求任务 std::endl;
}void readMySQL()
{std::cout 正在执行读取数据库任务 std::endl;
}void otherTask()
{std::cout 正在执行其他任务 std::endl;
}// 设计一个单例类管理所有任务
class ManageTask
{
public:static ManageTask* getInstance(){// 双检查加锁if(_pm nullptr) // 提高效率{_m.lock();if(_pm nullptr) // 保证线程安全{_pm new ManageTask();}_m.unlock();}return _pm;}// 加载类内成员void load(){// 增加任务描述与命令号的映射_dict.insert({_tasks.size(), web请求});// 增加命令_tasks.push_back(webRequest);_dict.insert({_tasks.size(), 读取数据库});_tasks.push_back(readMySQL);_dict.insert({_tasks.size(), 其它任务});_tasks.push_back(otherTask);}// 自定义新增任务void add(func_t cb, std::string str){_dict.insert({_tasks.size(), str});_tasks.push_back(cb);}// 自定义删除任务(任务号)void del(int command){_dict.erase(command);_tasks.erase(_tasks.begin() command);}// 展示当前任务void show(){for(auto e : _dict){std::cout e.first : e.second std::endl;}}// 获取任务func_t get_task(int command){return _tasks[command];}// 获取任务个数size_t get_size(){return _tasks.size();}// 获取命令描述std::string get_command(int command){return _dict[command];}
private:ManageTask(){}ManageTask(ManageTask m){}ManageTask operator(ManageTask m){}
private:static ManageTask* _pm;static std::mutex _m;std::vectorfunc_t _tasks;std::mapint, std::string _dict;
};// 初始化单例对象指针和锁
ManageTask* ManageTask::_pm nullptr;
std::mutex ManageTask::_m;
// main.cc文件主要实现线程池以及派发任务等逻辑
#include iostream
#include vector
#include cstdlib
#include unistd.h
#include sys/types.h
#include sys/wait.h
#include task.hpp#define NUM 6int main()
{srand(time(nullptr)); // 种下随机数种子ManageTask::getInstance()-load(); // 初始化任务表// 1、创建进程池std::vectorstd::pairpid_t, int desc; // 子进程pid与写端fdfor(int i 0; i NUM; i){// 1.1 创建管道文件int pipefd[2] {0};int n pipe(pipefd);if(n -1) {std::cerr create pipe fail std::endl;exit(1);}// 1.2 创建子进程int id fork();if(id 0){usleep(100);// 子进程读// 1.3 关闭管道写端close(pipefd[1]);// 1.4 进行读取数据int command 0;while(true){ssize_t sz read(pipefd[0], command, sizeof(command));if(sz ! sizeof(int) || command 0 || command ManageTask::getInstance()-get_size()){std::cerr 读取无效命令 std::endl;continue;}// 1.5执行命令对应任务ManageTask::getInstance()-get_task(command)();}exit(0);}// 父进程写// 1.3 关闭对应读端,并将写端保存起来close(pipefd[0]);desc.push_back(std::pairpid_t, int(id, pipefd[1]));}// 2、使用进程池// 2.1 获取菜单while(true){int select 0;int command 0;std::cout ****************************** std::endl;std::cout ****** 1、showTask ****** std::endl;std::cout ****** 2、execute ****** std::endl;std::cout ****************************** std::endl;// 2.2 从用户获取选择std::cout Enter select ;std::cin select;// 2.3 从用户获取命令选项if(select 1){ManageTask::getInstance()-show();}else if(select 2){// 2.4 获取命令std::cout Enter command ;std::cin command;// 2.5 选择一个子进程执行随机数实现负载均衡int proc rand() % ManageTask::getInstance()-get_size();// 2.6 向指定进程发送命令write(desc[proc].second, command, sizeof(command));std::cout 已经成功给 desc[proc].first 进程,对应命令 ManageTask::getInstance()-get_command(command) std::endl;}else{std::cerr 选择有误,请重新选择 std::endl;continue;}usleep(500);}// 3、关闭进程池// 关闭写端fdfor(int i 0; i desc.size(); i){close(desc[i].second);}// 回收子进程for(int i 0; i desc.size(); i){waitpid(desc[i].first, nullptr, 0);}return 0;
}
3、命名管道
1原理 命名管道的原理与匿名管道不同命名管道则是通过自己创建一个管道文件然后双方通过打开这个文件实现看到同一块内存资源的功能命名管道的最大优势在于命名管道可以使不具有血缘关系的两个进程进行通信原理非常简单这里就不做过多解释可通过后面代码来学习命名管道 2测试代码 在正是学习命名管道之前我们同样先补充一批接口的使用 首先我们学习一个命令行指令mkfifo该指令加上文件名即可创建一个指定名字的管道文件具体看如下演示 mkfifo创建一个管道文件这是一个函数与我们上述命令行指令重名而已 参数一这个参数为我们要创建管道文件的文件名默认在当前目录下创建文件 参数二这个参数为我们创建管道文件的权限这个权限会与我们默认权限掩码进行计算最终权限计算规则我们设置权限 ~默认权限 返回值若调用成功则返回0失败则返回-1且错误码被设置 unlink删除一个文件与我们命令行下的rm指令功能一致 参数要删除文件的路径 返回值若调用成功则返回0失败则返回-1错误码被设置 有了上述的知识铺垫我们可以很容易的写出一个命名管道通信的程序如下所示 // comm.hpp文件主要存放公共代码如管道文件名
#include iostream
#include string
#include cstdlib
#include cstring
#include sys/types.h
#include sys/stat.h
#include fcntl.h
#include unistd.h// 管道文件名
#define PipeName FIFO.ipc
// pipeServer.cc文件主要作为接收端该端要创建管道文件运行结束后要删除管道文件
#include comm.hppint main()
{umask(0); // 设置创建文件临时的权限掩码// 1、创建管道文件int n mkfifo(PipeName, 0666);if(n -1){perror(mkfifo);exit(1);}// 2、打开管道文件int fd open(PipeName, O_RDONLY, 0666);if(fd -1){perror(open);exit(2);}// 3、通信char buf[1024];while(true){ssize_t sz read(fd, buf, sizeof(buf) - 1);buf[sz] \0; // 去掉默认换行if(strcmp(buf, quit) 0){std::cout client quit, me too std::endl;break;}std::cout client# buf std::endl;}// 4、关闭管道文件close(fd);// 5、删除管道文件unlink(PipeName);return 0;
}// pipeClient.cc文件作为通信的发送端实现主要向服务端发送请求
#include comm.hppint main()
{// 1、打开管道文件int fd open(PipeName, O_WRONLY, 0666);if(fd -1){perror(open);exit(2);}// 2、通信std::string buf;while(true){std::getline(std::cin, buf);write(fd, buf.c_str(), buf.size());}// 3、关闭管道文件close(fd);return 0;
} 我们发现只要我们将管道文件创建好其他的就像我们操作普通文件一样简单命名管道的使用比匿名管道会容易很多 注意这里有一个细节在我们使用管道文件时若我们服务端使用open打开管道文件时此时若我们的客户端没有调用open函数打开管道文件我们的服务端会一只卡在open函数内直至我们客户端也使用open函数打开管道文件这时我们的服务端open函数才会返回文件描述符这个细节在我们后面共享内存的一份代码中有一定重要的作用 三、共享内存
1、共享内存的原理 之前在讲解进程地址空间时我们曾经讲过堆栈中间有一块共享区我们之前的动态库就是会被映射到这块共享区中而今天我们的主角共享内存的原理也与这块空间有关进程间通信的本质就是看到同一块内存空间而我们共享内存实现进程间通信的方案就是我们首先在内存中申请一块空间然后将我们需要进程间通信的进程与这块物理空间进行关联映射到自己进程地址空间的共享区中这样便可以实现进程间通信了如下图所示 这样进程间通信就只需要往自己的进程地址空间的某个位置写入读取即可 2、测试代码 在使用共享内存实现进程间通信前我们需要学习下面几个函数接口 shmget申请一块共享内存空间并返回对应的shmid / 获取一块共享内存空间的shmid 参数一通过key值获取或者申请一块共享内存空间每块共享内存空间的key值不同 参数二申请 / 获取共享内存空间的大小 参数三标志位通常是由 IPC_CREAT 或 IPC_EXCL 这两个宏加上权限组成 返回值若函数调用成功则返回 shmid这个也就是共享内存的句柄与fd类似若调用失败则返回-1错误码被设置 注意 1、关于参数三我们若想获取某个key对应的shmid我们直接填0即可若某个key对应的共享内存空间不存在我们需要创建则我们一般会填 IPC_CREAT | IPC_EXCL | 0666其中0666为共享内存空间的访问权限 IPC_CREAT若共享内存空间不存在则创建之 IPC_EXCL通常配合上面IPC_CREAT使用若共享空间存在则报错返回-1因此这两个选项配合使用可以保证获得到的共享内存是一个新创建的共享内存 2、关于上述的参数一key我们可以通过下面这个函数来获取 ftok通过路径和项目id生成唯一key值 参数一项目路径这里可以随便填写一个 参数二项目id这里也可以自己设置一个 返回值若函数调用成功则返回key值若失败则返回-1错误码被设置 shmat是共享内存空间与当前虚拟地址空间进行相关联建立映射关系 参数一shmid值之前我们在shmget中获取的 id 参数二我们想要与哪一块虚拟地址空间绑定建立映射关系这里通常填NULL表示让OS系统随机分配一块空间建立映射关系 参数三一般设置为0可不关心 返回值若调用成功则返回与共享内存建立映射的虚拟地址若调用使用则返回-1错误码被设置注意这里的-1被强制装换成了 void* 类型 shmdt将共享内存与我们的共享内存去关联与我们的shmat相对应 参数一要去关联的共享内存的地址也就是shmat的返回值 返回值若函数调用成功则返回0若失败则返回-1错误码被设置 shmctrl共享内存的控制 参数一shmid值 参数二这里有三个宏来控制这个函数的行为我们一般选择IPC_RMID表示我们要删除这块共享内存空间 参数三填NULL即可 返回值若调用成功则返回0若失败则返回-1错误码被设置 学习上述的四个函数我们就可以完成进程间通信了上面四个函数基本囊括了使用共享内存进行进程间通信的整个过程首先调用ftok获取key值然后再通过shmget 创建共享内存 / 获取shmid然后通过shmat 与当前进程的虚拟地址空间进行关联接着就可以开始进行通信了通信完毕后我们使用 shmdt 将虚拟地址与共享内存去关联最后我们使用shmctl删掉申请的共享内存空间 接着我们需要学习一些命令行来获取共享内存相关信息我们可以通过 ipcs -m 来获取当前机器的共享内存申请使用情况使用 ipcrm -m shmid 来删除指定的共享内存下面为我们使用共享内存进行进程间通信的代码 // comm.hpp 文件用于保存一些共享代码#include iostream
#include cstdlib
#include cstring
#include sys/types.h
#include sys/ipc.h
#include sys/stat.h
#include sys/shm.h
#include unistd.h// 形成key所需路径
#define pathName /home/zsw/linuxCode/shm
// 形成key所需项目id
#define proj_id 0x13
// 共享内存大小
#define SIZE 4096
// shmServer.cc 文件实现服务端接收客户端发送信息其中服务端承担创建共享内存删除共享内存的任务#include comm.hppint main()
{// 1、生成唯一keyint k ftok(pathName, proj_id);if(k -1){perror(ftok);exit(1);}std::cout ftok success\n;// 2、创建新的共享空间int shmid shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666); // 创建一个新的共享内存k存在则创建失败if(shmid -1){perror(shmget);exit(2);}std::cout shmget success\n;// 3、与共享空间相关联char* shmaddr (char*)shmat(shmid, nullptr, 0);if(*(int*)shmaddr -1){perror(shmat);// 退出前释放共享空间shmctl(shmid, IPC_RMID, nullptr);exit(3);}std::cout shmat success\n;// 4、通信(读)while(true){printf(%s\n, shmaddr);if(shmaddr[0] z)break;sleep(1);}// 5、去关联int n shmdt(shmaddr);if(n -1){perror(shmdt);exit(4);}std::cout shmdt success\n;// 6、释放共享空间shmctl(shmid, IPC_RMID, nullptr);std::cout shm rm success\n;return 0;
}
// shmClient.cc 文件用于客户端发送信息给服务端其中仅需对共享内存关联通信、去关联等操作即可#include comm.hppint main()
{// 1、获取唯一keyint k ftok(pathName, proj_id);if(k -1){perror(ftok);exit(1);}// 2、获取共享内存int shmid shmget(k, SIZE, 0);if(shmid -1){perror(shmget);exit(2);}// 3、关联char* shmaddr (char*)shmat(shmid, nullptr, 0);if(*(int*)shmaddr -1){perror(shmat);exit(3);}// 4、通信(写)char ch a;while(ch z){shmaddr[0] ch;ch;sleep(1);}// 5、去关联int n shmdt(shmaddr);if(n -1){perror(shmdt);exit(4);}return 0;
} 我们编译运行服务端代码如下所示 确实我们的服务端一直读取数据可是读取到的数据为空此时我们并没有运行客户端且客户端也不可能进行输出发送可我们依旧可以读取数据只不过数据为空罢了这就与我们的管道通信有了本质的区别共享内存的进程间通信并没有访问控制 接着我们通过ipcs -m 查找我们创建的共享内存确实存在其shmid为25key就是我们通过 ftok 生成的keyowner就是共享内存的拥有者也就是当前用户perms就是这块共享内存的访问权限我们设置成了 6664096就是我们申请这块共享内存的大小nattach就是关联到这块共享内存空间的进程数status就是这块共享内存的状态 我们接着启动客户端如下所示 我们发现服务端已经收到客户端发来的信息并且我们的nattach的数量也由1变成了2随后当客户端发送完26个英文字母后退出此时服务端也读取到了字符z两个进行相继推出nattach也由2变成了0共享内存被删除 前面的实验我们也不难发现我们的共享内存不具备访问控制那我们要是想使用共享内存实现类似管道的访问控制是否可以做到呢其实也不难我们可以通过管道来实现如下面的代码 // comm.hpp 文件#include iostream
#include string
#include cstdlib
#include cstring
#include sys/types.h
#include sys/ipc.h
#include sys/stat.h
#include sys/shm.h
#include unistd.h
#include fcntl.h// 形成key所需路径
#define pathName /home/zsw/linuxCode/shm/shm_pipe
// 形成key所需项目id
#define proj_id 0x14
// 共享内存大小
#define SIZE 4096
// 管道文件名
#define pipeName ./fifo.ipc// 负责创建与销毁管道文件
class Init
{
public:Init(){umask(0);int n mkfifo(pipeName, 0666);if(n -1){perror(mkfifo);exit(5);}}~Init(){unlink(pipeName);}
};// 通过read阻塞来形成等待效果
void wait(int fd)
{int tmp 1;ssize_t sz read(fd, tmp, sizeof(int));if(sz ! sizeof(tmp)){std::cerr fd: fd 等待错误\n;printf(sz:%d, sizeof(tmp):%d\n, sz, sizeof(tmp));perror(wait);exit(6);}
}// 通过write唤醒等待进程
void signal(int fd)
{int tmp 1;ssize_t sz write(fd, tmp, sizeof(tmp));if(sz ! sizeof(int)){std::cerr write err\n;exit(7);}
}// shmServer.cc#include comm.hppint main()
{Init init; // 创建管道文件// 打开管道文件int fd open(pipeName, O_RDONLY, 0666);// 1、生成唯一keyint k ftok(pathName, proj_id);if(k -1){perror(ftok);exit(1);}std::cout ftok success, k: k std::endl;// 2、创建新的共享空间int shmid shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666); // 创建一个新的共享内存k存在则创建失败if(shmid -1){perror(shmget);exit(2);}std::cout shmget success\n;// 3、与共享空间相关联char* shmaddr (char*)shmat(shmid, nullptr, 0);if(*(int*)shmaddr -1){perror(shmat);// 退出前释放共享空间shmctl(shmid, IPC_RMID, nullptr);exit(3);}std::cout shmat success\n;sleep(5);// 打开管道文件int fd open(pipeName, O_RDONLY, 0666);// 4、通信(读)while(true){wait(fd);printf(%s\n, shmaddr);}// 5、去关联int n shmdt(shmaddr);if(n -1){perror(shmdt);exit(4);}std::cout shmdt success\n;// 6、释放共享空间shmctl(shmid, IPC_RMID, nullptr);std::cout shm rm success\n;return 0;
}
// shmClient.cc文件#include comm.hppint main()
{// 小坑不能在这打开管道文件因为打开管道文件有一个特性如果对端不打开这里将会一直阻塞// 然后在后续过程中若客户端进程先执行下面shmget函数可能出现文件未创建的错误因为客户端// 执行的比服务端快服务端还未创建共享内存而客户端就想访问了// int fd open(pipeName, O_WRONLY, 0666);// 1、获取唯一keyint k ftok(pathName, proj_id);if(k -1){perror(ftok);exit(1);}std::cout ftok success, k: k std::endl;// 2、获取共享内存int shmid shmget(k, SIZE, 0);if(shmid -1){perror(shmget);exit(2);}std::cout shmget success\n;// 打开管道文件int fd open(pipeName, O_WRONLY, 0666);// 3、关联char* shmaddr (char*)shmat(shmid, nullptr, 0);if(*(int*)shmaddr -1){perror(shmat);exit(3);}std::cout shmat success\n;// 4、通信(写)std::string msg;while(true){std::cout Enter message ;getline(std::cin, msg);strcpy(shmaddr, msg.c_str());signal(fd);}// 5、去关联int n shmdt(shmaddr);if(n -1){perror(shmdt);exit(4);}std::cout shmdt success\n;return 0;
} 这段代码就有我们前面所说管道的一个性质并且这里有一个小坑正常情况下我们一般先运行服务端代码因为我要保证客户端代码在获取 shmid 时已经被创建了因此在测试代码一不加管道代码时我们总是先运行服务端代码 如上图所示若我们将打开管道文件的代码放到最上面也就是最先执行此时当我们的服务端打开管道文件时由于对端未打开管道文件因此我们会阻塞住此时我们接着运行客户端代码由于客户端打开管道文件时由于对端已经在等待了因此可以直接返回管道文件对应文件描述符此时若我们的客户端继续运行当调用shmget获取shmid时由于key所对应的共享内存并未创建因此我们的客户端会直接运行失败 3、共享内存的特性 1、共享内存仅需内存级读写即可与管道不同使用管道需要调用系统调用read、write等而共享内存仅需往指定虚拟地址空间写入、读取即可 2、共享内存是最快的通信方式因为拷贝次数最少写入最少拷贝仅需从键盘文件对应缓冲区拷贝到共享内存中即可写入仅需从共性内存中拷贝到要写入的缓冲区即可