一.为什么需要使用共享内存?
1.导语
共享内存是最高效的进程之间的通信,因为它不涉及到进程之间的任何传输,是系统出于多个进程之间通讯的考虑,预留的一块内存区域.
它在内核态和用户态之间交互了两次,一次是从输入文件到共享内存区域,另一次是从共享内存区域写出到输出文件之中。
进程在使用共享内存的时候,只有当通信完毕之后,才能继续保持,直到通信结束之后。是最快的IPC之一,不涉及进程之间的任何数据传输,所以我们必须用其他辅助手段来同步进程对于共享内存的访问
2.共享内存的内存模型
共享内存允许两个或更多进程访问同一块内存,就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。共享存储的一种形式,多个进程可以将同一个文件映射到一个地方
但是由于系统内核没有提供共享内存的同步方式,必须自己解决这类问题,以自己的同步措施来进行解决,不然一个进程和另外一个进程同时进行读写的话,会出现问题。
当一个进程想和另外一个进程通信的时候,它将按以下顺序运行:
获取mutex对象,锁定共享区域。
将要通信的数据写入共享区域。
释放mutex对象。
要使用一块共享内存
进程必须首先分配它
随后需要访问这个共享内存块的每一个进程都必须将这个共享内存绑定到自己的地址空间中
当完成通信之后,所有进程都将脱离共享内存,并且由一个进程释放该共享内存块
三.共享内存中的System V
- 使用System V 共享内存和共享文件映射, System V共享内存模型使用的是键和标识符,使用一个共享内存映射IPC要求创建一个磁盘文件,无需对于共享区域进行持续存储也需要这样做
一.System V
- 当我们创建了一块共享内存的时候,其实就是在tmpfs文件系统中也就是/dev/shm下的创建了一个文件,意味着创建了一个iNode 节点
-系列API的使用
#include <sys/shm.h>
#include <sys/types.h> key_t ftok ( const char* fname, int fd);成功返回一个key_t值, 失败返回-1
注意, 当文件路径和子序号都相同时返回的并不一定永远返回一样的key值, 如果该路径指向的文件或者目录被删除而又重新创建, 就算名字还是一样, 但是文件系统会赋予它不同的 iNode 信息, 所以返回的 key 值就不同了
- shmget
#include <sys/shm.h> int shmget (key_t key, size_t size, int shmflag);
用来创建一块共享内存并返回其 id 或者获得一块已经被创建的共享内存的 id
- key_t key 参数用来唯一标识一段全局共享内存, 通常通过 ftok 函-数获得,
- size_t size 参数是创建的共享内存的大小, 单位为字节, 如果是要创建一块共享内存, 此参数必须被指定, 如果是要获取一块创建好的共享内存的 id, 可以将其设置为 0
- int shmgflg 参数为 0 为获取共享内存 id, 为 IPC_CREAT 时是创建一个新的共享内存, 通常要同时指定权限 (和权限进行 | 运算)
同时还能取IPC_EXCL , 只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误。
此外shmgflg 还支持两个额外的标志 - SHM_HUGETLB含义是系统将使用"大页面"来分配空间
- SHM_NORESERVE,不为共享内存保留空间
- shmat
- 我们创建共享内存是在/dev/shm/下打开一个文件,并且把它映射到当前进程之中,然后直接读取映射后的地址内容,在不需要的时候可以使用shmdt 将其分离,让我们先看一下shmat吧
#include <sys/shm.h> void* shmat ( int shm_id, const void* shm_addr, int shmflag ); 成功返回映射到进程的地址空间 失败返回 (void *)-1并将错误存储于 errno
shm_id 参数是 shmget 返回的共享内存 id,
shm_addr 参数是指定共享内存在进程内存地址的映射位置, 推荐使用 NULL, 由内核自己决定
shmflag一般为0
- hmaddr非空:此时还要根据shmflg参数是否指定SHM_RND标志进行判断:
没有指定SHM_RND:共享内存区连接到调用进程的shmaddr指定的地址;
指定SHM_RND:共享内存区连接到shmaddr指定的地址向下舍入SHMLBA的位置。
shmflg:除了上面说的SHM_RND外,还有可以指定SHM_RDONLY标志,限定只读访问。一般该标志置为0。
shmdt用于将一个共享内存区从该进程内断接,当一个进程终止时,它连接的所有共享内存区会自动断接。
- shmdt
#include <sys/shm.h> int shmdt ( const void* shm_addr );
成功返回 0, 失败返回 -1
shm_addr参数表示共享内存在进程中的映射的地址,即shmat返回的值,调用 shmdt 并不会删除共享内存,只是解除映射
- shmctl
管理共享内存
#include int shmctl ( int shm_id, int command, struct shmid_ds* buf );
失败返回-1, 并存储错误于 errno, 成功时的返回值取决于 command shm_id 是共享内存标识符 command
指定要执行的命令
-
常用命令为 IPC_RMID, 即, 删除共享内存
-
如果共享内存已经与所有访问它的进程断开了连接,则调用IPC_RMID子命令后,系统将立即删除共享内存的标识符,并删除该共享内存区,以及所有相关的数据结构;
-
如果仍有别的进程与该共享内存保持连接,则调用IPC_RMID子命令后,该共享内存并不会被立即从系统中删除,而是被设置为IPC_PRIVATE状态,并被标记为"已被删除";直到已有连接全部断开,该共享内存才会最终从系统中消失。
- System V 共享内存区的限制,关于系统范围内对共享内存的限制
-
#include <bits/shm.h> struct shminfo {
unsigned long int shmmax; //一个共享内存区的最大字节数
unsigned long int shmmin; //一个共享内存区的最小字节数
unsigned long int shmmni; //系统范围内的共享内存区对象的最大个数
unsigned long int shmseg; //每个进程连接的最大共享内存区的数目
unsigned long int shmall; //系统范围内的共享内存区的最大页数
unsigned long int __unused1;
unsigned long int __unused2;
unsigned long int __unused3;
unsigned long int __unused4; }; -
在Linux 下shmctl中可以指定IPC_INFO来获取上面结构所示的系统范围内的限制。在Linux下,具体的限制值可以通过sysctl来查看,如下:
sysctl -a | grep shm
来进行显示
write.cpp
#include <iostream>
#include <cstdlib>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
using namespace std;
/*
1: 创建一块共享内存
2:连接到共享内存,并向共享内存中写入数据
3:脱离该共享内存区域
*/
#define SHMSIZE 1024
int main(int argc, char *argv[])
{
/*
if(argc < 2){
cout << "input the pathname fo generate key..."<< endl;
exit(-1);
}*/
key_t key = 0x1111;//ftok(argv[1], 'a');
int shmid = shmget(key, SHMSIZE, 0666|IPC_CREAT);
if(shmid == -1){
cerr << "shmget error ...." << endl;
exit(-1);
}
char * p = (char *)shmat(shmid, NULL, 0);
pid_t pid = getpid();
cout << "process " << pid << " write: ";
while(cin.getline(p, SHMSIZE)){
if(strcmp(p, "over") == 0){
break;
}
cout << "process " << pid << " write: ";
}
//脱离该共享内存区域,并删除该共享内存区域
shmdt(p);
shmctl(shmid, IPC_RMID, NULL);
cout << "已经删除了共享内存,并退出。\n";
exit(0);
}
read.cpp
#include <iostream>
#include <cstdlib>
#include <sys/ipc.h>
#include <string.h>
#include <sys/shm.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
/*
连接到共享内存,并向共享内存中读入数据
脱离该共享内存区域
*/
#define SHMSIZE 1024
int main(int argc, char *argv[])
{
key_t key = 0x1111;
//当共享内存中有数据的时候它才会立刻返回,否则该函数会阻塞在这里
int shmid = shmget(key, SHMSIZE, 0666);
if(shmid == -1){
cerr << "shmget error ...." << endl;
exit(-1);
}
pid_t pid = getpid();
char * p = (char *)shmat(shmid, NULL, 0);
cout << "process " << pid << " read: ";
while(1){
if(strlen(p) != 0)
cout << p << endl;
else
continue;
if(strcmp(p, "over") == 0){
break;
}
cout << "process " << pid << " read: ";
memset(p, 0, SHMSIZE);
}
cout << "进程退出。\n";
exit(0);
}
四.POSIX 共享内存
POSIX共享内存能够让无关进程共享一个映射区域,无需创建一个相应的文件映射,Linux使用挂载于/dev/shm目录下的专用tmpfs文件系统,这个文件系统具有内核持久性,即它所包含的共享内存对象会一直持久,即使当前不存在任何进程打开它,但这些对象会在系统关闭之后丢失。
注意:系统上POSIX共享内存区域占据的内存总量受限于底层的tmpfs文件系统的大小,这个文件系统通常会在启动时使用默认大小(如,256MB)进行挂载,如果有必要的话,root用户能够使用命令mount
-o remount,size=重新挂载这个文件系统来修改它的大小。
POSIX共享内存在一定程度上是与System V的使用很相像的,如果需要使用POSIX共享内存对象,需要完成下列工作:
1.首先使用shm_open()函数打开一个与指定的名字对应的对象,shm_open()与open系统调用是类似的 ,在这里很多人可能会好奇,如果shm_open与open相似的话,那为什么要有shm_open?
shm_open函数是在/dev/shm目录上生成一个文件,而且会校验该目录下是不是挂载了tmpfs文件系统,如果不是也不能正常打开的。所以一般还是用shm_open函数更规范一些,因为这个文件存在tmpfs文件系统下,在不用的情况系统会自动删除掉。
2.将上一部分获得的文件描述符拿出来传入到mmap(),
,并在其flags参数中指定MAP_SHARED
,这会将共享内存对象映射到进程的虚拟地址空间。与mmap()的其他用法一样,一旦映射了对象之后就能够关闭该文件描述符,而不会影响到这个映射。后续过程中还需使用fstat()与ftruncate()
调用来使用这个文件描述符
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>
// returns file descriptor on success, or -1 on error
int shm_open(const char *name, int oflag, mode_t mode);
int shm_unlink(const char *name);
//成功返回0,失败返回-1
创建一个新的共享区域或者附加在已有的共享区域上.区域被其名字标识,函数返回各文件的描述符.
shm_open用于创建一个新的共享内存区对象或打开一个已经存在的共享内存区对象。
name:POSIX IPC的名字,在这里的话必须是"/XXX"
这种形式
oflag:操作标志,包含:O_RDONLY,O_RDWR,O_CREAT,O_EXCL,O_TRUNC。其中O_RDONLY和O_RDWR
标志必须且仅能存在一项。
mode:
用于设置创建的共享内存区对象的权限属性。和open以及其他POSIX IPC的xxx_open函数不同的是,该参数必须一直存在,如果oflag参数中没有O_CREAT标志,该位可以置0;
shm_unlink
用于删除一个共享内存区对象,跟其他文件的unlink以及其他POSIX IPC的删除操作一样,对象的析构会到对该对象的所有引用全部关闭才会发生。
在使用POSIX共享内存中,必须以自己的方式来进行同步,信号量也好,互斥量也好,在这里就不详细列举了
POSIX共享内存和POSIX消息队列,有名信号量一样都是具有随内核持续性的特点。
下面是通过POSIX共享内存进行通信的测试代码,代码中通过POSIX信号量来进行进程间的同步操作。
#include <iostream>
#include <cstring>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/mman.h>
using namespace std;
#define SHM_NAME "/memmap"
#define SHM_NAME_SEM "/memmap_sem"
int main()
{
int fd;
sem_t *sem;
fd = shm_open(SHM_NAME, O_RDWR, 0);
sem = sem_open(SHM_NAME_SEM, 0);
if (fd < 0 || sem == SEM_FAILED)
{
cout << "shm_open or sem_open failed...";
cout << strerror(errno) << endl;
return -1;
}
struct stat fileStat;
fstat(fd, &fileStat);
char *memPtr;
memPtr = (char *)mmap(NULL, fileStat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// close(fd);
sem_wait(sem);
cout << "process:" << getpid() << " recv:" << memPtr << endl;
sem_close(sem);
return 0;
}
#include <iostream>
#include <cstring>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/mman.h>
using namespace std;
#define SHM_NAME "/memmap"
#define SHM_NAME_SEM "/memmap_sem"
char sharedMem[10];
int main()
{
int fd;
sem_t *sem;
fd = shm_open(SHM_NAME, O_RDWR | O_CREAT, 0666);
sem = sem_open(SHM_NAME_SEM, O_CREAT, 0666, 0);
if (fd < 0 || sem == SEM_FAILED)
{
cout << "shm_open or sem_open failed...";
cout << strerror(errno) << endl;
return -1;
}
ftruncate(fd, sizeof(sharedMem));
char *memPtr;
memPtr = (char *)mmap(NULL, sizeof(sharedMem), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// close(fd);
char msg[] = "yuki...";
memmove(memPtr, msg, sizeof(msg));
cout << "process:" << getpid() << " send:" << memPtr << endl;
sem_post(sem);
sem_close(sem);
return 0;
}