博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

[OS]Process Address Space

Posted on 2010-04-02 17:58  xuczhang  阅读(721)  评论(0编辑  收藏  举报

 

memory descriptor(内存描述符)

与进程地址空间相关的全部信息都包含在内存描述符中。其类型是mm_struct,记录在task_struct的mm成员中。

1. 有一个mmlist的链表链接所有的mm_struct。表头是init_mm(进程0的mm)。

2. mm_users和mm_count

当一个内存描述符有两个轻量级进程共享,此时mm_users为2.而mm_count为1。如果把内存描述符暂时借给一个内核线程,mm_count就加一。内存描述符当mm_count为0时释放。

3. mm_alloc()函数用来获得一个新的内存描述符,这些描述符是由slab分配器管理的,它调用kmem_cache_alloc(),并把mm_count和mm_users置为1。

内核线程的内存描述符

内核线程运行在普通线程中,所以其没有特殊的mm_struct,而是借用用户进程的,在task_struct中有两种内存描述符指针:mm和active_mm。对于用户进程这两个是一致的。对于内核进程而言,它的mm总是为NULL,active_mm被初始化为前一个运行进程的active_mm。

Question:只要处于内核态的一个进程为“高端”线性地址(>0xc0000000)修改了页表项,它就应当更新系统中所有进程页表集合中的相应表项。而修改所有的进程的页表项是非常费时的,如何解决这个问题?

Answer: Linux采用一种延迟方式。当高端地址被重新映射时,内核只更新主内存描述符的pgd指向的页全局目录swapper_pg_dir。主内存描述符在init_mm变量中,是给swapper内核进程使用的(即是进程0)。具体实现在缺页异常处理程序中说明。

memory region(线性区)

内核使用了一种新的资源来实现对进程动态内存的推迟分配,这就是线性区。线性区是用来表示线性地址区间的,线性区有起始线性地址,长度和一些存取权限来描述。线性区的类型为vm_area_struct:

image

1. 线性区合并与删除

进程所拥有的线性区不会重叠,并且内核尽力把新分配的线性区与紧邻的现有线性区合并(它们权限要相同)。

会出现以下几种情况:

image

2. vm_ops

vm_area_struct中的vm_ops变量指向线性区的方法,只有三个方法被定义:open,close,nopage。

image

image

open和close分别是添加和删除进程的线性区。而nopage是当进程试图访问RAM中不存在的一个页,但该页的线性地址属于线性区时由缺页异常程序调用。其格式为:

struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, int unused);
 
3. 线性区的数据结构
进程所拥有的所有线性区是通过一个简单的链表链接在一起的。如下图所示:
image 

vm_next指向链表的下一个元素:

image

mm_struct的mmap变量指向第一个线性区描述符。

map_count变量存放进程所拥有的线性区数目,一个进程最多拥有MAX_MAP_COUNTD=65536个不同的线性区。

4. 查找线性区

由于有时线性区链表会非常大,这时一个个去遍历链表就会带来非常低效的性能。内核用红黑树来检索线性区,这样就可以在log(n)的时间复杂度中完成操作。红黑树的根节点由mm_struct的mm_rb变量指向。在vm_area_struct的vm_rb变量中保存其在树中的的颜色,父节点和左右子节点。

image

image

image

5. 线性区存储权限

(1)线性区标志vm_flags

image

这里存放着有关这个线性区全部页的信息,例如它们包含什么内容,每个页的权限和线性区如何增长。

(2)线性区页表标志vm_page_prot

image

增加一个页时,内核根据vm_page_prot的值设置相应页页表项的标志。这里要注意的是由于要完成copy_on_write功能,并且页表的User/Supervisor标志必须总为1,因为用户态进程必须总能访问其中的页。

linux采取以下两种规则:(a)读总是隐含执行权限 (b)写总是隐含读权限

这样就把由read,write,exec和共享存取权限所产生的16种可能被精简为3种:

(1)页有写和共享两种权限,Read/Write=1

(2)页有读或执行权限,但既没有写也没有共享存取权限,Read/Write=0

(3)页没有任何存取权限,Present=0,以便每次访问都产生一个缺页中断。不过这里值得注意的是,如何把写时复制和真正页框不存在的情况区分出来,Linux通过把Page size位置置为1。Tricky?是的,这里钻了i386的一个小空子就是芯片在页目录项中检查Page size位,而不在页表项中检查

6. 线性区的处理

mmap_cahce保存进程最后一次引用线性区的描述符地址。这是为了减少查找线性区的时间做的优化。也就是说线性区的查找会先查找这个描述符,如果不是再从红黑树中查找。

内核提供了如下几种函数去处理线性区:

(1)find_vma() 查找线性区的vm_end大于addr的第一个线性区的位置。

(2)find_vma_intersection() 查找一个与给定地址区间相重叠的线性区

(3)arch_get_unmapped_area() 寻找一个空闲的地址空间

(4)insert_vm_struct() 向内存描述符链表插入一个线性区

7. 分配线性地址区间

