<深入浅出>进程地址空间与缺页

原创 2011年12月11日 21:26:38
 在学习一个新的东西时,最好能摸清来龙去脉,以达到格物致知的境界。

然而大家推崇的学习方法是一叶知秋,而非一叶障目。以前有一本很经典的情景分析,大家
夸赞它入木三分的同时,也惋惜只见树木,不见森林。

具体来讲,就是先理顺核心流程,去掉一些容错分支,再在此基础上逐步完善。

pthread库,即是对用户态线程的管理。
下面以pthread_create为例,说明线程创建和执行的流程。

先来看这么一段结论:
pthread库里的线程,实际上都是各自独立的微型进程。在同一个进程下的所有pthread线程,
有不同的进程描述符,栈空间,但是他们有相同的进程地址空间。

上面这段话怎么理解?
其实就是说,通过pthread_create创建的进程,都是通过do_fork创建的(不同的进程描述符),
每个进程的栈是独立分配的,并且fork时的参数为CLONE_VM(共享地址空间)

在深入这个问题之前,我们先看看,进程如何执行的。
举例来说,在shell提示符下,执行一个程序test时,首先
shell通过fork系统调用,生成一个新进程a, 在新进程里调用
exec加载test的二进制镜像到内存后,新进程切换成a',
在exec系统调用返回用户态时切换到新进程a'的入口函数

在上面的流程里,几个地方需要专门提出来:
1)fork出的新进程a的用户态栈,内核态栈,是如何指定的?
2)exec加载新进程a'后,第一次访问a'的代码段等区间时,系统会发生什么?


先来看第一个问题,当fork时,内核进入sys_fork

int sys_fork(struct pt_regs *regs)
{
 return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
}


可以看出,此时新进程的栈,用的是父进程发生fork系统调用时的栈指针。注意,这里的栈指的是用户态栈。


(一)下面来看用户态栈地址,是如何赋值给新进程的:

do_fork --> copy_process -->copy_thread

1) 子进程栈顶,准备存放上下文
x86

childregs = ((struct pt_regs *)
   (THREAD_SIZE + task_stack_page(p))) - 1;
mips64
childksp = (unsigned long)task_stack_page(p) + THREAD_SIZE - 32;
childregs = (struct pt_regs *) childksp - 1;
childksp = (unsigned long) childregs;

ps:  为什么x86的childregs不需要减去一个pt_regs长度? 因为x86是小端,而mips是大端。


2)子进程上下文

*childregs = *regs;

3)子进程返回用户态的栈地址,修改为指定值(fork的话父子进程一样)

childregs->regs[29] = usp;

4)子进程被调度到的时候,需要恢复的栈指针,以及PC指针

p->thread.reg29 = (unsigned long) childregs;      sp
p->thread.reg31 = (unsigned long) ret_from_fork;  ra

现在来看看,当新进程a被调度到时,栈是如何恢复的。

resume函数里,cpu_restore_nonscratch a1负责加载新进程的上下文。其中,有

LONG_L sp, THREAD_REG29(\thread)
OFFSET(THREAD_REG29, task_struct, thread.reg29);

操作之后,新进程的栈sp指向一个 pt_regs结构,而ra则为ret_from_fork
当进程切换执行
jr ra后
跳转至ret_from_fork
最后执行
RESTORE_SP_AND_RET的时候,根据sp上的值恢复用户态栈,也就是3步的sp
接着跳转至用户态入口

LONG_L k0, PT_EPC(sp) //这里的epc实际上是父进程执行fork时的pc值
LONG_L sp, PT_R29(sp)
jr k0
rfe


(二)进程的执行以及缺页

在加载进程elf镜像时,实际上最主要的两个步骤,分别是进程空间的建立,和文件操作集合与进程空间对应

关系的建立,后者通俗的讲,就是
缺页回调函数的设置。

当第一次访问代码段时,发生tlb miss,引发tlb refill,由于进程用户态页表里,代码段虚拟地址对应的

pte都是invalid pte,重填
tlb后,由于V=0, 再次访存引发tlb_load异常,进入page fault。

handle_mm_fault-->handle_pte_fault

