IPC之内存共享
定义
内存共享所指的是多个进程可以把同一段内存空间映射到自己的内存空间,以此来实现数据的共享与传输.它是所有IPC方式中最快的一种,因为它不需要中间环节,而是直接把一个内存段映射到调用进程的地址空间.内存共享是存在于内核级别的一种资源,在shell中使用命令ipcs可以用来查看当前系统IPC中的状态(包括Shared memory segments、Message queues、Semaphore arrays),在文件系统中/proc有对其进行描述的相应文件.
在系统内核为一个进程分配内存地址时,通过分页机制可以让一个进程的物理地址不连续,同时也可以让一段内存分配给不同的进程.所有的共享内存段的大小,都是Linux内存页大小的整数倍.Linux的页大小是4KB,不过程序员应该使用getpagesize函数来获得这个值.内存共享机制就是通过该原理来实现的.内存共享机制只提供数据的传输,至于如何处理数据读写的互斥和同步,则需要代码编写者自己来处理.比如,可以用记录锁来处理读写互斥的问题,用信号量来实现同步机制.象所有的System V IPC 对象一样,Linux 对共享内存的存取是通过对访问键和访问权限的检查来控制的.
详解
内核为每一个共享内存段(存在于它的地址空间)维护着一个特殊的数据结构shmid_ds,这个结构在/usr/include/linux/shm.h中定义如下:
struct shmid_ds { struct ipc_perm shm_perm; /* operation perms */ int shm_segsz; /* size of segment (bytes) */ time_t shm_atime; /* last attach time */ time_t shm_dtime; /* last detach time */ time_t shm_ctime; /* last change time */ unsigned short shm_cpid; /* pid of creator */ unsigned short shm_lpid; /* pid of last operator */ short shm_nattch; /* no. of current attaches */ unsigned short shm_npages; /* size of segment (pages) */
unsigned long *shm_pages; /* array of ptrs to frames -> SHMMAX */
struct vm_area_struct *attaches; /* descriptors for attaches */
};
shmid_ds结构初始化如下表所示:
shmid_ds结构数据 | 初值 |
shm_lpid | 0 |
shm_nattach | 0 |
shm_atime | 0 |
shm_dtime | 0 |
shm_ctime | 系统当前值 |
shm_segsz | 参数size |
数据结构ipc_perm的定义如下:
/*Obsolete, used only for backwards compatibility and libc5 compiles*/ struct ipc_perm { __kernel_key_t key; __kernel_uid_t uid; __kernel_gid_t gid; __kernel_uid_t cuid; __kernel_gid_t cgid; __kernel_mode_t mode; unsigned short seq; };
使用共享内存的函数时需要包含相应的头文件:#include <sys/shm.h>,#include <sys/types.h>
内存共享使用的系统函数及其说明如下:
(1) shmget:得到一个共享内存标识符或创建一个共享内存对象并返回共享内存标识符,函数原型如下:
int shmget(key_t key, size_t size, int shmflg);
参数说明:
key:0(IPC_PRIVATE):会建立新共享内存对象
size:有两种情况:大于0的整数:新建的共享内存大小,以字节为单位;0,只获取共享内存时指定为0;
shmflg:0:取共享内存标识符,若不存在则函数会报错;
IPC_CREAT:当shmflg&IPC_CREAT为真时,如果内核中不存在键值与key相等的共享内存,则新建一个共享内存;
如果存在这样的共享内存,返回此共享内存的标识符;
IPC_CREAT|IPC_EXCL:如果内核中不存在键值与key相等的共享内存,则新建一个共享内存;
如果存在这样的共享内存则报错;(此处容易出现权限问题,flag可以设置为:IPC_CREAT | IPC_EXCL | 0666以防止权限问题).
返回值:成功:返回共享内存的标识符;出错:-1,错误原因存于error中.
错误代码:EINVAL:参数size小于SHMMIN或大于SHMMAX
EEXIST:预建立key所指的共享内存,但已经存在
EIDRM:参数key所指的共享内存已经删除
ENOSPC:超过了系统允许建立的共享内存的最大值(SHMALL)
ENOENT:参数key所指的共享内存不存在,而参数shmflg未设IPC_CREAT位
EACCES:没有权限
ENOMEM:核心内存不足
在Linux环境中,对开始申请的共享内存空间进行了初始化,初始值为0x00.如果用shmget创建了一个新的消息队列对象时,则shmid_ds结构成员变量的值设置如下:shm_lpid、shm_nattach、shm_atime、shm_dtime设置为0.msg_ctime设置为当前时间.shm_segsz设成创建共享内存的大小.shmflg的读写权限放在shm_perm.mode中.shm_perm结构的uid和cuid成员被设置成当前进程的有效用户ID,gid和cuid成员被设置成当前进程的有效组ID.
(2)shmctl:共享内存管理,函数原型如下:
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
参数说明:
msqid:共享内存标识符
cmd:有以下几种情况:
(a)IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中
(b)IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内
(c)IPC_RMID:删除这片共享内存
buf:共享内存管理结构体.具体说明参见共享内存内核结构定义部分
函数返回值:成功:0;出错:-1,错误原因存于error中;
错误代码:EACCESS:参数cmd为IPC_STAT,确无权限读取该共享内存
EFAULT:参数buf指向无效的内存地址
EIDRM:标识符为msqid的共享内存已被删除
EINVAL:无效的参数cmd或shmid
EPERM:参数cmd为IPC_SET或IPC_RMID,却无足够的权限执行
(3)shmat:把共享内存区对象映射到调用进程的地址空间,函数原型为:
void *shmat(int shmid, const void *shmaddr, int shmflg)
参数说明:
msqid:共享内存标识符
shmaddr:指定共享内存出现在进程内存地址的什么位置,直接指定为NULL让内核自己决定一个合适的地址位置
shmflg:SHM_RDONLY:为只读模式,其他为读写模式
函数返回值:成功:附加好的共享内存地址;出错:-1,错误原因存于error中
附加说明:fork后子进程继承已连接的共享内存地址.exec后该子进程与已连接的共享内存地址自动脱离(detach).进程结束后,已连接的共享内存地址会自动脱离(detach)
错误代码:EACCES:无权限以指定方式连接共享内存
EINVAL:无效的参数shmid或shmaddr
ENOMEM:核心内存不足
(4)shmdt:断开共享内存连接,其函数原型如下:
int shmdt(const void *shmaddr)
参数说明:
shmaddr:连接的共享内存的起始地址
函数返回值:成功:0;出错:-1,错误原因存于error中;
附加说明:本函数调用并不删除所指定的共享内存区,而只是将先前用shmat函数连接(attach)好的共享内存脱离(detach)目前的进程.
错误代码:EINVAL:无效的参数shmaddr
共享内存的处理过程
某个进程第一次访问共享虚拟内存时将产生缺页异常.这时,Linux找出描述该内存的vm_area_struct结构,该结构中包含用来处理这种共享虚拟内存段的处理函数地址.共享内存缺页异常处理代码对shmid_ds的页表项表进行搜索,以便查看是否存在该共享虚拟内存的页表项.如果没有,系统将分配一个物理页并建立页表项,该页表项加入shmid_ds结构的同时也添加到进程的页表中.这就意味着当下一个进程试图访问这页内存时出现缺页异常,共享内存的缺页异常处理代码则把新创建的物理页给这个进程.因此说,第一个进程对共享内存的存取引起创建新的物理页面,而其它进程对共享内存的存取引起把那个页加添加到它们的地址空间.
当某个进程不再共享其虚拟内存时,利用系统调用将共享段从自己的虚拟地址区域中移去,并更新进程页表.当最后一个进程释放了共享段之后,系统将释放给共享段所分配的物理页.
当共享的虚拟内存没有被锁定到物理内存时,共享内存也可能会被交换到交换区中.
共享内存使用注意事项
共享内存相比其他几种方式有着更方便的数据控制能力,数据在读写过程中会更透明.当成功导入一块共享内存后,它只是相当于一个字符串指针来指向一块内存,在当前进程下用户可以随意的访问.缺点是,数据写入进程或数据读出进程中,需要附加的数据结构控制.
范例:
/***** testwrite.c *******/ #include <cerrno> #include <sys/ipc.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/shm.h> #include <sys/types.h> #include <unistd.h> typedef struct{ char name[4]; int age; } people; int main(int argc, char** argv) { int shm_id,i; key_t key; char temp; people *p_map; char* name = "/home/xxx/cpp/ipc_memsh/main.cpp"; key = ftok(name,0); if(key==-1) perror("ftok error"); shm_id=shmget(key,4096,IPC_CREAT | IPC_EXCL | 0666); if(shm_id==-1) { perror("shmget error"); return 0; } if((p_map=(people*)shmat(shm_id,NULL,0)) == (void *) -1) { printf("map the shm error: %s.\n", strerror(errno)); return 0; } // (void *) - 1 == 0xFFFFFFFF temp='a'; for(i = 0;i<10;i++) { temp+=1; memcpy((*(p_map+i)).name,&temp,1); (*(p_map+i)).age=20+i; } if(shmdt(p_map)==-1) perror(" detach error "); return 0; }
/********** testread.c ************/ #include <sys/ipc.h> #include <sys/shm.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> typedef struct{ char name[4]; int age; } people; int main(int argc, char** argv) { int shm_id,i; key_t key; people *p_map; char* name = "/home/xxx/cpp/ipc_memsh/main.cpp"; key = ftok(name,0); if(key == -1) perror("ftok error"); shm_id = shmget(key,4096,IPC_CREAT); if(shm_id == -1) { perror("shmget error"); return 0; } p_map = (people*)shmat(shm_id,NULL,0); for(i = 0;i<10;i++) { printf( "name:%s\n",(*(p_map+i)).name ); printf( "age %d\n",(*(p_map+i)).age ); } if(shmdt(p_map) == -1) perror(" detach error "); if(shmctl(shm_id, IPC_RMID, NULL) < 0) perror(" destroy error "); return 0; }
程序说明:
testwrite.c创建一个系统V共享内存区,并在其中写入格式化数据;testread.c访问同一个系统V共享内存区,读出其中的格式化数据.分别把两个程序编译为testwrite及testread,先后执行./testwrite及./testread 则./testread输出结果如下:
name: b age 20; name: c age 21; name: d age 22; name: e age 23; name: f age 24; name: g age 25; name: h age 26; name: I age 27; name: j age 28; name: k age 29;
父子进程通信
#include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/ipc.h> #include <sys/shm.h> #include <error.h> #define SIZE 1024 int main() { int shmid ; char *shmaddr ; struct shmid_ds buf ; int flag = 0 ; int pid ; shmid = shmget(IPC_PRIVATE, SIZE, IPC_CREAT|0600|IPC_EXCL) ; if ( shmid < 0 ) { perror("get shm ipc_id error") ; return -1 ; } pid = fork() ; if ( pid == 0 ) { shmaddr = (char *)shmat(shmid, NULL, 0) ; if ( (int)shmaddr == -1 ) { perror("shmat addr error") ; return -1 ; } strcpy(shmaddr, "Hi, I am child process!\n") ; shmdt(shmaddr) ; return 0; } else if ( pid > 0) { sleep(3) ; flag = shmctl( shmid, IPC_STAT, &buf) ; if ( flag == -1 ) { perror("shmctl shm error") ; return -1 ; } printf("shm_segsz =%d bytes\n", buf.shm_segsz ) ; printf("parent pid =%d, shm_cpid = %d \n", getpid(), buf.shm_cpid ) ; printf("chlid pid =%d, shm_lpid = %d \n",pid , buf.shm_lpid ) ; shmaddr = (char *) shmat(shmid, NULL, 0 ) ; if ( (int)shmaddr == -1 ) { perror("shmat addr error") ; return -1 ; } printf("%s", shmaddr) ; shmdt( shmaddr ) ; shmctl(shmid, IPC_RMID, NULL) ; } else { perror("fork error") ; shmctl(shmid, IPC_RMID, NULL) ; } return 0 ; }
执行结果如下:
shm_segsz =1024 bytes parent pid =23830, shm_cpid = 23830 chlid pid =23831, shm_lpid = 23831 Hi, I am child process!
小提示:使用shell命令删除共享内存的形式为:ipcrm -m shm_id删除此共享内存
本文组合了以下几篇文章的内容以及个人改动:
1 http://blog.csdn.net/guoping16/article/details/6584058
2 http://blog.chinaunix.net/uid-25940216-id-3117946.html
3 http://www.cnblogs.com/margincc/archive/2011/04/01/2095056.html
4 http://www.ibm.com/developerworks/cn/linux/l-ipc/part5/index2.html