1. 概述
用户进程的虚拟内存管理和内核内存管理比较起来更加复杂,一个对象要在多个数据结构中被管理,而且用户访问内存的不确定性,需要更多的安全检查。下面先看看虚拟内存管理的基本流程。
进程task_struct的struct mm_struct* mm 成员管理进程的虚拟的内存,当需要从文件中映射一块size大小的内存时,文件地址空间的相关操作找到一块连续的虚拟内存空间,文件的地址空间对象address_space负责进程的虚拟地址空间和文件的地址空间之间的映射。当然size需要页对齐的。因此我们需要一个管理虚拟内存区段的数据结构,也就是struct vm_area_struct。它包含了区段起始地址,结束地址,还有一个红黑树节点,用于mm_struct统一管理,这个跟vmalloc管理虚拟内存区段的方式相同。struct vm_area_struct 还包含了用于建立基于文件映射的逆向映射的优先树节点和匿名映射相关数据结构管理的相关成员。找到了连续的size大小的区域之后,分配一个vm_area_struct对象,然后将其加入到mm_struct管理的红黑树中,虽然分配了虚拟内存空间,但是并没有将其关联到实际的物理空间页,也没有填充对应的页表项。在进程的虚拟内存中,将虚拟内存地址和物理页关联起来是通过缺页异常的方式,讲到缺页异常就得看看产生新进程的方式了。
linux中新建一个进程是通过fork调用,fork会复制父进程的相关配置,包括文件描述符表,文件系统,虚拟内存空间(这也就包括了父进程的页表、vm_area_struct黑红树、匿名映射、逆向映射等相关配置),信号处理程序等。当复制虚拟内存空间的时候,复制父进程的页表,由于两个进程共享相同的页表,并且两个进程的页表只有读权限而不能写,这就保证了一个进程的操作不会影响到另外一个进程,当进程要写内存的时候,从来引发一个缺页异常,。进程的虚拟内存真正的读写权限就保存在vm_area_struct的vm_flags成员中,缺页异常判断访问的地址是否在vm_area_struct的红黑树中并且判断是否有写权限,若可以写,则分配一个struct page,并建立struct page的逆向映射。
2. 数据结构
/*
* This struct defines a memory VMM memory area. There is one of these
* per VM-area/task. A VM area is any part of the process virtual memory
* space that has a special rule for the page-fault handlers (ie a shared
* library, the executable area etc).
*/
struct vm_area_struct {
struct mm_struct * vm_mm; /* The address space we belong to. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
unsigned long vm_flags; /* Flags, see mm.h. */
struct rb_node vm_rb;
/*
* For areas with an address space and backing store,
* linkage into the address_space->i_mmap prio tree, or
* linkage to the list of like vmas hanging off its node, or
* linkage of vma in the address_space->i_mmap_nonlinear list.
*/
union {
struct {
struct list_head list;
void *parent; /* aligns with prio_tree_node parent */
struct vm_area_struct *head;
} vm_set;
struct raw_prio_tree_node prio_tree_node;
} shared;
/*
* A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
* list, after a COW of one of the file pages. A MAP_SHARED vma
* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
* or brk vma (with NULL file) can only be in an anon_vma list.
*/
struct list_head anon_vma_chain; /* Serialized by mmap_sem &
* page_table_lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;
/* Information about our backing store: */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units, *not* PAGE_CACHE_SIZE */
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
};
此结构如上所述管理虚拟地址区域,并且通过vm_rb成员将其串在mm_struct的红黑树上,用来管理vm_area_struct,红黑树以vm_end为键值。当需要查看某个虚拟地址是否在当前进程中已经映射的时候,只需遍历vm_area_struct红黑树。文件的地址空间对象用于管理文件的逆向映射:
struct address_space {
struct inode *host; /* owner: inode, block_device */
struct radix_tree_root page_tree; /* radix tree of all pages */
spinlock_t tree_lock; /* and lock protecting it */
unsigned int i_mmap_writable;/* count VM_SHARED mappings */
struct prio_tree_root i_mmap; /* tree of private and shared mappings */
struct list_head i_mmap_nonlinear;/*list VM_NONLINEAR mappings */
struct mutex i_mmap_mutex; /* protect tree, count, list */
/* Protected by tree_lock together with the radix tree */
unsigned long nrpages; /* number of total pages */
pgoff_t writeback_index;/* writeback starts here */
const struct address_space_operations *a_ops; /* methods */
unsigned long flags; /* error bits/gfp mask */
struct backing_dev_info *backing_dev_info; /* device readahead, etc */
spinlock_t private_lock; /* for use by the address_space */
struct list_head private_list; /* ditto */
struct address_space *assoc_mapping; /* ditto */
} __attribute__((aligned(sizeof(long))));
地址空间对象是和文件对应的,每一个文件都有一个地址空间对象,当进行文件映射的时候,将所有和本文件相关的vm_area_struct对象都通过其
prio_tree_node成员串在address_space的i_mmap上,当查找page的逆向映射只需遍历此优先树即可。
下面看看vm_area_struct的相关操作:
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
struct vm_area_struct *vma = NULL;
if (mm) {
/* Check the cache first. */
/* (Cache hit rate is typically around 35%.) */
vma = mm->mmap_cache;
if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) {
struct rb_node * rb_node;
rb_node = mm->mm_rb.rb_node;
vma = NULL;
while (rb_node) {
struct vm_area_struct * vma_tmp;
vma_tmp = rb_entry(rb_node,
struct vm_area_struct, vm_rb);
if (vma_tmp->vm_end > addr) {
vma = vma_tmp;
if (vma_tmp->vm_start <= addr)
break;
rb_node = rb_node->rb_left;
} else
rb_node = rb_node->rb_right;
}
if (vma)
mm->mmap_cache = vma;
}
}
return vma;
}
这是遍历红黑树找到addr对应的vm_area_struct对象,若地址没有映射,则返回null。还有一些插入区域,创建区域,区域合并等操作,下面看看内存映射的函数do_mmap_pgoff,这个函数大部分操作在安全检查,判断权限等操作。抽出两个重要的函数:
/* Obtain the address to map to. we verify (or select) it and ensure
* that it represents a valid section of the address space.
*/
addr = get_unmapped_area(file, addr, len, pgoff, flags);
这个就是通过address_space对象的
address_space_operations->get_unmapped_area映射文件地址空间和进程的虚拟地址空间,返回此文件映射在映射到的虚拟地址空间地址。
另个就是执行映射的mmap_region函数:
/*
* Determine the object being mapped and call the appropriate
* specific mapper. the address has already been validated, but
* not unmapped, but the maps are removed from the list.
*/
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
if (!vma) {
error = -ENOMEM;
goto unacct_error;
}
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags;
vma->vm_page_prot = vm_get_page_prot(vm_flags);
vma->vm_pgoff = pgoff;
INIT_LIST_HEAD(&vma->anon_vma_chain);
....
....
vma_link(mm, vma, prev, rb_link, rb_parent); //将其串到红黑树和优先树上
从上可以看出并没有填充页表和实际分配物理内存页,这都是通过缺页异常来惰性处理,只有当进程确实用到了对应的地址才会真实分配物理空间。
从虚拟内存中将vm_area_struct映射需要做很多工作,需要将vm_area_struct从红黑树上删除,从优先树上删除对应项,删除对应的页表项。
下面看看逆向映射,逆向映射用于查找struct page对应的所有的vm_area_struct项,找到了vm_area_struct就找到了mm_struct和相关的task_struct了,在页交换到磁盘的时候就需要用到。逆向映射关键在于要找到所有的用了page的进程,也就是说需要一个跨多个进程的东西的来统计和page相关的进程,并且可以通过page中的指针访问到,显然不能把所有引用的task_struct的指针全保存在page中,由于内存的page的数量巨大,应该尽可能保持page结构小。基于文件映射的逆向映射很好理解,因为有天然的全局唯一的对象,那就是文件,每一个文件对应一个address_space对象,page的mapping成员就指向对应文件的address_space。匿名映射就有点难搞了,因为不和文件相关,只能建立自己的数据结构,由于匿名映射不能再不相关的进程之间共享,而由于fork调用复制了父进程的匿名映射,因此只需管理父子进程之间的匿名页引用即可,linux通过匿名映射的管理数据结构anon_area来管理逆向映射,在匿名映射时,page的mapping成员指向每个进程唯一的anon_area
基于文件的逆向映射就没什么好讲的了,要想找到引用的一个page的vm_area_struct只要从page->mapping获得address_space,然后遍历优先树中的每一个vm_area_struct,通过page的index获得在此虚拟地址空间的地址,然后看一下是否在vm_area_struct中就可以了,若在,则vm_area_struct引用了此page。