内存映射mmap
内存映射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 表示文件描述符,起始位置 和长度。以下这个例子,父进程创建共享内存区域,子进程在该区域中写入值,父进程读取值。#define SIZE 4096 * 2 // 共享内存的大小,单位为字节
// 创建一个匿名的共享内存区域
int *shared_memory = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (shared_memory == MAP_FAILED) {
perror("mmap");
return 1;
}
*shared_memory = 100;
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
} else if (pid == 0) {
*shared_memory += 50;
} else {
wait(NULL);
printf("Parent process: shared_memory value = %d\n", *shared_memory);
}
munmap(shared_memory, SIZE);
页:
[1]