【linux】进程间通信(三)systemV共享内存,ftok,shmget,shmat,shmdt,shmctl,ipcs,ipcrm
一、共享内存的原理二、建立源文件三、接口介绍shmget,创建/获取共享内存 六个问题问题五 问题二问题一 谈谈keyftok命令行中查看共享内存,删除共享内存四、编码实现创建共享内存 问题四问题六 给共享内存设置权限共享内存挂接到进程地址空间上shmat,共享内存在地址空间上去挂接shmdt关闭共享内存 shmctl封装 创建/获取 共享内存通信五、共享内存的特性六、看看共享内存的属性 shmc
小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
linux系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
目录
前言
【linux】实现一个简单的日志插件——书接上文 详情请点击<——,本文会在上文的基础上进行讲解,所以对上文不了解的读者友友请点击前方的蓝字链接进行学习
本文由小编为大家介绍——【linux】进程间通信(三)共享内存
一、共享内存的原理
- 共享内存是最快的进程间通信ipc的方式,一旦这样这样的内存映射到共享它的进程的地址空间,这些进程间进行数据传递就不再涉及到内核,换句话来说,进程不再通过执行进入内核的系统调用来完成传递彼此的数据,如何理解呢?请看下图
- 进行进程间通信的本质就是要让不同进程看到同一份资源,那么共享内存又是如何做的呢?请看下图

- 上图中有物理内存,还有两个进程,分别是进程A以及进程B,还有这两个进程所对应的内核数据结构,task_struct,地址空间,页表
- 我们先以进程A为例进行讲解,如果要使用共享内存,顾名思义,首先就要有内存,即先使用某种手段在物理内存上申请内存,当有了内存之后,那么就要将这个内存通过页表建立物理地址与虚拟地址的映射关系,映射到进程地址空间的共享区位置,然后向用户层返回共享内存的首地址,即在地址空间上起始的虚拟地址
- 那么接下来进程B就来了,它想要通过共享内存与进程A进行通信,此时共享内存已经有了,那么对于进程B来将就只需要获取这个共享内存,将这个内存通过页表建立物理地址与虚拟地址的映射关系,映射到进程地址空间的共享区位置,然后向用户层返回共享内存的首地址,即在地址空间上起始的虚拟地址,此时进程A和进程B作为不同进程就可以看到同一份资源进行通信了,通常的,共享内存也是进行单向通信的(共享内存可以进行双向通信,具体看用户需求)
- 如果我们要释放共享内存,那么步骤只需要和建立共享内存的步骤相反不就行了:即1. 清除页表上关于共享内存建立的虚拟地址到物理地址的映射关系,2. 释放共享内存
- 上述的操作都是由进程直接来做的吗?不是的,如果是进程来做,那么进程自己申请的内存就独属于我自己这个进程,进程具有独立性,其它进程无法访问这个内存。所以必然的,这个操作不能由进程来做,而是要由操作系统来做
- 操作系统如何做?操作系统不相信任何人,操作系统对上层提供系统调用接口,所以进程如果想要创建共享内存,挂接到进程地址上等操作都要通过系统调用接口告诉操作系统,让操作系统去做
- 今天有进程A和进程B想要使用共享内存进行进程间通信,那么同时也有可能有进程C和进程D使用共享内存进行通信,进程E和进程F使用共享内存进行通信等等等等,所以系统中一定会存在多个共享内存,那么我操作系统要不要对这么多的共享内存管理起来?要管理,如何管理?先描述,再组织。先使用struct内核结构体描述这个共享内存的属性,诸如共享内存的大小,使用了多少空间,还有多少空间没有使用等等,之后再采用数据结构,例如链表,数组等将描述对象,即对应的内核结构体组织起来,此时对共享内存的管理以及操作就转换成了对某种数据结构的增删查改
二、建立源文件
- 首先我们需要一个自动化构建工具makefile,需要一个进程A的源文件processa.cc,进程B的源文件processb.cc,同时还应该有一个它们两个都能看到的一个头文件comm.hpp,以及一个日志源文件,关于日志的讲解以及源代码详情请点击<——

