本文共 3055 字,大约阅读时间需要 10 分钟。
mmap/munmap接口是用户空间最常用的一个系统调用接口,
无论是在用户程序中分配内存、读写大文件、链接动态库文件,还是多进程间共享内存,都可以看到mmap/munmap的身影。mmap/munmap函数声明如下:
#includevoid *mmap(void *addr, size_t length , int prot, int flags, int fd , off_t offset);int munmap(void *addr, size_t length);
❑ addr:用于指定映射到进程地址空间的起始地址,为了应用程序的可移植性,一般设置为NULL,让内核来选择一个合适的地址。
❑ length:表示映射到进程地址空间的大小。 ❑ prot:用于设置内存映射区域的读写属性等。 ❑ flags:用于设置内存映射的属性,例如共享映射、私有映射等。 ❑ fd:表示这个是一个文件映射,fd是打开文件的句柄。 ❑ offset:在文件映射时,表示文件的偏移量。prot参数通常表示映射页面的读写权限,可以有如下参数组合。
❑ PROT_EXEC:表示映射的页面是可以执行的。 ❑ PROT_READ:表示映射的页面是可以读取的。 ❑ PROT_WRITE:表示映射的页面是可以写入的。 ❑ PROT_NONE:表示映射的页面是不可访问的。flags参数也是一个很重要的参数,有如下常见参数。
❑ MAP_SHARED:创建一个共享映射的区域。多个进程可以通过共享映射方式来映射一个文件,这样其他进程也可以看到映射内容的改变,修改后的内容会同步到磁盘文件中。 ❑ MAP_PRIVATE:创建一个私有的写时复制的映射。多个进程可以通过私有映射的方式来映射一个文件,这样其他进程不会看到映射内容的改变,修改后的内容也不会同步到磁盘文件中。 ❑ MAP_ANONYMOUS:创建一个匿名映射,即没有关联到文件的映射。 ❑ MAP_FIXED:使用参数addr创建映射,如果在内核中无法映射指定的地址addr,那mmap会返回失败,参数addr要求按页对齐。如果addr和length指定的进程地址空间和已有的VMA区域重叠,那么内核会调用do_munmap()函数把这段重叠区域销毁,然后重新映射新的内容。 ❑ MAP_POPULATE:对于文件映射来说,会提前预读文件内容到映射区域,该特性只支持私用映射。参数fd可以看出mmap映射是否和文件相关联,因此在Linux内核中映射可以分成匿名映射和文件映射。
❑ 匿名映射:没有映射对应的相关文件,这种映射的内存区域的内容会被初始化为0。 ❑ 文件映射:映射和实际文件相关联,通常是把文件的内容映射到进程地址空间,这样应用程序就可以像操作进程地址空间一样读写文件。最后根据文件关联性和映射区域是否共享等属性,又可以分成如下4种情况:
私有匿名映射
当使用参数fd=-1 且 flags= MAP_ANONYMOUS | MAP_PRIVATE时,创建的mmap映射是私有匿名映射。 私有匿名映射最常见的用途是在glibc分配大块的内存中,当需要分配的内存大于MMAP_THREASHOLD(128KB)时,glibc会默认使用mmap代替brk来分配内存。共享匿名映射
当使用参数 fd=-1 且 flags= MAP_ANONYMOUS | MAP_SHARED 时,创建的mmap映射是共享匿名映射。 共享匿名映射让相关进程共享一块内存区域,通常用于父子进程之间通信。 创建共享匿名映射有如下两种方式。 (1)fd=-1且flags= MAP_ANONYMOUS | MAP_SHARED。 在这种情况下,do_mmap_pgoff()->mmap_region()函数最终会调用shmem_zero_setup()来打开一个“/dev/zero”特殊的设备文件。 (2)另外一种是直接打开“/dev/zero”设备文件,然后使用这个文件句柄来创建mmap。上述两种方式最终都是调用到shmem模块来创建共享匿名映射。
私有文件映射
创建文件映射时flags的标志位被设置为MAP_PRIVATE,那么就会创建私有文件映射。 私有文件映射最常用的场景是加载动态共享库。共享文件映射
创建文件映射时flags的标志位被设置为MAP_SHARED,那么就会创建共享文件映射。 如果prot参数指定了PROT_WRITE,那么打开文件时需要指定O_RDWR标志位。 共享文件映射通常有如下两个场景。 (1)读写文件。 把文件内容映射到进程地址空间,同时对映射的内容做了修改,内核的回写机制(writeback)最终会把修改的内容同步到磁盘中。 (2)进程间通信。 进程之间的进程地址空间相互隔离,一个进程不能访问到另外一个进程的地址空间。如果多个进程都同时映射到一个相同文件时,就实现了多进程间的共享内存通信。如果一个进程对映射内容做了修改,那么另外的进程是可以看到的。mmap机制在Linux内核中的代码流程
===>
查看mmap系统调用的代码实现,在do_mmap_pgoff()->mmap_region()函数里有如下一段代码: 这里再一次看到find_vma_links()函数 find_vma_links()函数会遍历该进程中所有的VMAs, 当检查到当前要映射的区域和已有的VMA有些许的重叠时,该函数都返回-ENOMEM, 然后在mmap_region()函数里调用do_munmap()函数,把这段将要映射区域先销毁,然后重新映射, 这就是第二次映射同样的地址并没有返回错误的原因。使用mmap来创建文件映射时,由于只建立了进程地址空间VMA,并没有马上分配page cache和建立映射关系。
因此当播放器真正读取文件时,产生了缺页中断才去读取文件内容到page cache中。 这样每次播放器真正读取文件时,会频繁地发生缺页中断,然后从文件中读取磁盘内容到page cache中,导致磁盘读性能比较差,从而造成播放视频的卡顿。有些读者认为在创建mmap映射之后调用madvise(add, len,MADV_WILLNEED |MADV_SEQUENTIAL)可能会对文件内容提前进行了预读和顺序,读所有利于改善磁盘读性能,但实际情况是:
❑ MADV_WILLNEED会立刻启动磁盘IO进行预读,仅预读指定的长度,因此在读取新的文件区域时,要重新调用\2ADV_WILLNEED,显然它不适合流媒体服务的场景,内核默认的预读功能更适合问题2的场景。MADV_ WILLNEED比较适合内核很难预测接下来要预读哪些内容的场景,例如随机读。 ❑ MADV_SEQUENTIAL适合问题2的场景,但是内核默认的预读功能也能很好的工作。能够有效提高流媒体服务I/O性能的方法是增大内核的默认预读窗口,现在内核默认预读的大小是128KB,可以通过“blockdev --setra”命令来修改。
转载地址:http://gvqbf.baihongyu.com/