一、共享内存基本概念
什么是共享内存?顾名思义,共享内存就是将内存进行共享,它允许多个不相关的进程访问同一个逻辑内存, 直接将一块裸露的内存放在需要数据传输的进程面前,让它们自己使用。因此,共享内存是效率最高的一种IPC通信机制, 它可以在多个进程之间共享和传递数据,进程间需要共享的数据被放在共享内存区域, 所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中,因此所有进程都可以访问共享内存中的地址, 就好像它们是由用C语言函数malloc分配的内存一样。
但是,这种共享的内存需要进程自己去维护好,如同步、互斥等工作,比如当进程1在读取共享内存的数据时, 进程2却修改了共享内存中的数据,那么必然会造成数据的混乱,进程1读取到的数据就是错误的,因此,共享内存是属于临界资源,在某一时刻最多只能有一个进程对其操作(读/写数据),共享内存一般不能单独使用,而要配合信号量、互斥锁等协调机制,让各个进程在高效交换数据的同时,不会发生数据践踏、破坏等意外。
共享内存的思想非常简单,进程与进程之间虚拟内存空间本来相互独立,不能互相访问的,但是可以通过某些方式, 使得相同的一块物理内存多次映射到不同的进程虚拟空间之中,这样的效果就相当于多个进程的虚拟内存空间部分重叠在一起, 如下图所示:
当进程1向共享内存写入数据后,共享内存的数据就变化了,那么进程2就能立即读取到变化了的数据, 而这中间并未经过内核的拷贝,因此效率极高。
共享内存有以下特点:
共享内存是进程间通信中效率最高的方式之一。
共享内存是系统出于多个进程之间通讯的考虑,而预留的的一块内存区,因此共享内存是以传输数据为目的的。
共享内存允许两个或更多进程访问同一块内存,当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。
共享内存无同步无互斥,需要配合信号量、互斥锁等协调机制。
二、shmget() - 创建共享内存函数
内核提供了shmget()函数的创建或获取一个共享内存对象,并返回共享内存标识符。函数原型如下:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数说明:
key:标识共享内存的键值,可以有以下取值:
0 或 IPC_PRIVATE。当key的取值为IPC_PRIVATE,则函数shmget()创建一块新的共享内存; 如果key的取值为0,而参数shmflg中设置了IPC_PRIVATE这个标志,则同样将创建一块新的共享内存。
大于0的32位整数:视参数shmflg来确定操作,通过ftok()函数可以获得一个独一无二的key值,key值用来让多个进程来访问同一块共享内存,如果想要多个进程访问同一块共享内存,则各进程中的key要相等。
#include <sys/types.h> #include <sys/ipc.h> key_t ftok(const char* pathname, int proj_id); pathname为路径,proj_id为id值,ftok函数会根据路径和id 成功时返回独一无二的key_t值,失败返回-1
- size:要创建共享内存的大小,所有的内存分配操作都是以页为单位的,所以即使只申请只有一个字节的内存, 内存也会分配整整一页。
shmflg:表示创建的共享内存的模式标志参数,在真正使用时需要与IPC对象存取权限mode(如0600)进行“|”运算来确定共享内存的存取权限。 msgflg有多种情况:
IPC_CREAT:如果内核中不存在关键字与key相等的共享内存,则新建一个共享内存;如果存在这样的共享内存,返回此共享内存的标识符。
IPC_EXCL:如果内核中不存在键值与key相等的共享内存,则新建一个共享内存;如果存在这样的共享内存则报错。
SHM_HUGETLB:使用“大页面”来分配共享内存,所谓的“大页面”指的是内核为了提高程序性能,对内存实行分页管理时,采用比默认尺寸(4KB)更大的分页,以减少缺页中断。Linux内核支持以2MB作为物理页面分页的基本单位。
SHM_NORESERVE:不在交换分区中为这块共享内存保留空间。
返回值:shmget()函数的返回值是共享内存的ID。
当调用shmget()函数失败时将产生错误代码,有如下取值:
EACCES:key指定的共享内存已存在,但调用进程没有权限访问它
EEXIST:key指定的共享内存已存在,而msgflg中同时指定IPC_CREAT和IPC_EXCL标志
EINVAL:创建共享内存时参数size小于SHMMIN或大于SHMMAX。
ENFILE:已达到系统范围内打开文件总数的限制。
ENOENT:给定的key不存在任何共享内存,并且未指定IPC_CREAT。
ENOMEM:内存不足,无法为共享内存分配内存。
三、shmat() - 内存映射函数
如果一个进程想要访问一个共享内存,那么需要将其映射到进程的虚拟空间中, 然后再去访问它,那么系统提供的shmat()函数就是把共享内存区对象映射到调用进程的地址空间。函数原型如下:
#include <sys/type.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明:
shmid:共享内存ID,通常是由shmget()函数返回的。
shmaddr:如果不为NULL,则系统会根据shmaddr来选择一个合适的内存区域, 如果为NULL,则系统会自动选择一个合适的虚拟内存空间地址去映射共享内存。
shmflg:操作共享内存的方式:
SHM_RDONLY:以只读方式映射共享内存。
SHM_REMAP:重新映射,此时shmaddr不能为NULL。
NULLSHM:自动选择比shmaddr小的最大页对齐地址。
shmat()函数调用成功后返回共享内存的起始地址,这样子我们就能操作这个共享内存了。
共享内存的映射有以下注意的要点:
共享内存只能以只读或者可读写方式映射,无法以只写方式映射。
shmat()第二个参数shmaddr一般都设为NULL,让系统自动找寻合适的地址。但当其确实不为空时, 那么要求SHM_RND在shmflg必须被设置,这样的话系统将会选择比shmaddr小而又最大的页对齐地址(即为SHMLBA的整数倍)作为共享内存区域的起始地址。 如果没有设置SHM_RND,那么shmaddr必须是严格的页对齐地址。
四、shmdt() - 解除映射函数
shmdt()函数与shmat()函数相反,是用来解除进程与共享内存之间的映射的,在解除映射后, 该进程不能再访问这个共享内存。函数原型如下:
#include <sys/type.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数说明:
shmaddr:映射的共享内存的起始地址。
shmdt()函数调用成功返回0,如果出错则返回-1,并且将错误原因存于errno中。
虽然shmdt()函数很简单,但是还是有注意要点的:该函数并不删除所指定的共享内存区, 而只是将先前用shmat()函数映射好的共享内存脱离当前进程,共享内存还是存在于物理内存中。
五、shmctl() -获取或设置属性函数
内核提供了shmctl()用于获取或者设置共享内存的相关属性。函数原型如下:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数说明:
shmid:共享内存标识符。
cmd:函数功能的控制命令,其取值如下:
IPC_STAT:获取属性信息,放置到buf中。
IPC_SET:设置属性信息为buf指向的内容。
IPC_RMID:删除该共享内存。
IPC_INFO:获得关于共享内存的系统限制值信息。
SHM_INFO:获得系统为共享内存消耗的资源信息。
SHM_STAT:与IPC_STAT具有相同的功能,但shmid为该SHM在内核中记录所有SHM信息的数组的下标, 因此通过迭代所有的下标可以获得系统中所有SHM的相关信息。
SHM_LOCK:禁止系统将该SHM交换至swap分区。
SHM_UNLOCK:允许系统将该SHM交换至swap分。
buf:共享内存属性信息结构体指针,设置或者获取信息都通过该结构体,shmid_ds结构如下:
注意:选项SHM_LOCK不是锁定读写权限,而是锁定SHM能否与swap分区发生交换。 一个SHM被交换至swap分区后如果被设置了SHM_LOCK,那么任何访问这个SHM的进程都将会遇到页错误。 进程可以通过IPC_STAT后得到的mode来检测SHM_LOCKED信息。
共享内存写程序:
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>#define PATHNAME "."
#define PROJ_ID 0x666
#define BUFSIZE 4096int main()
{void *shm = NULL;char buffer[BUFSIZE + 1];//用于保存输入的文本int shmid;//获取key值key_t key = ftok(PATHNAME, PROJ_ID);printf("key value: %d\n", key);//创建共享内存shmid = shmget(key, 4096, 0644 | IPC_CREAT);if(shmid == -1){fprintf(stderr, "shmget failed\n");exit(EXIT_FAILURE);}//将共享内存连接到当前进程的地址空间shm = shmat(shmid, (void*)0, 0);if(shm == (void*)-1){fprintf(stderr, "shmat failed\n");exit(EXIT_FAILURE);}printf("Memory attached at %p\n", shm);printf("Enter some text: ");while(1)//向共享内存中写数据{//向共享内存中写入数据ssize_t s = read(0, buffer, BUFSIZE - 1);if(s > 0){buffer[s - 1] = 0;strncpy(shm, buffer, 4096);//输入了end,退出循环(程序)if(strncmp(buffer, "end", 3) == 0)break;} }//把共享内存从当前进程的地址空间分离if(shmdt(shm) == -1){fprintf(stderr, "shmdt failed\n");exit(EXIT_FAILURE);}sleep(1);exit(EXIT_SUCCESS);
}
共享内存读程序:
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>#define PATHNAME "."
#define PROJ_ID 0x666int main(void)
{char *shm = NULL;//分配的共享内存的原始首地址int shmid;//共享内存标识符//获取key值key_t key = ftok(PATHNAME, PROJ_ID);printf("key value: %d\n", key);//创建共享内存shmid = shmget(key, 4096, IPC_CREAT|IPC_EXCL|0666);if(shmid == -1){fprintf(stderr, "shmget failed\n");exit(EXIT_FAILURE);}//将共享内存连接到当前进程的地址空间shm = shmat(shmid, 0, 0);if(shm == (void*)-1){fprintf(stderr, "shmat failed\n");exit(EXIT_FAILURE);}printf("\nMemory attached at %p\n", shm);while(1)//读取共享内存中的数据{printf("You wrote: %s", (char*)shm);printf("\n");sleep(1);//输入了end,退出循环(程序)if(strncmp(shm, "end", 3) == 0)break;}//把共享内存从当前进程的地址空间中分离if(shmdt(shm) == -1){fprintf(stderr, "shmdt failed\n");exit(EXIT_FAILURE);}//删除共享内存if(shmctl(shmid, IPC_RMID, 0) == -1){fprintf(stderr, "shmctl(IPC_RMID) failed\n");exit(EXIT_FAILURE);}exit(EXIT_SUCCESS);
}
注意:上述代码为了简化并没有加信号量、互斥锁来控制各进程访问共享内存 ,实际项目中需要添加信号量、互斥锁等协调机制。
若发送进程不是通过end字符退出(如Ctrl+C或Ctrl+D),则不会触发读进程主动删除共享内存,在这种情况下可通过 "ipcs -m" 命令查看到该共享内存依然存在,通过 "ipcrm -m [shmid]" 即可删除。