- 下面我们先搭建测试是否可以正常打印运行
//makefile
.PHONY:all
all:processa processb
processa:processa.cc
g++ $^ -o $@ -std=c++11
processb:processb.cc
g++ $^ -o $@ -std=c++11
.PHONY:clean
clean:
rm -f processa processb
//comm.hpp
#ifndef __COOM_HPP__
#define __COOM_HPP__
#include <iostream>
using namespace std;
#endif
//processa.cc
#include "comm.hpp"
int main()
{
cout << "processa" << endl;
return 0;
}
//processb.cc
#include "comm.hpp"
int main()
{
cout << "processb" << endl;
return 0;
}
运行结果如下,可以正常打印
三、接口介绍
shmget,创建/获取共享内存 六个问题
-
如何创建/获取共享内存,那么就是使用系统调用shmget,其中sh的意思是share共享,m是memory内存,shmget的意思即为共享内存获取/分配的意思

-
下面小编抛出几个问题,后面会针对这几个问题进行回答帮助大家更好理解shmget接口
-
问题一:如何保证让不同的进程看到同一个共享内存呢?与第一个参数key有关,后面讲解
-
问题二:如何得知这个共享内存是存在还是不存在呢?与第三个参数shmflg有关,后面讲解
-
问题三:如何理解第二个参数size创建共享内存的大小(单位是字节),有什么默认的申请规则吗?有的,共享内存的大小通常我们都是以一个内存的数据块为单位进行申请,即4KB,即4096byte,一个数据块的大小为4KB,4096byte,如果我们申请4097byte,实际中系统会为我们申请两个数据块8192,即两个数据块的大小,但是系统只会给我们4097字节,剩下的空间就浪费掉了,换句话来说就算我们申请1字节,系统还是会为我们申请一个数据块大小即4KB,4096byte,但是系统只给我们使用1字节,剩下的空间浪费,所以通常来讲共享内存的大小通常我们都是以一个内存的数据块为单位进行申请
-
问题四:shmget的返回值中,如果创建成功,那么返回的共享内存标识符如何理解?
-
问题五:shmflg中的IPC_CREAT以及IPC_EXCL如何理解
-
问题六:既然共享内存既然是内存中,那么是否有关于权限的概念?
问题五 问题二

- 首先回答问题五,其实shmget中的第三个参数shmflg的传递是通过比特位按位与的方式进行传参,原理在第三点文件系统调用中的比特位方式传递标志位,原理请点击<——
- 即本质上IPC_CREAT以及IPC_EXCL实际上是宏定义的比特位,即特定比特位上为1,其余比特位位置上为0
- IPC_CREAT(单独使用):如果你申请的共享内存不存在,那么就创建。如果你申请的共享内存存在,那么就获取并返回。
- IPC_EXCL不单独使用,而是和IPC_CREAT结合使用
- IPC_CREAT | IPC_EXCL:如果你申请的共享内存不存在,那么就创建。如果存在,那么就出错返回。这样可以确保我们申请了一个共享内存,这个共享内存一定是新的
- 同样的上面的也就回答了问题二,如何得知这个共享内存是存在还是不存在呢?那么就通过IPC_CREAT | IPC_EXCL可以确定这个共享内存存在还是不存在,因为对于IPC_CREAT | IPC_EXCL选项来讲存在就会出错返回,不存在就会创建
问题一 谈谈key
- 如何理解key呢?key作为shmget的第一个参数如何制作呢?

- 首先观察key的类型是key_t,其实key_t这个类型实际上一个int整数类型,即这个key是一个整数,这个整数是几,我们不关心。我们关心的是它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识
- 第一个进程可以获得key通过系统调用shmget创建共享内存,这样第二个之后的进程,只要拿着同一个key就可以和第一个进程看到同一个共享内存了
- 对于一个已经创建好的共享内存,这个key存在于哪里?key是有关共享内存的属性,所以必然会存储在共享内存的描述对象中
- 问题来了?第一次创建的时候必须要有一个key了,如何有?系统调用ftok即可获取
ftok

