1 简述
mmap系统调用能够将文件映射到内存空间,然后可以通过读写内存来读写文件。Android的binder机制也是基于mmap实现。
2 使用示例
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)
参数说明:
- start:指定要映射的内存地址,一般设置为 NULL 让操作系统自动选择合适的内存地址。
- length:映射地址空间的字节数,它从被映射文件开头 offset 个字节开始算起。
- prot:指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读), PROT_WRITE(可写), PROT_EXEC(可执行), PROT_NONE(不可访问)。
- flags:由以下几个常值指定:MAP_SHARED (共享的) MAP_PRIVATE(私有的), MAP_FIXED(表示必须使用 start 参数作为开始地址,如果失败不进行修正),其中,MAP_SHARED , MAP_PRIVATE必选其一,而 MAP_FIXED 则不推荐使用。
- fd:表示要映射的文件句柄。
- offset:表示映射文件的偏移量,一般设置为 0 表示从文件头部开始映射。
函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <zconf.h>
#include <string.h>
int main() {
printf("Hello, World!\n");
const char *file = "c_test/mmap_demo";
// 读写方式打开,所有人都有读写可执行权限
int m_fd = open(file, O_RDWR | O_CREAT, S_IRWXU);
ftruncate(m_fd, 128); // 改变文件大小
char *addr = mmap(0, 128, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED, m_fd, 0);
memset(addr, 128, 0);
sprintf(addr, "hello_modify!\n");
// 写到内存,如果想让内存写入文件需要调下面两个中的一个
munmap(addr, 128);
// msync(...);
close(m_fd);
return 0;
}
3 原理
虚拟内存与物理内存
32位linux操作系统中每个进程都有4GB的虚拟内存空间,但是物理内存空间(内存条)大小是固定的,配置的多少就是多少,所有进程都共用内存条上的空间。所以虚拟内存空间需要映射到物理内存空间才能使用。
内存的单位是内存页,这个自行查看操作系统。
虚拟内存管理
在Linux操作系统中,管理虚拟内存的基本单元是vm_area_struct,它描述的是一段连续的、具有相同访问属性的虚存空间,该虚存空间的大小为物理内存页面的整数倍。
vm_area_struct {
struct mm_struct * vm_mm;
unsigned long vm_start;
unsigned long vm_end;
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next;
pgprot_t vm_page_prot;
unsigned long vm_flags;
/* AVL tree of VM areas per task, sorted by address */
short vm_avl_height;
struct vm_area_struct * vm_avl_left;
struct vm_area_struct * vm_avl_right;
vm_area_struct *vm_next_share;
struct vm_area_struct **vm_pprev_share;
struct vm_operations_struct * vm_ops;
unsigned long vm_pgoff; /* offset in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */
struct file * vm_file;
unsigned long vm_raend;
void * vm_private_data; /* was vm_pte (shared mem) */
};
vm_start、vm_end指向虚拟内存一段连续空间的地址vm_next指向下一个vm_area_struct地址(链表结构)vm_file指向被映射的文件vm_avl_height、vm_avl_left、vm_avl_right树高、左子结点、右子结点实现AVL树,提升查询效率。
task_struct用于管理进程。mm_struct是内存描述符,在每个mm_struct又都有一个pgd_t * 使其指向页表,然后通过页表实现从虚拟地址到物理地址的映射。mmap指向虚拟内存管理单元链表vm_area_struct。
调用mmap
当用户程序调用mmap后。函数会在当前进程的空间中找到适合的vm_area_struct来描述自己将要映射的区域。这个区域的作用就是将mmap函数中文件描述符所指向的具体文件中内容映射过来。
mmap的执行,仅仅是在内核中建立了文件与虚拟内存空间的对应关系。用户访问这些虚拟内存空间时,页面表里面是没有这些空间的表项的。当用户程序试图访问这些映射的空间时,就会产生缺页异常。
缺页异常
缺页异常一般有两种情况:
- 程序设计的不当导致访问了非法的地址
- 访问的地址是合法的,但是该地址还未分配物理页框.
mmap仅仅只是建立了虚拟内存,并未分配物理内存,所以会缺页异常,异常后,会进入缺页异常流程(do_page_fault),将文件从高速缓存映射到物理内存。
然后我们操作文件,就是直接操作内存中的数据,不需要再read、write。
由于物理内存是所有进程共享的,所有mmap也会应用到进程间通信中。
注意
需要注意的是,我们通过mmap,可以做到像操作内存数据一样直接操作文件内容。但是我们的修改是在内存中生效,要想数据写回磁盘,还需要在操作后,调用msync或munmap。
4 总结
在unix/linux平台下读写文件,有两种方式:
- 正常操作,open文件,接着使用read系统调用读取文件的全部或一部分。于是内核将文件的内容从磁盘上读取到内核页高速缓冲,再从内核高速缓冲读取到用户进程的地址空间。这么做需要在内核和用户空间之间做多次数据拷贝。而且当多个进程同时读取一个文件时,则每一个进程在自己的地址空间都有这个文件的副本,这样也造成了物理内存的浪费。
- 内存映射,也就是用mmap去建立虚拟内存,然后通过缺页异常处理,在物理内存创建文件存储区域。
可以看出,mmap减少了内核态到用户态的拷贝,且进程AB操作同一个文件,都是直接操作内存,大大提升了读写效率。