1.概述
共享内存是进程通信的一种方式。此方式会开辟一部分可以被多个进程共享访问的物理内存区域。进行通信的多个进程分别将该内存区域映射到自己的虚拟地址空间,则这些进程就可直接访问共享内存,从而达到通信的目的。这种方法进程间共享数据时最快,一个进程修改共享内存的数据后,其他进程可以立即看到。
2 通信步骤
头文件:#include<sys/ipc.h> #include<sys/shm.h>
<1>创建共享内存
int shmget(key_t key, int size,int shmflg);
shmget()用来取得参数key所关联的共享内存标识号。若key为IPC_PRIVATE则会创建新的共享内存,大小有size决定。若key不为IPC_PRIVATE,也不是已建立的共享内存,则会根据shmflg决定。
参数:key 0:IPC_PRIVATE
size 内存大小
shmflg 旗标位
返回值:成功返回共享内存识别shmid,失败返回-1,错误存储在errno中
<2>连接到共享内存
void * shmat(int shmid,const void *shmaddr,int shmflg);
shmat()用来将参数shmid所指的共享内存和目前的进程连接。
参数:shmid 与连接的共享内存标识码
shmaddr 0 内核自动选择一个地址
shmaddr 不为0且shmflg未指定SHM_RND,以shmaddr为连接地址
shmaddr 不为0但shmflg指定SHM_RND,shmaddr会调整为SHMLBA整数倍
返回值:成功返回已连接地址,失败返回-1,错误存储在errno中
<3>断开连接
int shmdt(const void *shmaddr);
shmdt()使当前进程与共享内存断开连接
参数:shmaddr为先前shmat()返回的共享内存地址
返回值 成功返回0,失败返回-1,错误存储在errno中
<4>对共享内存进行操作
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
shmctl()提供了几种控制共享内存的方式
参数:shmid 为共享内存的标识码
cmd 如下
IPC_STAT 得到共享内存的状态,
IPC_SET 改变共享内存的状态
IPC_RMID 删除共享内存
struct shmid_ds *buf是一个结构体指针。IPC_STAT的时候,取得的状态放在这个结构体中。如果要改变共享内存的状态,用这个结构体指定。
返回值 成功返回0,失败返回-1,错误存储在errno中
举例
创建一个共享内存,子进程写入HelloWorld,由父进程读出
- #include<sys/types.h>
- #include<sys/ipc.h>
- #include<sys/shm.h>
- #include<stdio.h>
- #include<sys/stat.h>
- #include<stdlib.h>
- #define KEY 1234
- #define SIZE 2048
- int main()
- {
- pid_t pid;
- int shmid; /*共享内存ID*/
- char * shmaddr; /*连接地址*/
- struct shmid_ds buf;
- shmid=shmget(KEY,SIZE,IPC_CREAT|0600);/*建立共享内存*/
- pid=fork();
- if(pid<0)
- {
- perror("fork error!\n");
- exit(0);
- }
- if(pid==0)/*子进程将"Hello world!\n写入共享内存"*/
- {
- shmaddr=shmat(shmid,NULL,0);/*子进程与共享内存连接*/
- sprintf(shmaddr,"Hello world!\n");/*写入共享内存*/
- printf("Messgae have been write to shared memory!\n");
- shmdt(shmaddr);/*断开子进程与共享内存的连接*/
- exit(0);
- }
- sleep(3);/*待子进程结束*/
- shmctl(shmid,IPC_STAT,&buf);/*共享内存状态存入buf中*/
- shmaddr=(char *)shmat(shmid,NULL,0);/*父进程与共享内存连接*/
- printf("Message in shared memory:%s\n",shmaddr);/*输出共享内存数据*/
- shmdt(shmaddr);/*断开父进程与共享内存的连接*/
- shmctl(shmid,IPC_RMID,NULL);/*删除共享内存*/
- }
输出
Messgae have been write to shared memory!
Message in shared memory:Hello world!
另外也可重新创建一个进程,读取该共享内存的数据。将上个c文件中最后一行删除,使得程序结束时不删除共享内存,再执行下面程序,使新进程读取共享内存数据。(共享内存需一致,创建时KEY相同)
- #include<sys/types.h>
- #include<sys/ipc.h>
- #include<sys/shm.h>
- #include<stdio.h>
- #include<sys/stat.h>
- #include<stdlib.h>
- #define KEY 1234
- #define SIZE 2048
- int main()
- {
- pid_t pid;
- int shmid; /*共享内存ID*/
- char * shmaddr; /*连接地址*/
- struct shmid_ds buf;
- shmid=shmget(KEY,SIZE,IPC_CREAT|0600);/*建立共享内存*/
- shmctl(shmid,IPC_STAT,&buf);/*共享内存状态存入buf中*/
- shmaddr=(char *)shmat(shmid,NULL,0);/*进程与共享内存连接*/
- printf("Message in shared memory:%s\n",shmaddr);/*输出共享内存数据*/
- shmdt(shmaddr);/*断开进程与共享内存的连接*/
- shmctl(shmid,IPC_RMID,NULL);/*删除共享内存*/
- }
PS:更详细的解释如下:
共享内存(Shared Memory)
共享内存区域是被多个进程共享的一部分物理内存。如果多个进程都把该内存区域映射到自己的虚拟地址空间,则这些进程就都可以直接访问该共享内存区域,从而 可以通过该区域进行通信。共享内存是进程间共享数据的一种最快的方法,一个进程向共享内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中 的内容。这块共享虚拟内存的页面,出现在每一个共享该页面的进程的页表中。但是它不需要在所有进程的虚拟内存中都有相同的虚拟地址。
图 共享内存映射图
和所有的 System V IPC对象一样,对于共享内存对象的访问由key控制,并要进行访问权限检查。内存共享之后,对进程如何使用这块内存就不再做检查。它们必须依赖于其它机 制,比如System V的信号量(semaphone)来同步对于共享内存区域的访问。
每一个新创建的共享内存对象都用一个shmid_kernel数据结构来表达。系统中所有的shmid_kernel数据结构都保存在shm_segs向量表中,该向量表的每一个元素都是一个指向shmid_kernel数据结构的指针。shm_segs向量表的定义如下:
struct shmid_kernel *shm_segs[SHMMNI];
SHMMNI为128,表示系统中最多可以有128个共享内存对象。
数据结构shmid_kernel的定义如下:
struct shmid_kernel
{
struct shmid_ds u; /* the following are private */
unsigned long shm_npages; /* size of segment (pages) */
unsigned long *shm_pages; /* array of ptrs to frames -> SHMMAX */
struct vm_area_struct *attaches; /* descriptors for attaches */
};
其中:
shm_pages是该共享内存对象的页表,每个共享内存对象一个,它描述了如何把该共享内存区域映射到进程的地址空间的信息。
shm_npages是该共享内存区域的大小,以页为单位。
shmid_ds是一个数据结构,它描述了这个共享内存区的认证信息,字节大小,最后一次粘附时间、分离时间、改变时间,创建该共享区域的进程,最后一次对它操作的进程,当前有多少个进程在使用它等信息。其定义如下:
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
attaches描述被共享的物理内存对象所映射的各进程的虚拟内存区域。每一个希望共享这块内存的进程都必须通过系统调用将其粘附(attach)到它的虚拟内存中。这一过程将为该进程创建了一个新的描述这块共享内存的vm_area_struct数据结构。进程可以选择共享内存在它的虚拟地址空间的位置,也可以让Linux为它选择一块足够的空闲区域。
这个新的vm_area_struct结构除了要连接到进程的内存结构mm中以外,还被连接到共享内存数据结构shmid_kernel的一个链表中,该结构中的attaches指针指向该链表。vm_area_struct数据结构中专门提供了两个指针:vm_next_shared和 vm_prev_shared,用于连接该共享区域在使用它的各进程中所对应的vm_area_struct数据结构。其实,虚拟内存并没有在粘附的时候创建,而要等到第一个进程试图访问它的时候才创建。
图 System V IPC 机制 - 共享内存
Linux为共享内存提供了四种操作。
1. 共享内存对象的创建或获得。
与其它两种IPC机制一样,进程在使用共享内存区域以前,必须通过系统调用sys_ipc (call值为SHMGET)创建一个键值为key的共享内存对象,或获得已经存在的键值为key的某共享内存对象的引用标识符。以后对共享内存对象的访问都通过该引用标识符进行。对共享内存对象的创建或获得由函数sys_shmget完成,其定义如下:
int sys_shmget (key_t key, int size, int shmflg)
这里key是表示该共享内存对象的键值,size是该共享内存区域的大小(以字节为单位),shmflg是标志(对该共享内存对象的特殊要求)。
它所做的工作如下:
1) 如果key == IPC_PRIVATE,则创建一个新的共享内存对象。
* 算出size对应的页数,检查其合法性。
* 搜索向量表shm_segs,为新创建的共享内存对象找一个空位置。
* 申请一块内存用于建立shmid_kernel数据结构,注意这里申请的内存区域大小不包括真正的共享内存区,实际上,要等到第一个进程试图访问它的时候才真正创建共享内存区。
* 根据该共享内存区所占用的页数,为其申请一块空间用于建立页表(每页4个字节),将页表清0。
* 填写shmid_kernel数据结构,将其加入到向量表shm_segs中为其找到的空位置。
* 返回该共享内存对象的引用标识符。
2) 在向量表shm_segs中查找键值为key的共享内存对象,结果有三:
* 如果没有找到,而且在操作标志shmflg中没有指明要创建新共享内存,则错误返回,否则创建一个新的共享内存对象。
* 如果找到了,但该次操作要求必须创建一个键值为key的新对象,那么错误返回。
* 否则,合法性、认证检查,如有错,则错误返回;否则,返回该内存对象的引用标识符。
共享内存对象的创建者可以控制对于这块内存的访问权限和它的key是公开还是私有。如果有足够的权限,它也可以把共享内存锁定在物理内存中。
参见include/linux/shm.h
2. 粘附。
在创建或获得某个共享内存区域的引用标识符后,还必须将共享内存区域映射(粘附)到进程的虚拟地址空间,然后才能使用该共享内存区域。系统调用 sys_ipc(call值为SHMAT)用于共享内存区到进程虚拟地址空间的映射,而真正完成粘附动作的是函数sys_shmat,其定义如下:
int sys_shmat (int shmid, char *shmaddr, int shmflg, ulong *raddr)
其中:shmid是共享内存对象的引用标识符;
shmaddr该共享内存区域在进程的虚拟地址空间对应的虚拟地址;
shmflg是映射标志;
raddr是实际映射的虚拟空间地址。
该函数所做的工作如下:
1) 根据shmid找到共享内存对象。
2) 如果shmaddr为0,即用户没有指定该共享内存区域在它的虚拟空间中的位置,则由系统在进程的虚拟地址空间中为其找一块区域(从1G开始);否则,就用shmaddr作为映射的虚拟地址。
3) 检查虚拟地址的合法性(不能超过进程的最大虚拟空间大小(3G),不能太接近堆栈栈顶)。
4) 认证检查。
5) 申请一块内存用于建立数据结构vm_area_struct,填写该结构。
6) 检查该内存区域,将其加入到进程的mm结构和该共享内存对象的vm_area_struct队列中。
共享内存的粘附只是创建一个vm_area_struct数据结构,并将其加入到相应的队列中,此时并没有创建真正的共享内存页。
当进程第一次访问共享虚拟内存的某页时,因为所有的共享内存页还都没有分配,所以会发生一个page fault异常。当Linux处理这个page fault的时候,它找到发生异常的虚拟地址所在的vm_area_struct数据结构。在该数据结构中包含有这类共享虚拟内存的一组处理例程,其中的 nopage操作用来处理虚拟页对应的物理页不存在的情况。对共享内存,该操作是shm_nopage(定义在ipc/shm.c中)。该操作在描述这个共享内存的shmid_kernel数据结构的页表shm_pages中查找发生page fault异常的虚拟地址所对应的页表条目,看共享页是否存在(页表条目为0,表示共享页是第一次使用)。如果不存在,它就分配一个物理页,并为它创建一 个页表条目。这个条目不但进入当前进程的页表,同时也存到shmid_kernel数据结构的页表shm_pages中。
当下一个进程试图访问这块内存并得到一个page fault的时候,经过同样的路径,也会走到函数shm_nopage。此时,该函数查看shmid_kernel数据结构的页表shm_pages时, 发现共享页已经存在,它只需把这里的页表项填到进程页表的相应位置即可,而不需要重新创建物理页。所以,是第一个访问共享内存页的进程使得这一页被创建, 而随后访问它的其它进程仅把此页加到它们的虚拟地址空间。
3. 分离。
当进程不再需要共享虚拟内存的时候,它们与之分离(detach)。只要仍旧有其它进程在使用这块内存,这种分离就只会影响当前的进程,而不会影响 其它进程。当前进程的vm_area_struct数据结构被从shmid_ds中删除,并被释放。当前进程的页表也被更新,共享内存对应的虚拟内存页被 标记为无效。当共享这块内存的最后一个进程与之分离时,共享内存页被释放,同时,这块共享内存的shmid_kernel数据结构也被释放。
系统调用sys_ipc(call值为SHMDT)用于共享内存区与进程虚拟地址空间的分离,而真正完成分离动作的是函数sys_shmdt,其定义如 下:
int sys_shmdt (char *shmaddr)
其中shmaddr是进程要分离的共享页的开始虚拟地址。
该函数搜索进程的内存结构中的所有vm_area_struct数据结构,找到地址shmaddr对应的一个,调用函数do_munmap将其释放。
在函数do_munmap中,将要释放的vm_area_struct数据结构从进程的虚拟内存中摘下,清除它在进程页表中对应的页表项(可能占多个页表 项),调用该共享内存数据结构vm_area_struct的操作例程中的close操作(此处为shm_close)做进一步的处理。
在函数shm_close(定义在ipc/shm.c中)中,找到该共享内存对象在向量表shm_segs中的索引,从而找到该共享内存对象,将该共享内 存在当前进程中对应的vm_area_struct数据结构从对象的共享内存区域链表(由vm_next_share和vm_pprev_share指针 连接)中摘下。如果目前与该共享内存对象粘附的进程数变成了0,则释放共享内存页,释放共享内存页表,释放该对象的shmid_kernel数据结构,将 向量表shm_segs中该共享内存对象所占用的项改为IPC_UNUSED。
4. 控制。
Linux在共享内存上实现的第四种操作是共享内存的控制(call值为SHMCTL的sys_ipc调用),它由函数sys_shmctl实现。 控制操作包括获得共享内存对象的状态,设置共享内存对象的参数(如uid、gid、mode、ctime等),将共享内存对象在内存中锁定和释放(在对象 的mode上增加或去除SHM_LOCKED标志),释放共享内存对象资源等。
共享内存提供了一种快速灵活的机制,它允许进程之间直接共享大量的数据,而无须使用拷贝或系统调用。共享内存的主要局限性是它不能提供同步,如果两个进程 企图修改相同的共享内存区域,由于内核不能串行化这些动作,因此写的数据可能任意地互相混合。所以使用共享内存的进程必须设计它们自己的同步协议,如用信 号灯等。
以下是使用共享内存机制进行进程间通信的基本操作:
需要包含的头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
1.创建共享内存:
int shmget(key_t key,int size,int shmflg);
参数说明:
key:用来表示新建或者已经存在的共享内存去的关键字。
size:创建共享内存的大小。
shmflg:可以指定的特殊标志。IPC_CREATE,IPC_EXCL以及低九位的权限。
eg:
int shmid;
shmid=shmget(IPC_PRIVATE, 4096, IPC_CREATE|IPC_EXCL|0660);
if(shmid==-1)
perror("shmget()");
2.连接共享内存
char *shmat(int shmid,char *shmaddr,int shmflg);
参数说明:
shmid:共享内存的关键字
shmaddr:指定共享内存出现在进程内存地址的什么位置,通常我们让内核自己决定一个合适的地址位置,用的时候设为0。
shmflg:制定特殊的标志位。
eg:
int shmid;
char *shmp;
shmp=shmat(shmid,0,0);
if(shmp==(char *)(-1))
perror("shmat()\n");
3.使用共享内存
在使用共享内存是需要注意的是,为防止内存访问冲突,我们一般与信号量(semaphone)结合使用。
4.分离共享内存:当程序不再需要共享内后,我们需要将共享内存分离以便对其进行释放,分离共享内存的函数原形如下:
int shmdt(char *shmaddr);
5.释放共享内存
int shmctl(int shmid,int cmd,struct shmid_ds *buf);