《Linux内核 学习笔记》--- 第二章 内存管理 2.9 mmap
发布日期:2021-06-29 14:51:22 浏览次数:2 分类:技术文章

本文共 3055 字,大约阅读时间需要 10 分钟。

《Linux内核 学习笔记》--- 第二章 内存管理

2.9 mmap 概述

mmap/munmap接口是用户空间最常用的一个系统调用接口,

无论是在用户程序中分配内存、读写大文件、链接动态库文件,还是多进程间共享内存,都可以看到mmap/munmap的身影。

mmap/munmap函数声明如下:

#include 
void *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种情况:

  1. 私有匿名映射

    当使用参数fd=-1 且 flags= MAP_ANONYMOUS | MAP_PRIVATE时,创建的mmap映射是私有匿名映射。
    私有匿名映射最常见的用途是在glibc分配大块的内存中,当需要分配的内存大于MMAP_THREASHOLD(128KB)时,glibc会默认使用mmap代替brk来分配内存。

  2. 共享匿名映射

    当使用参数 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模块来创建共享匿名映射。

  1. 私有文件映射

    创建文件映射时flags的标志位被设置为MAP_PRIVATE,那么就会创建私有文件映射。
    私有文件映射最常用的场景是加载动态共享库。

  2. 共享文件映射

    创建文件映射时flags的标志位被设置为MAP_SHARED,那么就会创建共享文件映射。
    如果prot参数指定了PROT_WRITE,那么打开文件时需要指定O_RDWR标志位。
    共享文件映射通常有如下两个场景。
    (1)读写文件。
    把文件内容映射到进程地址空间,同时对映射的内容做了修改,内核的回写机制(writeback)最终会把修改的内容同步到磁盘中。
    (2)进程间通信。
    进程之间的进程地址空间相互隔离,一个进程不能访问到另外一个进程的地址空间。如果多个进程都同时映射到一个相同文件时,就实现了多进程间的共享内存通信。如果一个进程对映射内容做了修改,那么另外的进程是可以看到的。

mmap机制在Linux内核中的代码流程

在这里插入图片描述

问题1:请阅读Linux内核中mmap相关代码,找出第二次调用mmap会成功的原因?

===>

查看mmap系统调用的代码实现,在do_mmap_pgoff()->mmap_region()函数里有如下一段代码:
这里再一次看到find_vma_links()函数
find_vma_links()函数会遍历该进程中所有的VMAs,
当检查到当前要映射的区域和已有的VMA有些许的重叠时,该函数都返回-ENOMEM,
然后在mmap_region()函数里调用do_munmap()函数,把这段将要映射区域先销毁,然后重新映射,
这就是第二次映射同样的地址并没有返回错误的原因。

问题2:在一个播放系统中同时打开几十个不同的高清视频文件,发现播放有些卡顿,打开视频文件是用mmap函数,请简单分析原因。

使用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”命令来修改。

转载地址:https://ciellee.blog.csdn.net/article/details/105075708 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:数据结构与算法(1)数据结构与算法的基本概念
下一篇:《Linux内核 学习笔记》--- 第二章 内存管理 2.6 vmalloc & 2.7 VMA & 2.8 malloc

发表评论

最新留言

第一次来,支持一个
[***.219.124.196]2024年04月14日 14时36分18秒