1.进程间通信目的
1.数据传输:两个进程之间需要传递数据信息
2.资源共享:多个进程之间需要共享资源
3.通知时间:一个进程通知另一个进程发生了某个事件
4.进程控制:一个进程希望完全控制另一个进程的执行
2.进程间通信发展
1.最初版本:基于文件系统的通信方法
2.system v通信:基于键值+权限的内核有效的多进程间通信
3.POSIX通信:可实现网络间进程通信
3.匿名管道通信
由于进程之间具有独立性,所以进程间不能直接通信,我们需要使用独立于进程存在的文件来让进程间实现通信,管道文件就是这种文件,使用管道文件的基于文件系统的进程间通信就是管道通信
设计思路:复用文件系统的已有代码,模拟通信模块
3.1基本流程:
首先who进程会将标准输出流文件改为管道文件,所以who的命令输出给了管道文件,然后wc进程的标准输入文件改为管道文件,也就是wc读取的是管道文件数据,进行wc命令后将结果输出到显示器上
3.2匿名管道底层原理
模拟父进程读,子进程写的单项通信
我们先让父进程分别创建对管道文件拥有只读和只写权限的两个file结构体,然后让子进程不仅仅继承进程管理结构体,也写实拷贝上文件描述符数组中指向的两个file结构体
然后为了不让文件误操作干扰信息流的顺序,我们将父进程的只写权限file关闭,将子进程的只读权限file关闭,从此父进程就只能对管道文件进行读取操作,子进程就只能对管道文件进程写操作,实现进程间单向信息流动
注意:如果需要进行双向信息流动,只要创建两个管道文件同理操作即可
以上为简单过程示意图
3.3使用匿名管道通信
管道接口调用
pipe用于创建管道文件的接口,参数是一个元素个数为2的整形数组,索引为0存储的是具有只读权限的管道文件fd,索引为1存储的是具有只写权限的管道文件fd
注意:该参数是输出型参数,也就是说,该数组的数据是由接口申请完成后放入的,不是我们人为放入的
返回值为0表示成功创建,为-1表示失败
使用pipe接口
cpp#include<iostream> #include<cstdio> #include<unistd.h> #include<cstdlib> using namespace std; int main() { int pipefd[2] = {0}; int n = pipe(pipefd); if(n == -1) { perror("pipe"); exit(1); } else if(n == 0) { //创建成功 printf("creat pipe success: pipefd[0] : %d pipefd[1] : %d\n",pipefd[0],pipefd[1]); } return 0; }
3.4模拟子进程写,父进程读的通信
cpp#include<iostream> #include<cstdio> #include<unistd.h> #include<cstdlib> #include<sys/types.h> #include<sys/wait.h> using namespace std; int main() { //1.父进程创建管道 int pipefd[2] = {0}; int n = pipe(pipefd); if(n == -1) { perror("pipe"); exit(1); } else if(n == 0)//管道创建成功 { //2.创建子进程 pid_t id = fork(); if(id < 0) { perror("fork"); exit(1); } else if(id == 0) { //3.子进程关闭对应的fd close(pipefd[0]); //4.写入信息 string str = "subprocess"; string self = to_string(getpid()); int cnt = 1; while(1) { string message = str + ", " + self + ", " + to_string(cnt++); write(pipefd[1],message.c_str(),message.size()); sleep(1);//减缓写入速度,避免卡死 } } else { //3.父进程关闭对应的fd close(pipefd[1]); //4.读取信息 while(1) { char buff[1024]; ssize_t b = read(pipefd[0],buff,sizeof(buff)-1); if(b > 0) { buff[b] = 0; cout << "read " << buff << endl; } } pid_t rid = waitpid(id,nullptr,0); } //4.正常通信 } return 0; }总步骤:
1.父进程创建管道文件2.创建子进程
3.父子进程各自关闭对应fd文件
由于我们规定父进程只读,子进程只写,所以父进程应该关闭pipefd[1]的文件,子进程关闭pipefd[0]的文件
4.父子进程之间进行通信
父进程利用write写入内核管理的文件缓冲区(管道文件),然后子进程利用read读取管道文件的内容到buff数组中,最后输出读取到的数据
疑问:那我们父进程定义的全局变量不是也可以给子进程看到吗?这是不是通信?
子进程可以看到父进程定义的全局变量只是因为继承了父进程的数据,这是固定的数据传输,不是真正的通信
3.5匿名管道的四种情况与五大特性
四种情况:
1.写端不写,且写端不关:写端没数据,读端会阻塞
2.读端不读,且读端不关:写端一直写入数据,直到管道写满
3.写端不写,且写端关闭:读端一直读,直到读到文件末尾,read返回0
4.读端不读,且读端关闭:os直接用13号退出信号kill掉写入进程,且将管道文件清理释放
五大特性:
1.用于具有亲缘关系的进程之间进行,常用于父子通信
2.单向通信
3.管道生命周期与相关进程一致
4.面向字节流(读写次数没有强相关)
5.管道自带同步机制(写端停止写入后,读端也会阻塞停止)
3.6基于匿名管道通信的进程池实现
进程池基本业务:
1.利用匿名管道通信的特性,通过控制父进程写入数据来控制子进程启停
2.父进程写入不用特征数据,让子进程根据获取到的数据执行不同特定功能
第一点的实现原理:匿名管道中,写端不写且写端不关,读端会阻塞,从而达到停止子进程的目的,反之同理可以让子进程执行
第二点的实现原理:可以在子进程的代码块中设置多个条件判断语句,通过read到的数据来进入不同的执行代码中
1.初始化进程池:
cpp#include "ProcessPool.hpp" //父->子 int main() { for (int i = 1; i <= gdefault_process_num; i++) { // 创建管道文件 int pipefd[2] = {0}; int n = pipe(pipefd); if (n == 0) { // 创建子进程 // 关闭父子进程需要关闭的fd pid_t id = fork(); if (id == 0) // 子进程 { close(pipefd[1]); exit(0); } else if (id > 0) // 父进程 { close(pipefd[0]); } } } return 0; }对于单次信道创建,都是创建管道文件->创建子进程->父子进程关闭对应fd
而由于进程池需要有多个子进程,所以我们人为设定信道创建次数为gdefault_process_num
不过由于pipefd和子进程id都是局部有效,我们不能实现父进程向任意子进程相关信道传递信息的功能,所以我们需要创建一个类来管理信道信息
cppclass channel { public: channel() {} channel(int fd, const std::string &name, pid_t id) : _wfd(fd), _name(name), _sub_id(id) {} ~channel() {} private: int _wfd; // 写入端fd std::string _name; // 信道名 pid_t _sub_id; // 目标子进程id };然后我们在主函数中创建一个channel类型的vector数组就可以管理所有信道了
验证信道创建:
1.在class中创建Debug接口,用于打印信道信息
2.在父进程退出信道创建循环后进行打印
2.父进程控制子进程执行任务
由于我们是通过父进程给子进程分配任务,为了避免出现有的子进程任务量过大,而有的子进程任务量过小的情况,我们可以通过轮询唤醒的方式分配任务,从而实现子进程的负载均衡
channels类内get接口(用于将类内成员变量值返回到外部)
cppint Fd() { return _wfd; } std::string Name() { return _name; } pid_t Resubid() { return _sub_id; }
cpp// 轮询写入信息给子进程 int index = 0; while (true) { int who = index; index++; index %= channels.size(); int x = 1; printf("选择信道:%s, 目标子进程id: %d\n", channels[who].Name().c_str(), channels[who].Resubid()); write(channels[who].Fd(), &x, sizeof(x)); sleep(3); }注意:
1.为了能在显示器上打印出来printf信息,我们需要使用换行符
2.为了让写入间隔不会太短导致刷屏,我们使用sleep函数
3.我们现在是从写端写入数据进入管道,要保证读端没有关闭,所以要在子进程处设置长时间sleep,保证其不结束太快
3.父进程等待子进程退出
1.关闭子进程和等待子进程的接口
cppvoid Close() { close(_wfd); } void Wait() { pid_t id = waitpid(_sub_id, nullptr, 0); std::cout << "回收子进程" << id << std::endl; // todo }2.回收子进程函数
cpp// 等待子进程退出 void waitsubprocess() { // 子进程结束 for (auto &e : _channels) { e.Close(); } // 回收子进程,让子进程退出僵尸状态 for (auto &f : _channels) { f.Wait(); } }4.装载子进程可执行的任务
cpp#pragma once #include <iostream> #include <string> #include <vector> #include <functional> using task_t = std::function<void()>; // 3种任务 // task:0 void task0() { std::cout << "task 0" << std::endl; } // task:1 void task1() { std::cout << "task 1" << std::endl; } // task:2 void task2() { std::cout << "task 2" << std::endl; } std::vector<task_t> tasks; class Init { public: Init() { tasks.push_back(task0); tasks.push_back(task1); tasks.push_back(task2); } ~Init() { } }; Init init;1.利用Init类的构造函数完成任务的装载,从而tasks内就装载好了所需要执行的函数
2.将该文件包含到进程池.hpp文件中,子进程采取随机选取任务的方式调用任务函数
5.将前面的业务封装在类中
cpp//processpool.hpp #ifndef PROCESS_POOL #define PROCESS_POOL #include <iostream> #include <unistd.h> #include <cstdio> #include <stdlib.h> #include <functional> #include <string> #include <vector> #include <sys/types.h> #include <sys/wait.h> #include <ctime> #include "Task.hpp" const int gdefault_process_num = 5; using func_t = std::function<void(int fd)>; // 重命名特定类型函数对象 using task_t = std::function<void()>; extern std::vector<task_t> tasks; class Channel { public: Channel() {} Channel(int fd, const std::string &name, pid_t id) : _wfd(fd), _name(name), _sub_id(id) {} ~Channel() {} void Debug() { printf("channel name: %s, write fd: %d, target id: %d\n", _name.c_str(), _wfd, _sub_id); } int Fd() { return _wfd; } std::string Name() { return _name; } pid_t Resubid() { return _sub_id; } void Close() { close(_wfd); } void Wait() { pid_t id = waitpid(_sub_id, nullptr, 0); std::cout << "回收子进程" << id << std::endl; // todo } private: int _wfd; // 写入端fd std::string _name; // 信道名 pid_t _sub_id; // 目标子进程id }; class ProcessPool { public: ProcessPool(int num = gdefault_process_num) : _processnum(num) { srand(time(nullptr) ^ 0x666); // 确保随机性 } ~ProcessPool() {} bool InitProcessPool() // 1.初始化进程池 { for (int i = 1; i <= _processnum; i++) { // 创建管道文件 int pipefd[2] = {0}; int n = pipe(pipefd); if (n == 0) { // 创建子进程 // 关闭父子进程需要关闭的fd pid_t id = fork(); if (id == 0) // 子进程 { close(pipefd[1]); int code = 0; ssize_t n = read(pipefd[0], &code, sizeof(code)); if (n == sizeof(code)) // 子进程执行任务 { if (code >= 0 && code < tasks.size()) { tasks[code](); } else { std::cerr << "task wrong" << std::endl; } } else if (n == 0) { std::cout << "subprocess exit" << std::endl; } else { std::cerr << "read wrong" << std::endl; } exit(0); } else if (id > 0) // 父进程 { close(pipefd[0]); std::string name = "channel-" + std::to_string(i); _channels.emplace_back(pipefd[1], name, id); // Channel channel(pipefd[1],name,id); // channels.push_back(channel); } else { return false; } } else { return false; } } return true; } // 2.父进程控制子进程 // 轮询写入信息给子进程 void ctrlsubprocess() { int index = 0; while (true) { // 选择信道 int who = index; index++; index %= _channels.size(); // 指定子进程所要执行的任务 int x = rand() % tasks.size(); printf("选择信道:%s, 目标子进程id: %d\n", _channels[who].Name().c_str(), _channels[who].Resubid()); write(_channels[who].Fd(), &x, sizeof(x)); sleep(3); } } // 控制特定次数的轮询 void ctrlsubprocess(int count) { if (count <= 0) return; int index = 0; while (count--) { int who = index; index++; index %= _channels.size(); int x = rand() % tasks.size(); printf("选择信道:%s, 目标子进程id: %d\n", _channels[who].Name().c_str(), _channels[who].Resubid()); write(_channels[who].Fd(), &x, sizeof(x)); sleep(3); } } // 等待子进程退出 void waitsubprocess() { // 子进程结束 for (auto &e : _channels) { e.Close(); } // 回收子进程,让子进程退出僵尸状态 for (auto &f : _channels) { f.Wait(); } } private: std::vector<Channel> _channels; // 进程池所有信道 int _processnum; // 进程个数 }; #endif
cpp//Task.hpp #pragma once #include <iostream> #include <string> #include <vector> #include <functional> using task_t = std::function<void()>; // 3种任务 // task:0 void task0() { std::cout << "task 0" << std::endl; } // task:1 void task1() { std::cout << "task 1" << std::endl; } // task:2 void task2() { std::cout << "task 2" << std::endl; } std::vector<task_t> tasks; class Init { public: Init() { tasks.push_back(task0); tasks.push_back(task1); tasks.push_back(task2); } ~Init() { } }; Init init;6.bug理解
其实这里还存在一个关于子进程等待的bug
现象:如果直接从头开始进行close和wait会出现等待失败的情况
原因:父进程不断创建子进程的时候,子进程继承了前面所有父进程写端的管道fd,所以对应子进程无法关闭,因为写端引用计数没有减为0,进而等待失败
图示:
说明:
对于第一个子进程来说,没有问题因为父进程继承给子进程只有关于第一个管道文件的两个fd,子进程会关闭写端fd,父进程会关闭读端fd
对于第二个子进程来说,会多一个对第一个管道的写端fd
这是因为父进程继承给子进程的fd有三个,分别是第一个管道的写端fd,第二个管道的读端fd以及写端fd。由于我们之前只让子进程关闭新的管道写端fd,所以会多一个之前管道的写端fd
疑问:为什么子进程的读端fd一直是3?
因为父进程会不断关闭读端fd,而新创建的管道文件fd的读端会根据就近原则将其放置在fd为3的地方,之后继承给子进程,所以子进程读端fd一直是3
解决方法1:将close和wait分开进行,先将所有写端管道都关闭,然后再逐个等待子进程退出
解决方法2:倒着进行关闭管道以及等待子进程
终极方法:解决bug
我们可以在子进程创建出来的时候,顺便将父进程历史遗留管道的写端在子进程中也关闭掉
cpp//关闭继承的多余w端 for(auto &c : _channels) { c.Close(); }由于管道数组是在fork的时候就继承了,此时父进程还没有将通信当前子进程的管道加入进去,所以我们当前的管道都是该进程之前的进程的管道,我们直接close掉此时存储的w端即可
4.命名管道
4.1原理
首先我们的命名管道文件是存储在磁盘中的,将其文件属性和文件数据分别加载到内存中,进程1就用写端fd指向file(写权限),进程2就用读端fd指向file(读权限),然后两个进程就看到了同一份资源,从而可以实现通信
注意:命名管道文件其实也是磁盘中存储的文件,只是被特殊处理了,不再将缓冲区数据刷新到磁盘中
应用场景:用于毫无血缘关系的进程之间进行文件级通信
4.2使用命令
(1)创建管道文件:mkfifo
(2)输入数据进入管道
(3)读取管道数据
需要注意的是,输入数据进入管道的时候,是无法结束进程的,只有当读取数据端读了之后,缓冲区刷新,进程才会结束
也就是管道的两端都打开后才会正确进行
4.3使用接口
Makefile
bash.PHONY:all all:client server client:client.hpp g++ -o $@ $^ -std=c++11 server:server.hpp g++ -o $@ $^ -std=c++11 .PHONY:clean clean: rm -f client rm -f server我们创建一个服务端,一个用户端,所以需要两个可执行程序,可是Makefile默认只形成一个对象
所以我们需要让一个all对象依赖于client和server对象,这样我们从上到下扫描的时候Makefile就会去形成all对象,而all对象需要先形成client和server对象,从而可以达到形成两个可执行程序的目的
文件结构:
server是服务端,client是接收端,common用于将两端都需要用到的东西整合起来,然后两端再包含该文件
相关接口:
1.创建管道文件:mkfifo
这是用于创建管道的接口,第一个参数是管道名字,第二个是管道文件的权限码
返回值为0表示成功,为-1表示失败
2.删除管道文件:unlink
参数是管道名字,返回值为0表示成功,为-1表示失败
命名管道代码编写:
(1)NamePipe
1.创建管道
cppbool Creat() { // 创建管道 int n = mkfifo(fifoname.c_str(), mode); if (n == 0) { return true; } else { perror("mkfifo"); return false; } }2.关闭管道
cppvoid Remove() { // 关闭管道 int m = unlink(fifoname.c_str()); }3.以读权限打开文件
cppbool openforread() { // 打开文件 int fd = open(fifoname.c_str(), O_RDONLY); if (fd < 0) { perror("open"); return false; } _fd = fd; return true; }4.以写权限打开文件
cppbool openforwrite() { int fd = open(fifoname.c_str(), O_WRONLY); if (fd < 0) { perror("open"); return false; } _fd = fd; return true; }5.关闭fd文件
cppvoid Close() { if (_fd != defaultfd) close(_fd); }6.读取数据
cppbool Read(std::string *out) { // 读取信息 char buff[SIZE]; buff[0] = 0; ssize_t re = read(_fd, buff, sizeof(buff) - 1); if (re > 0) // 写端有信息 { buff[re] = 0; // 设置字符串结尾 *out = buff; } else if (re == 0) // 写端无信息 { return false; } // 读出错 else { return false; } return true; }out是输出型参数,也就是需要Read函数填充out变量的值
7.写入数据
cppvoid Write(const std::string &in) { write(_fd, in.c_str(), in.size()); }in是输入型参数,也就是接口外部填写in的值,然后传给write输入
8.内部成员变量
cppstd::string _name; int _fd;(2)common.hpp
cpp#ifndef COMMON_HPP #define COMMON_HPP #include<iostream> #include<string> #include<cstdio> #include <fcntl.h> #include<unistd.h> #include<sys/types.h> #include<sys/stat.h> const std::string fifoname = "fifo"; mode_t mode = 0666; #define SIZE 1024 #endif该文件是用于将多端文件都要包含的头文件以及全局变量集中起来包含,从而简化其它代码的显示
(3)server.cpp
cpp#include "NamedPipe.hpp" int main() { NamedPipe name_pipe(fifoname.c_str()); // 创建管道 name_pipe.Creat(); // 打开文件 name_pipe.openforread(); // 读取信息 std::string message; while (true) { bool n = name_pipe.Read(&message); if (n == true) { std::cout << message << std::endl; } else{ break; } } // 关闭文件 name_pipe.Close(); // 关闭管道 name_pipe.Remove(); return 0; }总体逻辑:
创建管道,以读权限打开管道文件,从管道文件读取信息并打印到显示器上,最后关闭写端文件,移除管道文件
(4)client.cpp
cpp#include "NamedPipe.hpp" int main() { NamedPipe name_pipe(fifoname); // server已经创建,这里不用重复创建 name_pipe.openforwrite(); while (true) { std::cout << "enter message"; std::string message; std::getline(std::cin, message); name_pipe.Write(message); } name_pipe.Close(); return 0; }总体逻辑:
直接以写权限打开管道文件,然后写入数据,写入结束后关闭写端文件
这里不用移除管道文件,因为写端关闭后读端还要读取,直到读端都没有数据读取的时候才会让管道关闭
注意:
1.运行顺序一定是先运行server再运行client,因为要先创建好管道文件,然后才能进行读写操作,否则是无法完成通信的(client如果先运行,他会打不开管道文件,因为没创建,进而写入的fd也是不存在的,非法的)
2.有的时候遇到代码基本不会错,但是链接出问题,有可能是文件损坏了,可以尝试重新创建文件
3.对于输入型参数,参数设置为const &类型,对于输出型参数设置为*,对于输入输出型参数设置为&类型
5.system v共享内存通信
5.1原理
system v共享内存通信:我们在物理内存中申请一块空内存空间,然后让需要通信的进程的共享区内特定区域虚拟地址和申请的空内存物理地址建立页表映射,从而两个进程都可以访问物理内存中的同一份空间,也就可以通信了
5.2相关接口
1.创建、获取共享内存空间:shmget
参数1:键值
参数2:申请的空间的大小(默认为4096的整数倍)
参数3:传递标志位图,类似(O_RDONLY | O_WRONLY的形式传参),可通过该参数设置目的,到底是创建空间还是获取空间
返回值:返回的是int型数据,性质和fd类似,若为-1表示函数执行失败返回
2.获取键值:ftok
通过文件名和项目id经过相应算法计算后得出键值,该键值是唯一的可以标志一个共享内存空间的值,在拥有多个共享内存的物理空间中就可以精确找出需要使用的是哪一个共享内存
3.控制共享内存:shmctl
功能:控制共享内存属性或者删除共享内存
参数1:用于用户层面标识共享内存空间的id值
参数2:命令位图
4.挂接共享内存:shmat
功能:将共享内存映射到当前进程的虚拟地址空间中
参数1:需要挂接的共享内存的shmid
参数2:需要挂接到的虚拟地址(一般默认写nullptr,让系统自己分配)
参数3:标志位图,一般使用缺省即可(缺省就是使用共享内存权限),设置为0
返回值:虚拟地址,就是共享内存成功挂接的段空间的起始虚拟地址,若失败就返回-1
5.解除挂接:shmdt
参数:挂接到的段起始虚拟地址(shmat的返回值)
返回值:成功返回0,失败返回-1
两个进程通信实现方法:
由于物理内存中有很多共享内存块,为了区别出内存,我们使用键值key
键值key是根据文件名和项目id共同计算得到的,只要让两个进程共同包含的头文件中约定好文件名和项目id,然后进程AB各自利用这些信息就可以计算出唯一的key值
利用同一个key值就可以访问到同一个共享内存空间了
key值会存储在管理共享内存块的结构体中
结论1:一定要有一方是创建空间的,另一方是获取空间的
标志1:IPC_CREAT
单独使用:若创建的内存不存在,则创建,若已经存在,则获取
标志2:IPC_EXCL(不能单独使用)
和标志1结合使用:若创建的内存不存在,则创建,若已经存在,错误返回
所以IPC_CREAT | IPC_EXCL的作用是创建一个全新的共享内存空间
标志3:IPC_STAT
单独使用,可以用于获取共享内存的属性信息
结论2:进程内存泄漏问题,在进程退出后泄漏的内存会被操作系统释放
不过由于大部分软件都是死循环的,只要用户不主动删除就不会退出,所以只要存在内存泄漏问题就可能导致闪退,从而降低用户体验
结论3:共享内存的生命周期是跟随内核的,进程退出不会销毁共享内存
所以为了避免共享内存泄漏导致操作系统卡死,我们需要使用命令或者接口将共享内存关闭
结论4:key是内核使用的用于标识唯一的shm,而shmid是给用户使用的
结论5:不管用户申请的共享内存空间是多少,系统都会申请4096的整数倍,但是只将用户申请的空间交给用户使用
5.3自封装代码
(1)shm.hpp
类:shared_memory
1.CreateHelper(private)
cppbool CreateHelper(int flags) { _key = ftok(gpathname.c_str(), gproj_id); if (_key < 0) { perror("fork"); return false; } printf("key:0x%x\n", _key); _shmid = shmget(_key, _size, flags); if (_shmid < 0) { perror("shmget"); return false; } return true; }首先使用ftok将约定好的key计算出来,再使用shmget创建键值为key的共享内存
由于创建端和获取端都是使用同一套代码,唯一的区别就是shmget的标志位传递,所以我们将主要代码封装为类内私有接口,然后让两端分别调用
(1)对于创建端:需要创建一个全新的共享内存(IPC_CREAT|IPC_EXCL|0666)
设置标志确保是新创建的空间,且还要设置好权限值,好让后续的挂接可以执行
可直接设置权限的原因:位图特意将尾三位留给权限设置
cppbool Create() // 创建全新共享内存 { return CreateHelper(IPC_CREAT | IPC_EXCL | 0666); }(2)对于获取端:需要获取一个已经存在的共享内存(IPC_CREAT)
cppbool Get()//获取共享内存 { return CreateHelper(IPC_CREAT); }2.RemoveShm(移除共享内存)
cppbool RemoveShm() { int n = shmctl(_shmid,IPC_RMID,nullptr); if(n < 0) { perror("shmctl"); return false; } std::cout << "删除共享内存成功" << std::endl; return true; }使用shmctl将指定共享内存删除(因为共享内存是内核级的,所以不销毁会有内存泄漏)
3.Attach(挂接)
cppbool Attach()//挂接共享内存 { _start_add = shmat(_shmid,nullptr,0); if((long long)_start_add == -1) { perror("shmat"); return false; } return true; }使用shmat挂接共享内存到当前进程的共享区中,并返回挂接到的段起始地址
这里需要创建共享内存的时候的权限支持
4.Detach(解除挂接)
cppbool Detach() // 解除挂接 { int n = shmdt(_start_add); if (n < 0) { perror("shmdt"); return false; } std::cout << "挂接移除" << std::endl; return true; }由于移除共享内存是强制的,不管共享内存是否已经无进程使用都会直接标记为待删除,新的进程无法使用该共享内存,但是已经链接的进程仍然可以正常使用,只有当所有进程都解除挂接才会真正删除。
所以为了避免影响其他进程的使用,我们不用共享内存就要进行解绑
使用shmdt移除当前进程对指定共享内存的挂接
5.AddChar与PopChar(模拟输入字符数据)
cppvoid AddChar(char ch)//添加字符数据 { if(_num == _size) return; ((char *)_start_add)[_windex++] = ch; _windex %= _size; _num++; } void PopChar(char *ch)//删除字符数据 { if(_num == 0) return; *ch = ((char *)_start_add)[_rindex++]; _rindex %= _size; _num--; }首先我们创建_num表示空间内字符个数,_windex表示写入光标索引,_rindex表示读取光标索引
读写原理:因为共享内存空间是直接映射到进程的共享区的,所以不需要调用系统调用接口进行读写,我们已经获取到了共享内存在进程中的虚拟地址:_strat_add,直接读写即可
读写方法:直接将空间看为字符数组,然后不断输入或者读取字符数据进行通信
问题:由于读写两端的进程都是独立实例化类shared_memory,所以他们的_num是不互通的,写端_num的变化不会影响读端的_num,所以读端一直不能进行读取,被if语句截取了
解决方法:约定共享内存的起始四个字节位置为_num的存储位置,然后由于两个进程都可以直接看到这个共享内存空间,从而就可以让一端_num的变化影响到另一端的_num值
在Attach代码中设置:
cpp_num = (int *)_start_add; _datastart = (char *)_start_add + sizeof(int);让_num的地址设置到共享内存的开始处,将初始值设置为0要放到另一个接口中(只有最开始的发送端才可以设置),让_datastart的地址设置在_num后面
修改添加字符和获取字符接口:
cppvoid AddChar(char ch) // 添加字符数据 { if (*_num == _size) return; ((char *)_datastart)[_windex++] = ch; _windex %= _size; (*_num)++; } void PopChar(char *ch) // 删除字符数据 { if (*_num == 0) return; *ch = ((char *)_datastart)[_rindex++]; _rindex %= _size; (*_num)--;//运算符优先级 }注意:
1.将初始记录数据地址换为_datastart
2.++的优先级高于*,所以我们要让*_num处于小括号中提高优先级,否则会出现问题
6.PrintAttr(用于打印属性信息)
cppbool PrintAttr() { struct shmid_ds ds; int n = shmctl(_shmid, IPC_STAT, &ds); if (n < 0) { perror("shmctl"); return false; } //打印属性信息 printf("key: 0x%x",ds.shm_perm.__key); return true; }系统中是使用shmid_ds结构体来保存属性信息的,所以我们设置IPC_STAT标志,然后shmctl就会将共享内存的属性放到ds中
打印key:key是ds中的shm_perm结构体内的成员变量
(2)client.cpp
cpp#include "shm.hpp" int main() { shared_memory shm; shm.Get(); shm.Attach(); shm.SetZero();//初始化_num值为0 // 写入信息 char ch = 'A'; for(;ch <= 'Z';ch++) { shm.AddChar(ch); } shm.Detach(); return 0; }流程:获取->挂接->写端初始化_num为0->写入数据->解除挂接
(3)server.cpp(读端)
cpp#include "shm.hpp" int main() { shared_memory shm; shm.Create(); shm.Attach(); shm.PrintAttr(); // 读取数据 char ch; while (true) { shm.PopChar(&ch); sleep(1); std::cout << "读取信息: " << ch << std::endl; } shm.Detach(); shm.RemoveShm(); return 0; }流程:创建->挂接->打印属性信息key->读取数据->解除挂接->移除共享内存
5.4共享内存特征
1.生命周期随内核
2.共享内存的数据传输速度是IPC中最快的(减少数据拷贝次数,通信期间不调用系统调用)
3.本身不提供同步和互斥机制来支持多进程间通信
5.5消息队列与信号量
(1)消息队列就是以队列的形式将共享内存空间使用起来,读写的都是队列管理的有类型的数据块,这里的类型其实就是标签(用来标识数据块的特征信息)
(2)并发编程特征
1.多个进程都能看到同一个资源,这叫共享资源
若某个共享资源一次只能一个进程访问(被保护的共享资源),那么他就是临界资源/互斥资源
2.保护资源的方式:互斥与同步
同步:访问临街资源有一定顺序性(一块大区域可以多个进程按顺序访问)
互斥:只允许一个执行流访问资源(一块处于大区域中的小区域,只允许一个进程访问)
3.临界区:涉及临界资源访问的代码叫临界区
对共享资源的保护本质是对访问共享资源的代码保护
(3)信号量
本质:描述临界资源数量的计数器
所有进程要访问临界资源前,需要先申请信号量(预定资源)
信号量分为二元信号量和多元信号量
二元信号量:信号量总数为1,只允许一个进程访问临界资源,可以用于实现互斥功能
多元信号量:信号量总数大于1,允许多个进程访问临界资源,用于实现互斥/同步功能
注意:信号量本身也是共享资源,所以我们对信号量也要有保护操作
具体来说就是对它++或--的使用使用原子操作(--是p操作,++是v操作)
(4)system v
他其实是一个os设计的专门用来通信的模块,他的许多通信方式都有共同点
这是内核中的管理结构图
结论1:内核中管理IPC资源使用的是数组(柔性数组)
ipc_id_ary数组:是ipc_perm指针类型的数组,该数组存储的是ipc_perm类型的对象地址
shmid_kernel(共享内存)
sem_array(信号量)
msg_queue(消息队列)
以上的IPC相关结构体第一个成员变量都是ipc_perm,且由于结构体的地址和结构体第一个变量的地址是数值上一样的(都是取首地址),所以只要指向了ipc_perm就相当于指向了对应的IPC通信(要使用具体的通信方式成员变量就对指针强制类型转换即可)
注意:柔性数组可以很方便的进行扩容,以适配IPC通信的灵活性需求,避免需求少时有浪费,需求多时有不够。柔性数组是和成员变量一起malloc的,前面是成员变量申请的空间,后面若干个字节则是根据数组存储数据类型以及元素个数来定
结论2:shmid以及信号量和消息队列的id值本质就是ipc_id_ary数组的下标
结论3:动态库的机制其实就是在system v体系中加上了将动态库从磁盘中加载到物理内存中这一步

























