1. system V共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
那么这到底是为什么呢?
1.1 共享内存示意图
我们来看看吧!下图就是shm的原理图。
所谓共享区,就是在内存中开辟的一块空间,然后将该内存空间挂接在进程的共享区中,这里的挂接就是将内存空间的实际地址由页表进行映射转为虚拟地址,将此虚拟地址存放入共享区(存放的还有该共享内存空间的其他相关信息,比如大小)。
这里要注意,我们要如何确保需要通信的两个进程能够指向同一块共享内存区呢?
这就需要该空间具有唯一标识的标志。
匿名管道是父子继承的方式,命名管道由路径进行唯一标识,那么共享内存区呢?
OS会依据唯一的key来对空间进行标识,后面函数篇详细讲解。
在后面我们使用共享内存区的缩写shm。
1.2 共享内存区的数据结构
OS当然会对shm进行管理,那就肯定有管理shm的结构体。
1.3 共享内存函数
1.3.1 shmget函数
功能:用来创建共享内存
原型 int shmget(key_t key, size_t size, int shmflg);
参数
key: OS标识该内存空间唯一的标识符,由ftok函数得来。
size: 共享内存大小 < 这里有一个细节,shm的基本单位是4KB,如果你传入4097Bytes,那 么该shm的实际空间大小是8KB,但你只能使用4097字节,因此建议传入4KB的整数倍>
shmflg: 由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的这里重点介绍IPC_CREAT与IPC_EXCL
只传入前者表示有则返回shmid,传入前者|后者表示有则出错返回。
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
1.3.2 ftok函数
功能:用来生成shmget的第一个参数key,使得操作系统可以开辟具有唯一标识的空间。
实质是一个类似哈希函数的算法,在函数内部对传入的参数进行一系列运算,最后生 成一个具有唯一标识能力的码,该函数确保传入参数相同时,每次返回的key相同。
原型 key_t ftok(const char *pathname, int proj_id);
参数
pathname: 一个稳定的存在的路径,OS会使用该文件的相关信息生成唯一标识key
id: 通常是一个ASCLL码字符
返回值:成功返回key值,失败返回-1
1.3.3 shmat函数
功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个字节;失败返回-1//通过该指针可以直接对内存空间进行操作
说明:
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr -
(shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
1.3.4 shmdt函数
功能:将共享内存段与当前进程脱离
原型 int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存
1.3.5 shmctl
功能:用于控制共享内存
原型 int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
1.3.6 指令小集
ipcs
这一指令可以查看当前所有进程间通信设施的状态信息
ipcs -m
仅查看进程间通信中以共享内存为设施的状态信息
ipcrm -m shmid
删除该内存空间
小细节
这里我们在写下面代码验证的时候遇到了下面这种情况:这里的shm_sever里会创建shm。
上一个进程已经结束了,重新启动我们创建shm却失败了,查看以后发现上一个进程创建的shm还在。
当我们删除shmid为1的shm后,再次启动进程就成功创建了shmid为2的shm。
这说明,shm的生命周期并不向管道一样随进程,而是随内核的,如果我们不主动释放它,他会一直存在直到系统关闭。
1.4 代码验证
common.hpp
该文件中包含创建、销毁shm,挂接进程与解除关联关系的一系列方法。
#pragma once
#include <iostream>
#include <cstring>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <unistd.h>
using namespace std;
//在这里将获取shmid的参数使用宏定义,免去后面在外部接口处传入
#define defaultsize 4096
#define PATH "./"
#define Projid 'A'
//获取key值
int GetKey()
{
return ftok(PATH, Projid);
}
//将key值转化为十六进制,与OS一致,方便查看
string ToHex(int key)
{
char buffer[1024];
snprintf(buffer, sizeof(buffer), "0X%x", key);
return buffer;
}
//创建或获取shm的接口,后续会进行封装,sever创建,而client要获取
int CreatShmOrDie(int size, int shmflg)
{
int key = GetKey();
int shmid = shmget(key, size, shmflg);
if (shmid < 0)
{
cerr << "shmget fail... errno" << errno << ",fail message: " << strerror(errno) << endl;
}
cout << "creat shm success..." << endl;
return shmid;
}
//secver创建shm
int CreatShm(int size)
{
return CreatShmOrDie(size, IPC_CREAT | IPC_EXCL | 0666);
}
//client获取shmid
int GetShm(int size)
{
return CreatShmOrDie(size, IPC_CREAT | 0666);
}
//销毁shm
void DelShm(int shmid)
{
int ret = shmctl(shmid, IPC_RMID, 0);
if (ret == -1)
{
cerr << "Delete shm fail... errno" << errno << ",fail message: " << strerror(errno) << endl;
}
cout << "Delete shm success..." << endl;
}
//进程与shm建立关联关系
void *SetRelationship(int shmid)
{
void *ptr = shmat(shmid, nullptr, 0);
if ((long long int)ptr == -1)
{
cerr << "process and shm set relationship fail... errno" << errno << ",fail message: " << strerror(errno) << endl;
return nullptr;
}
cout << "process and shm set relationship success..." << endl;
return ptr;
}
//解除进程与shm的关联关系
void RemoveRelationship(void *addr)
{
int ret = shmdt(addr);
if (ret == -1)
{
cerr << "remove relationship fail... errno" << errno << ",fail message: " << strerror(errno) << endl;
}
else
{
cout << "remove relationship success..." << endl;
}
}
fifo.hpp
这一文件是命名管道的一部分,由于shm通信并不具备同步互斥机制,shm的通信是借助信号量来保护的。这里简单起见,我们借用管道通信的保护机制来保护shm,具体是如何实现的详见代码注释。
#pragma once
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
#define PAtH "./fifo.txt"
#define MODE 0666
class FIFO // 管理fifo的创建与销毁
{
public:
FIFO(string path)
: _path(path)
{
int n = mkfifo(_path.c_str(), MODE); // 创建,成功返回0
if (n == 0)
{
cout << "name_pipe creat success..." << endl;
}
else
{
cerr << "name_pipe creat fail... errno: " << errno << ",errstring: " << strerror(errno) << endl;
}
}
~FIFO()
{
int n = unlink(_path.c_str()); // 销毁,成功返回0
if (n == 0)
{
cout << "name_pipe unlink success..." << endl;
}
else
{
cerr << "name_pipe unlink fail... errno: " << errno << ",errstring: " << strerror(errno) << endl;
// 标准错误输出
}
}
private:
string _path;
};
//控制对管道的操作(读和写),借助管道的同步机制保护shm
class Sync
{
public:
Sync()
: rfd(-1), wfd(-1)
{
}
void OpenRead()//打开管道的读端
{
rfd = open(PAtH, O_RDONLY);
if (rfd == -1)
{
cerr << "rfd open O_RDONLY fail... errno: " << errno << ",errstring: " << strerror(errno) << endl;
}
else
{
cout << "rfd open success" << "rfd=" << rfd << endl;
}
}
void OpenWrite()//打开管道的写端
{
wfd = open(PAtH, O_WRONLY);
if (wfd == -1)
{
cerr << "wfd open O_WRONLY fail... errno: " << errno << ",errstring: " << strerror(errno) << endl;
}
else
{
cout << "wfd open success" << "wfd=" << wfd << endl;
}
}
bool wake()//读端读,当读取结束或失败返回false
{
int c = 0;
int n = read(rfd, &c, sizeof(c));
if (n == sizeof(c))
return true;
if (n == 0)
return false;
return false;
}
bool wakeup()//写端写,当写入结束或失败返回false
{
int c = 0;
int n = write(wfd, &c, sizeof(c));
if (n == sizeof(c))
return true;
return false;
}
private:
int rfd;
int wfd;
};
shm_sever.cc
sever端对shm的内容进行读取,借用管道的同步机制,只要管道在读取,sever就对shm读取,一旦管道读取结束,sever对shm的读取也结束,进入下一阶段。
#include "common.hpp"
#include "fifo.hpp"
int main()
{
// 检查获取key值是否出错
int key = GetKey();
cout << ToHex(key) << endl;
// 创建shm,如果有报错的那种
int shmid = CreatShm(defaultsize);
cout << shmid << endl;
// 挂接shm
char *buffer = (char *)SetRelationship(shmid);
// 进程间通信 这里借用管道的同步互斥机制,当写端终止写入或关闭,读端读到0,
FIFO fifo(PAtH);
Sync syn;
syn.OpenRead();
while (1)
{
if (!syn.wake()) // 读端读到0,退出循环
break;
cout << "from client message: " << buffer << endl;
sleep(1);
}
// 解除挂接
RemoveRelationship(buffer);
// 销毁shm
DelShm(shmid);
return 0;
}
shm_client.cc
client端对shm进行写入,借用管道的同步机制,只要管道在写入,client就对shm写入,一旦管道写入结束,client对shm的写入也结束,进入下一阶段。
#include "common.hpp"
#include "fifo.hpp"
int main()
{
// 检查获取key值是否出错
int key = GetKey();
cout << ToHex(key) << endl;
// 获取shmid
int shmid = GetShm(defaultsize);
cout << shmid << endl;
// 进程与shm挂接
char *buffer = (char *)SetRelationship(shmid);
// 进程间通信 这里借用管道的同步互斥机制,当写端终止写入或关闭,读端读到0,
Sync syn;
syn.OpenWrite();
sleep(10);
for (char c = 'A'; c < 'H'; c++)
{
buffer[c - 'A'] = c;
sleep(1);
!syn.wakeup();//当读端不再读,OS会强制关闭该管道并释放信号杀死该进程
}
// 写端不再写,读端会返回0
// 解除挂接
RemoveRelationship(buffer);
return 0;
}
当我们终止sever与client的任意一端,管道机制都会收到,进而影响shm。
我们由sever端负责创建与销毁shm和管道,接收信息的任务,client端只需要发送信息即可。
下图nattch没有0-1的过程是因为该信息是每秒进行打印,而我们在两端并未进行等待,0-1,1-0的过程瞬间就已经完成。
1.5 小结
注意:共享内存的删除操作并非直接删除,而是拒绝后续映射,只有在当前映射链接数为0时,表示没有进程访问了,才会真正被删除
1.5.1 shm的优点 (部分)
这一进程通信方式速度是最快的,因为这一通信方式无需向管道那样使用系统调用,read与write系统调用的本质是拷贝函数,将数据在用户空间与内核空间之间传输。而shm相对而言非常快,两个进程访问同一内存空间,一个进程对该空间进行更改,另一个进程马上就能看到。
1.5.2 shm的缺点 (部分)
这一进程通信方式没有同步与互斥机制的保护,也就是说,存在写端还没写完,读端就已经把已写的部分数据进行读取了, 这会造成通信间双方数据不一致的问题。
共享内存的操作是非进程安全的,多个进程同时对共享内存读写是有可能会造成数据的交叉写入或读取,造成数据混乱