【Linux】Linux 操作系统 - 27 , 进程间通信(三) --System V 共享内存
文章目录
- ● 是什么 ?
- ● 共享内存的原理
- ● 使用到的系统调用
- ● 总结
- ● 共享内存的使用完整代码
- 一 、效果展示
- 二 、共享内存的优缺点(重点)
- ● 与管道通信方式对比
- ● 思维导图总结
● 是什么 ?
System V 共享内存是进程间通信(IPC)中性能最高的机制,其核心思想是让多个进程直接访问同一块物理内存区域,避免数据在进程地址空间之间的拷贝。 共享内存通过内存映射实现进程间数据的直接读写,适用于需要频繁交换大量数据的场景(如数据库缓存、高性能计算数据共享)。
● 共享内存的原理
上面提到了共享内存是通过映射的 , 下面具体看看怎么映射 .
那如果两个进程呢 ?? 共享内存又是什么呢 ??
现在 , 在内存中申请了一块空间 , 那么要通信 , 怎么做 ?
所以 , 这也是为什么叫共享内存的原因了 , 两个不同的进程间共享同一块内存进行通信 , 那操作系统会不会有很多进程都要通信呢 ?? 肯定要 , 所以 , 就会存在多个共享内存 , 那么这么多共享内存 , 操作系统要不要被管理呢 ??? 答 : 要 . 怎么管理 ??? 答 : 先描述 , 再组织 ! 那就意味着 , 在操作系统中一定是有管理共享内存的数据结构对象的 , 并不是简单的映射 !
- 再次理解 , 那么映射需要映射全部地址块吗 ??
● 使用到的系统调用
那么 , 问题来了 ?? 以上的原理映射是谁在做 ?? 答 : 操作系统 , 那么我们能直接访问操作系统吗 ?? 做不到 , 所以 , 我们需要用系统调用来让操作系统帮我们完成 !
所以 , 需要根据以下步骤创建 , 使用 , 销毁 .
- 创建共享内存
shmget 系统调用
(key 用 ftok
系统调用构建)
使用代码练习
#include <iostream>
// 创建共享内存的头文件
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>int main()
{// 创建共享内存// shmget(key_t key , size_t size , int shmflg)// 构建 Key//注意 : "." . 是一个硬链接 , 指向一个文件 , 所以本质是文件路径 key_t key = ftok("." , 0x112233);//返回值 : 成功返回 key , 否则返回 -1 并设置错误码if(key > 0)std::cout << "ftok success , ftok = " << key << std::endl; int shmid = shmget(key , 1024 , IPC_CREAT);//返回值 : 成功返回合法的标识符 , 失败返回 -1 并设置错误码// 这个返回值是给我们用户用的 , 我们访问共享内存都用这个标识符if(shmid > 0){std::cout << "shmget success , shmget = " << shmid << std::endl; }return 0;
}
- 查看共享内存命令
ipcs -m
可以查看创建的共享内存 !
- 映射共享内存 , 即 : 挂接共享内存
共享内存创建好了 , 就要映射到进程地址空间中了 . 即 : 将共享内存和进程关联(挂接) !
shmat 系统调用
, 可以通过查看共享内存中的相关信息 – nattch 查看关联数
auto shm_addrr = shmat(shmid , nullptr , 0);
- 去关联 , 即 : 去掉页表映射关系(删除共享内存前)
shmdt 系统调用
- 共享内存的删除 , 即 : 命令删除时用这个
ipcrm -m shmid
注意 : 删除时只能用 shmid 删除 , 即 : 唯一标识符 !
- 共享内存的控制 , 即 : 代码删除时用这个
shmctl 系统调用
● 总结
● 共享内存的使用完整代码
分为这样几个文件 :
客户端 : client.cc , 服务端 : server.cc , 共享内存的实现 : shm.hpp , 共同代码 : common.hpp , Makefile
共享内存的实现 : shm.hpp
// 这个文件是实现共享内存
// 因为共享内存被创建出来是映射到进程地址空间的 , 所以用户只需要拿到进程地址空间中被映射的起始虚拟地址
// 和共享内存的大小就能全部访问了
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include "common.hpp"// 共享内存
class Shm_m
{//这里面的所有函数不暴露给外面使用 , 只有自己使用
private:// 公共函数 , 共享内存帮助函数// 因为创建和获取只有选项不同 , 其余相同 !void Shm_mHelper(int shmflg){// 在创建之前先要生成 key// shmget(key_t key , size_t size, int shmflg);_shmid = shmget(_key, SHM_MNUM, shmflg);if (_shmid > 0)std::cout << "shmget shmid [ " << _shmid << " ] success ! " << std::endl;elseERR_EXIT("shmget");}void Create(){std::cout << CREATER << " creating ..." << std::endl;Shm_mHelper(IPC_CREAT | IPC_EXCL | gmode);}// 获取共享内存 -- client 来完成void GetShm_m(){std::cout << USER << " geting ..." << std::endl;Shm_mHelper(IPC_CREAT);}// 删除前想要去挂接 , 即 : 去掉映射关系void DeleteAttach(){// int shmdt(const void *shmaddr);// 传入挂接后的起始虚拟地址int n = shmdt(_shm_addrr);if (n == 0){std::cout << "shmdt shmid [ " << _shmid << " ] success !" << std::endl;}else{std::cout << "shmdt shmid [ " << _shmid << " ] fail ..." << std::endl;}}// 销毁共享内存void Destory(){// int shmctl(int shmid, int cmd, struct shmid_ds *buf);int n = shmctl(_shmid, IPC_RMID, nullptr); // 目前不用带回内核数据结构中的信息 !if (n != -1){std::cout << "shmctl rm [ " << _shmid << " ] success !" << std::endl;}else{std::cout << "shmctl rm [ " << _shmid << " ] fail .... " << std::endl;}}public:Shm_m(const std::string &usertype): _shm_addrr(nullptr), _size(SHM_MNUM), _shmid(-1), _key(-1), _usertype(usertype){// 创建 key_key = ftok(PATH, PRO_ID);if (_key < 0){ERR_EXIT("ftok");}// 只让服务端进行创建 , 也就是 CREATERif (usertype == CREATER){// 创建共享内存Create();}if (usertype == USER){// 获取共享内存GetShm_m();}}~Shm_m(){// 只让创建者删除共享内存if (_usertype == CREATER){// 去挂接DeleteAttach();Destory();}}// 创建共享内存 -- server 来完成// 两个进程都是要进行挂接的// 进行挂接 (映射到进程的虚拟的地址空间中)void Attach(){// void *shmat(int shmid, const void *shmaddr, int shmflg);// 返回的是挂接的虚拟地址空间位置的虚拟起始地址_shm_addrr = shmat(_shmid, nullptr, 0);int id = getpid();if (_shm_addrr != (void *)-1){std::cout << "shmat [ " << _shmid << " ] to pid [ " << id << " ] success !" << std::endl;}else{ERR_EXIT("shmat");}}// 虚拟地址void *VirtualAddar(){return _shm_addrr;}private:void *_shm_addrr; // 起始虚拟地址size_t _size; // 共享内存的大小key_t _key; // 给内核用的唯一标识符keyint _shmid; // 给我们用的共享内存标识符std::string _usertype; // 使用类型 , 即 : 目前谁使用
};
共同代码 : common.hpp
// 这个是共同文件 , 两个进程共同需要的代码放到这里//创建 key 需要路径和我们给定的一个值 , 二者结合才是 key
// "." 这个是硬链接 , 本质还是一个文件路径
#define PATH "."
#define PRO_ID 0x112233
#include <cstdio>
#include <cstdlib>// 要创建的共享内存多大
#define SHM_MNUM 4096//权限
const int gmode = 0666;
//定义两个身份
#define CREATER "Server"
#define USER "Client"//打印错误信息
#define ERR_EXIT(m) \
do { \perror(m); \exit(EXIT_FAILURE); \
}while(0)
服务端 : server.cc
// 这个文件是服务端
// 服务端负责 :
// 1 . 创建共享内存
// 2 . 接收共享内存传来的客户端发来的消息
// 3 . 结束后 , 销毁共享内存 , 必须用代码或指令删除 , 因为共享内存生命周期随内核 !#include "shm.hpp"int main()
{Shm_m shm(CREATER); // 构造时会自动创建共享内存 // 2. 挂接 , 映射到进程地址空间 shm.Attach();//挂接完成后 , 即 : 映射到两个进程虚拟地址空间中了 , 至此二者看到了同一份资源//然后就可以通信了 !//所以 , 服务端就可以使用共享内存来通信了 , 怎么使用 ?? 得到的就是一块内存 , 有地址直接用即可 !// 3. 获取映射后的虚拟起始地址char* addar = (char*)shm.VirtualAddar();// 4. 进行通信 , 读取共享内存中的内容 int cnt = 0;while(true){//直接读printf("I am %s , %s say : %s\n" , CREATER , USER , addar);std::string st = addar;if(st.rfind('Z') == 25){std::cout << USER << " exit , me too !" << std::endl;break;}sleep(1);} //服务端还要负责销毁共享内存 , 因为共享内存生命周期随内核 , 不用了需要销毁 !// 5 . 销毁 //这里进程结束 , 会走析构函数 , 析构时会自动进行销毁 return 0;
}
客户端 : client.cc
// 这个文件是客户端
// 客户端和服务端的通信采用共享内存的方式
// 客户端负责 :
// 给服务端发送消息 , 目的让客户端接收到消息// 所以 , 对客户端来说 , 只需要获取共享内存的起始虚拟地址和大小就可以拿到整个共享内存了#include "shm.hpp"int main()
{Shm_m shm(USER); // 构造时会自动获取共享内存 // 挂接shm.Attach();// 挂接完成后 , 即 : 映射到两个进程虚拟地址空间中了 , 至此二者看到了同一份资源//获取虚拟地址char* addar = (char*)shm.VirtualAddar();//获取后当做一个字符数组 , 客户端只负责写内容 !for(int i = 'A'; i <= 'Z'; ++i){addar[i-'A'] = i;sleep(1);}//告诉另一个进程我要退了 !std::cout << "I exit !" << std::endl;return 0;
}
Makefile
.PHONY:all # 这一行必写 , 因为默认执行第一行 , 否则无法直接生成两个可执行程序
all:server client
server:server.ccg++ -o $@ $^ -std=c++11
client:client.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f server client
一 、效果展示
可以看到 , 二者可以完成通信 , 但不知道有没有发现一个细节 , 以下笔者会讲 !
二 、共享内存的优缺点(重点)
这里再看一个图 :
这里可以看到一个现象 , 就是当 server 端创建好共享内存与client 进行通信时 , 一旦启动 server 端 , 那么 server 端会读共享内存中的内容 , 但是特点是 : 其不会等待其它进程 , 会直接从共享内存中读取内容 !
也就是说 , 共享内存通信的机制就是不用等待通信方发来消息我在读 , 而是只要有了共享内存启动了直接读(即使内容为空) !
这就与管道的通信机制不同了 , 因为管道是文件 , 用到 read 读取 , 所以管道通信时需要等通信方发来消息才会读 , 否则就一直阻塞等待了 !
- 那共享内存这样的机制有没有什么缺点呢 ??
肯定是有的 . 想想 , 共享内存不会等待通信方 , 所以就会导致读到的内容不一致 , 本来通信方想要发 : AABB , 想让读的一方这样读 , 但是因为共享内存的特殊性 , 只要有内容就读 , 所以会导致 : 通信的双方的数据不一致问题 , 这就是没有同步机制 , 这样的数据就不会被保护了 , 所以 , 共享内存没有保护机制 !
- 总结
那么 , 想要解决这种缺点有没有方法 ?? 答 : 有的 , 需要用到锁 !