目录
1. 前文
X86 CR3寄存器:
作用:用于存储页全局目录表(PGD)的物理地址。
进程切换时会将CR3更新为新进程的全局页目录表物理地址。其他体系架构一样。
所以每个进程的同一虚拟地址,经查询页表后得到不同物理地址。
PTE:多级页表的最后一级,存储最终虚拟地址对应的物理地址。
2. 原理
mmap含义:
将一个文件或其他对象(驱动中某设备物理地址)映射到进程虚拟地址空间。进程直接操作虚拟内存地址,实现文件或其他对象的读写。
mmap映射分类:
1. 文件映射和匿名映射:
1.1 文件映射:将文件物理地址映射到进程虚拟地址空间。对应内存页为文件页。
1.2 匿名映射:没有对应数据源(如文件),就单纯映射一块内存物理地址到进程虚拟地址空间。对应内存页为匿名页。
2. 共享映射和私有映射:
2.1 共享映射:修改映射区域时,其他进程可见,如果是文件共享映射,会修改磁盘文件。
2.2 私有映射:修改数据时复制副本,并修改副本,其他进程看不见,不影响数据源。
私有文件映射页表PTE为只读,写文件时会发生缺页中断,中断处理函数会COW写时复制。
共享文件映射页表PTE为可写,写文件时不会缺页中断,而是直接写入页缓存,最终回写到磁盘。
mmap原理:
1. 共享文件映射过程不涉及任何物理内存分配,只分配一段虚拟内存(vm_area_struct),新建共享映射时,文件没有对应页缓存。
2. 在各进程页表中,该映射的虚拟内存页表项PTE为空,读写该虚拟内存时,MMU产生缺页中断, 内核执行缺页中断处理程序。
3. 中断处理程序通过vm_area_struct->vm_pgoff在页缓存中查找文件。如果文件不在页缓存中,则分配内存页,并加入到页缓存中。
4. 最后调用readpage,来读取磁盘文件到新分配的内存页,最后填写页表项PTE。
mmap优点:
优点:零拷贝,按需加载,共享内存,适用于大文件。
mmap使用场景:
malloc分配大内存:使用mmap匿名映射。
文件IO:不使用read、write、页缓存。
进程间通信:匿名映射或文件映射。
驱动设备:将驱动buffer物理地址,映射到进程虚拟地址空间。
fd = open("/dev/mtdblock1",);
mmap映射该fd,最终调用mtdblock驱动struct file_operations中mmap。
零拷贝网络收发包:将文件映射到内存,直接从内存收发数据,无需用户空间和内核空间的多次拷贝。
open函数O_DIRECT标志:文件I/O数据的传送绕过页高速缓存,直接立刻回写到磁盘文件。
如下图:mmap映射区域在进程虚拟地址空间的堆和栈之间:
3. 应用层
应用层mmap函数原型:
void * mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)
addr:建立映射区的虚拟地址首地址,通常为NULL,即由Linux内核指定地址。
length:映射区长度。
prot:期望的映射区权限,不能与文件fd打开模式冲突。
PROT_READ:页内容可读
PROT_WRITE:页内容可写
PROT_EXEC:页内容可执行
PROT_NONE:页内容不访问
flags:指定映射类型。
MAP_PRIVATE:进程私有映射,修改映射时创建副本,不影响磁盘文件和其他进程。
MAP_SHARED:共享映射,修改映射时会回写到磁盘文件,可实现进程间通信。
MAP_ANONYMOUS:匿名映射。
fd:文件描述符。若匿名映射,则fd为-1。
总结:匿名映射通常是私有映射。共享匿名映射只能在父子进程共享。
那为什么共享匿名映射只能在父子进程共享?
因为匿名映射,选定的映射源物理内存地址是内核随机找的,其他进程在调用mmap无法找到这个映射源物理内存地址。而子进程fork时使用了CLONE_VM,父子进程都可通过同一虚拟地址(mmap返回的虚拟地址),找到相同物理内存地址。
4. 内核
常规文件读写过程:
1. 进程发起读文件请求。
2. 内核fd->struct file -> inode
3. 在inode的 struct address_space *i_mapping; 或struct file 的struct address_space *f_mapping;查找请求文件页是否在页缓存中。
如果存在,直接返回该文件页内容。
如果不存在,通过inode-> i_sb定位到文件磁盘物理地址,将文件复制到页缓存。
struct file { //表示一个打开文件
struct address_space *f_mapping; 包含该文件的页缓存信息。
}
struct inode { //表示磁盘上一个文件的元信息
struct address_space *i_mapping; 包含该文件的页缓存信息。
}
struct address_space {
struct radix_tree_root page_tree; 基数树,连接该文件所有占用的内存页(页缓存)。
}
如何判断一个文件指定偏移是否在页缓存中?
struct page *find_get_page(struct address_space *mapping, pgoff_t offset)
调用radix_tree_lookup(), 搜索拥有指定偏移量的基数树的叶子节点。
使用举例:page = find_get_page(inode->i_mapping, offset);
mmap映射过程:
1.1 在进程虚拟空间寻找空闲区域。
1.2 为此虚拟区域区分配并初始化vm_area_struct。
2.1 通过文件描述符fd,找到struct file,再调用struct file_operations中mmap,默认mmap为generic_file_mmap(struct file *filp, struct vm_area_struct *vma)。
2.2 通过inode->i_sb 超级块,得到文件块映射表,最后得到文件物理地址。
2.3 调用install_file_pte创建页表,此时页表没有对应物理地址。
ps:前两个阶段仅创建映射,没有拷贝文件到内存。
3.1 进程读写该映射虚拟地址,通过查询页表,页表没有对应物理地址,引发缺页异常。
3.2 缺页异常中断处理程序中,发起调页请求。
3.3 调页过程先在swap cache中寻找内存页,若失败则调用nopage函数把文件拷贝到内存
3.4 之后进程对该内存读写,延时回写脏页面到物理地址,完成读写文件。