- ftok可以通过结合用户传入的 自定义路径pathname + 用户自定义设置的proj_id 根据某种的算法,计算出一个整数,将这个整数进行返回,所以进程就可以拿到这个整数作为key
- 但是针对这两个pathname以及proj_id也是有要求的,其中pathname路径要求这个路径是存在的,proj_id要求不为0
- 所以针对用户想要进行通信的两个进程,用户约定,这两个进程使用的都是同一个pathname以及proj_id,那么同时ftok函数使用同一套算法进行计算,所以计算出来的整数一定是相同的,并且这个key值具有唯一性
- 我们知道管道通过路径+文件名确保唯一性,同时这里的ftok也借助了路径,一定的保证了这个key值具有唯一性,所以一个进程就可以借助key通过shmget创建共享内存,即去操作系统组织起来的共享内存的数据结构中进行比对,如果没有与管理共享内存的描述对象中存储的key产生冲突,那么就会创建一个共享内存,并且将key存储进描述共享内存的描述对象中,并且添加到组织描述对象的数据结构中
- 但是事无绝对,如果你计算的这个key真的产生了冲突,即与原有的svstemV系列的共享内存或者信号量,消息队列中的原有的key值产生了冲突,如何做,很简单,那么作为用户,更换一个pathname或者proj_id即可
命令行中查看共享内存,删除共享内存
- 命令行中查看共享内存使用的是ipcs -m
- 命令行中删除共享内存使用的是ipcrm -m 共享内存标识符
- 在后面的编码实现中,小编会演示并使用这两个指令
四、编码实现
创建共享内存 问题四
- 我们做一下准备工作,在comm.hpp中包含日志文件源代码,并且将实例化日志对象log,用于进行日志记录工作,日志的默认打印方式是向文件中进行打印,这也正是小编想要采用的方式
- 并且定义全局变量pathname,proj_id,size,其中pathname,proj_id是ftok所需要的参数,当comm.hpp被进程A和进程B包含comm.pathname,proj_id通过ftok函数拿到同一个key,进而就可以看见同一份资源,size是shmget的参数,用于定义共享内存的大小
//comm.hpp
#ifndef __COOM_HPP__
#define __COOM_HPP__
#include "log.hpp"
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <cstdlib>
#include <cstring>
#include <error.h>
#include <unistd.h>
using namespace std;
const char* pathname = "/home/wzx";
const int proj_id = 0x112233;
const int size = 4096;
Log log;
#endif
- 接下来我们先使用进程A先创建一个共享内存观察现象,如何创建,先使用ftok获取key,然后再使用shmget进行创建即可,同时我们使用日志进行记录,由于日志默认是向屏幕进行打印,所以这些信息就会打印在屏幕上
- 由于系统中的key存储是按照16进制进行存储,所以我们在进行日志打印的时候,将key也转换为16进制进行打印
//processa.cc
#include "comm.hpp"
int main()
{
key_t key = ftok(pathname, proj_id);
if(key == -1)
{
log(Fatal, "ftok %s", strerror(errno));
exit(1);
}
log(Info, "ftok %s, key: 0x%x", strerror(errno), key);
int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL);
if(shmid == -1)
{
log(Fatal, "shmget %s", strerror(errno));
exit(2);
}
log(Info, "creat share memory %s, shmid: %d", strerror(errno), shmid);
return 0;
}
运行结果如下
- 此时共享内存就被创建出来了,比对观察共享内存的key和我们日志打印的key是同一个key,shmid共享内存描述符是4。问题四:如何理解shmget的返回值shmdi的意义?key是在操作系统层面上用来标定唯一性的,shmid是在你的进程范围内,用来表示资源唯一性的,后续我们需要对共享内存的所有操作都需要借助这个shmid共享内存描述符
- 那么perms为什么是0?perms其实是共享内存的权限,由于我们并没有对其权限进行设置,所以perms为0也很正常
- natch为什么也是0?natch其实是共享内存的挂接数,即共享内存挂接到进程的地址空间的个数,我们上述的操作仅仅是创建了共享内存,第二步就是要将共享内存挂接到进程的地址空间上,此时我们还没有将共享内存挂接到任何一个进程的进程地址空间上,所以natch为0同样也很正常
- 同样的我们还应该注意,上面进程结束了,但是共享内存还存在,所以共享内存的生命周期不随进程,共享内存是由操作系统创建的,所以共享内存的生命周期随操作系统。只要用户不主动关闭,共享内存会一直存在,除非操作系统重启,换句话来说用户也可以手动释放共享内存
- 下面小编演示使用ipcrm -m 共享内存描述符,来删除一个共享内存
运行结果如下,删除成功
问题六 给共享内存设置权限
- 从上面的实验中,我们已经看出共享内存确实有权限的概念
- 所以我们应该在什么时机给共享内存设置权限呢?毫无疑问应该是共享内存开始被创建出来的时候,所以进程如何创建共享内存,使用系统调用shmget