再继续之前,反正我们已经走到这里,就顺便把handle_mm_fault里的几个重要函数分析一下。

pte = pte_alloc_map(mm, pmd, address);
这个pte_alloc_map的意思是,如果pte表不存在,则分配一个,并返回该pte页表里,对应此地址具体的某个

pte项指针

#define pte_alloc_map(mm, pmd, address)   \
 ((unlikely(!pmd_present(*(pmd))) && __pte_alloc(mm, pmd, address))? \
  NULL: pte_offset_map(pmd, address))

当pmd里的pte指针不存在时,就调用pte_alloc分配一个pte页表,并将该pte页表的地址填入pmd相应位置,最

后返回pte页表地址。
(pte页表是pte entry的集合,每个pte entry存放的是页框物理地址)


这里有个小的trick,__pte_alloc的实现,
mips里面,pte页表,乃至pmd,pud,pgd都是在低端内存分配的,而x86的话,32位和64位有不同的实现。
对于32位来说,如果配置了高端PTE,则pte是放到高端内存的,那么访问的话需要kmap

#if defined(CONFIG_HIGHPTE)
#define pte_offset_map(dir, address)     \
 ((pte_t *)kmap_atomic(pmd_page(*(dir))) +  \
  pte_index((address)))

为什么要把pte表放到高端内存呢,我们来看内核里蛋疼的解释:

The VM uses one page table entry for each page of physical memory. For systems with a lot of
RAM, this can be wasteful of precious low memory. Setting this option will put user-space page
table entries in high memory

言归正传,继续回到>handle_pte_fault

handle_pte_fault是缺页的核心所在,大体框架如下:

 entry = *pte;
 if(!pte_present(entry)){  //检查页是否存在于内存之中,就是检查pte entry是否有_PAGE_PRESENT标志
     if(pte_none(entry){   //pte entry根本不存在,也就是没有加载页框进来,就是检查pte_val是否为0
          if(vma->file->fault) //文件缺页,例如第一次访问代码段
          {
             return vma->vm_ops->fault(vma,addr);
          }else{
            return do_anonymous_fault(vma,addr); //匿名页,例如第一次访问malloc区域
          }
     }else{
          return do_swap_fault();  //pte entry存在,但该页代表的页框内存已经被交换到磁盘
     }
 }
 if(!pte_write(entry))
   return do_wp_fault(vma,addr); //该页属性不对,对写保护的页进行了写操作

 

pte_present是判断请求调页(demond paging)还是写时复制(COW)的关键,只要不在内存中,就需要请求调页

,否则是写时复制。

如果是第一次访问代码段,由于代码段目前预读到了缓存中,并没有建立对应的页表,因此发生文件缺页,

vma->vm_ops->fault的
回调函数是filemap_fault 。

如果是匿名请求调页,走的是do_anonymous_page,省略一些步骤,伪码如下:

static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
		unsigned long address, pte_t *page_table, pmd_t *pmd,
		int write_access)
{
	if (write_access) {
		/* Allocate our own private page. */
		
		anon_vma_prepare(vma);	
		page = alloc_page();
		entry = mk_pte(page);

		page->mapping = (struct address_space *) anon_vma;
	} else {
		/* Map the ZERO_PAGE - vm_page_prot is readonly */
		page = ZERO_PAGE();
		entry = mk_pte(page, vma->vm_page_prot);

	}

	set_pte_at(mm, address, page_table, entry);
}
匿名映射的请求调页流程,为什么会出现区分读写操作?因为对于一个malloc出来的空间,第一次读一定是返回0值,

既然这样如果是第一次读引起的匿名缺页我们就直接给他返回一个0,即将pte对应的页表项置为一个静态分配好的零页(页的内容为0),

这样免去了alloc_page可能造成的延时,并且将pte的属性置只读,这样在下次对这个malloc出来的区间进行写时,就走写流程。

在第一次写引起的缺页时,除了为其分配页框,还由于匿名映射是可能被回收交换的,所以会加入到系统的匿名映射链,这通过anon_vma_prepare

