最近项目中要用到共享内存来做不同线程间的数据传输以提高效率,以前都是通过类之间调用来复制数据,因此特地来做一个总结。
概念介绍
参考百度百科的介绍:共享内存是 Unix下的多进程之间的通信方法 ,这种方法通常用于一个程序的多进程间通信,实际上多个程序间也可以通过共享内存来传递信息。-------百度百科
说一下我自己的理解,我们在定义一个变量的时候,系统都会分一段内存空间用于数据存储,在C或者C++中我们可以借助指针间接的访问或修改该内存空间的数据,如果定义两个指针指向相同的内存空间,借助其中一个指针修改了该内存空间存储的数据,那么此时借助另一个指针去获取该内存空间的数据,得到的便是修改后的数据。假设这两个指针存在与不同的进程/线程之中,那么在一个进程/线程中通过指针修改内存空间的数据后,在另一个进程/线程中去访问就会得到最新值。进而可以实现不同进程/线程之间通信,这种方法直接从内存中进行数据的修改与访问,无需进行数据在不同进程/线程之间的频繁复制,效率明显提高。
接口介绍
头文件
#include <sys/shm.h>
ftok():把一个已存在的路径名和一个整数标识符转换成一个key_t值,称为IPC键值(也称IPC key键值)
extern key_t ftok (const char *__pathname, int __proj_id) __THROW;
返回值 key_t:
/* Type of an IPC key. */
参数:__pathname:
所在的文件系统的信息
__proj_id:
该文件在本文件系统内的索引节点号
(对参数实际意义没有做深入了解,感觉可以随便设置,该函数利用这两个信息生成一个固定的key。
这个key在之后初始化共享内存时会用到,感觉这个函数的意义是为了方便生成不重复的key.)
shmget(): 创建共享内存
/* Get shared memory segment. */
extern int shmget (key_t __key, size_t __size, int __shmflg) __THROW;
返回值 int:
返回创建的共享内存的ID号
参数
key_t:
参考ftok函数返回值;
__size:
内存大小(用字节表示)
__shmflg:
IPC_CREAT 01000 /* Create key if key does not exist. */
IPC_EXCL 02000 /* Fail if key exists. */
IPC_NOWAIT 04000 /* Return error on wait. */
实测发现如果不或上IPC_CREAT,则如果key不存在就直接返回-1也不会新建内存空间;如果或上IPC_EXCL,在key存在的情况下会返回-1;
通常在使用时要或上对文件的执行权限如666 ,具体意义与linux 常用的chmod 777 类似。
shmat():把共享内存区对象映射到调用进程的地址空间
/* Attach shared memory segment. */
extern void *shmat (int __shmid, const void *__shmaddr, int __shmflg)
返回值void *:
返回ID号对应的共享内存的首地址
参数
__shmid:
调用shmget()得到的id号。
__shmaddr:
指定共享内存出现在进程内存地址的什么位置,直接指定为NULL让内核自己决定一个合适的地址位置
(实测发现如果自己指定内存地址比较困难,试了几次都失败了,所以还是让系统自己分配吧)
__shmflg:
SHM_RDONLY 010000 /* attach read-only else read-write */
SHM_RND 020000 /* round attach address to SHMLBA */
SHM_REMAP 040000 /* take-over region on attach */
SHM_EXEC 0100000 /* execution access */
通常使用0代表可读写。
shmdt():与shmat函数相反,是用来断开与共享内存附加点的地址,禁止本进程访问此片共享内存
/* Detach shared memory segment. */
extern int shmdt (const void *__shmaddr) __THROW;
返回值 int:
返回操作结果:0成功,-1失败
参数
__shmaddr:
共享内存地址(此地址为调用shmat()绑定到该进程的地址)
实测发现该接口只会解除当前进程与共享内存的绑定,而不会删除共享内存空间
解除绑定后再借助该地址修改共享内存的数据系统会抛异常。
shmctl():对共享内存的控制,读写参数与删除共享内存
/* Shared memory control operation. */
extern int shmctl (int __shmid, int __cmd, struct shmid_ds *__buf) __THROW;
返回值 int:
返回操作结果:0成功,-1失败
参数
__shmid:
调用shmget()得到的id号。
__cmd:
IPC_RMID 0 /* Remove identifier. */
IPC_SET 1 /* Set `ipc_perm' options. */
IPC_STAT 2 /* Get `ipc_perm' options. */
上述参数主要是对根据不同的值执行不同的行为,简单来说就是实现共享内存的读写与删除。
通过在终端输入 ipcs -m 可以查看当前的共享空间,用上面的接口删除共享空间后可以通过该命令辅助查看删除结果。
shmid_ds:
一个描述共享空间状态的结构体,当读数据时数据将通过它传出去,当写数据时将通过它更新共享空间状态。
具体结构体内容参考具体定义。
实例
详细代码比较多,不做记录,仅对关键部分举例。
分别起两个不同的线程,一个线程负责向共享内存写数据,一个从共享内存读数据。
头文件相关定义:
struct SharedData
{
int iData = 0;
float fData[30] ={0.0};
};
key_t m_SharedKey;
int m_iSharedShmid = -1;
SharedData *m_pSharedData = NULL;
初始化相关:
m_SharedKey = ftok("./", 1234);
m_iSharedShmid = shmget(m_SharedKey,1024, IPC_CREAT|0666);
m_pSharedData = (SharedData *)shmat(m_iSharedShmid, NULL, 0);
SharedData为自己创建的结构体类型,因为函数返回的是void* 类型,转换为自己需要的数据类型方便操作。
在两个线程中都包含该初始化流程,之所以这样没有问题是因为,第一次共享内存创建完成后,只要key值,申请的空间大小不变。
第二次的时候获取到的id都是相同的,那么再调用shmat与各自的线程绑定后,绑定的共享内存是相同的。
但是注意的是shmat返回的地址是不同的,这是因为它返回的地址是当前进程与共享空间的映射,而不是实际的共享内存地址,该地址只在当前进程有效。
写数据:
int i = 0;
int iCount = 0;
SharedData data;
data.iData = 1;
for (i= 0; i < 30; i++)
{
data.fData[i] = iCount;
iCount++;
}
*m_pSharedData = data;
读数据:
SharedData data;
data = *m_pSharedData;
测试结果:
在一个线程里修改数据后,在另一个线程里去读,读到的数据便是修改后的数据。
共享内存功能跑通了,但是在实际项目应用的时候还要添加一些约束条件,比如读写的同步问题以及多个进程同时进行读写的处理,要解决这一问题可以借助信号量。之后将会分别总结信号量以及共享内存与信号量配合使用的实际案例。