共享内存原理
在C语言/C++中,malloc也可以在物理内存申请空间,将申请的物理内存空间通过页表映射到进程地址空间,将内存空间的起始地址(虚拟地址)返回,进而进程可以使用虚拟地址通过页表映射到物理内存的方式访问到申请的内存空间。
那么malloc和共享内存有什么区别呢?
区别就是malloc出来的空间,是进程在自己的堆上申请的空间,没有和其他进程建立映射关系,不能被其他进程共享!
- 共享内存是用来进程间通信的!
- 共享内存是一种通信方式,所有想通信的进程都可以使用!
- OS中一定会可能存在很多的共享内存!
共享内存概念
共享内存是最快的 IPC 方式之一。允许多个进程共享同一块物理内存区域。进程可以通过将共享内存区域映射到自己的地址空间来访问这块内存。这样,多个进程可以直接在共享内存区域中读写数据,大大提高了通信效率。
共享内存函数
shmget函数
- 创建共享内存
成功,返回共享内存的标识符;失败,返回-1。
shmflg
int shmflg 权限标志
常用选项:
- IPC_CREAT:如果shm不存在,则创建;存在,则获取。
- IPC_EXCL:不能单独使用。
IPC_CREAT | IPC_EXCL:如果shm不存在创建,则创建;存在,则出错返回。(对用户来讲,如果创建成功,一定是一个新的shm)
进程要进行通信,首先要让不同的进程看到同一份资源。如何保证两个进程看到的就是同一份共享内存呢?
key
通过接口int shmget(key_t key,size_t size,int shmflg),第二个参数key来保证。
key_t类型其实就是一个32位的整数。
key是什么不重要,重要的是它能进行唯一性标识一个共享内存段。
如何形成一个key呢?通过ftok函数来形成一个key。
ftok函数
- 将文件路径名和项目标识符转换为System V IPC(进程间通信)key(键)。
成功,返回成功创建的key;失败,返回-1。
如果进行通信的两个进程使用ftok函数,传递的两个参数是一样的,那么形成的key值也是同样的,就能找到同一个共享内存。一个进程通过key进行创建,另一个进程通过key获取。
makefile
.PHONY:all
all:shm_client shm_server
shm_client:shm_client.cc
g++ -o $@ $^ -std=c++11
shm_server:shm_server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f shm_client shm_server
comm.hpp
#ifndef _COMM_HPP_
#define _COMM_HPP_
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATHNAME "."
#define PROJ_ID 0x88 // 项目标识符,随便写的。
#define MAX_SIZE 4096 //共享内存大小。
//获取key
key_t getKey()
{
key_t k = ftok(PATHNAME, PROJ_ID);//使用ftok可以获取同样的一个key!
if (k < 0)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(1);
}
return k;
}
#endif
shm_server.cc
#include "comm.hpp"
int main()
{
key_t k = getKey();
printf("0x%x\n", k);
return 0;
}
shm_client.cc
#include "comm.hpp"
int main()
{
key_t k = getKey();
printf("0x%x\n", k);
return 0;
}
运行结果
size
共享内存大小一般建议为4KB的整数倍,系统分配共享内存是以4KB大小为单位的!这里我们定义了共享内存大小为4096KB,如果定为,例如:4097KB,那么内核给用户的共享内存大小会自动向上取整,申请4096*2KB。注意:内核给用户的申请的大小,和用户能用的大小是两码事,虽然内核会申请4096*2KB,但是用户依旧只能用4097KB(ipcs -m 查看共享内存大小依旧为4097KB)。
创建共享内存
comm.hpp
//获取key
key_t getKey()
{
key_t k = ftok(PATHNAME, PROJ_ID);//使用ftok可以获取同样的一个key!
if (k < 0)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(1);
}
return k;
}
int getShmHelper(key_t k, int flags)
{
int shmid = shmget(k, MAX_SIZE, flags);
if (shmid < 0)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(2);
}
return shmid;
}
//获取共享内存
int getShm(key_t k)
{
return getShmHelper(k, IPC_CREAT);
}
//创建共享内存
int createShm(key_t k)
{
return getShmHelper(k, IPC_CREAT | IPC_EXCL);
}
shm_server.cc
#include "comm.hpp"
int main()
{
key_t k = getKey();
printf("key: 0x%x\n", k);
int shmid = createShm(k);
printf("shmid: %d\n",shmid);
return 0;
}
shm_client.cc
#include "comm.hpp"
int main()
{
key_t k = getKey();
printf("key: 0x%x\n", k);
int shmid = getShm(k);
printf("shmid: %d\n",shmid);
return 0;
}
运行结果
如何理解key和shmid,有什么区别?
再谈key
基于上述理解,我们认识到:
我们先谈论两个小问题:
C语言C++ malloc的时候,例如申请1024字节的内存空间,int *p = malloc(size_t 1024),释放free(p),对空间释放不能局部释放,所以free释放的时候要传入所申请空间的起始地址。
问题是怎么知道从p开始释放多少个字节呢?
虽然用户申请了1024个字节,但是OS为了管理用户申请的空间,还会额外申请一些空间,用来记录用户malloc所申请出来的内存空间的属性信息,例如内存块大小,是否被使用等信息。所以free的时候,会自己去额外申请的空间中搜索内存块大小属性,帮助用户去释放。
在创建进程的时候,OS为了管理进程,以先描述,后组织的方式,还创建了一个描述该进程的内核数据结构PCB,用来记录进程的相关属性,所以OS管理进程就通过PCB来管理。
基于对上面两个问题的理解,那么:
OS中一定会存在很多共享内存,那么OS也要对共享内存以先描述,后组织的方式管理起来!
所以,这里并不单单只是用户申请一块物理内存空间,OS也一定会额外申请一些空间用来记录共享内存的相关属性信息,例如共享内存的权限、容量等。
所以共享内存 = 物理内存块 + 共享内存的相关属性!
实际上,用户在申请共享内存的时候,OS除了给用户申请一个物理内存块之后,还会给共享内存申请一个数据结构对象,OS要管理共享内存,并不是直接通过管理这个物理内存块,而是通过管理这个数据结构对象来进行管理。
创建共享内存的时候,是通过key来保证共享内存在系统中是唯一的。那么key在哪呢?
在创建共享内存时,key就作为共享内存的相关属性被填入到了描述共享内存相关属性的数据结构对象中。
eg:
struct shm{
//相关属性
//...
key_t k;
}
(下面还会谈到)
所以,当我们上层调用ftok创建key,再调用shmget创建共享内存时,把key作为参数传入shmget中,本质是把key设置进创建好的共享内存对应的数据结构对象中某一个属性字段里。另一个进程再去获取共享内存时,并不是通过共享内存对应的物理内存块,而是遍历共享内存所对应的相关属性,查找自己的key和共享内存的key是否相等。
总结:key要通过shmget,设置进共享内存属性中!用来标识该共享内存在内核中的唯一性!
shmid与key类似于文件概念里的fd与inode。使用具有唯一性的key值来标定特定的共享内存,给上层返回一个特定的整数,使用户可以通过这个特定的整数来访问特定的共享内存。
当我们进程退出,再执行的时候,这里报了一个文件已经存在的错误。进程已经结束了,共享内存资源依旧存在。
- 共享内存生命周期随OS,不是随进程的。除非显式的使用对应的命令或接口释放共享内存,否则共享内存会随着OS的运行一直存在。
- 生命周期随OS,不随进程,也是所有System V版本进程间通信的共性!
- 跟管道不一样,管道的生命周期随进程。
查看IPC资源
- ipcs -m/q/s
- ipcs -m 查看系统中共享内存段的相关信息
删除共享内存
- 使用命令:ipcrm -m + shmid
- 删除共享内存也有相关接口,使用shmctl函数。
shmctl函数
- 控制共享内存
获取共享内存的属性、设置共享内存的属性、删除共享内存都使用shmctl接口。
失败返回-1。
- shmid:共享内存id。
- cmd:控制共享内存的方式。
包含选项IPC_STAT、IPC_SET、IPC_RMID、IPC_INFO
其中IPC_RMID选项,就是删除共享内存。
- buffer:共享内存的相关属性。
comm.hpp
//获取共享内存
//...
//创建共享内存
//...
//删除共享内存
int delShm(int shmid)
{
if (shmctl(shmid, IPC_RMID, NULL) == -1)
{
std::cerr << errno << strerror(errno) << std::endl;
}
}
//...
shm_server.cc
#include "comm.hpp"
int main()
{
key_t k = getKey();
printf("key: 0x%x\n", k);
int shmid = createShm(k);
printf("shmid: %d\n", shmid);
sleep(5);
delShm(shmid);
return 0;
}
一般情况,共享内存谁创建谁删除。
至此,我们完成了共享内存的创建与删除工作,但是,目前所创建的共享内存还没有办法使用,因为共享内存还没有和进程关联起来。接下来,我们要完成共享内存与进程关联的工作。
shmat函数
- 将共享内存段连接到进程的地址空间
成功,返回,共享内存的起始地址;失败,返回-1。
- shmaddr:指定连接地址。
要将共享内存映射到哪一个地址空间。大部分情况下,这个参数可以不设置,因为我们并不清楚我们想将共享内存映射到虚拟地址空间中的哪个区域。设定位为NULL即可,系统会自动选择一个合适的地址来连接共享内存段。
- shmflg:标志位,用于控制共享内存段的连接方式和权限等。
常用的标志位为:
SHM_RDONLY:表示以只读方式连接共享内存。如果不设置(0),默认以读写方式连接共享内存段,即进程可以对连接后的共享内存段进行读和写操作。
comm.hpp
//获取共享内存
//...
//创建共享内存
//...
//挂接共享内存
void *attachShm(int shmid)
{
void *mem = shmat(shmid, nullptr, 0);
if ((long long)mem == -1L) // linux为64位系统,指针大小为8
{
std::cerr << errno << ": " << strerror(errno) << std::endl;
exit(3);
}
}
//删除共享内存
//...
shm_server.cc
#include "comm.hpp"
int main()
{
key_t k = getKey();
printf("key: 0x%x\n", k);
int shmid = createShm(k);
printf("shmid: %d\n", shmid);
char *start = (char*)attachShm(shmid);
printf("attch success,address start:%p\n",start);
delShm(shmid);
return 0;
}
此时,Permission denied 权限被拒绝了。创建共享内存的时候,添加权限即可。
comm.hpp
int getShmHelper(key_t k, int flags)
{
int shmid = shmget(k, MAX_SIZE, flags);
if (shmid < 0)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(2);
}
return shmid;
}
//获取共享内存
int getShm(key_t k)
{
return getShmHelper(k, IPC_CREAT | 0600);
}
int createShm(key_t k)
{
return getShmHelper(k, IPC_CREAT | IPC_EXCL | 0600);
}
//挂接共享内存
void *attachShm(int shmid)
{
void *mem = shmat(shmid, nullptr, 0);
if ((long long)mem == -1L) // linux为64位系统,指针大小为8
{
std::cerr << errno << ": " << strerror(errno) << std::endl;
exit(3);
}
}
//删除共享内存
//...
shm_server.cc
#include "comm.hpp"
int main()
{
key_t k = getKey();
printf("key: 0x%x\n", k);
int shmid = createShm(k);
printf("shmid: %d\n", shmid);
sleep(2);
char *start = (char*)attachShm(shmid);
printf("attch success,address start:%p\n",start);
sleep(2);
delShm(shmid);
return 0;
}
下面我们写一个监控脚本监测运行一下。
进程和共享内存挂接工作也已经完成了。接下来就可以进行通信了,当通信结束以后,要先去关联,将进程和共享内存的关联去掉,再删除共享内存。
shmdt函数
- 去掉进程和共享内存的关联性
成功,返回0;失败,返回-1。
comm.hpp
//创建共享内存
//...
//挂接共享内存
//...
//共享内存去关联
void detachShm(void *start)
{
if (shmdt(start) == -1)
{
std::cerr << errno << ": " << strerror(errno) << std::endl;
}
}
//删除共享内存
//...
shm_server.cc
#include "comm.hpp"
int main()
{
key_t k = getKey();
printf("key: 0x%x\n", k);
int shmid = createShm(k);
printf("shmid: %d\n", shmid);
sleep(2);
char *start = (char*)attachShm(shmid);
printf("attch success,address start:%p\n",start);
sleep(2);
//通信
//...
//去关联
detachShm(start);
sleep(2);
delShm(shmid);
return 0;
}
要通信,让client也和共享内存挂接起来。
shm_client.cc
#include "comm.hpp"
int main()
{
key_t k = getKey();
printf("key: 0x%x\n", k);
int shmid = getShm(k);
printf("shmid: %d\n",shmid);
sleep(5);
char *start = (char*)attachShm(shmid);
printf("attch success,address start:%p\n",start);
sleep(5);
//通信
//...
//去关联
detachShm(start);
sleep(2);
return 0;
}
共享内存直接就可以当作缓冲区,可以直接向共享内存输入输出。
共享内存通信
comm.hpp
#ifndef _COMM_HPP_
#define _COMM_HPP_
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATHNAME "."
#define PROJ_ID 0x66 // 项目标识符,随便写的。
#define MAX_SIZE 4096 // byte
key_t getKey()
{
key_t k = ftok(PATHNAME, PROJ_ID); // 使用ftok可以获取同样的一个key!
if (k < 0)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(1);
}
return k;
}
int getShmHelper(key_t k, int flags)
{
int shmid = shmget(k, MAX_SIZE, flags);
if (shmid < 0)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(2);
}
return shmid;
}
int getShm(key_t k)
{
return getShmHelper(k, IPC_CREAT | 0600);
}
int createShm(key_t k)
{
return getShmHelper(k, IPC_CREAT | IPC_EXCL | 0600);
}
void *attachShm(int shmid)
{
void *mem = shmat(shmid, nullptr, 0);
if ((long long)mem == -1L) // linux为64位系统,指针大小为8
{
std::cerr << errno << ": " << strerror(errno) << std::endl;
exit(3);
}
}
void detachShm(void *start)
{
if (shmdt(start) == -1)
{
std::cerr << errno << ": " << strerror(errno) << std::endl;
}
}
int delShm(int shmid)
{
if (shmctl(shmid, IPC_RMID, NULL) == -1)
{
std::cerr << errno << strerror(errno) << std::endl;
}
}
#endif
shm_client.cc
#include "comm.hpp"
int main()
{
key_t k = getKey();
printf("key: 0x%x\n", k);
int shmid = getShm(k);
printf("shmid: %d\n", shmid);
sleep(5);
char *start = (char *)attachShm(shmid);
printf("attch success,address start:%p\n", start);
//sleep(5);
// 通信
const char *message = "Hello server,我是另一个进程,我正在和你通信!";
pid_t id = getpid();
int cnt = 1;
while (true)
{
sleep(1);
snprintf(start, MAX_SIZE, "%s[pid:%d][消息编号:%d]", message, id, cnt++);
}
// 不需要再定义缓冲区,写到共享内存中。共享内存就可以当作用户层缓冲区,可以直接写入。跟管道不同!
// char buffer[1024];
// while (true)
// {
// snprintf(start, sizeof(buffer), "%s[pid:%d][消息编号:%d]", message, id, cnt++);
// memcpy(start, buffer, strlen(buffer) + 1);
// }
// 去关联
detachShm(start);
//sleep(2);
return 0;
}
shm_server.cc
#include "comm.hpp"
int main()
{
key_t k = getKey();
printf("key: 0x%x\n", k);
int shmid = createShm(k);
printf("shmid: %d\n", shmid);
// sleep(5);
char *start = (char *)attachShm(shmid);
printf("attch success,address start:%p\n", start);
// sleep(5);
// 通信
while (true)
{
//共享内存当字符串
printf("client say : %s\n", start);
sleep(1);
}
// 管道读取方式:
// char buffer[]; read(pipefd, buffer, ...)
// 去关联
detachShm(start);
// sleep(5);
delShm(shmid);
return 0;
}
至此,就完成了基于共享内存的client、server端的通信。
共享内存特点
- 优点:所有进程间通信,速度最快!
因为,共享内存被进程共享,当一个进程向共享内存区域写入数据后,其他进程可以立即从该区域读取到最新的数据,能大大的减少数据拷贝次数,无需像其他通信方式那样经过内核空间的中转和数据拷贝,从而大大减少了数据传输的时间开销,提高了通信效率。
同样的代码,使用管道和共享内存的方式通信,考虑键盘输入输出,分别需要几次拷贝?
- 缺点:没有进行同步与互斥操作,没有对数据做任何保护!
将client端改为每3秒写一次,而此时server读端为每1秒读一次,读比写快:
server端读取一次之后,3秒内一直在重复读取!跟管道不同,管道会阻塞,等待写入。当然,也可以设计出对数据做保护的共享内存,这里就不演示了。
共享内存的数据结构
shmctl可以允许用户调用特定接口获取指定id的共享内存的属性。
其中shmid_ds结构体是OS暴露给用户的一部分用户级数据结构,存放着共享内存的相关属性,也就是上面我们所说的OS为了管理共享内存额外申请空间创建的数据结构对象,当然,此数据结构是用户级的,跟内核中的还有一点差异,不过类似。
上面我们已经知道key是要被设置进共享内存属性中的!在shmid_ds中也可以看到key做了封装,封装到了shm_perm结构体中。
我们也可以获取共享内存的相关属性:
//...
while (true)
{
printf("client say : %s\n", start);
struct shmid_ds ds;
shmctl(shmid, IPC_STAT, &ds);
printf("获取属性:size:%d,pid:%d,myself:%d,key: ", ds.shm_segsz, ds.shm_cpid, getpid(),ds.shm_perm._key);
sleep(1);
}
//...