目录
(一)IPC共享内存和文件映射的区别 1 (二)共享内存实现流程总结 1
(三)存储映射I/O(包含实现原理说明) 2 文件映射API补充 4
(四)IPC共享存储(包含实现原理说明) 6 (五)共享内存实现基本原理 10 (六)IPC共享内存实现机制 11 (七)文件映射的实现机制 13 (一)IPC共享内存和文件映射的区别
1. 文件映射的页框是磁盘文件高速缓存中的页框,内核线程pdflush会将页框中的内容回写进磁盘, 如果是私有映射类型,将会进行写时复制。而IPC共享内存映射的是一种特殊文件系统中的文件高速缓存,它没有相应的磁盘映像。
2. IPC共享内存只存在于内存中,系统重新启动,数据将会丢失。而文件共享映射会将数据写回磁盘。
3. IPC共享内存的大小是在创建的时候指定,而且大小不能改变,而文件在创建时大小为0,此时还不能建立映射,文件的大小会间接的决定映射区的大小。例如文件的大小是123,而要求映射的区域大小是4096*2,但实际只会分配4096的映射空间,此时引用4096以后的线性空间将引起缺页异常。
4. 当第一次读取共享内存时IPC共享内存对象将分配一个新的页框,而文件映射分配新页框的同时会将磁盘中的数据写入新页框。
5. IPC共享内存不需要写回磁盘操作,完全是为共享内存而设计,所以使用效率会更高。
6. IPC共享内存对象必须调用shmctl()显示的撤销,否则会一直保留着,使用key或者id号定位一个共享内存对象,key和id号的对应关系并不是固定的。例如,第一次使用key建立一个共享内存对象为shm1对应的id为id1,之后系统重新启动,然后再使用key建立一个共享内存对象shm2,对应的id是id2,此时id2和id1是不同的。而文件映射使用相同的路径将会定位相同的磁盘文件。
总结:IPC共享内存和文件映射的实现机制是一样的,文件映射的目的是加快对文件的读写速度,而IPC共享内存就是为了共享内存而设计的,所以效率会高一些。 (二)共享内存实现流程总结
1. 建立一个线性区对象struct vm_area_struct 并加入进程的内存描述符current->mm中。
函数mmap()和shmat()就是用于建立并注册线性区对象,这个对象中的struct file *vm_file指向映射文件的文件对象,vm_page_prot是线性区中页框的访问许可权。但此时并未修改进程的页表,而是注册相应的缺页异常回调函数,注册在对象的vm_ops。
2. 当进程第一次访问共享内存区时,由于相应的页表还未填写,将产生缺页异常,并根据线性地址找到对应的线性区对象,然后调用前边注册过的缺页异常回调函数,并根据vm_file文件对象和vm_page_prot的信息来填写相应的页表项,最后重新执行产生缺页异常的代码。
说明:文件映射和IPC共享内存映射的物理页框都是磁盘文件的页高速缓存中的,IPC共享内存使用一种特殊文件系统,这个文件系统并没有对应的磁盘映像,只是复用了文件系统的框架。更详细的内容参见后边的五,六,七节。
下面3,4节是《UNIX环境高级编程》对文件映射和IPC共享内存的讲解,已经说明的很详细了,我在它的基础上附加了一些内核实现原理的说明,实现原理说明部分放在括号内。
(三)存储映射I/O(包含实现原理说明)
存储映射I/O使一个磁盘文件与存储空间中的一个缓存相映射。于是当从缓存中取数据,就相当于读文件中的相应字节。与其类似,将数据存入缓存,则相应字节就自动地写入文件。这样,就可以在不使用read和write的情况下执行I/O。
目录
(一)IPC共享内存和文件映射的区别 1 (二)共享内存实现流程总结 1
(三)存储映射I/O(包含实现原理说明) 2 文件映射API补充 4
(四)IPC共享存储(包含实现原理说明) 6 (五)共享内存实现基本原理 10 (六)IPC共享内存实现机制 11 (七)文件映射的实现机制 13 (一)IPC共享内存和文件映射的区别
1. 文件映射的页框是磁盘文件高速缓存中的页框,内核线程pdflush会将页框中的内容回写进磁盘, 如果是私有映射类型,将会进行写时复制。而IPC共享内存映射的是一种特殊文件系统中的文件高速缓存,它没有相应的磁盘映像。
2. IPC共享内存只存在于内存中,系统重新启动,数据将会丢失。而文件共享映射会将数据写回磁盘。
3. IPC共享内存的大小是在创建的时候指定,而且大小不能改变,而文件在创建时大小为0,此时还不能建立映射,文件的大小会间接的决定映射区的大小。例如文件的大小是123,而要求映射的区域大小是4096*2,但实际只会分配4096的映射空间,此时引用4096以后的线性空间将引起缺页异常。
4. 当第一次读取共享内存时IPC共享内存对象将分配一个新的页框,而文件映射分配新页框的同时会将磁盘中的数据写入新页框。
5. IPC共享内存不需要写回磁盘操作,完全是为共享内存而设计,所以使用效率会更高。
6. IPC共享内存对象必须调用shmctl()显示的撤销,否则会一直保留着,使用key或者id号定位一个共享内存对象,key和id号的对应关系并不是固定的。例如,第一次使用key建立一个共享内存对象为shm1对应的id为id1,之后系统重新启动,然后再使用key建立一个共享内存对象shm2,对应的id是id2,此时id2和id1是不同的。而文件映射使用相同的路径将会定位相同的磁盘文件。
总结:IPC共享内存和文件映射的实现机制是一样的,文件映射的目的是加快对文件的读写速度,而IPC共享内存就是为了共享内存而设计的,所以效率会高一些。 (二)共享内存实现流程总结
1. 建立一个线性区对象struct vm_area_struct 并加入进程的内存描述符current->mm中。
函数mmap()和shmat()就是用于建立并注册线性区对象,这个对象中的struct file *vm_file指向映射文件的文件对象,vm_page_prot是线性区中页框的访问许可权。但此时并未修改进程的页表,而是注册相应的缺页异常回调函数,注册在对象的vm_ops。
2. 当进程第一次访问共享内存区时,由于相应的页表还未填写,将产生缺页异常,并根据线性地址找到对应的线性区对象,然后调用前边注册过的缺页异常回调函数,并根据vm_file文件对象和vm_page_prot的信息来填写相应的页表项,最后重新执行产生缺页异常的代码。
说明:文件映射和IPC共享内存映射的物理页框都是磁盘文件的页高速缓存中的,IPC共享内存使用一种特殊文件系统,这个文件系统并没有对应的磁盘映像,只是复用了文件系统的框架。更详细的内容参见后边的五,六,七节。
下面3,4节是《UNIX环境高级编程》对文件映射和IPC共享内存的讲解,已经说明的很详细了,我在它的基础上附加了一些内核实现原理的说明,实现原理说明部分放在括号内。
(三)存储映射I/O(包含实现原理说明)
存储映射I/O使一个磁盘文件与存储空间中的一个缓存相映射。于是当从缓存中取数据,就相当于读文件中的相应字节。与其类似,将数据存入缓存,则相应字节就自动地写入文件。这样,就可以在不使用read和write的情况下执行I/O。
SIGBUS信号。例如,用文件长度映射一个文件,但在存访该映射区之前,另一个进程已将该文件截短。此时,如果进程企图存取对应于该文件尾端部分的映射区,则接收到SIGBUS信号。(对信号的实现机制有待进一步分析)
在fork之后,子进程继承存储映射区(因为子进程复制父进程地址空间,而存储映射区是该地址空间中的一部分),但是由于同样的理由,exec后的新程序则不继承此存储映射区。(关闭文件描述符也不影响存储映射区,磁盘文件的页高速缓存并不会因为进程的撤销而撤销,如果有足够的空闲内存,页高速缓存中的页将长期存在,使其它进程再使用该页时不再访问磁盘。)
进程终止时,或调用了munmap之后,存储映射区就被自动去除。关闭文件描述符fd并不解除映射区。(关闭存储映射区,只是撤销进程页表中的相应目录项,并不影响页高速缓存。)
#include #include
int munmap(void addr,size_t len) 返回:若成功则为0,若出错则为- 1
munmap 并不影响被映射的对象—也就是说,调用munmap并不使映射区的内容写到磁盘文件上。对于MAP_SHARED区磁盘文件的更新,在写到存储映射区时按内核虚存算法自动进行。(pdflush内核线程用于刷新脏页)
例子程序:
#include #include #include #include #include #include
int main(int argc, char *argv[]) {
int fd, i, counter; pid_t pid;
char *area = NULL;
if((fd = open("test", O_RDWR) )<= 0) printf("open error\n");
area = (char *)mmap(0, sizeof(long), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
printf("area:%p\n", area); close(fd);
*(area + 1) = 'c'; }
文件映射API补充
msync函数的使用原型:?? #include ?? int msync(const void *start, size_t length, int flags);msync函数用来把映像的文件写入磁盘。调用msync可以用对内存中的映像的更新写入一个被映像的文件,被强行写入到磁盘的内存取从start指定的地址开始,写入length个字节的数据。flags可以是下面的一个值或多个的逻辑“或”:?? 1、MS_ASYNC 调度一次写入操作然后返回?? 2、MS_SYNC 在msync返回前写入数据?? 3、MS_INVALIDATE 让映像到同一文件的映像无效,以便用新数据更新它们(MS_INVALIDATE的作用是使映射的页高速缓存中的内容无效,重新从磁盘写入数据到映射的页高速缓存。 可以使用MS_INVALIDATE来测试内核是否进行页高速缓存数据的回写磁盘操作,测试过程:写一个字符到映射区,然后使用MS_INVALIDATE使映射区的数据失效,并从磁盘写入数据,从测试结果看字符会被写入磁盘,也就是说内核几乎在对映射区进行写入操作的同时就进行了回写磁盘操作)
SIGBUS信号。例如,用文件长度映射一个文件,但在存访该映射区之前,另一个进程已将该文件截短。此时,如果进程企图存取对应于该文件尾端部分的映射区,则接收到SIGBUS信号。(对信号的实现机制有待进一步分析)
在fork之后,子进程继承存储映射区(因为子进程复制父进程地址空间,而存储映射区是该地址空间中的一部分),但是由于同样的理由,exec后的新程序则不继承此存储映射区。(关闭文件描述符也不影响存储映射区,磁盘文件的页高速缓存并不会因为进程的撤销而撤销,如果有足够的空闲内存,页高速缓存中的页将长期存在,使其它进程再使用该页时不再访问磁盘。)
进程终止时,或调用了munmap之后,存储映射区就被自动去除。关闭文件描述符fd并不解除映射区。(关闭存储映射区,只是撤销进程页表中的相应目录项,并不影响页高速缓存。)
#include #include
int munmap(void addr,size_t len) 返回:若成功则为0,若出错则为- 1
munmap 并不影响被映射的对象—也就是说,调用munmap并不使映射区的内容写到磁盘文件上。对于MAP_SHARED区磁盘文件的更新,在写到存储映射区时按内核虚存算法自动进行。(pdflush内核线程用于刷新脏页)
例子程序:
#include #include #include #include #include #include
int main(int argc, char *argv[]) {
int fd, i, counter; pid_t pid;
char *area = NULL;
if((fd = open("test", O_RDWR) )<= 0) printf("open error\n");
area = (char *)mmap(0, sizeof(long), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
printf("area:%p\n", area); close(fd);
*(area + 1) = 'c'; }
文件映射API补充
msync函数的使用原型:?? #include ?? int msync(const void *start, size_t length, int flags);msync函数用来把映像的文件写入磁盘。调用msync可以用对内存中的映像的更新写入一个被映像的文件,被强行写入到磁盘的内存取从start指定的地址开始,写入length个字节的数据。flags可以是下面的一个值或多个的逻辑“或”:?? 1、MS_ASYNC 调度一次写入操作然后返回?? 2、MS_SYNC 在msync返回前写入数据?? 3、MS_INVALIDATE 让映像到同一文件的映像无效,以便用新数据更新它们(MS_INVALIDATE的作用是使映射的页高速缓存中的内容无效,重新从磁盘写入数据到映射的页高速缓存。 可以使用MS_INVALIDATE来测试内核是否进行页高速缓存数据的回写磁盘操作,测试过程:写一个字符到映射区,然后使用MS_INVALIDATE使映射区的数据失效,并从磁盘写入数据,从测试结果看字符会被写入磁盘,也就是说内核几乎在对映射区进行写入操作的同时就进行了回写磁盘操作)
size是要建立共享内存的长度。所有的内存分配操作都是以页为单位的。实际的大小是((bytes进位到4096整数倍)/4096 + 4) * 4096。(因为线性区和物理内存的分配都是以页为单位)
shmflg主要和一些标志有关,其中有效的包括IPC_CREAT和IPC_EXCL,它们的功能与open()的O_CREAT和O_EXCL相当。
IPC_CREAT 如果共享内存不存在,则创建一个共享内存,否则打开操作。
IPC_EXCL 只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误。 如果单独使用IPC_CREAT,shmget()函数要么返回一个已经存在的共享内存的操作符,要么返回一个新建的共享内存的标识符。如果将IPC_CREAT和IPC_EXCL标志一起使用,shmget()将返回一个新建的共享内存的标识符;如果该共享内存已存在,或者返回-1。IPC_EXEL标志本身并没有太大的意义,但是和IPC_CREAT标志一起使用可以用来保证所得的对象是新建的,而不是打开已有的对象。 返回值
成功返回共享内存的标识符;不成功返回-1,errno储存错误原因。 EINVAL 参数size小于SHMMIN或大于SHMMAX。 EEXIST 预建立key所致的共享内存,但已经存在。 EIDRM 参数key所致的共享内存已经删除。
ENOSPC 超过了系统允许建立的共享内存的最大值(SHMALL )。
ENOENT 参数key所指的共享内存不存在,参数shmflg也未设IPC_CREAT位。 EACCES 没有权限。 ENOMEM 核心内存不足。 struct shmid_ds
shmid_ds数据结构表示每个新建的共享内存。当shmget()创建了一块新的共享内存后,返回一个可以用于引用该共享内存的shmid_ds数据结构的标识符。 include/linux/shm.h struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */ int shm_segsz; /* 共享内存的大小 */
__kernel_time_t shm_atime; /* 最后一次附加这个共享内存的时间 */ __kernel_time_t shm_dtime; /*最后一次分离这个共享内存的时间 */ __kernel_time_t shm_ctime; /*最后一次改变这个共享内存结构的时间*/ __kernel_ipc_pid_t shm_cpid; /* 建立这个共享内存的进程识别码 */ __kernel_ipc_pid_t shm_lpid; /*最后一个操作共享内存的进程识别码*/ unsigned short shm_nattch; /* 附加这个共享内存的进程个数 */ unsigned short shm_unused; /* compatibility */ void *shm_unused2; /* ditto - used by DIPC */ void *shm_unused3; /* unused */ };
struct ipc_perm
对于每个IPC对象,系统共用一个struct ipc_perm的数据结构来存放权限信息,以确定一个ipc操作是否可以访问该IPC对象。 struct ipc_perm {
__kernel_key_t key; //共享内存对象的key
__kernel_uid_t uid; //共享内存所属的用户识别码(可以修改) __kernel_gid_t gid; //共享内存所属的组识别码(可以修改) __kernel_uid_t cuid; //建立共享内对象的用户识别码 __kernel_gid_t cgid; //建立共享内对象的组识别码
__kernel_mode_t mode; //这个共享内存的读写权限(可以修改) unsigned short seq; //序号 };
shmctl函数对共享存储段执行多种操作。
#include #include #include
int shmctl(int shmid, int cmd, struct shmid_ds * buf) 返回:若成功则为0,若出错则为- 1
cmd参数指定下列5种命令中一种,使其在shmid指定的段上执行。 ? IPC_STAT 对此段取shmid_ds结构,并存放在由buf指向的结构中。
? IPC_SET按buf指向的结构中的值设置与此段相关结构中的下列三个字段:(只能修改这3个字段)shm_perm.uid、shm_perm.gid以及shm_perm.mode。此命令只能由下列两种进程执行:一种是其有效用户ID等于shm_perm.cuid或shm_perm.uid的进程;另一种是具有超级用户特权的进程。
? IPC_RMID 从系统中删除该共享存储段。因为每个共享存储段有一个连接计数(shm_nattch在shmid_ds结构中),所以除非使用该段的最后一个进程终止或与该段脱接,否则不会实际上删除该存储段。不管此段是否仍在使用,该段标识符立即被删除,所以不能再用shmat与该段连接。此命令只能由下列两种进程执行:一种是其有效用户ID等于shm_perm.cuid或shm_perm.uid的进程;另一种是具有超级用户特权的进程。 ? SHM_LOCK锁住共享存储段。此命令只能由超级用户执行。 ? SHM_UNLOCK解锁共享存储段。此命令只能由超级用户执行。
一旦创建了一个共享存储段,进程就可调用shmat将其连接到它的地址空间中。 #include #include #include
void *shmat(int shmid, void *addr, int flag)
返回:若成功则为指向共享存储段的指针,若出错则为- 1。
共享存储段连接到调用进程的哪个地址上与addr参数以及在flag中是否指定SHM_RND位有关。
(1) 如果addr为0,则此段连接到由内核选择的第一个可用地址上。
(2) 如果addr非0,并且没有指定SHM_RND,则此段连接到addr所指定的地址上。 (3) 如果addr非0,并且指定了SHM_RND,则此段连接到( addr-(addr mod SHMLBA))所表示的地址上。SHM_RND命令的意思是:取整。SHMLBA的意思是:低边界地址倍数,它总是2的乘方。该算式是将地址向下取最近1个SHMLBA的倍数。
除非只计划在一种硬件上运行应用程序(这在当今是不大可能的),否则不用指定共享段所连接到的地址。所以一般应指定addr为0,以便由内核选择地址。
如果在f l a g中指定了SHM_RDONLY位,则以只读方式连接此段。否则以读写方式连接此段。(在进程页表项中设置只读标志,试图修改该页时将产生缺页异常,这些都是由CPU的页寻址硬件控制的)
shmat的返回值是该段所连接的实际地址,如果出错则返回-1。
当对共享存储段的操作已经结束时,则调用shmdt脱接该段。注意,这并不从系统中删除其标识符以及其数据结构。该标识符仍然存在,直至某个进程(一般是服务器)调用shmctl(带命令IP C_RMID)特地删除它。(连接是进程将共享内存的物理页加入进程页表,脱离是从页表中撤销该物理页的信息,并不改变实际的物理页) #include #include #include int shmdt(void * addr);
返回:若成功则为0,若出错则为- 1 addr参数是以前调用shmat时的返回值。
下面是和IPC共享内存有关的内核参数,可以修改的。
其中shmall是所有共享内存段可以使用的最大页个数 shmmni是一个共享内存段的最小字节数
阅读(8491) | 评论(1) | 转发(2) |