本文介绍在POSIX环境使用文件映射IO操作的方法,文件映射IO又被称为存储映射IO,对于普通文件而言,很多时候它是高效的,它实际减少了数据的复制;同时它也可以用于特殊的地方,用于进程之间的通信,共享内存的一种方式。
我们能够把一个文件想象成一块连续的数据,从纯粹的数据角度来看,任何普通文件都可以这么理解。文件映射实际上是把文件的这块数据与我们程序里的一块内存对应上了,使用我们操作这块内存的时候,看上去实际在操作这个文件。这就是文件映射的概念。这个概念很伟大,它直接避免了内核与用户之间的一层数据复制,所以很多时候,它会比其它方式的文件操作更快一些,尤其对于普通的磁盘文件而言更是如此。
创建映射区:mmap
我们通过函数mmap来告诉操作系统把哪个文件映射哪块内存去,并且设置我们可能对这块内存的不能操作,就是对文件一样。
#include<sys/mman.h>
void* mmap(void* addr, size_t len, int port, int flag, int filedes,
off_t off)
返回值:成功返回被映射的内存地址,失败返回MAP_FIALED
参数 addr
这个只有在极少数情况下才不为0,这个参数告诉内核使用addr指定的值来映射指定文件。当指定为0的时候,告诉内核返回什么地址内其自身决定。除非非常了解系统进程模式,或者对当前环境非常了解,否则的话手工指定这个值总是不可取。
参数 len
指定被映射的内存区域的长度。
参数 port
这个参数对应open函数的权限位,我们可以指定为:PROT_READ,映射区可读;PROT_WRITE,映射区可写;PROT_EXEC,映射区可执行;PROT_NONE,映射区不可访问。由于只能映射已经打开的文件,所以这个权限位不能超出open函数指定的权限,比如说在open的时候指定为只读,那就不能在此时指定PORT_WRITE。
参数 flag
这个参数指定了映射区的其它一些属性,权限的属性已经在port中指定。这里可能存在的典型值有:MAP_FIXED,针对addr属性,如果指定这个位,那么要求系统必需在指定的地址映射,这往往是不可取的;MAP_SHARED,此标志说明指定映射区是共享的,意思就是说对内存的操作与对文件的操作是相对应的,它不能与MAP_PRIVATE标志一直使用,因为它们表达的意图是相反的;MAP_PRIVATE,该标志说明映射区是私用的,此时被映射的内存只能被当前里程使用,当进程操作的内存将会产生原文件的一个副本。
MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。
MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
MAP_DENYWRITE //这个标志被忽略。
MAP_EXECUTABLE //同上
MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。
MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。
MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。
MAP_ANON //MAP_ANONYMOUS的别称,不再被使用。
MAP_FILE //兼容标志,被忽略。
MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。
MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。
fd:有效的文件描述词。一般是由open()函数返回,其值也可以设置为-1,此时需要指定flags参数中的MAP_ANON,表明进行的是匿名映射。
off_toffset:被映射对象内容的起点。
设置与同步映射区:mprotect、msync
在mmap中我们有很多选项来控制最后得到的映射区的一些属性,在调用mmap函数之后,我仍然可以对其中的一些属性进行调整,这通过mprotect函数完成。此外在我们更新了内存的内容之后,这时可能想把这些内容同步到磁盘中的文件,这通过msync函数来完成。
mprotect 函数可以更改一个已经存在的映射区的访问权限。
#include<sys/mman.h>
int mprotect(void* addr, size_t len, int port)
返回值:成功返回0,失败返回-1
参数 addr
这个参数是mmap返回的数值,此时它就是mprotect作用的范围。
参数 len
指定映射区的长度,它需要与mmap中指定相同。
参数 port
在上面我们已经介绍了port的可能取值,mprotect功能就是把这个port指定的属性施加于相应的映射区上。
在映射区的内容更新了,内核并不是实时同步映射区与文件的,相反内核很少主动去同步,除非我们调用了函数msync或者关闭映射区(关闭映射区的时候,也不是立即同步的)。
#include<sys/mman.h>
int msync(void* addr, size_t len, int flags)
返回:成功为0,失败为-1
参数 addr与len
这两个参数完全等同于mprotect中的相应的参数。
参数 flags
我们可以通过指定flags为不同的值来要求内核进行的相应的同步操作:MS_ASYNC,这实际上不要求内核做什么,让内核自主去执行同步;MS_SYNC,要求内核在返回之前把写操作完成;MS_INVALIDATE,是一个可选的标志,它告诉内核丢弃没有同步的部分。
解除映射区:munmap
在进程寻出或者我们调用munmap的时候,可以解除一个已经存在的映射区。而关闭映射区对应的文件是不会引起映射区的解除的。
#include<sys/mman.h>
int munmap(void* addr, size_t len)
返回:成功返回0,失败返回-1
munmap的参数含义是明显而平凡的,所以在这里不作描述,它们与之前的几个含义完全相同。
注意:信号、进程、页、文件
对映射区的操作可能引起两个信号:SIGSEGV 与 SIGBUS。内核会在进程访问了不可用的内存时发送SIGSEGV信号给进程,指示进程这一异常行为。比如对一个只读的映射区执行写操作将收到这一信号。在映射的时候,如果文件本身的大小没有映射区的长度大,那么在进程第一次访问超过文件大小的内存区域的时候,内核会发送信号SIGBUS信号,之后再次访问此区域之前的地方都可以正常使用,但一旦再次超过,两样也会收到信号SIGBUS。所以一般情况都在映射之前调用诸如lseek+write这样的函数来改变文件现有大小以适合映射区需要的长度。
进程调用fork一类函数的时候子进程会复制父进程的地址空间,所以被映射的区域也会被映射,如此映射区是父子进程共享的。可以通过这种方式实现父子进程的内存共享与通信。
从内核的角度,内存是安页来管理的,所以在映射的时候内核在指定大小的基础上按页向上取整。页的大小是系统相关的,在POSIX环境下可以通过调用sysconf函数来确定。
只有在调用mmap时指定MAP_SHARED的情况文件内容才会真正改变,否则文件内容不会被同步,即使我们调用了msync或解除映射或进程终止。