在Linux的广袤世界里,内存映射就像是一座桥梁,连接着磁盘上的文件与内存空间,是操作系统中至关重要的概念,在文件操作、进程间通信等诸多场景中发挥着不可替代的作用。无论是处理大数据量的文件读写,还是实现多个进程间的高效数据共享,内存映射都能展现出其独特的优势,为系统性能带来质的飞跃 。
想象一下,当你需要处理一个超级大的文件时,如果按照传统的文件 I/O 方式,数据在磁盘、内核缓冲区和用户空间之间来回拷贝,不仅效率低下,还可能耗费大量的时间和系统资源。而内存映射则打破了这种繁琐的流程,它让你直接将文件映射到内存空间,就像文件数据已经在内存中一样,你可以像操作内存一样轻松地对文件进行读写,极大地提高了效率。
再比如,在多个进程需要共享数据的场景下,内存映射可以让这些进程共享同一块内存区域,实现数据的实时同步和高效交互,避免了复杂的数据传递和同步机制。接下来,就让我们深入探索 Linux 内存映射的奥秘吧!
一、内存映射是什么
1.1定义剖析
内存映射,英文名为 Memory - mapped I/O,从字面意思理解,就是将磁盘文件的数据映射到内存中。在 Linux 系统中,这一机制允许进程把一个文件或者设备的数据关联到内存地址空间,使得进程能够像访问内存一样对文件进行操作 。举个简单的例子,假设有一个文本文件,通常我们读取它时,会使用read函数,数据从磁盘先读取到内核缓冲区,再拷贝到用户空间。而内存映射则直接在进程的虚拟地址空间中为这个文件创建一个映射区域,进程可以直接通过指针访问这个映射区域,就好像文件数据已经在内存中一样,大大简化了文件操作的流程 。
1.2工作原理大揭秘
内存映射的工作原理涉及到虚拟内存、页表以及文件系统等多个方面的知识。当进程调用mmap函数进行内存映射时,大致会经历以下几个关键步骤 :
虚拟内存区域创建:系统首先在进程的虚拟地址空间中寻找一段满足要求的连续空闲虚拟地址,然后为这段虚拟地址分配一个vm_area_struct结构,这个结构用于描述虚拟内存区域的各种属性,如起始地址、结束地址、权限等,并将其插入到进程的虚拟地址区域链表或树中 。就好比在一片空地上,规划出一块特定大小和用途的区域,并做好标记。
地址映射建立:通过待映射的文件指针,找到对应的文件描述符,进而链接到内核 “已打开文件集” 中该文件的文件结构体。再通过这个文件结构体,调用内核函数mmap,定位到文件磁盘物理地址,然后通过remap_pfn_range函数建立页表,实现文件物理地址和进程虚拟地址的一一映射关系 。这一步就像是在规划好的区域和实际的文件存储位置之间建立起一条通道,让数据能够顺利流通。不过,此时只是建立了地址映射,真正的数据还没有拷贝到内存中 。
数据加载(缺页异常处理):当进程首次访问映射区域中的数据时,由于数据还未在物理内存中,会触发缺页异常。内核会捕获这个异常,然后在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有找到,则调用nopage函数把所缺的页从磁盘装入到主存中 。这个过程就像是当你需要使用某个物品,但它不在身边,你就需要去存放它的地方把它取回来。之后,进程就可以对这片主存进行正常的读或写操作,如果写操作改变了数据内容,系统会在一定时间后自动将脏页面回写脏页面到对应磁盘地址,完成写入到文件的过程 。当然,也可以调用msync函数来强制同步,让数据立即保存到文件里 。
二、内存映射机制
mmap内存映射的实现过程,总的来说可以分为三个阶段:
①进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
1、进程在用户空间调用库函数mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
2、在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址
3、为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化
4、将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中
②调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系
5、为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。
6、通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库函数。
7、内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。
8、通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中。
③进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝
注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。
9、进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。
10、缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。
11、调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。
12、之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。
注:修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。
3.1内存映射分类
(1)按文件分
文件映射:简单来说,就是把文件的一个区间映射到进程的虚拟地址空间,数据源来自存储设备上的文件。这种映射类型在很多场景中都有广泛应用,比如当我们需要读取一个大文件时,如果使用传统的read函数,数据会先从磁盘读取到内核缓冲区,再拷贝到用户空间,这个过程涉及多次数据拷贝,效率较低 。而文件映射则直接将文件映射到进程的虚拟地址空间,进程可以像访问内存一样直接对文件进行读写操作 。
假设我们有一个数据库文件,里面存储着大量的数据。当数据库管理系统需要读取其中的数据时,可以通过文件映射将文件映射到内存中,这样数据库系统就可以直接在内存中快速查找和读取数据,大大提高了数据访问的速度。在进行文件映射时,通常会使用mmap函数,通过设置合适的参数,如文件描述符、映射长度、权限等,来实现文件到内存的映射 。
匿名映射:匿名映射与文件映射不同,它没有文件支持,是直接将物理内存映射到进程的虚拟地址空间,没有明确的数据源 。匿名映射通常用于需要分配一段临时内存的场景,比如进程在运行过程中需要创建一些临时的数据结构,这些数据不需要持久化存储在文件中,就可以使用匿名映射来分配内存 。在 C 语言中,当我们使用malloc函数申请较大内存时(通常大于 128KB,这个阈值可能因系统而异),glibc库的内存分配器ptmalloc会使用mmap进行匿名映射来向内核申请虚拟内存 。
在多线程编程中,当一个线程需要分配一些私有的临时内存来存储中间计算结果时,也可以使用匿名映射。匿名映射的特点是数据只存在于内存中,进程结束后,映射的内存会被自动回收,不会对磁盘文件产生任何影响 。在使用mmap函数进行匿名映射时,需要设置MAP_ANONYMOUS标志,同时文件描述符参数fd一般设置为 - 1 。
(2)按权限分私有映射:写时复制,变更不会再底层文件进行共享映射:变更发生在底层文件
将上面两两组合:
私有文件映射:使用一个文件的内容来初始化一块内存区域私有匿名映射:为一个进程分配新的内存共享文件映射:代替 read() 和 write() 、IPC共享匿名映射:实现相关进程实现类似于共享内存
进程执行 exec() 时映射会丢失,但通过 fork() 的子进程会继承映射
3.2API函数
(1)创建一个映射
复制
#include <sys/mman.h>
void *mmap( void *addr, size_t length, int prot, int flags, int fd, off_t offset );1.2.
成功返回新映射的起始地址,失败返回 MAP_FAILED。
复制
参数 addr:映射被放置的虚拟地址,推荐为NULL(内核会自动选择合适地址)
参数 length:映射的字节数
参数 prot:位掩码,可以取OR1.2.3.
违反了保护信息,内核会向进程发送SIGSEGV信号。
PROT_NONE:区域无法访问,可以作为一个进程分配的起始位置或结束位置的守护分页PROT_WRITE:区域内容可修改PROT_READ:区域内容可读取PROT_EXEC:区域内容可执行
复制
参数 flags:位掩码,必须包含下列值中的一个
MAP_PROVATE:创建私有映射
MAP_SHARED:创建共享映射1.2.3.
参数 fd:被映射的文件的文件描述符(调用之后就能够关闭文件描述符)。在打开描述符 fd 引用的文件时必须要具备与 prot 和 flags参数值匹配的权限。特别的,文件必须总是被打开允许读取。
参数 offset:映射在文件中的起点
(2)解除映射区域
复制
#include <sys/mman.h>
int munmap( void *addr, size_t length );1.2.
参数 addr:待解除映射的起始地址参数 length:待解除映射区域的字节数
可以解除一个映射的部分映射,这样原来的映射要么收缩,要么被分成两个,这取决于在何处开始解除映射。还可以指定一个跨越多个映射的地址范围,这样的话所有在范围内的映射都会被解除。
(3)同步映射区域
复制
#include <sys/mman.h>
int msync( void *addr, size_t length, int flags );1.2.
参数 flags:
MS_SYNC:阻塞直到内存区域中所有被修改过的分页被写入磁盘MS_ASYNC:在某个时刻被写入磁盘(4)重写映射一个映射区域
复制
#define _GNU_SOURCE
#include <sys/mman.h>
void *mremap( void *old_address, size_t old_size, size_t new_size, int fflags, ... );1.2.3.
参数 old_address 和 old_size 指既有映射的位置和大小。参数 new_size 指定新映射的大小
复制
参数 flags:
0
MREMAP_MAYMOVE:为映射在进程的虚拟地址空间中重新指定一个位置
MREMAP_FIXED:配合 MREMAP_MAYMOVE 一起使用,mremap 会接收一个额外的参数 void *new_address1.2.3.4.
(5)创建私有文件映射
创建一个私有文件映射,并打印文件内容
复制
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
int main( int argc, char **argv )
{
int fd = open( argv[1], O_RDONLY );
if( fd == -1 ) {
perror("open");
}
/*获取文件信息*/
struct stat sb;
if( fstat( fd, &sb ) == -1 ) {
perror("fstat");
}
/*私有文件映射*/
char *addr = mmap( NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0 );
if( addr == MAP_FAILED ) {
perror("mmap");
}
/*将addr的内容写到标准输出*/
if( write( STDOUT_FILENO, addr, sb.st_size ) != sb.st_size ) {
perror("write");
}
exit( EXIT_SUCCESS );
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.
(6)创建共享匿名映射
复制
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
int main( int argc, char **argv )
{
/*获取虚拟设备的文件描述符*/
int fd = open( "/dev/zero", O_RDWR );
if( fd == -1 ) {
perror("open");
}
int *addr = mmap( NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 );
if( addr == MAP_FAILED ) {
perror("mmap");
}
if( close( fd ) == -1 ) {
perror("close");
}
*addr = 1;
switch( fork() ) {
case -1:
perror("fork");
break;
case 0:
printf("child *addr = %d\n", *addr);
(*addr)++;
/*解除映射*/
if( munmap(addr, sizeof(int)) == -1 ) {
perror("munmap");
}
_exit( EXIT_SUCCESS );
break;
default:
/*等待子进程结束*/
if( wait(NULL) == -1 ) {
perror("wait");
}
printf("parent *addr = %d\n", *addr );
if( munmap( addr, sizeof(int) ) == -1 ) {
perror("munmap");
}
exit( EXIT_SUCCESS );
break;
}
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.
三、内存映射系统调用mmap
在 Linux 内存映射的实现过程中,mmap函数扮演着核心角色,它就像是一把神奇的钥匙,能够打开内存与文件之间的通道,让我们可以轻松地进行内存映射操作 。
3.1mmap函数参数详解
mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。
注:实际上,mmap()系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。而Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。
mmap()系统调用形式如下:
复制
void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )1.
参数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()的参数,读者可参考mmap()手册页获得进一步的信息。
下面来详细解读一下各个参数的含义和作用 :
start:它是映射区的开始地址,当我们将其设置为NULL时,就意味着把选择映射起始地址的权利交给了内核,内核会根据系统的实际情况,在进程地址空间中挑选一个合适的地址来建立映射 。比如在一个进程中,我们调用mmap函数并将start设为NULL,内核就会在该进程可用的虚拟地址空间里找到一段满足条件的连续地址作为映射的起始点 。
length:这个参数表示映射区的长度,也就是我们希望将文件的多大区域映射到内存中,它决定了映射区域的大小 。假如我们有一个 10MB 的文件,而我们只想将其中的 1MB 映射到内存中进行操作,那么就可以将length设置为 1MB 对应的字节数 。
prot:它指定了期望的内存保护标志,这个标志不能与文件的打开模式产生冲突,常见的取值有以下几种 :
PROT_EXEC:表示映射的页内容可以被执行,当我们映射的是一个可执行文件或者共享库中的代码段时,就需要设置这个标志 。比如在运行一个 C 语言程序时,程序中的可执行代码部分被映射到内存中,就会设置PROT_EXEC标志,使得这些代码能够被 CPU 执行 。PROT_READ:意味着页内容可以被读取,这是最常用的标志之一,当我们需要读取文件内容时,就会设置这个标志 。比如读取一个文本文件的内容,就需要设置PROT_READ标志来允许对映射区域进行读取操作 。PROT_WRITE:表示页可以被写入,如果我们想要对映射的文件进行修改,就需要设置这个标志 。例如我们打开一个文件进行读写操作,在调用mmap函数时就需要设置PROT_WRITE标志 。PROT_NONE:表示页不可访问,这种情况比较特殊,一般用于某些特定的内存管理场景,比如在隔离一些敏感数据区域时可能会用到 。
flags:该参数指定了映射对象的类型、映射选项以及映射页是否可以共享,它的值可以是一个或者多个以下位的组合体 :
MAP_SHARED:这个标志非常重要,它表示与其它所有映射这个对象的进程共享映射空间 。当一个进程对共享区进行写入操作时,就相当于输出到文件 。不过,直到调用msync函数或者munmap函数,文件实际上才会被更新 。在多进程协作处理同一个文件的场景中,就可以使用MAP_SHARED标志 。比如多个进程需要同时读取和修改一个配置文件,通过设置MAP_SHARED标志,它们可以共享同一个映射空间,实现数据的实时同步 。MAP_PRIVATE:用于建立一个写入时拷贝的私有映射,在这种映射方式下,内存区域的写入不会影响到原文件 。这个标志和MAP_SHARED是互斥的,只能使用其中一个 。当我们希望对文件进行一些临时的修改,而又不想影响原文件时,就可以使用MAP_PRIVATE标志 。比如在对一个文件进行临时的分析和处理时,我们可以使用MAP_PRIVATE映射,对映射区域的修改不会改变原文件 。MAP_ANONYMOUS:表示匿名映射,即映射区不与任何文件关联 。当我们需要分配一段临时的内存空间,而不需要从文件中读取数据或者将数据写入文件时,就可以使用匿名映射 。比如在进行一些临时的计算任务时,我们可以使用MAP_ANONYMOUS标志分配一块内存来存储中间结果 。MAP_FIXED:使用指定的映射起始地址,如果由start和length参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃 。如果指定的起始地址不可用,操作将会失败,并且起始地址必须落在页的边界上 。一般情况下,我们不建议使用这个标志,因为它可能会导致一些不可预测的问题,除非我们对内存布局有非常明确的需求 。
fd:它是有效的文件描述词,用于标识要映射的文件 。当我们进行文件映射时,需要先使用open函数打开文件,然后将返回的文件描述符传递给mmap函数 。如果设置了MAP_ANONYMOUS标志,为了兼容问题,其值应为 - 1 。比如我们要映射一个名为test.txt的文件,首先使用open("test.txt", O_RDWR)打开文件,得到文件描述符fd,然后将fd传递给mmap函数进行映射操作 。
offset:表示被映射对象内容的起点,也就是从文件的哪个位置开始映射,这个值必须是分页大小的整数倍 。在大多数情况下,我们会将其设置为 0,表示从文件的开头开始映射 。比如文件的分页大小是 4KB,如果我们想从文件的第 8KB 位置开始映射,那么offset就应该设置为 8KB 。
系统调用mmap()用于共享内存的两种方式:
①使用普通文件提供的内存映射:适用于任何进程之间;此时,需要打开或创建一个文件,然后再调用mmap();典型调用代码如下:
复制
fd=open(name, flag, mode);
if(fd<0
...1.2.3.
ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 通过mmap()实现共享内存的通信方式有许多特点和要注意的地方,我们将在范例中进行具体说明。
②使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间;由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区域进行通信了。注意,这里不是一般的继承关系。一般来说,子进程单独维护从父进程继承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。
对于具有亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射的方式。此时,不必指定具体的文件,只要设置相应的标志即可,参见范例2。
系统调用munmap()
复制
int munmap( void * addr, size_t len )1.
该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。
系统调用msync()
复制
int msync ( void * addr , size_t len, int flags)1.
一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。
3.2返回值解析
mmap函数的返回值也很关键,它能告诉我们映射操作是否成功 。当mmap成功执行时,会返回被映射区的指针,我们可以通过这个指针来访问映射的内存区域 。例如:
复制
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
int fd = open("test.txt", O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
size_t length = 1024; // 映射1024字节
void *ptr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
// 使用ptr访问映射区域
//...
// 解除映射
if (munmap(ptr, length) == -1) {
perror("munmap");
}
close(fd);
return 0;
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.
在上述代码中,如果mmap函数成功,ptr就会指向映射区的起始地址,我们可以通过ptr来对映射区域进行读写等操作 。而当mmap函数失败时,会返回MAP_FAILED,其值为(void *)-1,同时errno会被设置为相应的错误代码,以指示错误的原因 。
常见的错误原因包括:
EACCES:表示访问出错,可能是因为权限不足,比如我们尝试以写权限映射一个只读文件时,就会出现这个错误 。EBADF:意味着fd不是有效的文件描述词,可能是文件没有正确打开或者文件描述符已经被关闭 。EINVAL:表示一个或者多个参数无效,比如length为负数,或者offset不是分页大小的整数倍等 。ENOMEM:表示内存不足,或者进程已超出最大内存映射数量,当系统内存紧张,无法为映射分配足够的内存时,就会出现这个错误 。
3.3mmap系统调用和直接使用IPC共享内存之间的差异
mmap系统调用用于将文件映射到进程的地址空间中,而共享内存是一种不同的机制,用于进程间通信。这两种方法都用于数据共享和高效的内存访问,但它们有一些关键区别:
(1)数据源和持久化mmap: 通过 mmap 映射的数据通常来自文件系统中的文件。这意味着数据是持久化的——即使程序终止,文件中的数据依然存在。当你通过映射的内存区域修改数据时,这些更改最终会反映到磁盘上的文件中。共享内存:共享内存是一块匿名的(或者有时与特定文件关联的)内存区域,它可以被多个进程访问。与 mmap 映射的文件不同,共享内存通常是非持久的,即数据仅在计算机运行时存在,一旦系统关闭或重启,存储在共享内存中的数据就会丢失。(2)使用场景mmap:mmap 特别适合于需要频繁读写大文件的场景,因为它可以减少磁盘 I/O 操作的次数。它也允许文件的一部分被映射到内存中,这对于处理大型文件尤为有用。共享内存:共享内存通常用于进程间通信(IPC),允许多个进程访问相同的内存区域,这样可以非常高效地在进程之间交换数据。(3)性能和效率mmap:映射文件到内存可以提高文件访问的效率,尤其是对于随机访问或频繁读写的场景。系统可以利用虚拟内存管理和页面缓存机制来优化访问。共享内存:共享内存提供了一种非常快速的数据交换方式,因为所有的通信都在内存中进行,没有文件 I/O 操作。(4)同步和一致性mmap:使用 mmap 时,必须考虑到文件内容的同步问题。例如,使用 msync 调用来确保内存中的更改被同步到磁盘文件中。共享内存:在共享内存的环境中,进程需要使用某种形式的同步机制(如信号量、互斥锁)来避免竞争条件和数据不一致。
四、存储映射I/O
在现在的项目中需要用到mmap建立内存映射文件,顺便把存储映射I/O看了一下,这个东西还真是加载索引的良好工具,存储映射I/O可以使一个磁盘文件与存储空间中的一个缓冲区相映射,这样可以从缓冲区中读取数据,就相当于读文件中的相应字节,而当将数据存入缓冲区时,最后相应字节就自动写入文件中。
利用mmap建立内存映射文件一般会分为两条线:写文件,读文件,在分别介绍这两条线之前首先将存储映射I/O的常用函数介绍一下。
4.1存储映射I/O基本函数
(1) mmap函数, 这个函数会告诉内核将一个给定的文件映射到一个存储区域中,其函数原型为:
复制
void* mmap(void *addr,size_t len,int prot,int flags,int fields,off_t off);1.
其中,参数addr用于指定存储映射区的起始地址,通常设定为0,这表示由系统选择该映射区的起始地址,参数len是指定映射的字节数,参数port指定映射区的方式,如PROT_READ,PROT_WRITE,值得注意的是映射区的保护不能超过文件open模式访问权限。参数flags是设置映射区的属性,一般设为MAP_SHARED,这一标志说明本进程的存储操作相当于文件的write操作,参数fields是指定操作的文件描述符,参数off是要映射字节在文件中的起始偏移量。如果函数调用成功,函数的返回值是存储映射区的起始地址;如果调用失败,则返回MAP_FAILED。
(2) msync函数,这个函数会将存储映射区的修改冲洗到被映射的文件中,其函数原型为:
复制
int msync(void *addr,size_t len,int flags)1.
其中,参数flags参数设定如何控制冲洗存储区,可以选择MS_ASYNC,这表明是异步操作,函数调用立即返回,而选择MS_SYNC,函数调用则等待写操作完成后才会返回。
(3) munmap函数,这个函数会解除文件和存储映射区之间的映射。
复制
int munmap(caddr_t addr,size_t len)1.
4.2写入映射缓冲区
当我们想向映射缓冲区中写入数据时,首先需要确定映射文件的大小,在打开文件后,可以利用修改文件大小的函数重新设定文件的大小,接下来就可以对该缓冲区进行写操作。
复制
int fd = open(file_name,O_RDWR|O_CREAT);
ftruncate(fd,size);
mmap(0,size,PROT_WRITE,MAP_SHARED,fd,0);1.2.3.
4.3从映射缓冲区读取
当我们想从映射缓冲区中读取数据时,需要利用stat系列函数得到文件大小,进行利用在映射存储区中打开该文件。
复制
int fd = open(file_name,O_RDONLY);
struct stat stat_buf;
fstat(fd,&stat_buf);
void *data = mmap(0,stat_buf.st_size,PROT_READ,
MAP_SHARED,fd,0);1.2.3.4.5.
4.4实例:用存储映射 I/O 复制文件
复制
#include "apue.h"
#include <fcntl.h>
#include <sys/mman.h>
#define COPYINCR (1024*1024*1024) /* 1 GB */
int
main(int argc, char *argv[])
{
int fdin, fdout;
void *src, *dst;
size_t copysz;
struct stat sbuf;
off_t fsz = 0;
if (argc != 3)
err_quit("usage: %s <fromfile> <tofile>", argv[0]);
if ((fdin = open(argv[1], O_RDONLY)) < 0)
err_sys("cant open %s for reading", argv[1]);
if ((fdout = open(argv[2], O_RDWR | O_CREAT | O_TRUNC,
FILE_MODE)) < 0)
err_sys("cant creat %s for writing", argv[2]);
if (fstat(fdin, &sbuf) < 0) /* need size of input file */
err_sys("fstat error");
if (ftruncate(fdout, sbuf.st_size) < 0) /* set output file size */
err_sys("ftruncate error");
while (fsz < sbuf.st_size) {
if ((sbuf.st_size - fsz) > COPYINCR)
copysz = COPYINCR;
else
copysz = sbuf.st_size - fsz;
if ((src = mmap(0, copysz, PROT_READ, MAP_SHARED,
fdin, fsz)) == MAP_FAILED)
err_sys("mmap error for input");
if ((dst = mmap(0, copysz, PROT_READ | PROT_WRITE,
MAP_SHARED, fdout, fsz)) == MAP_FAILED)
err_sys("mmap error for output");
memcpy(dst, src, copysz); /* does the file copy */
munmap(src, copysz);
munmap(dst, copysz);
fsz += copysz;
}
exit(0);
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.
五、内存映射的应用场景
5.1大文件处理
在当今数据爆炸的时代,处理大文件是许多应用场景中不可避免的挑战。无论是数据分析、多媒体处理还是日志管理,大文件的读写操作都对系统性能提出了极高的要求 。传统的文件I/O方式在面对大文件时往往显得力不从心,频繁的磁盘 I/O 操作会导致系统性能大幅下降,而内存映射技术则为大文件处理提供了一种高效的解决方案 。
内存映射之所以能显著提升大文件处理效率,关键在于它减少了数据的读写次数。以一个 1GB 的日志文件为例,假设我们需要统计其中特定关键词出现的次数。如果使用传统的read函数逐块读取文件内容到用户空间进行处理,每读取一次都涉及数据从磁盘到内核缓冲区,再到用户空间的拷贝过程 。
而采用内存映射,文件直接被映射到进程的虚拟地址空间,进程可以像访问内存一样直接读取文件内容,避免了数据在不同缓冲区之间的多次拷贝,大大减少了 I/O 操作的开销 。并且,内存映射还可以利用操作系统的页缓存机制,当进程访问映射区域中的数据时,如果数据已经在页缓存中,就可以直接从内存中读取,无需再次访问磁盘,进一步提高了数据访问的速度 。
5.2进程间通信
在多进程编程的世界里,进程间通信(IPC)是实现不同进程之间数据交互和协作的关键。内存映射为进程间通信提供了一种高效且直接的方式,通过映射同一文件或匿名内存,不同进程可以共享同一块内存区域,实现数据的实时共享和交互 。
以一个简单的生产者 - 消费者模型为例,生产者进程和消费者进程需要共享一个数据缓冲区。我们可以通过内存映射创建一个共享的内存区域,生产者进程将数据写入这个共享区域,消费者进程则从该区域读取数据 。在这个过程中,内存映射利用了操作系统的虚拟内存机制,使得不同进程的虚拟地址可以映射到相同的物理内存页,从而实现数据的共享 。
并且,为了保证数据的一致性和同步性,通常会结合信号量、互斥锁等同步机制来协调不同进程对共享内存的访问 。比如,生产者在向共享内存写入数据前,先获取互斥锁,防止其他进程同时写入;写入完成后,释放互斥锁,并发送信号量通知消费者有新数据可用 。这样,通过内存映射和同步机制的配合,不同进程可以高效、安全地进行数据共享和通信 。
5.3动态库加载
在 Linux 系统中,动态库是一种重要的代码共享机制,它允许多个程序共享同一份代码和数据,从而节省内存空间和磁盘空间 。内存映射在动态库加载过程中扮演着至关重要的角色,它将动态库的代码段和数据段映射到进程的虚拟地址空间,使得进程能够高效地访问动态库中的函数和变量 。
当一个可执行程序依赖于某个动态库时,在程序启动阶段,系统会通过内存映射将动态库加载到内存中。具体来说,动态链接器(如ld.so)会首先解析可执行文件的依赖关系,找到需要加载的动态库 。然后,它使用内存映射将动态库的代码段映射到进程的虚拟地址空间,并设置相应的权限,如代码段通常设置为只读和可执行权限 。对于动态库的数据段,也会根据其属性进行映射,如全局变量所在的数据段可能设置为可读写权限 。
在映射过程中,动态链接器还会处理动态库中的符号表,将可执行文件中的符号引用与动态库中的实际函数和变量地址进行绑定,确保程序在运行时能够正确地调用动态库中的功能 。通过内存映射加载动态库,不仅提高了程序的启动速度,还实现了代码的共享,多个进程可以共享同一个动态库的内存映射,减少了内存的占用 。
六、实战演练:代码中的内存映射
6.1简单文件读写示例
下面是一个使用mmap函数进行文件读写的简单示例代码:
复制
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
const char *filepath = "example.txt";
// 打开文件
int fd = open(filepath, O_RDWR);
if (fd < 0) {
perror("open");
return EXIT_FAILURE;
}
// 获取文件大小
struct stat sb;
if (fstat(fd, &sb) == -1) {
perror("fstat");
close(fd);
return EXIT_FAILURE;
}
// 将文件映射到内存
char *mapped = mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mapped == MAP_FAILED) {
perror("mmap");
close(fd);
return EXIT_FAILURE;
}
// 现在可以像操作普通内存一样访问文件内容
printf("File contents before modification:\n%s\n", mapped);
// 修改文件内容
strcpy(mapped, "Hello, mmap!");
// 解除映射
if (munmap(mapped, sb.st_size) == -1) {
perror("munmap");
}
// 关闭文件
close(fd);
return EXIT_SUCCESS;
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.
代码解释:
打开文件:使用open函数打开名为example.txt的文件,O_RDWR标志表示以读写模式打开文件。如果打开失败,通过perror函数打印错误信息并返回。获取文件大小:利用fstat函数获取文件的相关信息,包括文件大小,将结果存储在sb结构体中。若获取失败,同样打印错误信息并关闭文件返回。内存映射:调用mmap函数将文件映射到内存中。NULL表示让系统自动选择映射的起始地址,sb.st_size指定映射的长度为文件的大小,PROT_READ | PROT_WRITE表示映射区域具有可读可写权限,MAP_SHARED表示映射区域的修改会同步到文件,fd是前面打开文件返回的文件描述符,0表示从文件开头开始映射。如果映射失败,打印错误信息并关闭文件返回。访问和修改映射区域:通过mapped指针可以像操作普通内存一样访问和修改文件内容。这里使用strcpy函数将字符串Hello, mmap!复制到映射区域,从而修改了文件的内容。解除映射:使用munmap函数解除内存映射,参数为映射的起始地址mapped和映射长度sb.st_size。如果解除失败,打印错误信息。关闭文件:最后使用close函数关闭文件。
6.2进程间通信示例
以下是通过内存映射实现进程间通信的示例代码,这里以父子进程为例:
复制
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define SHM_SIZE 1024
int main() {
int fd;
char *shared_memory;
pid_t pid;
// 创建一个临时文件用于内存映射
fd = open("temp_file", O_RDWR | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return EXIT_FAILURE;
}
// 拓展文件大小
if (lseek(fd, SHM_SIZE - 1, SEEK_SET) == -1) {
perror("lseek");
close(fd);
return EXIT_FAILURE;
}
// 写入一个字节,使文件大小达到SHM_SIZE
if (write(fd, "", 1) != 1) {
perror("write");
close(fd);
return EXIT_FAILURE;
}
// 将文件映射到内存
shared_memory = (char *)mmap(0, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (shared_memory == MAP_FAILED) {
perror("mmap");
close(fd);
return EXIT_FAILURE;
}
// 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
munmap(shared_memory, SHM_SIZE);
close(fd);
return EXIT_FAILURE;
} else if (pid == 0) {
// 子进程
strcpy(shared_memory, "Hello from child!");
_exit(EXIT_SUCCESS);
} else {
// 父进程
wait(NULL);
printf("Data read from shared memory: %s\n", shared_memory);
wait(NULL);
}
// 解除映射
if (munmap(shared_memory, SHM_SIZE) == -1) {
perror("munmap");
}
// 关闭文件
close(fd);
// 删除临时文件
if (unlink("temp_file") == -1) {
perror("unlink");
}
return EXIT_SUCCESS;
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.77.78.
关键步骤分析:
创建临时文件并拓展大小:使用open函数创建一个名为temp_file的临时文件,O_RDWR | O_CREAT | O_TRUNC标志表示以读写模式创建文件,如果文件存在则截断。然后通过lseek函数将文件指针移动到SHM_SIZE - 1的位置,再使用write函数写入一个字节,使文件大小达到SHM_SIZE,为后续的内存映射做准备。内存映射:调用mmap函数将临时文件映射到内存中,得到一个指向共享内存区域的指针shared_memory。创建子进程:使用fork函数创建子进程。子进程和父进程会共享这个内存映射区域。子进程操作:在子进程中,使用strcpy函数将字符串Hello from child!复制到共享内存区域,然后调用_exit函数退出子进程。父进程操作:父进程通过wait函数等待子进程结束,然后从共享内存区域读取数据并打印。清理资源:最后,父进程解除内存映射,关闭文件,并删除临时文件,释放相关资源。
6.3分块内存映射处理大文件示例
(1)内存映射文件可以用于3个不同的目的:
系统使用内存映射文件,以便加载和执行. exe和DLL文件。这可以大大节省页文件空间和应用程序启动运行所需的时间。可以使用内存映射文件来访问磁盘上的数据文件。这使你可以不必对文件执行I/O操作,并且可以不必对文件内容进行缓存。可以使用内存映射文件,使同一台计算机上运行的多个进程能够相互之间共享数据。Windows确实提供了其他一些方法,以便在进程之间进行数据通信,但是这些方法都是使用内存映射文件来实现的,这使得内存映射文件成为单个计算机上的多个进程互相进行通信的最有效的方法。
(2)使用内存映射数据文件
若要使用内存映射文件,必须执行下列操作步骤:
1) 创建或打开一个文件内核对象,该对象用于标识磁盘上你想用作内存映射文件的文件。2) 创建一个文件映射内核对象,告诉系统该文件的大小和你打算如何访问该文件。3) 让系统将文件映射对象的全部或一部分映射到你的进程地址空间中。
当完成对内存映射文件的使用时,必须执行下面这些步骤将它清除:
1) 告诉系统从你的进程的地址空间中撤消文件映射内核对象的映像。2) 关闭文件映射内核对象。3) 关闭文件内核对象。
文件操作是应用程序最为基本的功能之一,Win32 API和MFC均提供有支持文件处理的函数和类,常用的有Win32 API的CreateFile()、WriteFile()、ReadFile()和MFC提供的CFile类等。一般来说,以上这些函数可以满足大多数场合的要求,但是对于某些特殊应用领域所需要的动辄几十GB、几百GB、乃至几TB的海量存储,再以通常的文件处理方法进行处理显然是行不通的。所以可以使用内存文件映射来处理数据,网上也有铺天盖地的文章,但是映射大文件的时候又往往会出错,需要进行文件分块内存映射,这里就是这样的一个例子,教你如何把文件分块映射到内存。
复制
//
// 该函数用于读取从CCD摄像头采集来的RAW视频数据当中的某一帧图像,
// RAW视频前596字节为头部信息,可以从其中读出视频总的帧数,
// 帧格式为1024*576*8
/*
参数:
pszPath:文件名
dwFrame: 要读取第几帧,默认读取第2帧
*/
BOOL MyFreeImage::LoadXRFrames(TCHAR *pszPath, DWORD dwFrame/* = 2*/ )
{
// get the frames of X-Ray frames
BOOL bLoop = TRUE;
int i;
int width = 1024;
int height = 576;
int bitcount = 8; //1, 4, 8, 24, 32
//
//Build bitmap header
BITMAPFILEHEADER bitmapFileHeader;
BITMAPINFOHEADER bitmapInfoHeader;
BYTE rgbquad[4]; // RGBQUAD
int index = 0;
DWORD widthbytes = ((bitcount*width + 31)/32)*4; //每行都是4的倍数 DWORD的倍数 这里是 576-
TRACE1("widthbytes=%d\n", widthbytes);
switch(bitcount) {
case 1:
index = 2;
bitmapFileHeader.bfOffBits = (DWORD)(sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + 2*4);
break;
case 4:
index = 16;
bitmapFileHeader.bfOffBits = (DWORD)(sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + 16*4);
break;
case 8:
index = 256;
bitmapFileHeader.bfOffBits = (DWORD)(sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + 256*sizeof(RGBQUAD));
break;
case 24:
case 32:
index = 0;
bitmapFileHeader.bfOffBits = (DWORD)(sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER));
break;
default:
break;
}
//构造Bitmap文件头BITMAPFILEHEADER
bitmapFileHeader.bfType = 0x4d42; // 很重要的标志位 BM 标识
bitmapFileHeader.bfSize = (DWORD)(bitmapFileHeader.bfOffBits + height * widthbytes); //bmp文件长度
bitmapFileHeader.bfReserved1 = 0;
bitmapFileHeader.bfReserved2 = 0;
//构造Bitmap文件信息头BITMAPINFOHEADER
bitmapInfoHeader.biSize = sizeof(BITMAPINFOHEADER);
bitmapInfoHeader.biWidth = width;
bitmapInfoHeader.biHeight = height;
bitmapInfoHeader.biPlanes = 1;
bitmapInfoHeader.biBitCount = bitcount;
bitmapInfoHeader.biCompression = BI_RGB; // 未压缩
bitmapInfoHeader.biSizeImage = height * widthbytes;
bitmapInfoHeader.biXPelsPerMeter = 3780;
bitmapInfoHeader.biYPelsPerMeter = 3780;
bitmapInfoHeader.biClrUsed = 0;
bitmapInfoHeader.biClrImportant = 0;
//创建BMP内存映像,写入位图头部
BYTE *pMyBmp = new BYTE[bitmapFileHeader.bfSize]; // 我的位图pMyBmp
BYTE *curr = pMyBmp; // curr指针指示pMyBmp的位置
memset(curr, 0, bitmapFileHeader.bfSize);
//写入头信息
memcpy(curr, &bitmapFileHeader,sizeof(BITMAPFILEHEADER));
curr = pMyBmp + sizeof(BITMAPFILEHEADER);
memcpy(curr, &bitmapInfoHeader,sizeof(BITMAPINFOHEADER));
curr += sizeof(BITMAPINFOHEADER);
//构造调色板 , 当像素大于8位时,就没有调色板了。
if(bitcount == 8)
{
rgbquad[3] = 0; //rgbReserved
for(i = 0; i < index; i++)
{
rgbquad[0] = rgbquad[1] = rgbquad[2] = i;
memcpy(curr, rgbquad, sizeof(RGBQUAD));
curr += sizeof(RGBQUAD);
}
}else if(bitcount == 1)
{
rgbquad[3] = 0; //rgbReserved
for(i = 0; i < index; i++)
{
rgbquad[0] = rgbquad[1] = rgbquad[2] = (256 - i)%256;
memcpy(curr, rgbquad, sizeof(RGBQUAD));
curr += sizeof(RGBQUAD);
}
}
//
// 文件映射,从文件中查找图像的数据
//Open the real file on the file system
HANDLE hFile = CreateFile(pszPath, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
DWORD dwError = GetLastError();
ATLTRACE(_T("MapFile, Failed in call to CreateFile, Error:%d\n"), dwError);
SetLastError(dwError);
bLoop = FALSE;
return FALSE;
}
//Create the file mapping object
HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL);
if (hMapping == NULL)
{
DWORD dwError = GetLastError();
ATLTRACE(_T("MapFile, Failed in call to CreateFileMapping, Error:%d\n"), dwError);
// Close handle
if (hFile != INVALID_HANDLE_VALUE)
{
CloseHandle(hFile);
hFile = INVALID_HANDLE_VALUE;
}
SetLastError(dwError);
bLoop = FALSE;
return FALSE;
}
// Retrieve allocation granularity
SYSTEM_INFO sinf;
GetSystemInfo(&sinf);
DWORD dwAllocationGranularity = sinf.dwAllocationGranularity;
// Retrieve file size
// Retrieve file size
DWORD dwFileSizeHigh;
__int64 qwFileSize = GetFileSize(hFile, &dwFileSizeHigh);
qwFileSize |= (((__int64)dwFileSizeHigh) << 32);
CloseHandle(hFile);
// Read Image
__int64 qwFileOffset = 0; // 偏移地址
DWORD dwBytesInBlock = 0, // 映射的块大小
dwStandardBlock = 100* dwAllocationGranularity ; // 标准块大小
DWORD dwFrameSize = height*width; // 计算一帧图像的数据量,不包括头部信息
DWORD dwCurrentFrame = 1;
dwBytesInBlock = dwStandardBlock;
if (qwFileSize < dwStandardBlock)
dwBytesInBlock = (DWORD)qwFileSize;
//Map the view
LPVOID lpData = MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS,
static_cast<DWORD>((qwFileOffset & 0xFFFFFFFF00000000) >> 32), static_cast<DWORD>(qwFileOffset & 0xFFFFFFFF), dwBytesInBlock);
if (lpData == NULL)
{
DWORD dwError = GetLastError();
ATLTRACE(_T("MapFile, Failed in call to MapViewOfFile, Error:%d\n"), dwError);
// Close Handle
if (hMapping != NULL)
{
CloseHandle(hMapping);
hMapping = NULL;
}
SetLastError(dwError);
bLoop = FALSE;
return FALSE;
}
BYTE *lpBits = (BYTE *)lpData;
BYTE *curr1, *curr2, *lpEnd;
curr1 = lpBits; // seek to start
curr2 = lpBits + 596; // seek to first frame
lpEnd = lpBits + dwBytesInBlock; // seek to end
// Read video infomation
KMemDataStream streamData( curr1, dwBytesInBlock);
ReadXRHeader(streamData);
while(bLoop)
{
DWORD dwTmp = lpEnd - curr2; //内存缓冲剩余的字节
if ( dwTmp >= dwFrameSize )
{
if(dwCurrentFrame == dwFrame)
{
memcpy(curr, curr2, dwFrameSize);
bLoop = FALSE;
}
curr2 += dwFrameSize;
}else //内存中不够一帧数据
{
DWORD dwTmp2 = dwFrameSize - dwTmp; // 一副完整的帧还需要dwTmp2字节
if (dwCurrentFrame == dwFrame)
{
memcpy(curr, curr2, dwTmp);
curr += dwTmp;
}
//1、首先计算文件的偏移位置
qwFileOffset += dwBytesInBlock;1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.77.78.79.80.81.82.83.84.85.86.87.88.89.90.91.92.93.94.95.96.97.98.99.100.101.102.103.104.105.106.107.108.109.110.111.112.113.114.115.116.117.118.119.120.121.122.123.124.125.126.127.128.129.130.131.132.133.134.135.136.137.138.139.140.141.142.143.144.145.146.147.148.149.150.151.152.153.154.155.156.157.158.159.160.161.162.163.164.165.166.167.168.169.170.171.172.173.174.175.176.177.178.179.180.181.182.183.184.185.186.187.188.189.190.191.192.193.194.195.196.197.198.199.200.201.202.203.204.205.206.207.208.209.210.211.212.