什么是共享内存
共享内存是一种计算机编程中的技术,它允许多个进程访问同一块内存区域,以此作为进程间通信(IPC, Inter-Process Communication)的一种方式。这种方式相对于管道、套接字等通信手段,具有更高的效率,因为数据不需要在用户空间和内核空间之间进行复制,也不需要经过序列化和反序列化的复杂过程。
特点:
- 高速度:由于省去了数据复制和上下文切换的开销,共享内存提供了非常高的数据交换速度。
- 低延迟:适用于需要快速响应和大数据量传输的场景。
- 同步需求:虽然高效,但多个进程同时访问同一块内存可能会导致数据不一致。因此,需要使用如互斥锁、信号量等同步工具来确保数据的正确性和完整性。
- 生命周期管理:共享内存段需要显式创建、映射到进程地址空间、使用后断开连接,并在不再需要时销毁,以避免资源泄露。
- 共享内存在系统中可以存在多个,供不同进程之间进行通信
共享内存的原理
每一个进程都有属于自己的进程地址空间,假设操作系统在物理内存开辟了一段空间,该进程可以创建一段虚拟内存,将这段虚拟内存的起始与结束地址通过页表与物理内存的空间构建联系
如果另一个进程,也通过上述方式,通过页表映射到同一段物理内存,那就实现了让多个进程看到同一段空间,这样当一个进程向这段物理空间写入数据,另一个进程就可以马上从这段空间读取数据了,就可以实现进程间的通信了
共享内存的使用
(一)创建共享内存
#原型
int shmget(key_t key, size_t size, int shmflg);
#参数
key:用户自定义共享内存的标识
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
#返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
【解释】:
# key:由于进程间具有独立性,所以共享内存一定不是某一个进程自己创建的,而是进程通过函数调用让操作系统创建的,而为了使另一个进程可以找到该共享内存,每一个共享内存一定有一个唯一性的标识,但是这个标识一定不能是操作系统自己独立生成的,因为这样只有要创建共享内存的那个进程能找到该共享内存。所以用户可以通过key自己设定唯一的标识,key一般使用函数调用生成
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
pathname为文件路径,proj_id为项目id,这两个都是由用户自己设定的,该函数会通过特定的算法将参数形成唯一的key值
#size:共享内存的大小,建议设置为4096个字节的倍数,例如:当我们将共享内存设置为4097个字节,OS会申请4096*2大小的空间,由于我们申请的是4097个字节,剩下的4095个字节我们不能使用,就会浪费掉
#shmflg: 标志位参数有两种:IPC_CREAT、IPC_EXCL,常用的反方式有两种
- IPC_CREAT: 如果要创建的共享内存不存在那就创建,如果存在就返回该共享内存
- IPC_CREAT | IPC_EXCL:如果创建的共享内存不存在那就创建,如果存在就报错
- 在使用时后面一般还要加上权限,防止进程无法与共享内存联系(注意)
ps:第二种使用方法可以保证每次创建的共享内存都是新创建的,所以在使用上,IPC_CREAT | IPC_EXCL一般用于创建共享内存,IPC_CREAT一般用于获取共享内存
#返回值:共享内存创建成功就返回该共享内存的shmid,失败就返回-1
key和shmid都是标识共享内存的唯一性字段,不过key是用户自定义的,用于让内核区分shm唯一性的,用户不能通过key进行对shm管理,而shmid是有内核返回的一个值,是让用户对共享内存进行管理的id值
(二)删除共享内存
- 查看所有的共享内存:
ipcs -m
- 利用指令删除共享内存:
ipcrm -m shmid
- 代码删除共享内存:
shmctl函数
#功能:用于控制共享内存
#原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
#参数:
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
#返回值:成功返回0;失败返回-1
命令 | 说明 |
IPC_STAT | 把shmid_ds数据结构中的数据设置为共享内存当前关联值 |
IPC_SET | 在进程有足够权限的前提下,把共享内存当前关联值设置为shmid_ds数据结构中给出的值 |
IPC_RMID | 删除共享内存段 |
ps.删除共享内存关心第三个参数,可以直接把他设置为nullptr
(三)将共享内存连接到进程地址空间
shmat函数
#功能:将共享内存段连接到进程地址空间
#原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
#参数
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
#返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
(四)将共享内存从进程地址空间脱离
#功能:将共享内存段与当前进程脱离
#原型
int shmdt(const void *shmaddr);
#参数
shmaddr: 由shmat所返回的指针
#返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
补充:共享内存不具有进程之间的同步机制,假设一个进程负责读,一个进程负责写,就算共享内存中没有写入数据,读进程还是会一直读,这就可能会发生写进程才写了一半的数据就被读走了,造成数据不一致问题。我们可以利用管道解决这个问题,因为管道具有同步机制,我们让写端写完以后,通过管道传输信号,只有读端通过管道接受到信号以后,才会进行对共享内存的读取
(向管道中写的信号是什么不重要,只要向共享内存中写后,向管道中发送信息,在接收到管道信号后,才读取共享内存的内容,这样就可以让共享内存也存在向管道一样的同步机制,写端写一条,读端读一条)
代码完整使用
shm.hpp
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define Creater 1
#define User 2
const char *pathname = "/home/zyq/mydir/dir/Shm";
int proj_id = 0x666;
class Shm
{
private:
key_t GetComkey()
{
key_t key = ftok(_pathname, proj_id);
if (key < 0)
{
perror("ftok");
return -1;
}
return key;
}
int GetShmid(key_t key, int size, int flag)
{
int shmid = shmget(_key, size, flag);
if (shmid < 0)
perror("shmget");
return shmid;
}
void AttachShm()
{
_addrshm = shmat(_shmid, nullptr, 0);
if (_addrshm == nullptr)
{
perror("shmat");
}
}
void DetachShm()
{
if (_addrshm != nullptr)
{
int n = shmdt(_addrshm);
if (n < 0)
perror("shmdt");
}
}
public:
Shm(const char *pathname, int proj_id, int who)
: _pathname(pathname), _proj_id(proj_id), _who(who), _addrshm(nullptr)
{
_key = GetComkey();
if (who == Creater)
{
GetCreatershmid();
}
else
{
GetUsershmid();
}
//将共享内存连接到进程地址空间
AttachShm();
std::cout << "_key:" << _key << std::endl;
std::cout << "_shmid:" << _shmid << std::endl;
}
~Shm()
{
//删除进程地址空间
shmctl(_shmid,IPC_RMID,nullptr);
//将共享内存脱离进程地址空间
DetachShm();
}
bool GetCreatershmid()
{
_shmid = GetShmid(_key, 4096, IPC_CREAT | IPC_EXCL|0666 );
if (_shmid < 0)
return false;
else
{
std::cout << "Create shm done!" << std::endl;
return true;
}
}
bool GetUsershmid()
{
_shmid = GetShmid(_key, 4096, IPC_CREAT|0666);
if (_shmid < 0)
return false;
else
{
std::cout << "Get shm done!" << std::endl;
return true;
}
}
void* Addr()
{
return _addrshm;
}
private:
key_t _key;
int _shmid;
const char *_pathname;
int _proj_id;
int _who;
void *_addrshm;
};
server.cc
#include "shm.hpp"
#include "namedpipe.hpp"
int main()
{
// 创建共享内存并连接
Shm shm(pathname, proj_id, Creater);
char *shmaddr = (char *)shm.Addr();
// 创建管道
NamedPipe fifo(path, Creater);
fifo.OpenforRead();
while (true)
{
//读共享内存前先获取唤醒信号
std::string str;
fifo.ReadNamedPipe(&str);
std::cout << "shm content:" << shmaddr << std::endl;
sleep(1);
}
sleep(10);
return 0;
}
client.cc
#include"shm.hpp"
#include"namedpipe.hpp"
int main()
{
//获取共享内并连接
Shm shm(pathname,proj_id,User);
char* shmaddr=(char*)shm.Addr();
//获取管道
NamedPipe fifo(path,User);
fifo.OpenforWrite();
char ch='A';
while(ch<'Z')
{
shmaddr[ch-'A']=ch;
//写完以后,向管道发送唤醒信息
//向管道中写的内容不重要,主要是利用管道的同步机制
std::cout<<"add "<<ch<<" into shm"<<std::endl;
std::string str="WakeupRead";
fifo.WriteNamedPipe(str);
ch++;
sleep(2);
}
sleep(10);
return 0;
}