- 观察一下上面shmget的三个参数,什么地方最适合传入设置的权限?相信读者友友心里已经有了答案,那么就是shmget的shmflg参数传参中进行按位与 | 文件权限0666即可
#include "comm.hpp"
int main()
{
key_t key = ftok(pathname, proj_id);
if(key == -1)
{
log(Fatal, "ftok %s", strerror(errno));
exit(1);
}
log(Info, "ftok %s, key: 0x%x", strerror(errno), key);
int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);
if(shmid == -1)
{
log(Fatal, "shmget %s", strerror(errno));
exit(2);
}
log(Info, "creat share memory %s, shmid: %d", strerror(errno), shmid);
return 0;
}
运行结果如下,此时共享内存的权限就被设置好了
共享内存挂接到进程地址空间上shmat,共享内存在地址空间上去挂接shmdt
-
如何将共享内存挂接到进程的地址空间上呢?由于是要对内核数据结构页表,进程地址空间进行操作,换句话来来讲就是要访问内核,内核是何等地位,你进程又是算得上什么,你进程没有权限访问内核,我内核的事情,我内核自己做,所以进程就要通过系统调用 shmat 访问内核,将共享内存挂接到进程地址空间上

-
使用shmat即可共享内存挂接到进程地址空间上,attach就是挂接的意思,shmat的意思就算挂接共享内存
-
小编为什么shmat这里还有三个返回值,不会又要讲提出好几个问题拓展讲好久吧,不会,这里我们仅仅关心第一个参数shmid,这个我们早在shmget创建共享内存的时候就获取了它的返回值,即shmid共享内存描述符,这里直接传入即可
-
第二个参数实际上是任意位置,即这个共享内存实际上可以挂接到属于这个进程的地址空间的任意位置,但是我们知道应该挂接到哪里吗?实际上是不知道的,所以这里我们设置为nullptr让系统去共享区默认帮我们找一个位置就行,第三个参数是有关共享内存权限的,这里我们已经在创建共享内存的时候已经设置过了,所以这里我们不关心共享内存权限,所以第三个参数我们传默认值0即可
-
对于shmat的返回值shmaddr其实是操作系统在共享区挂接的共享内存的起始地址,所以对于shmat的返回值我们是一定要接收的,因为后续我们还需要将共享内存去挂接需要使用到这个shmaddr,由于返回值是void*,我们期望使用char类型的指针进行接收,所以shmat还需要类似于malloc一样进行强制类型转换,这里需要强制类型转换为char
-
其实我们对比一下这里的shmat的作用类似于malloc,都是要申请内存,建立虚拟地址和物理地址的映射,只不过malloc可以仅仅返回虚拟地址,当用户实际使用到虚拟地址的时候,在页表上发现这个虚拟地址没有与物理地址建立映射,所以会触发缺页中断进行加载罢了
-
此时我们可以将共享内存挂接到进程地址空间上了,那么可不可以将共享内存在地址空间上去挂接呢?可以的,通过shmdt即可

