1. 进程地址空间
其实内核除了管理本身的内存外,还必须管理进程的地址空间。Linux操作系统采用虚拟内存技术,因此系统中的所有进程之间以虚拟方式共享内存。
进程地址空间由每个进程中的线性地址区组成,而且内核允许进程使用该空间中的地址。进程之间可以选择共享地址空间,我们称为这样的进程为线程。
内存地址是一个给定的值,它要在地址空间范围之内的,这些可被访问的合法地址区间被称为内存区域,通过内核,进程可以给自己的地址空间动态地添加或减少内存区域。
进程只能访问有效范围的内存地址。每个内存区域也具有相应进程必须遵循的特定访问属性。如果一个进程访问了不存在有效范围中的地址,或以不正确的方式访问有效地址,那么内核就会终止该进程,并返回段错误信息。
内存区域可以包含各种内存对象。
2. 内存描述符
内核使用内存描述符结构体表示进程的地址空间。内存描述符mm_struct结构体表示,定义在文件<linux/sched.h>中。
该数据结构中同时使用了两个计数器是为了区别主使用计数(mm_count)器好和使用该地址空间的进程的数目(mm_users)。当mm_users的值为0时,mm_count的值才能变为0。
mmap和mm_rb这个两个不同数据结构描述的对象是相同的:该地址空间中的全部内存区域,但是以链表形式存放而后者以红-黑树的形式存放。前者是链表,利于简单、高效地遍历所有元素;而后者是二叉树,更适合搜索指定元素。
2.1 分配内存描述符
在进程描述符中,mm域存放着该进程使用的内存描述符,所以current->mm便指向当前进程的内存描述符。函数fork利用复制copy_mm函数复制父进程的内存描述符,也就是current->mm域给其子进程,而子进程中的mm_struct结构体是通过文件kernel/fork.c中allocte_mm宏从mm_cachep_slab缓存中分配得到的。每个进程都有唯一的mm_struct结构体。
如果父进程希望和其子进程共享地址空间,可以调用clone时,设置CLONE_VM标志。这样的进程被称为线程。
2.2. 销魂内存描述符
当进程退出时,内核会调用exit_mm函数,该函数会做销毁工作,更新统计量。其中,该函数会调用mmput减少内存描述符中的mm_users用户计数,如果用户计数为0,继续调用mmdrop减少mm_count使用计数。如果使用计数为0,就调用free_mm宏通过kmem_cache_free函数将mm_struct结构体归还到mm_cachep_slab缓存中。
2.3. mm_struct和内核线程
内核线程没有进程地址空间,也没用相关的内存描述符,所以,内核线程没有用户上下文,在用户空间没有任何页。为了避免内核线程为内存描述符和页表浪费空间,并当新内核线程运行时避免浪费处理器周期想新地址空间进行切换,内核线程将直接使用前一个进程的的内存描述符。
3. 内存区域
内存区域由vm_area_struct结构体描述,定义在文件<linux/mm.h>中,内存区域在内核中也称为虚拟内存区域或VMA。它描述了指定地址空间内存连续区间上的一个独立内存范围。
3.1. VMA标志
VMA是一种位标志,定义在<linux/mm.h>。它包含在vm_flags域内,标志了内存区域所包含的页面的行为和信息。和物理页的访问权限不同,VMA标志反映了内核处理页面所需要遵守的行为准则,而不是硬件要求。
3.2. VMA操作
vm_area_struct结构体的vm_ops域指向内存区域相关的操作函数表,内核使用表中的方法操作VMA。
操作函数表由结构体表表示,定义在<linux/mm.h>中:
vm_opterations_struct{
void(*open)(struct vm_area_struct *);
void(*close)(struct vm_area_struct *);
struct page *(*nopage)(struct vm_area_struct *,unsigned long, int);
void(*populate)(struct vm_area_struct *, unsigned long, unsigned long,
pgprot_t, unsigned long, int);
};
3.3. 实际使用中的内存区域
可以使用/proc文件系统和pmap工具查看给定进程的内存空间和其中所含的内存区域。
3.4. 操作内存区域
内核时常需要判断进程地址空间中的内存区域是否满足某些条件,比如某个指定地址是否包含在某个内存区域内。
3.4.1. find_vma函数
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr);
该函数在指定的地址空间中搜索第一个vm_end大于addr的内存区域。换句话说,该函数寻找第一个包含addr或首地址大于addr的内存区域,如果没有发现这样的区域,该函数返回NULL;否则返回指向匹配的内存区域的vm_area_struct结构体指针。注意,由于返回的VMA首地址可能大于addr,所以指定的地址并不一定包含在返回的VMA中。
首先,该函数检查mmap_cache,看看缓存的VMA释放包含了需要的地址。其次,如果缓存没有就必须搜索红-黑树。如果没有存在满足要求的VMA,那该函数就返回NULL。
3.4.2. find_vma_prev函数
struct vm_area_struct *find_vma_prev(struct mm_struct *mm, unsigned long addr,
struct vm_area_struct **pprev);
该函数与find_vma的工作方式相同,但是它返回第一个小于addr的VMA。参数pprev指向先于addr的VMA指针。
3.4.3. find_vma_intersection函数
static inline struct vm_area_struct *find_vma_intersection(struct mm_struct *mm,
unsigned long start_addr, unsigned long end_addr);
该函数返回第一个和指定地址区间相交的VMA。参数pprev指向先于addr的VMA指针。
4. 创建地址空间
内核使用do_mmap函数创建一个新的线性地址区间。该函数创建一个新VMA,如果创建的地址空间区间和一个已经存在的地址区间相邻,并且它们有相同的访问权限,那么这两个区间将合并为一个。
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,该情况被称作匿名映射(anonymous mapping)。如果指定了文件名和偏移量,该映射被称为文件映射(file-backed mapping)。
参数addr是可选参数,指定搜索空闲区域的起始位置。
参数prot指定内存区域中页面的访问权限。访问权限标志定义在<asm/mmam.h>中。
如果系统调用do_mmap的参数中有无效参数,那么返回一个负值;否则,返回在虚拟内存中分配一个合适的新内存区域。
4.1. mmap系统调用
在用户空间可以通过调用mmap系统调用获取内核函数do_mmap的功能:
void *mmap2(void *start, size_t length, int prot, int flags, int fd, off_t pgoff);
由于该系统调用时mmap调用的第二种变种。mmap调用中最后一个参数是字节偏移量,而mmap2使用的是页面偏移量。使用页面偏移量可以映射更大的文件和更大的偏移位置。
5. 删除地址空间
do_munmap函数从特定的进程地址空间中删除指定地址空间。
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len);
第一个参数指定要删除区域所在的地址空间,删除从地址start开始,长度为len字节的地址区间。如果成功,返回0,否则返回负值。
5.1. munmap系统调用
系统调用munmap给用户空间程序提供了一种从自身地址空间删除指定地址区间的方法:
int munmap(void *start, size_t length);
6. 页表
虽然应用程序操作的对象是映射到物理内存之上的虚拟内存,但是处理器直接操作的却是物理内存。所以当应用程序访问一个虚拟地址时,首先必须将虚拟地址转化成物理地址,然后处理器才能解析地址访问请求。地址转换工作需要通过查询页表才能完成,概况的说,地址转换需要虚拟地址分段,使每段地址都作为一个索引指向页表,而页表项则指向下一级别的页表或者执行最终的物理页面。
Linux中使用三级页表完成地址转换。利用多级页表能够节约地址转换需要占用的存放空间。
*页表是页全局页目录(PGD)。PGD包含了一个pgd_t类型数组,多数体系结构中的pgd_t类型等同于无符号长整型。PGD的表项执行二级页目录的表项:PMD。
二级页表是中间页目录(PMD)。PMD是个pmd_t类型数组,其中的表项指向物理页面。
最后一级的页表简称页表,其中包含了pte_t类型的页表项,该页表项指向物理页面。
每个进程都有自己的页表(线程会共享页表)。内存描述符的pgd域指向的就是进程的页全局目录。注意操作和检索页表时必须使用page_table_lock锁,该锁在相应的进程的内存描述符中,以防止竞争条件。
页表对应的结构体依赖于具体的体系结构,定义在<asm/page.h>中。页表操作性能非常关键,为了加快搜索,多数体系结构实现了一个翻译后缓冲器(translation lookaside buffer,TLB)。TLB是一个将虚拟地址映射到物理地址的硬件缓存。
2.6内核对页表管理的主要改进时:从高端内存分配部分页表。今后可能的改进包括通过写时拷贝(copy-on-write)的方式共享页表。