1. 前言
在学习Linux中的程序地址空间时,
善于观察的同学可能会发现在栈区
和堆区中间有一个共享区,这是用来
干啥的?今天就来揭晓一下!
本章重点:
本篇文章着重介绍进程间通信的一种
方式:共享内存
的概念,接口使用以及
它的底层原理,最后会介绍进程间互斥
的一些基本概念
2. 共享内存的原理
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
说大白话就是: 共享内存的通信方式就是在物理内存中开辟一份空间,然后将这份空间映射到两个进程的共享区中,这两个进程可以直接在自己的地址空间中读取或写入数据,不会经过内核,效率很快!
3. 实现共享内存的基本步骤
使用shm系统函数来实现这一功能:
第一步: 获取key值
要通信的双方怎样保证自己看见的是和对方一样共享内存? -> 通过一个key值来保证,只要两个进程拿到同一个key值,再用这个key值去创建共享内存,那么它们看见的就是同一份共享内存!
使用ftok函数可以形成一个唯一的key值
函数的参数随意指定,只要保证通信双方
传入的参数一样,就能备注拿到一样的key
第二步: 创建/获取共享内存
通信双方只需要一方来创建共享内存,另外一方可以直接通过key值获取到共享内存(毕竟物理内存只需要创建一份),不管是创建还是获取共享内存使用的都是同一个函数!
第三步: 将共享内存映射到地址空间
当双方进程都拿到共享内存段的标识码后,此时需要将这份共享空间从物理地址映射到进程自己内部的地址空间中,方便后续使用!
第四步: 使用完后将共享内存与进程去关联
值得注意的是,共享内存使用完后和指针一样需要"释放",否则会导致内存泄漏问题
第五步: 删除共享内存
将共享内存与进程去关联后,并不代表它就被删除了,共享内存的生命周期是随内核的,而我们的云服务器是永远不会关闭的,如果不删除共享内存,将来能使用的空间会越来越少
使用ipcs -m
指令查看共享内存
使用ipcrm -m shmid
指令删除共享内存
函数删除共享内存的方法
4. 共享内存编码实现
由于双方进程编写代码有很多重复的地方
所以使用一个commom.h文件存放公共内容
commom.h文件中:
#pragma once #include<iostream> #include<string> #include<unistd.h> #include<cstdio> #include<sys/shm.h> #include<sys/ipc.h> #include<sys/types.h> #include<cassert> #include<cstring> #include<sys/stat.h> #include<fcntl.h> using namespace std; #define PATHNAME "/home/kwy" //形成key值的字符串 #define PROJ_ID 0x666 //形成key值的整数 #define SHM_SIZE 4096 //共享内存的大小最好是页(PAGE : 4096)的整数倍
client端代码:
#include"common.hpp" int main() { //获取key key_t k = ftok(PATHNAME, PROJ_ID); if (k < 0) { perror("ftok"); exit(1); } cout<<"creat key success: "<<k<<endl; // 获取共享内存 int shmid = shmget(k, SHM_SIZE, 0); if(shmid < 0) { perror("shmget"); exit(2); } cout<<"get shard success: "<<shmid<<endl; //挂接共享内存 char *shmaddr = (char *)shmat(shmid, nullptr, 0); if(shmaddr == nullptr) { perror("shmat"); exit(3); } cout<<"attach success!"<<endl; //将共享内存看作数组,写入数据 char ch ='a'; for(;ch<='z';ch++) { snprintf(shmaddr,SHM_SIZE-1,"hello server,i am other proc,mypid: %d ,inc: %c",getpid(),ch); sleep(2); } // 去关联 int n = shmdt(shmaddr); assert(n != -1); cout<<"detach success!"<<endl; // client 要不要chmctl删除呢?不需要!! return 0; }
server端代码:
#include"common.hpp" int main() { //创建公共的key的值 key_t key = ftok(PATHNAME,PROJ_ID); cout<<"server key: "<<TransToHex(key)<<endl; assert(key!=-1); //创建共享内存(全新的共享内存) int shmid = shmget(key,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666); if(shmid==-1) { perror("shmid"); exit(1); } cout<<"creat shmid: "<<shmid<<endl; //将创建好的共享内容挂接到虚拟地址 char* shmaddr = (char*)shmat(shmid,nullptr,0);//shmat的返回值是void*,与malloc相似要强转 cout<<"attach success!"<<endl; //使用共享内存,将共享内存当作一个大字符串 //使用完共享内存后,将指定的内存与进程去关联 int n1 = shmdt(shmaddr); if(n1==-1) perror("shmdt"); else cout<<"detach success!"<<endl; //最后用代码删除共享内存 int n2 = shmctl(shmid,IPC_RMID,nullptr); assert(n2!=-1); cout<<"delete shared memory: "<<shmid<<endl; return 0; }
5. 进程互斥相关概念
大家在学习共享内存时有没有发现一个问题:
假如共享区有一个变量a,client进程在将a进行++操作的同时,server进程将a的值从10改为了100,这里就会出现问题,已经被改为100的a值在client进程执行完操作后,会把在CPU中计算出来的11赋值给a,那么进程server就白修改了,这里显然是有问题的
没错,在这个地方,共享内存被称为共享资源,共享资源如果被同时访问可能会出现某些预想不到的问题,所以各个进程不能在同一时间访问共享资源,这种关系叫做进程互斥
对于进程互斥和共享资源以及临界区的认识远远没有结束,今天我们只是窥见了冰山一角,在后续的学习中我们还会重点讲这一个概念!
6. 总结
进程间通信的方式远不止管道和共享内存这两种,还有很经典的消息队列以及信号量等方式这里并没有过多讲述,因为我们只是学习重点,并不是全部都要学会,但是学有余力的同学还是很有必要区了解一下的
🔎 下期预告:进程信号 🔍