内核除了管理本身的内存外,还必须管理用户空间中进程的内存,我们称这个内存为进程地址空间,也就是系统中每个用户空间进程所看到的内存,linux操作系统采用虚拟内存技术,因此,系统中的所有进程之间以虚拟方式共享内存,对一个进程而言,它好像可以访问整个系统的所有物理内存。即使单独一个进程,它所拥有的地址空间也可以远远大于系统的物理内存。
地址空间
进程地址空间由进程可寻址的虚拟内存组成,而且更为重要的是内核允许进程使用这种虚拟内存中的地址。每个进程都有一个平坦的地址空间,平坦指的是地址空间的范围是一个独立连续的空间。通常情况下每个进程都有唯一的这种平坦地址空间,一个进程的地址空间与另一个进程的地址空间即使有相同的内存地址,实际上也彼此互不相干,这种进程称为线程。
可以被合法访问的地址空间称为内存区域,通过内核,进程可以给自己的地址空间动态的添加或减少内存区域。进程只能访问有效区域内的内存地址,每个内存区域都有相关权限对有关进程的权限进行说明。进程地址空间中的任何有效的地址都只能位于唯一的区域,这些内存区域不能相互覆盖。
内存描述符
内核使用内存描述符结构体来表示进程的地址空间,该结构体包含了和进程地址空间有关的全部信息,内存描述符用mm_struct结构体表示。
struct mm_struct {
struct vm_area_struct *mmap; /* list of memory areas */
struct rb_root mm_rb; /* red-black tree of VMAs */
struct vm_area_struct *mmap_cache; /* last used memory area */
unsigned long free_area_cache; /* 1st address space hole */
pgd_t *pgd; /* page global directory */
atomic_t mm_users; /* address space users */
atomic_t mm_count; /* primary usage counter */
int map_count; /* number of memory areas */
struct rw_semaphore mmap_sem; /* memory area semaphore */
spinlock_t page_table_lock; /* page table lock */
struct list_head mmlist; /* list of all mm_structs */
unsigned long start_code; /* start address of code */
unsigned long end_code; /* final address of code */
unsigned long start_data; /* start address of data */
unsigned long end_data; /* final address of data */
unsigned long start_brk; /* start address of heap */
unsigned long brk; /* final address of heap */
unsigned long start_stack; /* start address of stack */
unsigned long arg_start; /* start of arguments */
unsigned long arg_end; /* end of arguments */
unsigned long env_start; /* start of environment */
unsigned long env_end; /* end of environment */
unsigned long rss; /* pages allocated */
unsigned long total_vm; /* total number of pages */
unsigned long locked_vm; /* number of locked pages */
unsigned long def_flags; /* default access flags */
unsigned long cpu_vm_mask; /* lazy TLB switch mask */
unsigned long swap_address; /* last scanned address */
unsigned dumpable:1; /* can this mm core dump? */
int used_hugetlb; /* used hugetlb pages? */
mm_context_t context; /* arch-specific data */
int core_waiters; /* thread core dump waiters */
struct completion *core_startup_done; /* core start completion */
struct completion core_done; /* core end completion */
rwlock_t ioctx_list_lock; /* AIO I/O list lock */
struct kioctx *ioctx_list; /* AIO I/O list */
struct kioctx default_kioctx; /* AIO default I/O context */
};
mm_users记录正在使用该地址的进程数目。例如有两个线程共享该地址,则mm_users值为2
mm_count是mm_struct的主引用计数,所有的mm_users都等于mm_count的增加量,mm_count的值为0时说明已经没有任何指向该结构体的引用了,这时该结构体会被撤销。
mmap和mm_rb这两个不同的数据结构体描述的对象是相同的,即该地址空间中的全部内存区域,只不过前者以链表的形式存放,后者以红黑树的形式存放。mmap结构体作为链表,利于简单高效的遍历所有元素,mm_rb则更适合搜索指定的元素。
所有的mm_struct结构体都通过自身的mmlist域连接在一个双向链表中,该链表的首元素是init_mm内存描述符,代表init进程的地址空间。
分配进程描述符
在进程描述符中mm域存放着该进程的内存描述符,current->mm指向当前进程的内存描述符,fork()函数利用copy_mm()函数复制父进程的内存描述符,子进程中的mm_struct结构体是从mm_cachep slab缓存中分配得到的,每个进程通常都有唯一的mm_struct,如果父进程希望和子进程共享地址空间,在调用clone()时设置clone_vm标志,这样的进程称为线程。这就是linux中进程和线程的唯一区别,线程对内核来说仅仅是一个共享特定资源的进程而已。如果设置了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_cachep slab缓存中。
mm_struct与内核线程
内核线程没有地址空间,也没有内存描述符,所以内核线程对应的进程描述符中的mm域为空,即内核线程没有用户上下文。但是访问内核内存时,还是需要一些数据的,为了避免内核线程为内存描述符和页表浪费空间,内核线程使用前一个进程的内存描述符。
当一个进程被调度时,该进程的mm域指向的地址空间会被加载到内存,进程描述符中的active_mm域会被更新,执行新的地址空间,由于内核进程没有地址空间,所以mm域为null,这时就会保留前一个进程的地址空间,然后更新active_mm域。
虚拟内存区域
内存区域由vm_area_struct结构体描述,内存区域在linux内核中经常称为虚拟内存区域,即VMAs,vm_area_struct结构体描述了指定地址空间内连续区间上的一个独立内存范围,内核将每个内存区域作为一个单独的内存对象管理,每个内存区域都有一致的属性。
struct vm_area_struct {
struct mm_struct *vm_mm; /* associated mm_struct */
unsigned long vm_start; /* VMA start, inclusive */
unsigned long vm_end; /* VMA end , exclusive */
struct vm_area_struct *vm_next; /* list of VMA's */
pgprot_t vm_page_prot; /* access permissions */
unsigned long vm_flags; /* flags */
struct rb_node vm_rb; /* VMA's node in the tree */
union { /* links to address_space->i_mmap or i_mmap_nonlinear */
struct {
struct list_head list;
void *parent;
struct vm_area_struct *head;
} vm_set;
struct prio_tree_node prio_tree_node;
} shared;
struct list_head anon_vma_node; /* anon_vma entry */
struct anon_vma *anon_vma; /* anonymous VMA object */
struct vm_operations_struct *vm_ops; /* associated ops */
unsigned long vm_pgoff; /* offset within file */
struct file *vm_file; /* mapped file, if any */
void *vm_private_data; /* private data */
};
每个内存描述符度对应于进程地址空间中唯一的区间,在同一个地址空间内的不同内存区域不能重叠,vm_mm域指向和VMA相关的mm_struct结构体,每个VMA对其相关的mm_struct结构体来说都是唯一的,所以即使两个独立的进程将同一个文件映射到各自的地址空间,他们分别都会有一个vm_area_struct结构体来标志自己的内存区域。如果线程共享一个地址空间,则他们也会共享其中所有的vm_area_struct结构体。
VMA标志
标志了内存区域所包含的页面的行为和信息,反映了内核处理页面所需要遵循的行为准则。
VMA操作
在vm_area_struct结构体中的vm_ops域指向域指定内存区域相关的操作函数表,内核使用表中的方法操作VMA。vm_area_struct作为通用对象代表了任何类型的内存区域,而操作表描述针对特定的对象实例的特定方法。操作函数表由vm_operations_struct结构体表示。
struct vm_operations_struct {
void (*open) (struct vm_area_struct *);
void (*close) (struct vm_area_struct *);
struct page * (*nopage) (struct vm_area_struct *, unsigned long, int);
int (*populate) (struct vm_area_struct *, unsigned long, unsigned long,pgprot_t, unsigned long, int);
};
内存区域的树形结构和内存区域的链表结构
mmap和mm_rb都各自独立的指向与内存描述符相关的全体内存区域对象,他们包含完全相同的vm_area_struct结构体指针,仅仅是组织方法不同。链表用于需要遍历全部节点的时候,而红黑树适用于在地址空间中定位特定内存区域的时候。
mmap使用单独链表连接所有的内存区域对象,所有的区域按地址增长的方向排序,mmap域指向链表中的第一个内存区域,链表中最后一个结构体指针指向空。
mm_rb使用红黑树连接所有的内存区域对象,mm_rb指向红黑树的根节点。
mmap()和do_mmap()创建地址区间
内核使用do_mmap函数创建一个新的线性地址空间,如果新创建的地址区间和一个已经存在的地址区间相邻,并且具有相同的访问权限,两个区间将合并为一个,如果系统调用do_mmap的参数无效,则返回一个负值,否则它会在虚拟内存中分配一个合适的新内存区域。
用户空间可以通过mmap()系统调用获取内核函数do_mmap的功能。
munamp()和do_munmap()删除地址区间
do_munmap()函数从特定的进程地址空间中删除指定的地址区间,在用户空间中可以调用do_munmap()函数获得相同的功能。
页表
应用程序操作的对象是映射到物理内存之上的虚拟内存,但是处理器直接操作的确是物理内存,所以当程序访问一个虚拟地址时,必须先将虚拟地址转化成物理地址,然后处理器才能解析地址访问请求,这个转换地址的工作由页表完成。
linux使用三级页表完成地址转换,多级页表能够节约地址转换需要占用的存放空间,linux对所有体系机构,即使不支持三级页表的体系结构都使用三级页表管理,因为三级页表结构可以利用‘最大公约数’思想,可以按照需要在编译时简化使用页表的三级结构,比如使用两级。
顶级页表是全局目录PGD,它包含pgd_t类型的数组,PGD中的表项指向二级页目录中的表项,即:PMD。PMD是二级页表,即中间目录,它包含pmd_t类型的数组,期中的表项指向PTE中的表项。PTE就是最后一级页表,包含了pte_t类型的页表项,指向物理页面。很多体系结构中,搜索页表是由硬件完成的,每个进程都有自己的页表,内存描述符中pgd域指向的就是该进程的全局页目录。
由于每次对虚拟内存的访问都必须解析这样一个过程,从而得到物理内存中对应的地址,所以页表性能非常关键,因此为了加快搜索,多数体系结构都有一个翻译后的缓冲器TLB,TLB是将虚拟地址映射到物理地址的硬件缓存,当访问一个虚拟地址时会首先检查TLB中是否有缓存,如果有直接命中,没有的话再搜索页表。