mmap是POSIX规范接口中用来处理内存映射的一个系统调用,它本身的使用场景非常多,可以用来申请大块内存,可以用来申请共享内存,也可以将文件或设备直接映射到内存中,进程可以像访问普通内存一样访问被映射的文件,在实际开发过程使用场景非常多。这次要聊的重点是mmap映射普通文件的一些原理。
备注:文中引用的所有linux相关源码均基于4.1版本。
1、Linux内存管理
如果要了解mmap的原理,需要先了解一下linux对内存是怎么管理的。 计算机系统中物理内存的大小总是有限的,而我们需要在计算机中运行多个进程,如果允许进程直接访问物理内存,那么可能会发生一些致命的问题,比如A进程不小心把数据写到了B进程的内存中,产生的问题可能是灾难性的。 为了更加安全有效的对内存进行管理,在现代计算机系统中对物理内存做了一层抽象,即虚拟内存。它为每一个进程都提供一块连续的私有地址空间,在32位模式下,每一块虚拟地址空间大小为4GB。 系统将虚拟内存分割成一块块固定大小的虚拟页(Virtual Page),同样的,物理内存也会被分割成物理页(Physical Page),当进程访问内存时,CPU通过内存管理单元(MMU)根据页表(Page Table)将虚拟地址翻译成物理地址,最终取到内存数据。这样在每个进程内部都像是独享整个主存。
而Linux操作系统中,会把高地址的1GB内存作为内核空间,低地址的3GB作为用户空间。 下面是Linux进程内存的布局:
从上图可以看出,一个进程的虚拟空间有多个部分组成,mmap的文件所处的内存空间在内存映射段中。 Linux内核使用vm_area_struct来表示一块内存区域,因为一个进程中会出现多个不同的内存区块,每一个内存块都会使用vm_area_struct结构体来表示, vm_area_struct的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 struct vm_area_struct { unsigned long vm_start; unsigned long vm_end; struct vm_area_struct *vm_next , *vm_prev ; struct rb_node vm_rb ; unsigned long rb_subtree_gap; struct mm_struct *vm_mm ; pgprot_t vm_page_prot; unsigned long vm_flags; struct { struct rb_node rb ; unsigned long rb_subtree_last; } shared; struct list_head anon_vma_chain ; struct anon_vma *anon_vma ; const struct vm_operations_struct *vm_ops ; unsigned long vm_pgoff; struct file * vm_file ; void * vm_private_data; ... };
其中需要重点关注的是vm_ops变量,它指向的是一组函数指针,定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 struct vm_operations_struct { void (*open)(struct vm_area_struct * area); void (*close)(struct vm_area_struct * area); int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf); void (*map_pages)(struct vm_area_struct *vma, struct vm_fault *vmf); int (*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf); int (*pfn_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf); int (*access)(struct vm_area_struct *vma, unsigned long addr, void *buf, int len, int write); const char *(*name)(struct vm_area_struct *vma); struct page *(*find_special_page )(struct vm_area_struct *vma , unsigned long addr ); };
这是一组用于针对不同内存类型的不同操作,可以看到有open(打开)、close(关闭)、fault(缺页异常)等等接口,后面会着重讲针对文件映射的操作原理。 当进程在申请的内存的时候,linux内核其实只分配一块虚拟内存地址,并没有分配实际的物理内存,相当于操作系统只给进程这一块地址的使用权。只有当程序真正使用这块内存时,会产生一个缺页异常,这时内核去真正为进程分配物理页,并建立对应的页表,从而将虚拟内存和物理内存建立一个映射关系,这样可以做到充分利用到物理内存。
2、mmap原理
mmap系统调用的定义:
1 void *mmap (void *start, size_t length, int prot, int flags, int fd, off_t offset) ;
参数说明: start:映射空间的起始地址,一般设置为 NULL; length:映射空间的长度; prot:内存保护标志,包括PROT_EXEC(可执行)、PROT_READ(可读)、PROT_WRITE(可写)、PROT_NONE(不可访问) ; flags:映射类型,通常用来标记共享内存(MAP_SHARED)、匿名映射(MAP_ANONYMOUS)等。 fd:真正要映射的文件描述符; offset:映射文件的偏移量。
一个简单的demo如下:
1 2 3 4 5 6 7 8 9 10 11 12 int main (int argc, char **argv) { char *filename = "/tmp/foo.data" ; struct stat stat ; int fd = open(filename, O_RDWR, 0 ); fstat(fd, &stat); void *bufp = mmap(NULL , stat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 ); memcpy (bufp, "Linuxdd" , 7 ); munmap(bufp, stat.st_size); close(fd); return 0 ; }
从demo中可以看出,mmap是将一个文件直接映射到进程的地址空间,进程可以像操作内存一样去读写磁盘上的文件内容,而不需要再调用read/write等系统调用。 下面分析一下mmap的实现原理:
1 2 3 4 5 6 7 8 9 10 11 12 13 SYSCALL_DEFINE6(mmap, unsigned long , addr, unsigned long , len, unsigned long , prot, unsigned long , flags, unsigned long , fd, unsigned long , off) { long error; error = -EINVAL; if (off & ~PAGE_MASK) goto out; error = sys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT); out: return error; }
内部直接调用的是sys_mmap_pgoff函数,流程如下:
再经过vm_mmap_pgoff到do_mmap_pgoff,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 unsigned long do_mmap_pgoff (struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long pgoff, unsigned long *populate) { struct vm_area_struct *vma ; vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL); if (!vma) goto error_getting_vma; if (file) { region->vm_file = get_file(file); vma->vm_file = get_file(file); } down_write(&nommu_region_sem); if (file && vma->vm_flags & VM_SHARED) ret = do_mmap_shared_file(vma); else ret = do_mmap_private(vma, region, len, capabilities); add_vma_to_mm(current->mm, vma); }
在做文件映射时,如果不是共享的文件,则调用的是do_mmap_private函数,此函数流程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static int do_mmap_private (struct vm_area_struct *vma, struct vm_region *region, unsigned long len, unsigned long capabilities) { if (capabilities & NOMMU_MAP_DIRECT) { ret = vma->vm_file->f_op->mmap(vma->vm_file, vma); } }
此处f_op->mmap指向的是generic_file_mmap:
1 2 3 4 5 6 7 8 9 10 int generic_file_mmap (struct file * file, struct vm_area_struct * vma) { struct address_space *mapping = file->f_mapping; if (!mapping->a_ops->readpage) return -ENOEXEC; file_accessed(file); vma->vm_ops = &generic_file_vm_ops; return 0 ; }
内部就是给前面提到的vm_ops函数指针的集合赋值,generic_file_vm_ops指向的是针对文件操作的一系列函数:
1 2 3 4 5 const struct vm_operations_struct generic_file_vm_ops = { .fault = filemap_fault, .map_pages = filemap_map_pages, .page_mkwrite = filemap_page_mkwrite, };
其中包括缺页处理,映射页,置为可写三个操作;其中缺页异常的处理逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 int filemap_fault (struct vm_area_struct *vma, struct vm_fault *vmf) { page = find_get_page(mapping, offset); if (likely(page) && !(vmf->flags & FAULT_FLAG_TRIED)) { do_async_mmap_readahead(vma, ra, file, page, offset); } else if (!page) { do_sync_mmap_readahead(vma, ra, file, offset); count_vm_event(PGMAJFAULT); mem_cgroup_count_vm_event(vma->vm_mm, PGMAJFAULT); ret = VM_FAULT_MAJOR; retry_find: page = find_get_page(mapping, offset); if (!page) goto no_cached_page; } vmf->page = page; return ret | VM_FAULT_LOCKED; }
总结mmap文件映射过程: a) 用户在进程中触发mmap操作; b) 内核对参数做基本的校验,并针对映射长度做一些内存对齐; c) 分配vm_area_struct结构,并对其进行初始化; d) 调用文件系统的mmap映射,将缺页异常等函数指针赋于vm_ops; e) 将新建的vm_area_struct结构插入到mm链表中; f) 当进程访问这片内存时,引发缺页异常,从而调用filemap_fault; g) 缺页异常查找cache中有无请求的页,如果没有,内核发起请求将数据从磁盘装入内存;
3、mmap和read/write的区别
read的系统调用的流程大概如下图所示:
a) 用户进程发起read操作; b) 内核会做一些基本的page cache判断,从磁盘中读取数据到kernel buffer中; c) 然后内核将buffer的数据再拷贝至用户态的user buffer; d) 唤醒用户进程继续执行;
而mmap的流程如下图所示:
内核直接将内存暴露给用户态,用户态对内存的修改也直接反映到内核态,少了一次的内核态至用户态的内存拷贝,速度上会有一定的提升;
4、总结
经过对linux内核源码的一些剖析,对mmap的原理会有更深层次的理解 。 mmap的优点有很多,相比传统的read/write等I/O方式,直接将虚拟地址的区域映射到文件,没有任何数据拷贝的操作,当发现有缺页时,通过映射关系将磁盘的数据加载到内存,用户态程序直接可见,提高了文件读取的效率。对索引数据这种大文件的读取、cache、换页等操作直接交由操作系统去调度,间接减少了用户程序的复杂度,并提高了运行效率。