进程地址空间
- 1 内存描述符
- 分配内存描述符
- 销毁内存描述符
- mm_struct与内核线程
- 2 内存区域
- VMA标志
- VMA操作
- 内存区域的树形结构和内存区域的链表结构
- 3 操作内存区域
- find_vma()
- find_vma_prev()
- find_vma_intersection()
- 4 mmap()和do_mmap():创建地址空间
- mmap() 系统调用
- 5 munmap()和do_munmap():删除地址空间
- munmap()系统调用
- 6 页表
内核除了管理本身的内存外,还必须管理进程的地址空间,也就是系统中每个用户空间地址所对应的内存。Linux操作系统采用虚拟内存技术,因此,系统中的所有进程之间以虚拟方法共享内存,对每个进程来说,它们好像都可以访问整个系统的所有物理内存。
进程地址空间由每个进程中的线性地址区组成,每个进程都有一个32位或64位的平坦地址空间,空间的具体大小取决于体系结构,平坦地址空间是指地址空间范围是一个独立的连续空间(比如,地址从0扩展到429496729位地址空间)。一些操作系统提供了段地址空间,这种地址空间并非是一个独立的线性区域,而是被分段的,但现代采用虚拟内存的操作系统通常都使用平坦地址空间而不是分段式的内存模式。通常情况下,每个进程都有唯一的这种平坦地址空间,而进程地址空间之间彼此互不相干,两个不同的进程可以在它们各自地址空间的相同地址内存放不同的数据。进程之间也可以选择共享地址空间,我们称这样的进程为线程。
1 内存描述符
内核使用内存描述符结构体表示进程的地址空间,该结构包含了和进程地址空间有关的全部信息。内存描述符由mm_struct结构体表示,定义在文件linux/sched.h中。
struct mm_struct {struct vm_area_struct * mmap; /* list of VMAs */struct rb_root mm_rb;struct vm_area_struct * mmap_cache; /* last find_vma result */unsigned long (*get_unmapped_area) (struct file *filp,unsigned long addr, unsigned long len,unsigned long pgoff, unsigned long flags);void (*unmap_area) (struct vm_area_struct *area);unsigned long mmap_base; /* base of mmap area */unsigned long free_area_cache; /* first hole */pgd_t * pgd;atomic_t mm_users; /* How many users with user space? */atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */int map_count; /* number of VMAs */struct rw_semaphore mmap_sem;spinlock_t page_table_lock; /* Protects page tables, mm->rss, mm->anon_rss */struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung* together off init_mm.mmlist, and are protected* by mmlist_lock*/unsigned long start_code, end_code, start_data, end_data;unsigned long start_brk, brk, start_stack;unsigned long arg_start, arg_end, env_start, env_end;unsigned long rss, anon_rss, total_vm, locked_vm, shared_vm;unsigned long exec_vm, stack_vm, reserved_vm, def_flags, nr_ptes;unsigned long saved_auxv[42]; /* for /proc/PID/auxv */unsigned dumpable:1;cpumask_t cpu_vm_mask;/* Architecture-specific MM context */mm_context_t context;/* Token based thrashing protection. */unsigned long swap_token_time;char recent_pagein;/* coredumping support */int core_waiters;struct completion *core_startup_done, core_done;/* aio bits */rwlock_t ioctx_list_lock;struct kioctx *ioctx_list;struct kioctx default_kioctx;
};
mm_users域记录正在使用该地址的进程数目。比如,有两个进程共享该地址空间,那么mm_users的值便等于2;mm_count是mm_struct的主引用计数,只要mm_users不为0,那么mm_count值就等于1。当mm_users的值减为0时,mm_count域的值才为0,如果mm_count的值等于0,说明已经没有任何指向该mm_struct结构体的引用了,这个时候该结构体会被销毁。
mmap和mm_rb这两个数据结构描述的对象是相同的:该地址空间中的全部内存区域。但是mmap是以链表形式存放而后者以红-黑树形式存放。mmap结构体作为链表,利于简单、高效地遍历所有元素,而mm_rb结构体更适合搜索指定元素。
所有的mm_struct结构体都通过自身的mmlist域连接在一个双向链表中,该链表的首元素是init_mm内存描述符,它代表init进程的地址空间,内存描述符的总数存放在mmlist_nr全局变量中,该变量定义在kernel/fork.c中。
分配内存描述符
在进程的struct task_struct
进程描述符中,mm域存放着该进程使用的内存描述符,所以current->mm便指向当前进程的内存描述符。fork函数利用copy_mm()函数复制父进程的内存描述符,也就是current->mm域给其子进程,而子进程中的mm_struct结构体实际是通过文件kernel/fork.c中的allocate_mm()宏从mm_cachep slab缓存中分配得到的。
如果父进程希望和其子进程共享地址空间,可以在调用clone()时,设置CLONE_VM标志。我们把这样的进程称作线程。当CLONE_VM被指定后,内核就不再需要调用allocate_mm()函数了,而仅仅需要在调用copy_mm()函数中将mm域指向其父进程的内存描述符就可以了。
销毁内存描述符
当进程退出时,内核会调用exit_mm函数,该函数执行一些常规的销毁工作,同时更新一些统计量。其中,该函数会调用mmput()函数减少内存描述符的mm_users用户计数,如果mm_users降到0,继续调用mmdrop()函数,减少mm_count,如果mm_count也等于0了,说明该内存描述符不再有任何使用者了,那么调用free_mm宏通过kmem_cache_free()将mm_struct结构体归还到mm_cachep_slab缓存中。
mm_struct与内核线程
内核线程没有进程地址空间,也没有相关的内存描述符,所以内核线程对应的进程描述符中mm域为空。
当一个进程被调度时,该进程的mm域指向的地址空间被装载到内存,进程描述符中的active_mm域会被更新,指向新的地址空间。内核线程没有地址空间,所以mm域为NULL,于是当一个内核线程被调用时,就会保留前一个进程的地址空间,随后内核更新内核线程对应的进程描述符中的active_mm域,使其指向前一个进程的内存描述符。
2 内存区域
内存区域由vm_area_struct结构体描述,定义在文件linux/mm.h中,内存区域在内核中也经常被称作虚拟内存区域或VMA。
vm_area_struct结构体描述了指定地址空间内连续区间上的一个独立内存范围。内核将每个内存区域作为一个单独的内存对象管理,每个内存区域都拥有一致的属性,
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 addresswithin vm_mm. *//* linked list of VM areas per task, sorted by address */struct vm_area_struct *vm_next;pgprot_t vm_page_prot; /* Access permissions of this VMA. */unsigned long vm_flags; /* Flags, listed below. */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 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_node; /* Serialized by anon_vma->lock */struct anon_vma *anon_vma; /* Serialized by page_table_lock *//* Function pointers to deal with this struct. */struct vm_operations_struct * vm_ops;/* Information about our backing store: */unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZEunits, *not* PAGE_CACHE_SIZE */struct file * vm_file; /* File we map to (can be NULL). */void * vm_private_data; /* was vm_pte (shared mem) */#ifdef CONFIG_NUMAstruct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
};
每个内存描述符都对应于进程地址空间的唯一区间,vm_start域指向区间的首地址,vm_end域指向区间的尾地址之后的第一个字节,vm_end~vm_start的大小便是内存区间的长度,内存区域的位置就在[vm_start,vm_end]之中,注意,在同一个地址空间内的不同内存区间不能重叠。
vm_mm域指向和VMA相关的mm_struct结构体,注意每个VMA对其相关的mm_struct来说都是唯一的,所以即使两个独立的进程将同一个文件映射到各自的地址空间,它们分别都会有一个vm_area_struct结构体标志自己的内存区域,但是如果两个线程共享一个地址空间,那么它们也同时共享其中所有的vm_area_struct结构体。
VMA标志
VMA标志是一种位标志,其定义在linux/mm.h中,它包含在vm_flags域内,标志了内存区域所包含的页面的行为和信息,和物理页的访问权限不同,VMA标志反映了内核处理页面所需要遵守的行为准则,而不是硬件要求。
VMA操作
vm_area_struct结构体中的vm_ops指向与指定内存区域相关的操作函数表,内核使用表中的方法操作VMA。vm_area_struct作为通用对象代表了任何类型的内存区域,而操作表描述针对特定的对象实例的特定方法。
操作函数表由vm_operations_struct结构体表示,定义在文件linux/mm.h中
/** These are the virtual MM functions - opening of an area, closing and* unmapping it (needed to keep files on disk up-to-date etc), pointer* to the functions called when a no-page or a wp-page exception occurs. */
struct vm_operations_struct {void (*open)(struct vm_area_struct * area);void (*close)(struct vm_area_struct * area);struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, int *type);int (*populate)(struct vm_area_struct * area, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock);
#ifdef CONFIG_NUMAint (*set_policy)(struct vm_area_struct *vma, struct mempolicy *new);struct mempolicy *(*get_policy)(struct vm_area_struct *vma,unsigned long addr);
#endif
};
内存区域的树形结构和内存区域的链表结构
上面说过,可以通过内存描述符中的mmap和mm_rb域之一访问内存区域,这两个域各自独立地指向与内存描述符相关的全部内存区域对象vm_area_struct。
mmap使用单独链表连接所有的内存区域对象vm_area_struct,每一个vm_area_struct结构体通过自身的vm_next域被连入链表,所有的区域按地址增长的方向排序,mmap域指向链表中第一个内存区域,链中最后一个VMA结构体指针指向空。
mm_rb域使用红-黑树连接所有内存区域对象,mm_rb域指向红-黑树的根结点,地址空间中每一个vm_area_struct结构体通过自身的vm_rb域连接到树中。
链表用于需要遍历全部结点的时候,而红-黑树适用于在地址空间中定位特定内存区域的时候。内核为了内存区域上的各种不同操作都能获得高性能,所以同时使用了这两种数据结构。
3 操作内存区域
内核定义了许多内存区域操作函数,它们都声明在文件linux/mm.h中
find_vma()
find_vma()函数定义在mm/mmap.c中。
/* Look up the first VMA which satisfies addr < vm_end, NULL if none. */
extern struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr);
该函数在指定的地址空间中搜索第一个vm_end大于addr的内存区域。换句话说,该函数寻找第一个包含addr或首地址大于addr的内存区域,如果没有发现这样的区域,该函数返回NULL。否则返回指向匹配的内存区域的vm_area_struct结构体指针,返回的结构会被缓存在内存描述符的mmap_cache域中,所以find_vma会先在缓存中查找,如果指定的地址不在缓存中,那么必须搜搜和内存描述符相关的所有内存区域,这种搜索通过红-黑树进行。
find_vma_prev()
find_vma_prev()函数和find_vma()工作方式相同,但是它返回第一个小于addr的VMA。该函数定义和声明分别在文件mm/mmap.c中和文件linux/mm.h中
extern struct vm_area_struct * find_vma_prev(struct mm_struct * mm, unsigned long addr,struct vm_area_struct **pprev)
pprev参数存放指向先于addr的VMA指针。
find_vma_intersection()
find_vma_intersection()返回第一个和指定地址区间相交的VMA。因为该函数和内联函数,所以定义在文件linux/mm.h中:
static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)
{struct vm_area_struct * vma = find_vma(mm,start_addr);if (vma && end_addr <= vma->vm_start)vma = NULL;return vma;
}
第一个参数是要搜索的地址空间,start_addr是区间的开始首位置,end_addr是区间的尾位置,
4 mmap()和do_mmap():创建地址空间
内核使用do_mmap()函数创建一个新的线性地址区间。如果创建的地址区间和一个已经存在的地址区间相邻,并且它们具有相同的访问权限的话,那么两个区间将合并为一个。do_mmap()函数会将一个地址区间加入到进程的地址空间中。
do_mmap()函数定义在linux/mm.h中
static inline unsigned long do_mmap(struct file *file, unsigned long addr,unsigned long len, unsigned long prot,unsigned long flag, unsigned long offset)
该函数映射由file指定的文件,具体映射的是文件中从偏移量offset处开始,长度为len字节的范围内的数据。如果file参数是NULL并且offset参数也是0,那么就代码这次映射没有和文件相关,该情况被称作匿名映射,如果指定了文件名和偏移量,那么该映射被称为文件映射。
addr是可选参数,它指定搜索空闲区域的起始位置。
prot参数指定内存区域中页面的访问权限。访问权限标志定义在文件asm/mman.h中。
flag参数指定了VMA标志,这些标志也定义在文件asm/mman.h中
mmap() 系统调用
在用户空间可以通过mmap()系统调用获取内核函数do_mmap()的功能。
5 munmap()和do_munmap():删除地址空间
do_munmap()函数从特定的进程地址空间中删除指定地址区间,该函数定义在文件linux/mm.h中:
extern int do_munmap(struct mm_struct *mm, unsigned long start, size_t len);
第一个参数指定要删除区域所在的地址空间,删除从地址start开始,长度为len字节的地址区间,如果成功,返回0.
munmap()系统调用
系统调用munmap()给用户空间程序提供了一种从自身地址空间删除指定区间的方法。
int munmap(void *start ,size_t length)
该系统调用定义在mm/mmap.c中,它是对do_munmap的一个简单封装
6 页表
虽然应用程序操作的对象是映射到物理内存之上的虚拟内存,但是处理器直接操作的却是物理内存,所以当应用程序访问一个虚拟地址时,首先必须将虚拟地址转化为物理地址,然后处理器才能解析地址访问请求。地址的转换工作是通过查询页表完成的。
页表对应的结构体依赖具体的体系结构,所以定义在文件asm/page.h中