来看下上文介绍的mmap()的函数原型是怎样的:

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

看起来参数好像比较多,但对照著下面这张图看,应该会清晰不少。

其中fd, offsetlength都是用来描述要映射的文件区域的,fd是文件描述符,对于匿名映射,fd应该是-1(如果是通过打开/dev/zero这个特殊的文件来创建匿名映射,则它也是有正常的fd值的)。offset是文件中映射的起始位置,length是映射的长度。如果访问了超出映射的区域,则有可能触发SIGSEGV异常(segmentation fault)。为什么说是有可能呢?因为只有访问的地址没有落在进程的任何VMA(关于VMA请参考这篇文章)里,才应该触发SIGSEGV,如果超出映射的这段区域正好落在另一个VMA里,那就可以侥幸逃过SIGSEGV。其实这样更糟糕,触发SIGSEGV可以让你及时知道事故的第一现场,如果继续运行,然后在某个不可预知的地方崩溃,那这bug排查起来就困难了。

linux是以page为单位管理内存的,mmap也是以page为单位建立映射的,因此offset必须是按page size对齐的(不对齐的话就会映射失败)。文件是有长度的,offset和offset+length的值都应该小于被映射文件的长度,然而,mmap()并不会对此作出检查,所以有可能建立的映射区域并不完全在文件的长度范围内。比如一个文件的长度是4KB,而你映射了8KB,那么在访问后面的这4KB内存的时候,就会触发SIGBUS异常,表明你访问的这段内存区域,没有对应的文件。

如果用户给定的length不是按page size对齐的,那么内核会填充一部分长度以保证对齐(可见,offset对齐是用户自己保证的,length对齐是靠内核来保证的)。比如文件长度是10KB,你映射了5KB,那么内核会将其扩充到8KB。如果你访问下图中5120位元组到8191位元组的这段内存,因为既没有超出映射的内存区域,也没有超出文件的长度,所以是不会触发任何异常的。

你以为只要保证映射的区域完全落在文件的长度范围内就可以高枕无忧了么?事务都是变化的,被映射的文件也不例外,比如它可以通过truncate()/ftruncate()截断,截断之后文件的长度如果减小了(truncate()是可以增大的),然后你刚好访问了被截断的这段区域,依然会触发SIGBUS。

映射区域的大小是可以通过mremap()动态调整的,事实上,glibc中的realloc()就是调用mremap()实现的。

prot是protection的意思,表示的是对内存映射区域的保护,包括PROT_READ(可读),PROT_WRITE(可写)和PROT_EXEC(可执行)。还有一个很特殊的PROT_NONE,就是既不可读也不可写更不可执行,啥操作都不可以,那映射出来干吗?普通场景下没有用,不代表特殊场景下没用,PROT_NONE可以用于实现防范攻击的guard page,如果攻击者访问了某个guard page,就会触发SIGSEV,作用和地雷是差不多的,在布了地雷的土地上,你不能种田,不能盖房,但是可以防范敌人的入侵,只是,你得记住地雷放的位置,别自己踩著了。prot属性是可以通过mprotect()动态修改的(mprotect不仅限于操作由mmap映射的内存区域,它可以操作任意区域的内存),这篇文章介绍的JIT实现就是对此一个颇有意思的应用。

flags用于指定映射是基于文件的还是匿名(MAP_ANONYMOUS)的,是共享的(MAP_SHARED)还是私有的(MAP_PRIVATE)。上文介绍过,共享映射和私有映射在写操作上是有区别的,前者是直接写,后者是COW,先copy再写。

addr用于指定映射到的VMA的起始地址,也必须按page size对齐。映射是由内核完成的,但进程可以通过addr参数建议一个它认为的最佳地址(没有这种要求就设置addr为NULL),毕竟进程最了解它自身的应用场景嘛,但这对内核来说不是强制的,如果addr和addr+length之间的虚拟内存空间恰好是可用的,那么内核会满足进程的这一要求。如果flags中加上MAP_FIXED,那就是进程要求必须映射到这个addr起始的区域,当然,这会增加映射失败的概率。

实际映射到的VMA起始地址保存在mmap()的返回值中。

同read()和write()一样,要获得mmap()中的参数fd,自然是要先open()一下,而调用open()的时候是需要设置对文件的读写许可权的,比如O_RDONLY(只读),O_RDWD(可读写),mmap()中prot和flags的设置需要和进程打开文件时的属性设置(存储在file->f_mode中)保持一致。上文提到,mmap的实现主要是在进程虚拟地址空间创建了一个VMA,而一个VMA是有VM_READ, VM_WRITE, VM_EXEC, VM_SHARED等诸多属性的。如果是通过mmap创建的VMA,则这些属性就是由mmap()中prot和flags参数传递进去的。

参考:

《Linux环境编程:从应用到内核》第11.4节

推荐阅读:

相关文章