-
shmdt参数很简单,即传入shmat的返回值shmaddr,即共享内存在进程地址空间上虚拟的首地址即可进行去挂接
-
所以此时小编可以使用shmat进行对共享内存挂接到进程地址空间上,同样也可以将共享内存在地址空间上使用shmdt进行去挂接,同时小编期望观察到这个挂接数字的变化,那么接下来小编在程序中共享内存的挂接与去挂接的位置处进行休眠,并且使用监控脚本进行检测,期望观察到挂接数字的变化
//监控脚本
while :; do ipcs -m | head -4; sleep 1; done
#include "comm.hpp"
int main()
{
key_t key = ftok(pathname, proj_id);
if(key == -1)
{
log(Fatal, "ftok %s", strerror(errno));
exit(1);
}
log(Info, "ftok %s, key: 0x%x", strerror(errno), key);
int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);
if(shmid == -1)
{
log(Fatal, "shmget %s", strerror(errno));
exit(2);
}
log(Info, "creat share memory %s, shmid: %d", strerror(errno), shmid);
sleep(2);
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
sleep(2);
shmdt(shmaddr);
return 0;
}
运行结果如下,正确,现象符合我们的预期
关闭共享内存 shmctl
- 如果我们作为一个用户,即进程想要关闭共享内存,那么使用同样的需要使用系统调用进行关闭,使用shmctl即可进行关闭共享内存