do_mmap()

do_munmap()

两步走。

Page Fault Handler

 image

 

image

1. 非连续区地址

在kernel mode下对非连续区地址的访问,就会发生进程的第4GB的地址空间与swap_pg_dir不一致的情况,如果错误是由这种情况引起的就会把swap_pg_dir的页表项复制过来。

image

 

image

image

2. 用户态堆栈

由于堆栈的大小是不固定的,所以当一个push或者pusha指令就可能访问超过线性区的地址,所以这里就需要缺页处理程序单独处理。

image

image

3. demand paging(请求调页)

demand paging是一种动态内存分配技术,它把页框的分配推迟到不能再推迟为止。请求调页会发生在以下两种情况下(页表的present都为0):

1) 进程从来没有访问过这个页。页表项被填充为0,也就是pte_none宏返回1。调用do_no_page()函数,有两种方法装入所缺页,这取决于这个页是否被映射到一个磁盘文件。

是否映射磁盘文件是由vma(线性区对象)的nopage函数来确定。

(1) 当vma->vm_ops->nopage != NULL,线性区映射磁盘文件,通过调用nopage函数来把文件读到内存中。

(2) 当vma->vm_ops->nopage == NULL,线性区没有映射磁盘文件,do_no_page()调用do_anonymous_page()来获得一个新的页框。

这个函数根据write和read请求选择了不同的策略,首先当请求是read时,由于进程是第一次对它访问,给进程一个填充为0的页要比给它一个由其他进程使用过的旧业更为安全,所以没有必要立即给进程分配一个填充为0的新页框。内核把已有的zero page分配出去,这样可以进一步推迟页框的分配。由于这个页被标志为不可写,如果进程试图写这个页的时候会使用copy_on_write机制。当请求是write时就调用alloc_page()来分配一个新的页框。

2) 页被swap到磁盘上了。页表项不为0。

image

4. copy on write(写时复制)

当页表的present=1,write=0时,对该页框的写操作就会引起缺页中断,此时调用do_wp_page()来实现copy_on_write。分两种情况

1) 当页描述符的count=1时,表示只有一个进程使用这个页,就没有必要复制了。

2) 当页描述符的count>1时,就把旧页框的内容复制到新分配的页框。

image 

Process Address Space Layout

我们可以通过pmap工具或者直接查看/proc来看到进程的地址空间:

image

如上图所示,一开始是libc的code,data,bss段,接下来是ld的code,data,bss段,然后是a.out程序的code,data,bass段。接下来是heap和stack段。这里的每个区间都是由一个vm_area_struct对象相对应。

image

 

brk() && sbrk()

brk()会调用系统调用sys_brk(addr),其功能是修改堆的大小,addr指定新的heap的末尾地址。而旧的heap的末尾地址存放在mm->brk中。brk()会调用do_brk(oldbrk,newbrk)来扩展heap的vm_area_struct,do_brk相当于是do_mmap的简化版本(其中去除了映射文件的部分),相当于:

do_mmap(NULL, oldbrk, newbrk-oldbrk, PROT_READ|PROT_WRITE|PROT_EXEC,
                    MAP_FIXED|MAP_PRIVATE, 0);
 

image

sbrk(incr)类似于,不过incr参数指定增加还是减少以字节为单位的堆大小。如果是减少incr就为负数。不过sbrk不是系统调用,它只是调用了brk()来实现的。

 

malloc() && free()

malloc是libc库的函数,对于操作系统来说,有一个heap的vm_area_struct,其中包含了一串线性地址空间。而对这组空间的管理就会交给malloc和free函数。操作系统只提供了brk()和sbrk()来控制heap的线性区(sbrk()也是libc库,brk()是系统调用)。就是说libc可以通过brk来增大或者减少heap的大小,不过如何管理这部分空间,就是由malloc和free自己实现了。

我们可以做一个实验:

#include <stdio.h>
#include <malloc.h>

int main(int argc, char **argv) /* arguments aren't used */
{
	int i;
	int size = 1024 * 16;
	void* pList[128];
	for(i = 0; i < 128; ++i)
	{
		printf("malloc: %d\n",size);
		void* p = malloc(size);
		pList[i] = p;
	}
	sleep(8);

	for(i = 0; i < 128; ++i)
	{
		printf("free: %d\n",size);
		void* p = pList[i];
		free(p);
	}

	while (1) {}
}

当上面的代码不断malloc,此时heap的大小就不断增加。而当所有的指针被free掉之后heap就回到原来的值了。不过这里有一个问题就是heap的vm_area_struct是允许空洞的。比如我们malloc了128个指针,然后释放前面的64个,而后面64个没有释放,那heap的大小也不能够改动。

由于malloc是调用brk(),也就是说,实际上malloc只是分配了进程的线性地址,而没有分配物理地址,只有当实际用到的时候才会通过缺页中断来把物理页框分配出来。

 

malloc && free算法

A Lab to implement malloc:

http://www.cs.purdue.edu/homes/cs354/lab4/index.html