内存映射
储存映射
除了标准文件I/O,内核还提供了一个接口,支持应用程序将文件映射到内存中,即内存地址和文件数据一一对应。这样开发人员就可以直接通过内存来访问文件,就像操作内存中的数据块一样,甚至可以写入内存数据区,然后通过透明的映射机制将文件写入磁盘
mmap()
mmap()调用请求内核将文件描述符fd所指向的对象的len个字节数据映射到内存中,起始位置从offset开始。如果指定addr,表示优先使用addr作为内存中的起始地址。参数prot指定了访存权限,flags指定了其他操作行为
1 |
|
addr参数告诉内核映射内存文件的最佳地址,但仅仅是作为提示信息,而不是强制性的,大部分用户对该参数传递0.调用返回内存映射区域的真实开始地址
prot参数描述了对内存区域所请求的访问权限。如果是PROT_NONE,表示无法访问映射区域的页(基本不用),也可以是以下标志位的比特位或运算值:
PROT_READ 页可读
PROT_WRITE 页可写
PROT_EXEC 页可执行
prot参数所设置的访问权限不能和打开文件的访问模式冲突。即如果文件只读模式打开,prot不能设置PROT_WRITE
flag参数描述了内存映射的类型及其一些行为。其值为以下值按位或运算的结果:
MAP_FIXED : 表示mmap()强制接收参数addr,而不是作为提示信息。如果内核无法映射文件到指定地址,调用会失败。如果地址和长度指定的内存和已有映射有重叠区域,重叠区域的原有内容被丢弃,通过新的内容填充。该选项需要深入了解进程的地址空间,不可移植,因此不鼓励使用
MAP_PRIVATE : 表示映射区不共享。文件映射采用了写时复制,进程对内存的任何改变不影响真正的文件或其他进程的映射
MAP_SHARED : 表示和其他映射该文件的进程共享映射内存。该映射区域会受到其他进程写操作的影响
MAP_SHARED和MAP_PRIVATE必须指定其中的一个,但是不能同时指定
当映射文件描述符时,文件的引用计数会加1.因此,如果映射文件后关闭文件,进程依然可以访问该文件。当取消映射或进程终止时,对应的文件引用计数会减1
1 |
|
页大小
页是内存管理单元(MMU)的粒度单位。因此,它是内存中允许具有不同权限和行为的最小单元。页是内存映射的基本块,因而也是进程地址空间的基本块
mmap()系统调用的操作单元是页。参数addr和offset都必须按页大小对齐。也就是说,它们必须是页大小的整数倍
所以,映射区域是页大小的整数倍。如果调用方提供的len参数没有按页对其,映射区域会一直占满最后一个页,多出来的内存,即最后一个有效字节到映射区域边界这一部分区域,会用0填充。该区域所有读写操作都将返回0.写操作都不会影响文件的最后部分,即使使用MAP_SHARED进行映射,只有最前面len个字节会写到文件中。
标准POSIX规定,获得页大小的方法是通过sysconf()函数,它将返回一系列系统特定的信息:
1 |
|
sysconf()调用会返回配置项name值,如果name无效,返回-1.出错时,errno被设置为EINVAL。因为-1对某些项来说是有效值,所以明智的做法是在调用前清空errno,并在调用后检查
POSIX定义_SC_PAGESIZE(_SC_PAGE_SIZE与其同义)表示页大小,因此获取页大小很简单:
1 |
|
Linux也提供了getpagesize()函数来获得页大小:
1 |
|
并不是所有的UNIX系统都支持此函数,POSIX 1003.1-2001弃用了该函数
页大小是由<asm/pages.h>中的宏PAGE_SIZE定义的。因此第三种获取页大小的方法是:
1 |
|
为了可移植性和代码的兼容性,sysconf()是最好的选择
返回值和错误码
成功时,mmap()返回映射区域的地址。失败时,返回MAP_FAILED,并相应设置errno值。mmap()调用,永远不会返回0
errno可能为:
EACESS : 指定的文件描述符不是普通文件,或者打开模式和参数prot或flags冲突
EAGAIN : 文件已通过文件锁锁定
EBADF : 指定文件描述符非法
EINVAL : 参数addr、len、off中的一个或多个非法
ENFILE : 打开文件数达到系统上限
ENODEV : 文件所在文件系统不支持存储映射
ENOMEM : 内存不足
EOVERFLOW : 参数addr+len的结果超过了地址空间大小
EPERM : 设定了参数PORT_EXEC,但文件系统以不可执行方式挂载
相关信号
和映射区域相关的两个信号如下:
SIGBUS 当进程试图访问一块已经失效的映射区域时,会生成该信号
SIGSEGV 当进程试图写一块只读的映射区域时,会生成该信号
munmap()
Linux提供了munmap()系统调用,来取消mmap()所创建的映射
1 |
|
munmap()会消除进程地址空间从addr开始,len字节长的内存中的所有页面映射。一旦映射被消除,之前关联的内存区域就不再有效,如果试图再次访问会生成SIGSEGV信号
一般来说,传递给munmap()的参数是上次mmap()调用的返回值及其参数len
成功时,munmap()返回0;失败时返回-1,并设置相应errno值。唯一标准的errno值是EINVAL,它表示一个或多个参数无效
存储映射实例
下面是将文件输出到标准输出的例子:
1 |
|
mmap()的优点
相对于read()和write()而言,使用mmap()处理文件有很多优点:
- 使用read()或write(),需要从用户缓冲区进行数据读写,而使用映射文件进行操作,可以避免多余的数据拷贝操作。
- 除了可能潜在页错误,读写映射文件不会带来系统调用和上下文切换的开销,它就像直接操作内存一样简单
- 当多个进程把同一个对象映射到内存中时,数据会在所有进程间共享。只读和写共享的映射在全体中都是共享的;私有可写的映射对尚未进行写时拷贝的页是共享的
- 映射对象中搜索只需要很简单的指针操作,不需要使用系统调用lseek()
基于以上理由,mmap()是很多应用的明智选择
mmap()的不足
使用mmap()需要注意以下几点:
- 由于映射区域的大小总是页的整数倍,因此,文件大小与页大小的整数倍之间有空间浪费。对于小文件,空间浪费比较严重。例如,如果页大小是4KB,一个7字节的映射就会浪费4089字节
- 存储映射区域必须在进程地址空间内。对于32位的地址空间,大量的大小不同的映射会导致生成大量的碎片,使得很难找到连续的大片空内存。当然,这个问题在64位地址空间就不是很明显
- 创建和维护映射以及相关的内核数据结构有一定的开销。不过,由于mmap()消除了读写时不必要的拷贝,这种开销几乎可以忽略,对于大文件和频繁访问的文件更是如此
基于以上理由,处理大文件(浪费空间很小),或者在文件大小恰好被怕个大小整除(没有空间浪费)时,mmap()的优势就会非常显著
给出映射提示
Linux提供了系统调用madvise(),进程对自己期望如何访问映射区域给内核一些提示信息。内核会根据此优化自己的行为,尽量更好的利用映射区域。
调用madvise()会告诉内核该如何对于起始地址为addr,长度为len的内存映射区域进行操作
1 |
|
如果len为0,内核将把提示信息应用于所有起始地址为addr的映射。参数advice表示提示信息,可以是下列值之一:
MADV_NORMAL 对指定的内存区域,应用没有特殊提示,按正常方式操作
MADV_RANDOM 应用将以随机访问方式,访问指定范围的页
MADV_SEQUENTIAL 应用期望从低地址到高地址顺序访问指定范围的页
MADV_WILLNEED 应用期望会很快访问指定范围的页
MADV_DONTNEED 应用在短期内不会访问指定范围内的页