函数来完成的。一个page对应着一个anon_vma链表,这个链表里存放的是所有映射到此页面的vma。当请求调页写流程走到anon_vma_prepare

后,

int anon_vma_prepare(struct vm_area_struct *vma)
{
	struct anon_vma *anon_vma = vma->anon_vma;
	if (!anon_vma) {
		vma->anon_vma = anon_vma_alloc();
		list_add_tail(&vma->anon_vma_node, &anon_vma->head);	
	}
	return 0;
}


linux页面缓存【笔记】

页面缓存(page cache)也叫File Cache,使用页面缓存是为了提高磁盘对文件的访问速度。顾名思义,“页面”是物理内存的概念,因此page cache是以物理页为单位缓存(不存在虚拟内存)...
  • xzongyuan
  • xzongyuan
  • 2014-03-03 14:31:20
  • 2104

由mmap引发的SIGBUS

一直以来都觉得使用mmap读文件是非常高效、非常优雅的做法(参见《从"read"看系统调用的耗时》)。mmap之后,就可以通过内存访问的方式访问到文件里的内容,省去了read这样的系统调用。 却不曾...
  • ctthunagchneg
  • ctthunagchneg
  • 2013-05-12 00:32:01
  • 2189

Mmap Internals

欢迎转载,转载请注明出处:http://forever.blog.chinaunix.net! Mmap Internals Author: Tony tingw.liu@gmail.co...
  • Fybon
  • Fybon
  • 2014-09-29 16:47:43
  • 1042

用户空间缺页异常pte_handle_fault()分析--(下)--写时复制

在pte_handle_fault()中,如果触发异常的页存在于主存中,那么该异常往往是由写了一个只读页触发的,此时需要进行COW(写时复制操作)。如当一个父进程通过fork()创建了一个子进程时,子...
  • vanbreaker
  • vanbreaker
  • 2012-09-07 16:35:58
  • 6795

程序调优方法之一:是否缺页

引言                                         HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体...
  • DC_Neo
  • DC_Neo
  • 2014-06-05 09:59:55
  • 702

用户空间缺页异常pte_handle_fault()分析--(上)

前面简单的分析了内核处理用户空间缺页异常的流程,进入到了handle_mm_fault()函数,该函数为触发缺页异常的地址address分配各级的页目录,也就是说现在已经拥有了一个和address配对...
  • vanbreaker
  • vanbreaker
  • 2012-08-18 20:30:27
  • 9771

请求调页

上一篇博文引出了“请求调页”技术,术语“请求调页”指的是一种动态内存分配技术,它把页框的分配推迟到不能再推迟为止,也就是说,一直推迟到进程要访问的页不在物理RAM中时为止,由此引起一个缺页异常。 请求...
  • yunsongice
  • yunsongice
  • 2010-05-31 20:24:00
  • 3029

缺页异常详解

首先明确下什么是缺页异常,CPU通过地址总线可以访问连接在地址总线上的所有外设,包括物理内存、IO设备等等,但从CPU发出的访问地址并非是这些外设在地址总线上的物理地址,而是一个虚拟地址,由MMU将虚...
  • cosmoslhf
  • cosmoslhf
  • 2015-01-15 16:48:45
  • 9245

Linux内核源代码情景分析-交换分区

在Linux内核源代码情景分析-共享内存中,共享内存,当内存紧张时是换出到交换分区。    在Linux内核源代码情景分析-mmap后,文件与虚拟区间建立映射中,文件映射的页面,当内存紧张时是换出到硬...
  • jltxgcy
  • jltxgcy
  • 2015-04-22 20:22:32
  • 1110

通过创建MapFile来定位程序崩溃地址

想必大家对于程序莫名其妙的程序崩溃感到苦恼了,但更苦恼的却是没有一个好的方法去解决它。近日,看了这篇的文章,甚有大的收获。现将心得记录下来,以供大家分享。我就直接列出步骤了:1、在图一的Debug I...
  • ytfrdfiw
  • ytfrdfiw
  • 2007-09-03 10:55:00
  • 1277
收藏助手
不良信息举报
您举报文章:<深入浅出>进程地址空间与缺页
举报原因:
原因补充:

(最多只允许输入30个字)