- shmctl 顾名思义其中的ctl就是control控制的意思,即使用这个系统调用接口控制共享内存,第一个参数老样子还是共享内存描述符shmid,第二个参数是一个整数类型的cmd命令,不用想我就知道这个还是老样子,即通过宏定义不同位置的比特位传达不同的命令,下面我们认识一个宏定义,即IPC_RMID,即remove移除shmid,也就是将共享内存释放/移除的意思,之后小编就会使用shmctl演示移除共享内存
- 第三个参数是一段缓冲区,这里小编先不进行讲解,后面小编会讲解这个缓冲区的作用,这里我们传入默认值nullptr即可
- 同样的,小编希望使用脚本指令观察到共享内存从创建到挂接,挂接数加1,去挂接,挂接数减1,最后共享内存被释放移除
//脚本指令
while :; do ipcs -m | head -4; sleep 1; done
#include "comm.hpp"
int main()
{
key_t key = ftok(pathname, proj_id);
if(key == -1)
{
log(Fatal, "ftok %s", strerror(errno));
exit(1);
}
log(Info, "ftok %s, key: 0x%x", strerror(errno), key);
int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);
if(shmid == -1)
{
log(Fatal, "shmget %s", strerror(errno));
exit(2);
}
log(Info, "creat share memory %s, shmid: %d", strerror(errno), shmid);
sleep(1);
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
sleep(1);
shmdt(shmaddr);
sleep(2);
shmctl(shmid, IPC_RMID, nullptr);
return 0;
}
运行结果如下
封装 创建/获取 共享内存
- 那么我们现在已经可以创建共享内存,挂接共享内存,去挂接共享内存,移除清除共享内存了,下面小编将进程A中的代码抽象出来,封装成获取key值 创建共享内存
- 我们知道共享内存只需要创建一次,所以我们期望将这个创建共享内存的任务交给进程A,那么对于进程B来讲,它的目的是获取共享内存,所以进程B和进程A一个是获取共享内存,一个是创建共享内存,那么如何区分呢?
- 很简单,一个是获取共享内存,一个是创建共享内存无非就是shmget的第三个参数shmflg的选项不同,那么对于获取共享内存GetShm来讲那么选项就是IPC_CREAT,对于创建共享内存CreatShm来讲选项就是IPC_CREAT | IPC_EXCL | 0666,那么我们将shmget封装成为一个函数GetShareMemHelper,传入一个int类型的flag标志,这个flag标志作为shmget的第三个参数shmflg的选项进行使用,传入不同的参数即可实现,那么分别让获取共享内存GetShm传入IPC_CREAT选项进行调用,对于创建共享内存CreatShm传入IPC_CREAT | IPC_EXCL | 0666进行调用即可
#ifndef __COOM_HPP__
#define __COOM_HPP__
#include "log.hpp"
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <cstdlib>
#include <cstring>
#include <error.h>
#include <unistd.h>
using namespace std;
const char* pathname = "/home/wzx";
const int proj_id = 0x112233;
const int size = 4096;
Log log;
key_t GetKey()
{
key_t key = ftok(pathname, proj_id);
if(key == -1)
{
log(Fatal, "ftok %s", strerror(errno));
exit(1);
}
log(Info, "ftok %s, key: 0x%x", strerror(errno), key);
return key;
}
int GetShareMemHelper(int flag)
{
key_t key = GetKey();
int shmid = shmget(key, size, flag);
if(shmid == -1)
{
log(Fatal, "shmget %s", strerror(errno));
exit(2);
}
log(Info, "creat share memory %s, shmid: %d", strerror(errno), shmid);
return shmid;
}
int CreatShm()
{
return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
int GetShm()
{
return GetShareMemHelper(IPC_CREAT);
}
#endif
通信
- 创建共享内存,挂接共享内存,去挂接共享内存,移除清除共享内存,上面小编上面的操作仅仅是使用的进程A进行的操作,并且将创建/获取 共享内存进行了封装,可是我们今天的目的是使用共享内存进行进程间通信,现在,通信了吗?
- 没有,甚至我们连进程B的共享内存都获取,所以我们接下来就让B获取共享内存,并且进行对应的挂接到进程B的地址空间上,并且在最后进行去挂接
- 那么进程A则是进程对应的创建共享内存,挂接共享内存,再去挂接,最后再移除释放共享内存
- 那么请读者友友猜想一下ipc进程间通信应该是在哪一个位置?是的,没有错是在进程A和进程B进行了对应的挂接操作之后,在进程A和进程进行对应的去挂接操作之前
- 如何通信?其实共享内存,共享内存,并且还映射到了进程的虚拟地址空间上了,我们使用malloc申请空间,是在堆上进行申请的,返回的是堆在虚拟进程地址空间上的对应位置的地址,我们可以直接拿着malloc后的空间进行使用

- 同样的而我们的共享内存同样也是在虚拟进程地址空间上进行了对应的映射,难道说我们使用共享内存可以像使用全局变量,堆空间一样直接使用???是的,没有猜错!!!
- 共享内存通常也是单向通信的(同样的,如果用户需要,共享内存也可以双向通信),那么我们让进程A作为读端,进程B作为写端进行通信即可,那么由于用户进行输入可能会有空格,所以进程B进行获取用户输入的时候,应该一次获取一行,即遇到换行才结束读取,所以我们这里可以使用fgets从标准输入,即键盘上一次获取一行
- 并且我们希望将共享内存当作字符串来进行使用,那么自然而然的结尾会有我们输入的字符串结尾会有’\0’,因为这是在内存中,得按照c语言的规则来,c语言规定字符串的结尾有’\0’,所以我们使用键盘进行输入的时候,由于我们使用的获取用户输入一行的接口fgets是c语言的接口,所以fgets会默认的在我们输入的字符串的结尾添加’\0’,那么进程A在进行读取的时候就可以直接使用cout进行打印了,由于是要进行一直进行通信的,所以上述通信过程都应该是死循环式进行通信
//processa.cc
#include "comm.hpp"
int main()
{
//创建
int shmid = CreatShm();
//挂接
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
//ipc进程间通信
while(true)
{
cout << "client say@ "<< shmaddr << endl;
sleep(5);
}
//去挂接
shmdt(shmaddr);
//移除释放
shmctl(shmid, IPC_RMID, nullptr);
return 0;
}
//processb.cc
#include "comm.hpp"
int main()
{
//获取
int shmid = GetShm();
//挂接
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
//ipc进程间通信
while(true)
{
cout << "Please Enter@ ";
fgets(shmaddr, size, stdin);
}
//去挂接
shmdt(shmaddr);
return 0;
}
运行结果如下
此时我们将左侧的进程A运行起来,但是此时进程B还没有运行,即此时进程B作为写端没有就绪,进程A作为读端,这个进程A就直接运行起来了,即此时进程A直接去共享内存中读取内容了,所以共享内存的进程双方不会进行协作,同步与互斥
此时我们将右侧的进程B运行起来之后,那么输入,通信成功,左侧进程A接收到了进程B的信息
五、共享内存的特性
- 共享内存没有进程间协同,同步互斥之类的保护机制
- 共享内存是所有进程间通信速度最快的,原因是数据没有拷贝,观察下图,进程B将数据通过进程地址空间直接将数据写到物理内存的共享内存中,那么进程A直接通过进程地址空间就可以访问物理内存的共享内存中的数据。管道则是经过两次拷贝,第一次拷贝是将用户层的数据拷贝给write内核缓冲区,第二次拷贝是read,将内核缓冲区的数据读取,即拷贝到用户缓冲区

- 共享内存内部的数据,由用户自己维护。即你想要将这个共享内存当作什么来进行使用由你用户来决定,例如像小编上面使用的一样,将共享内存当作字符串来使用,并且用户还可以自行控制读取的格式写入的格式。
六、看看共享内存的属性 shmctl
- 小编,小编,你之前介绍了关于描述共享内存的描述对象中存储着共享内存的一系列属性,可是真的有吗?小编,小编你可不可以将共享内存的属性拿出来给我们看看?可以,没问题,接下来我们重新认识一下shmctl,它可以帮助我们将共享内存的部分属性拷贝到一个结构体中

- 如上cmd命名中有一个选项IPC_STAT可以将内核数据块中描述共享内存的属性拷贝到类型为struct shmid_ds类型的结构体中,那么这个struct shmid_ds类型的结构体中有什么属性呢?请看下图

- 观察这个struct shmid_ds类型的结构体中还嵌套有一个类型为struct ipc_perm的结构体,这个嵌套的结构体中包含共享内存的部分属性,包括key,mode权限等,那么struct shmid_ds类型的结构体中还包含共享内存的大小shm_segsz,我们想一下为什么我们进行共享内存释放移除的时候,仅仅给shmctl传入一个共享内存的起始地址就可以,而不是传入共享内存的起始和终止地址?
- 就是因为描述共享内存的描述对象中有存储的共享内存的大小,那么此时仅需要拿到共享内存的起始地址加上共享内存的大小就可以算出共享内存的终止地址进而才可以进行释放共享内存空间,free也是同样的道理
- struct shmid_ds类型的结构体中还包含有共享内存的挂接数shm_nattch,下面小编将使用shmctl逐个获取进行打印,为了便于观察到挂接数,由于进程退出后会自动将进程关联的共享内存进行去挂接,所以为了观察到共享内存的挂接数为1,那么进程不能退出,那么小编就在循环中让进程睡眠1000秒,接下来我们复制ssh渠道,在另一个命令行中使用ipcs -m查看共享内存的相关信息
#include "comm.hpp"
int main()
{
// 创建
int shmid = CreatShm();
// 挂接
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
// ipc进程间通信
while (true)
{
struct shmid_ds buf;
shmctl(shmid, IPC_STAT, &buf);
printf("key: 0x%x\n", buf.shm_perm.__key);
cout << "mode: " << buf.shm_perm.mode << endl;
cout << "size: " << buf.shm_segsz << endl;
cout << "nattch: " << buf.shm_nattch << endl;
sleep(1000);
}
// 去挂接
shmdt(shmaddr);
// 移除释放
shmctl(shmid, IPC_RMID, nullptr);
return 0;
}
运行结果如下
此时key吻合,共享内存大小size吻合,共享内存挂接数nattch吻合,但是权限不吻合,因为系统中权限是8进制表示,而小编这里没有做8进制转换,所以不吻合是正常的,但是这里我们可以将0666进行8进制转换:0 * 8 * 6 + 1 * 8 * 6 + 2 * 8 * 6 = 438所以权限也是吻合的
七、如何给共享内存添加同步机制
- 通过之前的测试我们是可以得出的,共享内存是没有同步机制的,那么我们之前学过管道可是有同步机制的,那么我们可以利用命名管道进行两个不相关进程的同步工作
- 那么小编将小编之前讲解的命名管道的代码直接拿来用,不清楚如何使用命名管道的读者友友详情请点击<——
comm.hpp
//comm.hpp
#ifndef __COOM_HPP__
#define __COOM_HPP__
#include "log.hpp"
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <cstdlib>
#include <cstring>
#include <error.h>
#include <unistd.h>
using namespace std;
const char* pathname = "/home/wzx";
const int proj_id = 0x112233;
const int size = 4096;
Log log;
key_t GetKey()
{
key_t key = ftok(pathname, proj_id);
if(key == -1)
{
log(Fatal, "ftok %s", strerror(errno));
exit(1);
}
log(Info, "ftok %s, key: 0x%x", strerror(errno), key);
return key;
}
int GetShareMemHelper(int flag)
{
key_t key = GetKey();
int shmid = shmget(key, size, flag);
if(shmid == -1)
{
log(Fatal, "shmget %s", strerror(errno));
exit(2);
}
log(Info, "creat share memory %s, shmid: %d", strerror(errno), shmid);
return shmid;
}
int CreatShm()
{
return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
int GetShm()
{
return GetShareMemHelper(IPC_CREAT);
}
#define FIFO_FILE "./myfifo"
#define MODE 0664
enum
{
FIFO_CREAT_ERR = 1,
FIFO_DELETE_ERR, // 如果第二个不初始化,那么就会在第一个的基础上加1为2
FIFO_OPEN_ERR // 同理第三个变量在第二个变量的基础上加1为3
};
class Init
{
public:
Init()
{
// 创建命名管道
int n = mkfifo(FIFO_FILE, MODE);
if (n == -1)
{
log(Fatal, "mkfifo %s", strerror(errno));
exit(FIFO_CREAT_ERR);
}
log(Info, "creat fifo %s", strerror(errno));
}
~Init()
{
// 清理命名管道
int m = unlink(FIFO_FILE);
if (m == -1)
{
log(Fatal, "unlink %s", strerror(errno));
exit(FIFO_DELETE_ERR);
}
log(Info, "delete fifo %s", strerror(errno));
}
};
#endif
processa.cc
//processa.cc
#include "comm.hpp"
int main()
{
//创建命名管道
Init init;
//打开命名管道
int fd = open(FIFO_FILE, O_RDONLY);
// 创建共享内存
int shmid = CreatShm();
// 挂接
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
// ipc进程间通信
while (true)
{
char ch;
int n = read(fd, &ch, 1);
if(n == 0)
{
log(Debug, "client exit, me to, errno string: %s, errno: %d", strerror(errno), errno);
break;
}
cout << "client say@ "<< shmaddr;
}
// struct shmid_ds buf;
// shmctl(shmid, IPC_STAT, &buf);
// printf("key: 0x%x\n", buf.shm_perm.__key);
// cout << "mode: " << buf.shm_perm.mode << endl;
// cout << "size: " << buf.shm_segsz << endl;
// cout << "nattch: " << buf.shm_nattch << endl;
// sleep(1000);
// 去挂接
shmdt(shmaddr);
// 移除释放
shmctl(shmid, IPC_RMID, nullptr);
return 0;
}
processb.cc
//processb.cc
#include "comm.hpp"
int main()
{
//打开命名管道
int fd = open(FIFO_FILE, O_WRONLY, 0664);
//获取
int shmid = GetShm();
//挂接
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
//ipc进程间通信
while(true)
{
char ch = 'c';
write(fd, &ch, 1);
cout << "Please Enter@ ";
fgets(shmaddr, size, stdin);
}
//去挂接
shmdt(shmaddr);
return 0;
}
运行结果如下
首先我们此时先运行进程A,此时进程A先创建命名管道,进程A作为读端,卡在了open打开命名管道那个地方,因为此时写端进程B还没有就绪,所以自然就会停留,自然的此时共享内存也无法执行创建
此时我们将右侧进程B运行起来,那么此时写端进程B准备就绪,此时读写端就会open打开命名管道,进而进程A就可以向后执行代码,创建共享内存,挂接共享内存,同样的jinchegnB也可以向后执行代码,获取共享内存,挂接共享内存
此时我们就可以使用共享内存进行进程间通信了
最后退出进程B,此时进程B作为写端退出,进程A读端的read就会读到0,进而退出循环,进行去挂接,释放共享内存,以及释放命名管道的操作,完美
总结
以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐















所有评论(0)