在上篇文章中介绍了基于显式链表的内存的分配与释放,这篇文章在此基础上进行内存访问管理的介绍。在页机制下,虚拟内存的访问可能产生缺页异常,这篇文章主要对产生缺页异常前后的处理过程进行介绍,其中会涉及到页面在内存和硬盘换入换出的过程,实现细节不作具体介绍。
以上提到的过程都是虚拟存储的一部分,其中关键环节是页映射,为了便于理解这个过程我们站在操作系统的角度阐述一个可执行文件如何被装载。
进程的建立
(1)创建虚拟地址空间
对我们来说,我们看到的地址其实是可执行文件中体现的虚拟地址,并不是可执行文件实际在内存的位置,可以这样认为:虚拟空间和物理空间通过一组页映射函数进行映射,这个映射函数就是要建立起的页目录和页表的内容。创建虚拟空间实际上是创建映射函数所需要的相应的数据结构。这个数据结构可以如下所示。
/*
* 对于一个进程(可以把内核也看成一个进程),我们希望能管理维护它独有的虚拟地址空间,
* 包括虚拟地址到物理地址的映射,以及不同地址段的可能拥有的不同权限。
* mm_struct结构描述了一个进程的整个虚拟地址空间,vma_struct描述了虚拟地址空间的一个区间,例如代码段对应一个vma_struct,数据段对应一个vma_struct。
* 对于内核线程,不拥有任何内存描述符,mm成员总是设为NULL
*/
// the control struct for a set of vma using the same PDT
struct mm_struct {
list_entry_t mmap_list; // linear list link which sorted by start addr of vma
struct vma_struct *mmap_cache; // current accessed vma, used for speed purpose
pde_t *pgdir; // the PDT of these vma
int map_count; // the count of these vma
void *sm_priv; // 指向已分配页链表头,用于遍历起点,先入先出算法
};
// the virtual continuous memory area(vma), [vm_start, vm_end),
// addr belong to a vma means vma.vm_start<= addr <vma.vm_end
struct vma_struct {
struct mm_struct *vm_mm; // the set of vma using the same PDT
uintptr_t vm_start; // start addr of vma
uintptr_t vm_end; // end addr of vma, not include the vm_end itself
uint32_t vm_flags; // flags of vma
list_entry_t list_link; // linear list link which sorted by start addr of vma
};
在i386的Linux下,创建虚拟地址空间实际上只是分配一个页目录(Page Directory)就可以了,甚至不设置页映射关系,这些映射关系等到后面程序发生页错误的时候再进行设置。
(2)读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系。
上面那一步的页映射关系函数是虚拟空间到物理内存的映射关系(建立mm_struct和vma_struct数据结构),而这一步所做的是虚拟空间与可执行文件的映射关系,填写上一步建立的数据结构成员变量。当程序执行发生页错误时,操作系统将从物理内存中分配一个物理页,然后将该“缺页”从磁盘中读取到内存中,再设置缺页的虚拟页和物理页的映射关系,建立映射函数,这样程序才得以正常运行。 但是很明显的一点是,当操作系统捕获到缺页错误时,它应知道程序当前所需要的页在可执行文件中的哪一个位置。这就是虚拟空间与可执行文件之间的映射关系。从某种角度来看,这一步是整个装载过程中最重要的一步,也是传统意义上“装载”的过程。这种映射关系保存在上一步建立的数据结构中,Linux中将进程虚拟空间中的一个段叫做虚拟内存区域(VMA,Virtual Memory Area)。 例如,操作系统创建进程后,会在进程相应的VMA数据结构中设置.text段的信息:它在虚拟空间中的地址为0x08048000~0x08049000,它对应ELF文件中偏移为0的.text,它的属性为只读(一般代码段都是只读的),还有一些其他的属性。
(3)将CPU指令寄存器设置成可执行文件入口,启动运行。
第三步其实也是最简单的一部,操作系统通过设置CPU的指令寄存器将控制权转交给进程,由此进程开始执行。这一步看似简单,实际上在操作系统层面上比较复杂,它涉及内核堆栈和用户堆栈的切换、CPU运行权限的切换。不过从进程的角度看这一步可以简单地认为操作系统执行了一条跳转指令,直接跳转到可执行文件的入口地址。
页错误
可执行文件的真正指令和数据被装入到内存中发生在页错误后。之前操作系统只是通过可执行文件头部的信息建立起可执行文件和进程虚存之间的映射关系而已(建立相应的数据结构)。
假设在上面的例子中,程序的入口地址为0x08048000,即刚好是.text段的起始地址。当CPU开始打算执行这个地址的指令时,发现页面0x08048000~0x08049000是个空页面,于是它就认为这是一个页错误(Page Fault)产生中断异常。CPU将控制权交给操作系统,操作系统有专门的页错误处理例程来处理这种情况。这时候我们前面提到的装载过程的第二步建立的数据结构起到了很关键的作用,操作系统将查询这个数据结构,然后找到空页面所在的VMA,计算出相应的页面在可执行文件中的偏移,然后在物理内存中分配一个物理页面,将进程中该虚拟页与分配的物理页之间建立映射关系,然后把控制权再还回给进程,进程从刚才页错误的位置重新开始执行。一个简单的页错误处理例程如下:
/*
* do_pgfault - interrupt handler to process the page fault execption
* @mm : 维护当前进程的虚拟内存区域的数据结构;
* @error_code : 当前页错误给出的错误码
* @addr : CR2寄存器给出的访问出错的线性地址
*/
int
do_pgfault(struct mm_struct *mm, uint32_t error_code, uintptr_t addr) {
int ret = -E_INVAL;
struct vma_struct *vma = find_vma(mm, addr); //try to find a struct vma which includes the addr.
pgfault_num++;
if (vma == NULL || vma->vm_start > addr) { //whether the addr is in the range of a mm's vma?
cprintf("not valid addr %x, and can not find it in vma\n", addr);
goto failed;
}
/*
* error_code:
* The P flag (bit 0) == 0 means a not-present page, 1 means protection fault;
* The W/R flag (bit 1) == 0 means read error, 1 means write error;
* The U/S flag (bit 2) == 0 means the processor was executing at supervisor mode, 1 means user mode.
*/
switch (error_code & 3) { //every bit in error_code represents error
default: //error code flag : default is 3 ( W/R=1, P=1): write erroe, present
case 2: //error code flag : (W/R=1, P=0): write error, not present */
if (!(vma->vm_flags & VM_WRITE)) { //若vma->vm_flags表明不可写的,直接结束
cprintf("do_pgfault failed: error code flag = write AND not present, but the addr's vma cannot write\n");
goto failed;
}
break;
case 1: //error code flag : (W/R=0, P=1): read error, present */
cprintf("do_pgfault failed: error code flag = read AND present\n");
goto failed;
case 0: /* error code flag : (W/R=0, P=0): read error, not present */
if (!(vma->vm_flags & (VM_READ | VM_EXEC))) {
cprintf("do_pgfault failed: error code flag = read AND not present, but the addr's vma cannot read or exec\n");
goto failed;
}
}
uint32_t perm = PTE_U;
if (vma->vm_flags & VM_WRITE) {
perm |= PTE_W;
}
addr = ROUNDDOWN(addr, PGSIZE);
ret = -E_NO_MEM;
pte_t *ptep=NULL;
/*
* get_pte - 返回addr所对应的页表项的地址
* @mm->pgdir :当前进程页目录的地址
* @addr :需要访问的物理地址
* @parameter '1' :'1'表明若页表不存在,申请一个空闲页作为页表
*/
if ((ptep = get_pte(mm->pgdir, addr, 1)) == NULL) {
cprintf("get_pte in do_pgfault failed\n");
goto failed;
}
/*
* pgdir_alloc_page - 申请一个物理页,填写页表项,建立起线性地址与物理页的映射关系,并将磁盘数据加载进物理页内存
* @pgdir :当前进程页目录的地址
* @la :需要访问的物理地址
* @perm :permission包含三位标志位
*/
if (*ptep == 0) { // if the phy addr isn't exist, then alloc a page & map the phy addr with logical addr
if (pgdir_alloc_page(mm->pgdir, addr, perm) == NULL) {
cprintf("pgdir_alloc_page in do_pgfault failed\n");
goto failed;
}
} else { //页表项非空,但Present标志位=0,说明访问内容在swap磁盘分区上// if this pte is a swap entry, then load data from disk to a page with phy addr and call page_insert to map the phy addr with logical addr
if(swap_init_ok) {
struct Page *page=NULL;
if ((ret = swap_in(mm, addr, &page)) != 0) {
cprintf("swap_in in do_pgfault failed\n");
goto failed;
}
page_insert(mm->pgdir, page, addr, perm); //填写页表项,建立起线性地址与物理页的映射关系
swap_map_swappable(mm, addr, page, 1); //插入 可用于换出(swappable) 链表
} else {
cprintf("no swap_init_ok but ptep is %x, failed\n",*ptep);
goto failed;
}
}
ret = 0;
failed:
return ret;
}