谷动谷力

 找回密码
 立即注册
查看: 1189|回复: 0
打印 上一主题 下一主题
收起左侧

内存映射mmap

[复制链接]
跳转到指定楼层
楼主
发表于 2023-10-30 20:06:01 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
内存映射mmap

本文将分析下内存映射在内核中的实现,包括文件内存映射和匿名内存映射,使用场景,以及特点,最后给出一个例子。

一、文件内存映射

文件内存映射的逻辑图如下:
当用户进程通过系统调用 mmap 创建文件的内存映射时,系统会给文件分配一个 vma,并且 vma 的 vm_file 指向文件描述符 struct file。
文件描述符 struct file 的 f_mapping 会指向文件地址空间 struct address_space, 这是一个很重要的结构,它连接着虚拟内存空间 vmas 和  物理页面 pages。

先看下虚拟内存空间部分,成员 i_mmap 通过红黑树存储了所有的虚拟内存空间 vmas(不同进程的);并且,vma 的 vm_ops 定义这个 vma 的操作函数,对于文件mmap,它的 vm_ops 是generic_file_vm_op。

再看下物理页面部分,成员 i_pages 通过struct xarray (可扩展的数组)存储了所有的pages。
(相关代码:mmap-> ksys_mmap_pgoff -> vm_mmap_pgoff -> do_mmap -> mmap_region。)

当创建子进程时,子进程会复制父进程的文件描述符和vma。因此,父子进程的vma 会指向同一个文件描述符,因此,子进程的 vma 会加入到vma->vm_file->f_mapping->i_mmap 中。
(相关代码:kernel_clone->copy_process->copy_mm->dup_mm->dup_mmap。)

出于效率的原因,内存映射创建之后系统并没有立即把页框分配给它,而是尽可能地向后推迟到不能再推迟-----也就是说,当进程试图对其中一页进行寻址时,就产生一个缺页异常。缺页异常 handle_pte_fault 就会执行 vma->vm_ops->fault(vmf),也就是函数 filemap_fault。在这个函数里面,会分配页框。需要注意的是,page->index表示此page 在文件的offset,vma->vm_pgoff表示此vma在文件内的offset,通过这两个offset,再加上vma->vm_start ,就可以很容易计算出一个page在某个vma里面的虚拟地址。知道虚拟地址、物理地址,就可以填充页表。
稍微了解下文件映射缺页的核心函数 filemap_fault,定义于 mm\filemap.c中,其逻辑是:
  • 判断页面是否在文件缓存中,如存在则判断是否需要异步预读;
  • 如不存在则进行同步预读,再创建页面;


怎么判断文件的某段内容在不在内存中呢?还是以第一张图为例。
文件的address_space 的成员 struct xarray ,它把文件的偏移量作为slot的索引,slot的值存储了“这个文件偏移量对应的struct page” ,当slot 的值为空,说明文件这段数据还没有加载到内存中,具体实现在函数 mapping_get_entry (mm\filemap.c)中。

综上所述,系统唯一的文件路径-> 系统唯一的inode -> 相同的address_space ->相同的页面(page cache),通过这样的方式,多个进程的文件mmap对一个文件进行操作。

文件映射主要有三个方面的应用:操作文件、进程间通信、私有文件映射。私有文件映射最典型就是加载动态库,多个进程共享相同的文本段。

二、匿名映射 mmap

匿名内存映射分为私有匿名映射和共享匿名映射。
私有匿名映射通常用于内存分配,典型的例子就是glibc 的malloc 实现,当 需要分配的内存大于MMAP_THREASHOLD(128KB)时,glibc 会默认使用 mmap 代替 brk 来分配内存。

重点了解下共享匿名映射。创建共享匿名映射有如下两种方式:
1、使用参数参数 fd = -1 且 flags = MAP_ANONYMOUS | MAP_SHARED。在这种情况下,mmap_region() 函数最终调用 shmem_zero_setup() 来打开一个特殊设备文件 “/dev/zero”。
2、直接打开 “/dev/zero”设备文件,然后使用这个文件句柄来创建 mmap。

/dev/zero

在类UNIX 系统中是一个特殊的设备文件。

它在被读取时会提供无限的空字符,使用mmap 将 /dev/zero 映射到一个虚拟的内存空间,这个操作的效果等同于使用一段匿名的内存。
以下是共享匿名映射的逻辑图。
它和文件映射非常非常相似,不同的点在于:一是缺页异常时调用的函数,二是文件是固定的/dev/zero,三是 slot 可能什么都没存,可能存储的是struct page*, 也可能是 swap_entry.

这里就有个问题,这里 slot 上存放的是正常的page,还是 swap-out 后留下的 swap_entry 呢?

如果结构体包含了 int 或者 long 成员,那么结构体肯定是四字节对齐。指向结构体的指针 存储的是 结构体的内存地址,也就是指针的最低两位是可以利用起来的。

这里slot 如果存储的是 swap_entry,那么将它的最低位设置为1,即 &{slot} >> 1 就是 swap_entry 的 val 值。这些逻辑实现都在函数 shmem_fault 中。

最后,总结下共享匿名映射。

共享匿名映射通过挂载在/dev/shm 下的 tmpfs 内存文件系统实现的。Tmpfs 是一套虚拟的文件系统,在其中创建的文件都是基于内存的,机器重启即消失。Tmpfs/shmem 是一个介于文件和匿名内存之间的东西。一方面,它具有文件的属性,能够像操作文件一样去操作它。它有自己的 inode、有自己的 page cache;另一方面,它也有匿名内存的属性。由于没有像磁盘这样的外部存储介质,内核在内存紧缺时不能简单的将page 从它们的page cache 中丢弃,而是需要 swap-out;

三、例子

函数mmap声明如下:
第一个参数 addr 表示“虚拟地址”,一般设置为NULL,表示交给 内核去选择合适的位置。
第三个参数 prot, 是 protect 的缩写, 表示共享内存的访问权限,有4个可选,分别是 可读、可写、可执行、不可访问。
第四个参数 flags ,表示映射的属性,有 MAP_SHARED(共享的)MAP_PRIVATE(私有的)。
剩下的 fd、offset和length 表示文件描述符,起始位置 和长度。
以下这个例子,父进程创建共享内存区域,子进程在该区域中写入值,父进程读取值。
  1. #define SIZE 4096 * 2 // 共享内存的大小,单位为字节
  2.      // 创建一个匿名的共享内存区域
  3.     int *shared_memory = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
  4.     if (shared_memory == MAP_FAILED) {
  5.         perror("mmap");
  6.         return 1;
  7.     }
  8.     *shared_memory = 100;
  9.     pid_t pid = fork();
  10.     if (pid < 0) {
  11.         perror("fork");
  12.         return 1;
  13.     } else if (pid == 0) {
  14.         *shared_memory += 50;
  15.     } else {
  16.         wait(NULL);
  17.         printf("Parent process: shared_memory value = %d\n", *shared_memory);
  18.     }
  19.     munmap(shared_memory, SIZE);
复制代码

+10
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|深圳市光明谷科技有限公司|光明谷商城|Sunshine Silicon Corpporation ( 粤ICP备14060730号|Sitemap

GMT+8, 2024-5-19 16:10 , Processed in 0.090911 second(s), 42 queries .

Powered by Discuz! X3.2 Licensed

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表