【Linux】从零开始认识进程间通信 —— 共享内存
CSDN 2024-08-10 10:07:06 阅读 56
送给大家一句话:
吃苦受难绝不是乐事一桩,但是如果您恰好陷入困境,我很想告诉您:“尽管眼前十分困难,可日后这段经历说不定就会开花结果。”请您这样换位思考、奋力前行。
-- 村上春树
🔆🔆🔆🔆🔆🔆🔆🔆
共享内存
1 ❤️🔥前言2 ❤️🔥共享内存的原理3 ❤️🔥代码实现 -- 补充理论知识 -- 相关接口🎁创建🎁删除🎁封装🎁挂接到进程🎁开始通信
5 ❤️🔥获取共享内存的属性Thanks♪(・ω・)ノ谢谢阅读!!!下一篇文章见!!!
1 ❤️🔥前言
前面我们讲解了匿名管道和命名管道,通过其底层实现,我们可以发现管道是基于文件系统的通信方式。通过文件的内存缓冲区的写端和读端的文件描述符(<code>fd),使用对应的read / write
就可以支持单向通信
也就是说管道并不是一个单独的模块,还是沿用文件的管理模块,而接下来的共享内存就是一个单独设计的通信模块
共享内存是本地通信方案(System V IPC
)的一种。
System V IPC
包含主要有三种方式: 共享内存,消息队列 ,信号量。这些在今天逐渐被边缘化,很少再用到他们,但其中的共享内存很值得我们来学习一下,了解里面的思想。
2 ❤️🔥共享内存的原理
首先,共享内存是一种进程间通信的方案,那么就得满足进程间通信的需求:两个进程要看到同一块内存资源!才可以进行通信。
先来看两个进程的关系,进程具有独立性,两个进程分别会指向自己的地址空间,在通过页表映射到真的的物理地址,物理地址中储存着该进程的代码和数据。
接下来我们来看共享内存是如何实现的:
首先在物理内存中存在一片内存空间,这里用来管理共享内存共享内存和管道的创建方式很像,一个进程通过对应的系统调用来创建共享内存,这个共享内存归OS管理。再联想动态库相关知识,动态库就是加载到物理内存中,通过页表映射到进程地址空间的共享区(储存动态库函数的起止位置)。动态库可以被多个进程同时使用,就是说动态库这片内存可以被不同进程同时看到。共享内存就是类似的道理那么在内存空间中假如存在这样一个空的内存,再在共享区申请一片空间,然后也通过页表映射到进程地址空间的共享区,那么不就可以让不同进程通过这个映射关系看到同一片内存了吗!不就可以在这片内存读取写入数据了吗!!!
这就是共享内存!!!
当然通过原来图也只是比较浅显的认识,接下来我们来深入探索一下!!!
上述创建物理内存 , 建立页表映射等操作,只能是操作系统来做!但是操作系统并不知道什么时候来进行操作。所以操作系统必须提供对应操作的系统调用,供用户进程A,B来使用AB 进程可以使用共享内存来通信,那CD进程也想 ,EF进程也想— 所以共享内存在操作系统中是存在多份的,供不同个数的进程来进行通信!既然是存在多份的共享内存,那么操作系统就要进行统一管理 — 共享内存不是简单的一段内存空间,也要有描述并管理共享内存的数据结构和匹配的算法!!!共享内存 = 内存空间(数据) + 共享内存的属性!(和进程,文件的管理很类似啊!)
接下来我们在实践中再加深我们的理解
3 ❤️🔥代码实现 – 补充理论知识 – 相关接口
🎁创建
为了使用共享内存,我们先来认识一下对应的系统调用:
<code>SHMGET(2) Linux Programmer's Manual SHMGET(2)
NAME
shmget - allocates a System V shared memory segment
SYNOPSIS
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
DESCRIPTION
shmget() returns the identifier of the System V shared memory segment associated with the value of the argument key. It may be used either to obtain the
identifier of a previously created shared memory segment (when shmflg is zero and key does not have the value IPC_PRIVATE), or to create a new set.
shmget
接口:作用是申请一个system V
标准的共享内存
它有三个参数: (key值我们稍后再谈)
返回值是共享内存的标识符(和 key 不同!!!)size_t size : 表示要创建多大的共享内存空间(通常时候4096的N倍)int shmflg :这是个标记位,会有很多的标记位(比如IPC_CREAT
和 IPC_EXCL
)。本质是使用位图来储存的。
IPC_CREAT
:如果要创建的共享内存不存在,就新创建一个。如果存在了就直接回去该共享内存并返回。 — 这个总能获取一个共享内存!IPC_EXCL
:单独使用没有意义!!!只有和IPC_CREAT
组合才有意义!IPC_CREAT | IPC_EXCL
: 如果要创建的共享内存不存在,就新创建一个。如果存在,就出错返回 — 这个如果成功返回了意味着共享内存(shm)是全新的!
和命名管道类似,共享内存也需要对使用者进行权限划分,创建者可以创建共享内存,使用者只需要获取即可!
那么IPC_CREAT | IPC_EXCL
就用来创建共享内存,IPC_CREAT
这个用来获取共享内存!
那么进程如何知道操作系统内存在共享内存呢???可以猜测:应该是通过struct shm
属性里的用于标识共享内存的唯一性的字段!那这个字段如果让操作系统OS自动生成(类似PID)行不行呢?如果是OS创建的,那其他进程如何获取呢?
首先,如果是类似PID这种由操作系统建立的唯一性字段,只有该进程自己知道,其他进程是无法获取到的。所以为了其他进程可以获取到共享内存的信息就诞生了key_t key
参数。这个key
是用户设置的值,任何一个进程都可以得到这个key。通过系统调用把这个key植入到共享内存中,那么其他进程通过计算得到的key,就可以获取到该共享内存了!
ftok()
就是用来创建key的函数,传入文件路径和一个项目ID,通过一个算法来得到key。所以任何一个进程都可以得到该key值,就可以找到该共享内存!
FTOK(3) Linux Programmer's Manual FTOK(3)
NAME
ftok - convert a pathname and a project identifier to a System V IPC key
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
接下来我们上代码:
首先我们来实现一个获取key的函数
首先需要一个路径名字pathname(取当前路径即可) 和 一个项目ID proj_id(0x66)然后就可以得到一个key,我们封装一个获取key 的函数GetCommKey(const std::string& pathname , int proj_id)进行测试 — 并加入把key转换为16进制的函数 std::string ToHex(key_t key)格式化输出
shm.hpp
#ifndef __SHM__HPP__
#define __SHM__HPP__
#include <sys/ipc.h>
#include <sys/shm.h>
#include <iostream>
#include <string>
#include <cstdio>
const std::string pathname = "/home/jlx/code/shm";
const int proj_id = 0x66;
//转换为16进制
std::string ToHex(key_t key)
{
char buffer[128];
snprintf(buffer , 128 , "0%x", key);
return buffer;
}
//获取共享内存的key
key_t GetCommKey(const std::string& pathname , int proj_id)
{
key_t key = ftok(pathname.c_str() , proj_id);
if(key < 0)
{
perror("ftok");
}
return key;
}
#endif
运行测试一下:
很好,两个进程都可以获取同一个key!!!
然后来对 shmget()进行封装,我们需要需要key size 标志位
<code>//创建一个共享内存
int ShmGet(key_t key, int size)
{
int shmid = shmget(key , size , IPC_CREAT | IPC_EXCL);
if(shmid < 0)
{
perror("shmget");
}
return shmid;
}
我们测试一下
第一次创建的shmid是 0 ,key是随机数。
为什么进程退出了,共享内存不会退出呢???第一次成功创建,第二次就报错–共享内存无法创建!说明共享内存不随着进程的释放而自动释放! 创建之后一直存在,直到重启,所以释放共享内存需要一些系统调用来手动释放!
<code>共享内存的生命周期随内核
🎁删除
我们来看看这个共享内存在哪里:使用指令ipcs -m
这不就找到了吗!!!
删除的指令是:<code>ipcrm -m (shmid),用户删除需要使用shmid。
为什么用户删除只能使用shmid???我们对比看看
key VS shmid
key:属于用户形成,内核使用的一个字段,用户不能使用key来进行共享内存的管理。是内核进行区分shm的唯一性的!
shmid: 内核是用户返回的一个标识符,用来进行用户级对共享内存的管理的id值
保证内核与用户的解耦!
每次通过指令来删除共享内存太矬了,那有没有对应的系统调用可以让我们删除共享内存呢?
当然有了:
<code>HMCTL(2) Linux Programmer's Manual SHMCTL(2)
NAME
shmctl - System V shared memory control
SYNOPSIS
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
也有三个参数
int shmid:就是对应的共享内存的idint cmd:这是指令位(IPC_STAT IPC_RMID IPC_INFO...
)用户需要什么操作就可以传入对应指令来进行操作!struct shmid_ds *buf:这是内核提供的数据结构,是输出型参数,让用户可以获取共享内存的属性信息。不需要就设置为nullptr
那么怎么删除呢?
使用指令IPC_RMID
--> shmctl(int shmid , IPC_RMID , nullptr)
就可以了
void ShmRemove()
{
if (_who == gCreater)
{
int res = shmctl(_shmid, IPC_RMID, nullptr);
std::cout << "shm remove done..." << std::endl;
}
}
这样就可以进行删除了
🎁封装
然后我们进行对共享内存的封装,把这些函数封装为Shm类,让用户可以更加方便的使用!
封装真的是优雅
shm.hpp
#ifndef __SHM__HPP__
#define __SHM__HPP__
#include <sys/ipc.h>
#include <sys/shm.h>
#include <iostream>
#include <string>
#include <cstdio>
#define gCreater 1
#define gUser 2
const std::string pathname = "/home/jlx/code/shm";
const int proj_id = 0x66;
const int ShmSize = 4096;
class Shm
{
private:
// 转换为16进制
std::string ToHex(key_t key)
{
char buffer[128];
snprintf(buffer, 128, "0%x", key);
return buffer;
}
// 获取共享内存的key
key_t GetCommKey()
{
key_t key = ftok(_pathname.c_str(), _proj_id);
if (key < 0)
{
perror("ftok");
}
return key;
}
// 创建一个共享内存
int GetShmHelper(int key, int size, int flag)
{
int shmid = shmget(_key, size, flag);
if (shmid < 0)
{
perror("shmget");
}
return shmid;
}
bool GetShmForCreate()
{
if (_who == gCreater)
{
_shmid = GetShmHelper(_key, ShmSize, IPC_CREAT | IPC_EXCL | 0666);
if (_shmid >= 0)
{
std::cout << "shm create done..." << std::endl;
return true;
}
}
return false;
}
bool GetShmForUse()
{
if (_who == gUser)
{
_shmid = GetShmHelper(_key, ShmSize, IPC_CREAT | 0666);
if (_shmid >= 0)
{
std::cout << "shm get done..." << std::endl;
return true;
}
}
return false;
}
public:
Shm(const std::string &pathname, const int proj_id, int who)
: _pathname(pathname), _proj_id(proj_id), _who(who)
{
_key = GetCommKey();
if (_who == gCreater)
{
GetShmForCreate();
}
else if(_who == gUser)
{
GetShmForUse();
}
std::cout << "key:" << ToHex(_key) << std::endl;
std::cout << "shmid:" << _shmid << std::endl;
}
~Shm()
{
if (_who == gCreater)
{
int res = shmctl(_shmid, IPC_RMID, nullptr);
std::cout << "shm remove done..." << std::endl;
}
}
private:
key_t _key;
int _shmid;
const std::string _pathname;
const int _proj_id;
int _who;
};
#endif
这样我们在用户级别上只需要建立一个实例化的类对象,就保证了两个进程可以看到同一内存:
🎁挂接到进程
上面我们已经可以正常建立共享内存了,接下来就要想办法来使用共享内存:把共享内存挂接到进程地址空间的共享区!
需要使用系统调用<code>shmat(挂载) --- shmdt(去除挂载)
SHMOP(2) Linux Programmer's Manual SHMOP(2)
NAME
shmat, shmdt - System V shared memory operations
SYNOPSIS
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
shmat(int shmid, const void *shmaddr, int shmflg)
有三个参数:
int shmid: 需要挂载的共享内存的idconst void *shmaddr:要挂载到的地址空间位置,一般设置为nullptrint shmflg:挂载方式—只读,只写 ,读写
返回值:挂载成功之后,会返回共享内存的起始虚拟地址(类似malloc , 在堆上申请空间,返回首地址)
我们在类中加入挂接的函数AttachShm()
(使用起来很像malloc)
void* AttachShm()
{
void* shmaddr = shmat(_shmid , nullptr , 0);
if(shmaddr == nullptr)
{
perror("shmat");
}
return shmaddr;
}
char* addr = (char*)shm.AttachShm();
这样我们在用户端就可以使用共享内存了!
我们来看看挂载数会怎么变化:
这样就让共享内存挂接到了进程中的共享区!!!可以看到该共享内存的权限是<code>666 -- 011011011 可读可写!
取出挂载的系统调用shmdt(const void *shmaddr)
(类似free)传入首地址即可
void DetachShm(void* shmaddr)
{
if(shmaddr == nullptr) return ;
shmdt(shmaddr);
}
这样就可以取消挂载了!
当然, 我们建立共享内存的时候,肯定是想要进行通信的,挂接是肯定要进行的,所以用户来进行挂载显得有些多余。我们可以在共享内存建立的时候就进行挂接,析构的时候进行取消挂接。所以我们封装一下:
private:
void *AttachShm()
{
//取消挂接
if(_shmaddr != nullptr) DetachShm();
void *shmaddr = shmat(_shmid, nullptr, 0);
if (shmaddr == nullptr)
{
perror("shmat");
}
std::cout << "who:" << RoleToString() << " attach shm..." << std::endl;
return shmaddr;
}
void DetachShm()
{
if(_shmaddr == nullptr) return ;
shmdt(_shmaddr);
}
public:
Shm(const std::string &pathname, const int proj_id, int who)
: _pathname(pathname), _proj_id(proj_id), _who(who),_shmaddr(nullptr)
{
_key = GetCommKey();
if (_who == gCreater)
{
GetShmForCreate();
}
else if (_who == gUser)
{
GetShmForUse();
}
//肯定是要进行挂接的
_shmaddr = AttachShm();
std::cout << "key:" << ToHex(_key) << std::endl;
std::cout << "shmid:" << _shmid << std::endl;
}
~Shm()
{
DetachShm();
if (_who == gCreater)
{
int res = shmctl(_shmid, IPC_RMID, nullptr);
std::cout << "shm remove done..." << std::endl;
}
}
这样在我们构造的时候就完成了挂接,析构的时候就取消挂接了,不需要用户再来进行操作了。
为了进行通信,我们还需要通过返回地址的函数:
<code> void * Addr()
{
return _shmaddr;
}
//清零函数!
void Zero()
{
if (_shmaddr)
{
memset(_shmaddr, 0, ShmSize);
}
}
来测试一下:
非常好!!!这样就很优雅的完成了挂接的任务!!!
🎁开始通信
上面我们讲过,挂载成功之后,会返回共享内存的起始虚拟地址**(类似malloc , 在堆上申请空间,返回首地址。
所以我们使用起来也可以当成字符串来使用!
我们来进行通信试试:
来看效果:
这样就进行通信了,但是好像有些问题:
共享内存不提供对共享内存的保护机制!会造成数据不一致问题!管道是有保护机制的, 可以使用管道来辅助,管道来负责告诉进程是否写完读完。我们在访问共享内存的时候没有使用任何系统调用!共享内存是所有进程IPC中速度最快的,共享内存大大减少了数据的拷贝次数!
那么我们通过向共享内存写入时也向管道中写入一个“wakeup”信息,服务器端从管道读到wakeup”信息再在共享内存中读取。这样就有了保护机制:
我们运行看看:
这样就好了!可以进行通信了!!!
5 ❤️🔥获取共享内存的属性
<code>int shmctl(int shmid, int cmd, struct shmid_ds *buf); 这个系统调用为我们提供了获取属性的输出型参数,我们如果想要获取共享内存的属性,就可以传入IPC_STAT
IPC_STAT
Copy information from the kernel data structure associated with
shmid into the shmid_ds structure pointed to by buf. The caller
must have read permission on the shared memory segment.
并传入结构体struct shmid_ds ds
我们获取到:
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Creation time/time of last
modification via shmctl() */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
struct ipc_perm {
key_t __key; /* Key supplied to shmget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions + SHM_DEST and
SHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};
这里面就包含了key shmid...
等信息!
Thanks♪(・ω・)ノ谢谢阅读!!!
下一篇文章见!!!
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。