内存映射, 就是指把外设的内存映射到用户空间访问。系统调用为:
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
其中fd 可以为打开的普通文件或设备文件。返回的地址为用户地址(即vma的地址, vm_area_struct, 跟描述vmalloc返回内核虚拟地址的vm_struct 不一样。)
驱动程序中的mmap原型为:
int (*mmap) (struct file *filp, struct vm_area_struct *vma);
在sysmmap系统调用到f->ops->mmap前,linux OS 已做了大量工作,包含分配vma (或合并原vma)。到此时,vma已经填好,只要将分配物理内存和建立对应的页表即可。如果有物理内存,可以通过remap_pfn_range 一次性建立全部页表,或者在vma的nopage(现新内核改成fault)中每次映射一个页面。
用户进程访问用户空间地址的时候直接通过页表访问,跟vma没任何关系。vma是内核管理用户进程的用户态内存区域的。如果访问地址失败(或者缺页表,或者缺物理内存),都将将引发缺页异常。缺页异常会判定异常的原因,如果是映射的file原因,则调用vma的nopage函数,返回page指针,然后据此计算页帧号并写入对应的页表像pte。这样下次就可以访问了。
nopage函数需要返回一个page指针(page为内核描述物理内存的结构)。如果是vmalloc分配的物理页(单个页面不划算,要分配页表),则调用vmalloc_to_page 返回页面指针;如果是get_free_pages(或者kmalloc)返回的内存,则调用virt_to_page返回page指针。(其实这两个函数实现一样,都是 mem_map [ (kaddr -3G)>>12].
remap不允许映射常规内存,除非需要mem_map_reserve 先设置成保留,因常规内存由OS自己管理。
PCI设备有两次映射,第一次是将外设存储映射到统一的物理地址,在系统启动的时候已经由bios做好了,分配的物理地址记录在PCI外设配置空间里面。在启动后,调用ioremap(pci_resource_start()) 返回内核虚拟地址(跟vmalloc的类似),原型为:
void * ioremap (unsigned long phys_addr, unsigned long size) 然后可以用专用的IO端口操作函数操作 ioread/iowrite.
DMA映射则相反,DMA映射是分配的缓冲区(内核逻辑地址,不管是dma_alloc_coherent, kmalloc等等,本质都是get_free_pages(DMA))跟一组设备可访问的物理地址的组合。dma_alloc_coherent 内部调用get_free_pages(DMA),返回该地址,同时设置对应的物理地址(总线地址返回)。 内核使用逻辑地址设置数据,然后将对应的物理地址写入DMA寄存器(即写上面的ioremap返回的地址),让DMA完成操作。