赞
踩
http://kenby.iteye.com/blog/1164700
共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式, 因为进程可以直接读写内存,而不需要任何
数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则
只拷贝两次数据: 一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内
存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直
到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映
射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
一. 传统文件访问
UNIX访问文件的传统方法是用open打开它们, 如果有多个进程访问同一个文件, 则每一个进程在自己的地址空间都包含有该
文件的副本,这不必要地浪费了存储空间. 下图说明了两个进程同时读一个文件的同一页的情形. 系统要将该页从磁盘读到高
速缓冲区中, 每个进程再执行一个存储器内的复制操作将数据从高速缓冲区读到自己的地址空间.
二. 共享存储映射
现在考虑另一种处理方法: 进程A和进程B都将该页映射到自己的地址空间, 当进程A第一次访问该页中的数据时, 它生成一
个缺页中断. 内核此时读入这一页到内存并更新页表使之指向它.以后, 当进程B访问同一页面而出现缺页中断时, 该页已经在
内存, 内核只需要将进程B的页表登记项指向次页即可. 如下图所示:
三、mmap()及其相关系统调用
mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访
问普通内存一样对文件进行访问,不必再调用read(),write()等操作。
mmap()系统调用形式如下:
void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
mmap的作用是映射文件描述符fd指定文件的 [off,off + len]区域至调用进程的[addr, addr + len]的内存区域, 如下图所示:
参数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的
MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的
进程间通信)。
len是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。
prot 参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。
flags由以下几个常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必
选其一,而MAP_FIXED则不推荐使用。
offset参数一般设为0,表示从文件头开始映射。
参数addr指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。函
数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。
总结一下就是, 文件大小, mmap的参数 len 都不能决定进程能访问的大小, 而是容纳文件被映射部分的最小页面数决定
进程能访问的大小. 下面看一个实例:
http://wenku.baidu.com/view/e7d9b222bcd126fff7050b53.html
http://learn.akae.cn/media/ch28s09.html
http://blog.nosqlfan.com/html/2956.html
下面是一篇翻译文章,原文出自MongoDB的核心开发工程师 Kristina Chodorow 的个人博客,由NoSQLFan翻译整理。
我们都知道,MongoDB 使用内存映射的方式来进行数据文件的存取操作。本文的目的就在于描述操作系统虚拟内存的使用及内存映射的内部实现。
以下是译文
当你运行一个程序,程序中有许多东西需要存储,堆、栈以及各种功能库。而这一切在你写程序时可能都不需要自己控制,Linux内核会帮你完成这些存储的调度,你只需要告诉它你需要做什么,内核就会在合适的地方给你分配内存空间。本文主要通过几个实例程序的内存使用研究,来为大家展示Linux的内存使用状况。
第一个例子:下面一段程序会打印出程序的pid(进程号)后挂起。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
printf("run `pmap %d`\n", getpid());
pause();
}
将上面代码保存成文件 mem_munch.c 然后运行下面程序编译并执行:
$ gcc mem_munch.c -o mem_munch
$ ./mem_munch
run `pmap 25681`
上面进程号是25681,可能你试验的结果会不太一样。
下面我们通过pmap命令来查看一下这个小程序的内存使用情况
$ pmap 25681
25681: ./mem_munch
0000000000400000 4K r-x-- /home/user/mem_munch
0000000000600000 4K r---- /home/user/mem_munch
0000000000601000 4K rw--- /home/user/mem_munch
00007fcf5af88000 1576K r-x-- /lib/x86_64-linux-gnu/libc-2.13.so
00007fcf5b112000 2044K ----- /lib/x86_64-linux-gnu/libc-2.13.so
00007fcf5b311000 16K r---- /lib/x86_64-linux-gnu/libc-2.13.so
00007fcf5b315000 4K rw--- /lib/x86_64-linux-gnu/libc-2.13.so
00007fcf5b316000 24K rw--- [ anon ]
00007fcf5b31c000 132K r-x-- /lib/x86_64-linux-gnu/ld-2.13.so
00007fcf5b512000 12K rw--- [ anon ]
00007fcf5b539000 12K rw--- [ anon ]
00007fcf5b53c000 4K r---- /lib/x86_64-linux-gnu/ld-2.13.so
00007fcf5b53d000 8K rw--- /lib/x86_64-linux-gnu/ld-2.13.so
00007fff7efd8000 132K rw--- [ stack ]
00007fff7efff000 4K r-x-- [ anon ]
ffffffffff600000 4K r-x-- [ anon ]
total 3984K
上面的结果是这个程序的内存使用情况,其实更确切的说是这个程序认为它使用内存的情况。从上面的结果我们能看到,当你访问libc库时,实际上是对内存地址00007fcf5af88000的访问,当你访问ld库时,实际上是对内存地址00007fcf5b31c000的访问。
上面的输出可能还比较抽象,下面我们修改一下上面的程序,我们在程序的堆和栈上各放一块数据。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main() {
int on_stack, *on_heap;
//局部变量是放在栈上的,所以 on_stack 的地址就是栈的初始地址
on_stack = 42;
printf("stack address: %p\n", &on_stack);
//malloc 的内存是在堆上分配的
on_heap = (int*)malloc(sizeof(int));
printf("heap address: %p\n", on_heap);
printf("run `pmap %d`\n", getpid());
pause();
}
编译运行:
$ ./mem_munch
stack address: 0x7fff497670bc
heap address: 0x1b84010
run `pmap 11972`
然后再用pmap命令查看一下内存使用:
$ pmap 11972
11972: ./mem_munch
0000000000400000 4K r-x-- /home/user/mem_munch
0000000000600000 4K r---- /home/user/mem_munch
0000000000601000 4K rw--- /home/user/mem_munch
0000000001b84000 132K rw--- [ anon ]
00007f3ec4d98000 1576K r-x-- /lib/x86_64-linux-gnu/libc-2.13.so
00007f3ec4f22000 2044K ----- /lib/x86_64-linux-gnu/libc-2.13.so
00007f3ec5121000 16K r---- /lib/x86_64-linux-gnu/libc-2.13.so
00007f3ec5125000 4K rw--- /lib/x86_64-linux-gnu/libc-2.13.so
00007f3ec5126000 24K rw--- [ anon ]
00007f3ec512c000 132K r-x-- /lib/x86_64-linux-gnu/ld-2.13.so
00007f3ec5322000 12K rw--- [ anon ]
00007f3ec5349000 12K rw--- [ anon ]
00007f3ec534c000 4K r---- /lib/x86_64-linux-gnu/ld-2.13.so
00007f3ec534d000 8K rw--- /lib/x86_64-linux-gnu/ld-2.13.so
00007fff49747000 132K rw--- [ stack ]
00007fff497bb000 4K r-x-- [ anon ]
ffffffffff600000 4K r-x-- [ anon ]
total 4116K
这次多出了上面红色的一行内容,红色内容就是堆的起始位置:
0000000001b84000 132K rw--- [ anon ]
在我们程序运行的输出里也有一行红色的输出,这是这个地址在程序中的内存地址:
heap address: 0x1b84010
这两个地址基本上是一样的,其中的anon是Anonymous的缩写,表明这段内存是没有文件映射的。
我们再看上面绿色的两行,与上面相对应,这两行分别是用pmap 和应用程序看到的栈起始地址:
00007fff49747000 132K rw--- [ stack ]
stack address: 0x7fff497670bc
上面说到的内存使用,都只是程序认为自己对内存的使用,实际上程序在分配内存是不知道系统内存的状态的。所以上面的输出都只是从程序自己的角度看到的内存使用状况。比如在上面的例子中,我们看到程序的内存地址空间是从0×0000000000400000到0xffffffffff600000的所有地址(而0xffffffffff600000到0×00007fffffffffffffff之间的地址是有特殊用处的,这里不多讲)。这样算下来,我们总共可以使用的内存空间有1千万TB。
但是实际上目前没有硬件能有1千万TB的物理内存。为什么操作系统会如此设计呢?原因有很多,可以看这里,但也正因此,我们可以使用远远超出物理内存大小的内存空间。
内存映射的原理就是让操作系统将一个文件映射到一段内存中,然后在操作这个文件内存就可以像操作内存一样。比如我们创建一个完全内容随机的文件,然后将它用内存映射的方式映射到一段内存空间中。那么我们在这段内存中随便取一位就相当于取到了一个随机数。下面就让我们来做这个实验,先用下面命令生成一个内容随机的文件。
$ dd if=/dev/urandom bs=1024 count=1000000 of=/home/user/random
1000000+0 records in
1000000+0 records out
1024000000 bytes (1.0 GB) copied, 123.293 s, 8.3 MB/s
$ ls -lh random
-rw-r--r-- 1 user user 977M 2011-08-29 16:46 random
然后我们用下面程序来将这个文件内容映射到内存,再从中取出随机数
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/mman.h>
int main() {
char *random_bytes;
FILE *f;
int offset = 0;
// open "random" for reading
f = fopen("/home/user/random", "r");
if (!f) {
perror("couldn't open file");
return -1;
}
// we want to inspect memory before mapping the file
printf("run `pmap %d`, then press ", getpid());
getchar();
random_bytes = mmap(0, 1000000000, PROT_READ, MAP_SHARED, fileno(f), 0);
if (random_bytes == MAP_FAILED) {
perror("error mapping the file");
return -1;
}
while (1) {
printf("random number: %d (press for next number)", *(int*)(random_bytes+offset));
getchar();
offset += 4;
}
}
然后运行这个程序:
$ ./mem_munch
run `pmap 12727`, then press
下面我们通过一次次的按下回车键来从这个文件中读取随机数,按下几次后我们可以再通过pmap来查看其内存空间的情况:
$ pmap 12727
12727: ./mem_munch
0000000000400000 4K r-x-- /home/user/mem_munch
0000000000600000 4K r---- /home/user/mem_munch
0000000000601000 4K rw--- /home/user/mem_munch
000000000147d000 132K rw--- [ anon ]
00007fe261c6f000 976564K r--s- /home/user/random
00007fe29d61c000 1576K r-x-- /lib/x86_64-linux-gnu/libc-2.13.so
00007fe29d7a6000 2044K ----- /lib/x86_64-linux-gnu/libc-2.13.so
00007fe29d9a5000 16K r---- /lib/x86_64-linux-gnu/libc-2.13.so
00007fe29d9a9000 4K rw--- /lib/x86_64-linux-gnu/libc-2.13.so
00007fe29d9aa000 24K rw--- [ anon ]
00007fe29d9b0000 132K r-x-- /lib/x86_64-linux-gnu/ld-2.13.so
00007fe29dba6000 12K rw--- [ anon ]
00007fe29dbcc000 16K rw--- [ anon ]
00007fe29dbd0000 4K r---- /lib/x86_64-linux-gnu/ld-2.13.so
00007fe29dbd1000 8K rw--- /lib/x86_64-linux-gnu/ld-2.13.so
00007ffff29b2000 132K rw--- [ stack ]
00007ffff29de000 4K r-x-- [ anon ]
ffffffffff600000 4K r-x-- [ anon ]
total 980684K
上面的输出和之前的大同小异,但是多出了上面红色的一行。这是我们上面的随机文件映射到内存中的内存。我们再使用pmap -x 选项来查看一下程序的内存使用,会得到下面的内容,其中RSS(resident set size)列表示真实占用的内存。
pmap -x 12727
12727: ./mem_munch
Address Kbytes RSS Dirty Mode Mapping
0000000000400000 0 4 0 r-x-- mem_munch
0000000000600000 0 4 4 r---- mem_munch
0000000000601000 0 4 4 rw--- mem_munch
000000000147d000 0 4 4 rw--- [ anon ]
00007fe261c6f000 0 4 0 r--s- random
00007fe29d61c000 0 288 0 r-x-- libc-2.13.so
00007fe29d7a6000 0 0 0 ----- libc-2.13.so
00007fe29d9a5000 0 16 16 r---- libc-2.13.so
00007fe29d9a9000 0 4 4 rw--- libc-2.13.so
00007fe29d9aa000 0 16 16 rw--- [ anon ]
00007fe29d9b0000 0 108 0 r-x-- ld-2.13.so
00007fe29dba6000 0 12 12 rw--- [ anon ]
00007fe29dbcc000 0 16 16 rw--- [ anon ]
00007fe29dbd0000 0 4 4 r---- ld-2.13.so
00007fe29dbd1000 0 8 8 rw--- ld-2.13.so
00007ffff29b2000 0 12 12 rw--- [ stack ]
00007ffff29de000 0 4 0 r-x-- [ anon ]
ffffffffff600000 0 0 0 r-x-- [ anon ]
---------------- ------ ------ ------
total kB 980684 508 100
如果你的虚拟内存占用(上面的Kbytes列)都是0,不用担心,这是一个在Debian/Ubuntu系统上pmap -x命令的bug。最后一行输出的总占用量是正确的。
现在你可以看一下RSS那一列,这就是实际内存占用。在random文件上,你的程序实际上可以访问在00007fe261c6f000之前的数十亿字节的内存地址,但是只要你访问的地址超过4KB,那么操作系统就会去磁盘上查找内容。也就是说实际上只有4KB的物理内存被使用了。只有访问这4KB的东西时,才是真正的内存操作。其它部分虽然你使用的也是内存操作函数来访问它,但是由于它没有被加载到内存中,所以在这些内容被访问的时候,操作系统会先去磁盘读random中读取内容到内存中。
如果我们把程序再修改一下,修改成下面这样,让程序把整个random文件都访问一遍。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/mman.h>
int main() {
char *random_bytes;
FILE *f;
int offset = 0;
// open "random" for reading
f = fopen("/home/user/random", "r");
if (!f) {
perror("couldn't open file");
return -1;
}
random_bytes = mmap(0, 1000000000, PROT_READ, MAP_SHARED, fileno(f), 0);
if (random_bytes == MAP_FAILED) {
printf("error mapping the file\n");
return -1;
}
for (offset = 0; offset < 1000000000; offset += 4) {
int i = *(int*)(random_bytes+offset);
// to show we're making progress
if (offset % 1000000 == 0) {
printf(".");
}
}
// at the end, wait for signal so we can check mem
printf("\ndone, run `pmap -x %d`\n", getpid());
pause();
}
现在我们的pmap -x命令就会得到如下输出:
$ pmap -x 5378
5378: ./mem_munch
Address Kbytes RSS Dirty Mode Mapping
0000000000400000 0 4 4 r-x-- mem_munch
0000000000600000 0 4 4 r---- mem_munch
0000000000601000 0 4 4 rw--- mem_munch
0000000002271000 0 4 4 rw--- [ anon ]
00007fc2aa333000 0 976564 0 r--s- random
00007fc2e5ce0000 0 292 0 r-x-- libc-2.13.so
00007fc2e5e6a000 0 0 0 ----- libc-2.13.so
00007fc2e6069000 0 16 16 r---- libc-2.13.so
00007fc2e606d000 0 4 4 rw--- libc-2.13.so
00007fc2e606e000 0 16 16 rw--- [ anon ]
00007fc2e6074000 0 108 0 r-x-- ld-2.13.so
00007fc2e626a000 0 12 12 rw--- [ anon ]
00007fc2e6290000 0 16 16 rw--- [ anon ]
00007fc2e6294000 0 4 4 r---- ld-2.13.so
00007fc2e6295000 0 8 8 rw--- ld-2.13.so
00007fff037e6000 0 12 12 rw--- [ stack ]
00007fff039c9000 0 4 0 r-x-- [ anon ]
ffffffffff600000 0 0 0 r-x-- [ anon ]
---------------- ------ ------ ------
total kB 980684 977072 104
我们可以看到,random文件映射实际占用内存量已经和random文件大小一致了,也就是也random文件通过循环访问,其内容已经完全加载到内存中了。现在我们再访问random文件的任何部分,实际上都是内存操作。而不会穿透到磁盘。
话说回来,这就是为什么MongoDB的内存使用,可以远远超出操作系统物理内存大小。
mmap: memory map 在讲述文件映射的概念时, 不可避免的要牵涉到虚存(SVR 4的VM). 实际上, 文件映射是虚存的中心概念, 文件映射一方面给用户提供了一组措施, 好似用户将文件映射到自己地址空间的某个部分, 使用简单的内存访问指令读写文件;另一方面, 它也可以用于内核的基本组织模式, 在这种模式种, 内核将整个地址空间视为诸如文件之类的一组不同对象的映射. 中的传统文件访问方式是, 首先用open系统调用打开文件, 然后使用read, write以及lseek等调用进行顺序或者随即的I/O. 这种方式是非常低效的, 每一次I/O操作都需要一次系统调用. 另外, 如果若干个进程访问同一个文件, 每个进程都要在自己的地址空间维护一个副本, 浪费了内存空间. 而如果能够通过一定的机制将页面映射到进程的地址空间中, 也就是说首先通过简单的产生某些内存管理数据结构完成映射的创建. 当进程访问页面时产生一个缺页中断, 内核将页面读入内存并且更新页表指向该页面. 而且这种方式非常方便于同一副本的共享. VM是面向对象的方法设计的, 这里的对象是指内存对象: 内存对象是一个软件抽象的概念, 它描述内存区与后备存储之间的映射. 系统可以使用多种类型的后备存储, 比如交换空间, 本地或者远程文件以及帧缓存等等. VM系统对它们统一处理, 采用同一操作集操作, 比如读取页面或者回写页面等. 每种不同的后备存储都可以用不同的方法实现这些操作. 这样, 系统定义了一套统一的接口, 每种后备存储给出自己的实现方法. 这样, 进程的地址空间就被视为一组映射到不同数据对象上的的映射组成. 所有的有效地址就是那些映射到数据对象上的地址. 这些对象为映射它的页面提供了持久性的后备存储. 映射使得用户可以直接寻址这些对象. 值得提出的是, VM体系结构独立于Unix系统, 所有的Unix系统语义, 如正文, 数据及堆栈区都可以建构在基本VM系统之上. 同时, VM体系结构也是独立于存储管理的, 存储管理是由操作系统实施的, 如: 究竟采取什么样的对换和请求调页算法, 究竟是采取分段还是分页机制进行存储管理, 究竟是如何将虚拟地址转换成为物理地址等等(Linux中是一种叫Three Level Page Table的机制), 这些都与内存对象的概念无关. 下面介绍Linux中VM的实现. struct vm_area_struct { /*公共的, 与vma类型无关的 */ struct mm_struct * vm_mm; /* 与类型相关的 */ struct vm_operations_struct * vm_ops; 介绍完VM的基本概念后, 我们可以讲述mmap和munmap系统调用了. mmap调用实际上就是一个内存对象vma的创建过程, mmap的调用格式是: void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset); 其中start是映射地址, length是映射长度, 如果flags的MAP_FIXED不被置位, 则该参数通常被忽略, 而查找进程地址空间中第一个长度符合的空闲区域;Fd是映射文件的文件句柄, offset是映射文件中的偏移地址;prot是映射保护权限, 可以是PROT_EXEC, PROT_READ, PROT_WRITE, PROT_NONE, flags则是指映射类型, 可以是MAP_FIXED, MAP_PRIVATE, MAP_SHARED, 该参数必须被指定为MAP_PRIVATE和MAP_SHARED其中之一, MAP_PRIVATE是创建一个写时拷贝映射(copy-on-write), 也就是说如果有多个进程同时映射到一个文件上, 映射建立时只是共享同样的存储页面, 但是某进程企图修改页面内容, 则复制一个副本给该进程私用, 它的任何修改对其它进程都不可见. 而MAP_SHARED则无论修改与否都使用同一副本, 任何进程对页面的修改对其它进程都是可见的. mmap系统调用的实现过程是: munmap(void * start, size_t length): 该调用可以看作是mmap的一个逆过程. 它将进程中从start开始length长度的一段区域的映射关闭, 如果该区域不是恰好对应一个vma, 则有可能会分割几个或几个vma. msync(void * start, size_t length, int flags): 把映射区域的修改回写到后备存储中. 因为munmap时并不保证页面回写, 如果不调用msync, 那么有可能在munmap后丢失对映射区的修改. 其中flags可以是MS_SYNC, MS_ASYNC, MS_INVALIDATE, MS_SYNC要求回写完成后才返回, MS_ASYNC发出回写请求后立即返回, MS_INVALIDATE使用回写的内容更新该文件的其它映射. 该系统调用是通过调用映射文件的sync函数来完成工作的. brk(void * end_data_segement): 将进程的数据段扩展到end_data_segement指定的地址, 该系统调用和mmap的实现方式十分相似, 同样是产生一个vma, 然后指定其属性. 不过在此之前需要做一些合法性检查, 比如该地址是否大于mm->end_code, end_data_segement和mm->brk之间是否还存在其它vma等等. 通过brk产生的vma映射的文件为空, 这和匿名映射产生的vma相似, 关于匿名映射不做进一步介绍. 库函数malloc就是通过brk实现的. |
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。