进程的一生——请求调页篇

来源:互联网 发布:姚明各年场年场均数据 编辑:程序博客网 时间:2024/05/19 13:08

本文主要解答了三个问题:
1、为什么会有请页机制
2、Linux内核怎么处理缺页
3、写时复制的内核实现

注:本文所有的的内核代码都是来自于kernel3.14.54,读者可以未经作者允许随意转载,但请保证文章的完整性。

在讲内存之前有几个很重要的结构体简单分析以下。
1、mm_struct结构体:进程内存描述符结构体,进程的task_struct的mm和active_mm结构体指向该进程的内存描述符结构体(普通进程的这两个字段是相等的,内核线程的mm字段是NULL(线程没有自己的线性地址空间),active__mm是指向上一个运行的内核线程)。主要字段有:线性区链表的头结点,线性区红黑树的根(线性区采用红黑树和),全局页表,线性区的个数,主计数器,次计数器,程序各个段的起始地址和最后地址等等。
2、vm_area_struct:描述线性区结构体。进程的线性地址空间是用一个个的线性区组织起来(组织的方式有红黑树和双向链表,线性区定义中有next,prev和rb_node)。主要字段有:vm的起始地址,vm的最后地址,拥有这个vm的进程mm_struct和vm的操作函数(全部是钩子函数)。

Linux内存管理采用四级页表,pgd(页全局目录),pud(页上级目录),pmd(页中间目录),pte(页表)。

1、为什么会有请页

linux内核认为自己的所有任务都是很紧急的,前面分析调度函数我们已经了解了内核态所有的任务都比用户态任务优先级高(还记得吗?全局就绪队列会优先扫描实时进程,然后是普通进程,最后是空闲进程)。进程的空间管理类似,内核认为自己的所有要求都是不可推迟的,内核要求的空间会直接分配,但是对于用户态的任务会采用延迟处理的方式,刚刚产生的新进程并没有自己的空间,而是去共享父进程的页框,当子进程真的需要一个页面的时候(exec或者对共享页框执行写操作等一些不能再延迟的时候),会产生一个异常,启动请页机制。所以用户态进程的内存分配基本上都会触发请页机制。
在do_fork函数中,新创建的子进程会调用函数copy_mm复制父进程的内存管理等部分,下面来简单看一下函数copy_mm的部分实现代码:

oldmm = current->mm;if (clone_flags & CLONE_VM) {   //kernel thread do this       atomic_inc(&oldmm->mm_users);     mm = oldmm;     goto good_mm;}mm = dup_mm(tsk);   //else process do thistsk->mm = mm;tsk->active_mm = mm;

tsk是新创建的进程。不难看出,新创建的子进程是完全复制了父进程的mm。

2、Linux怎么处理缺页

如果子进程执行了修改页内容(或者执行exec),这时候会引发一个缺页异常,调用缺页异常处理程序do_page_fault。部分代码如下:

if (!mm || in_atomic())    goto no_context;if (address >= TASK_SIZE)    goto vmalloc_fault;retry:down_read(&mm->mmap_sem);vma = find_vma(mm, address);if (!vma)    goto bad_area;if (vma->vm_start <= address)    goto good_area;...good_area:...     //出于安全,进行vma的权限判断fault = handle_mm_fault(mm, vma, address, flags);if ((fault & VM_FAULT_RETRY) && fatal_signal_pending(current))return;if (flags & FAULT_FLAG_ALLOW_RETRY) {    goto retry;    }up_read(&mm->mmap_sem);return ;}

如果正在执行原子操作或者正在执行内核线程(内核线程的mm为NULL),函数不执行退出。如果虚拟地址超出了4GB(程序访问了不可访问的地址),跳到错误处理。获取读信号量(缺页异常的处理都是在读信号量中执行的),找出距离给定地址最近的一个线性区(vma描述);如果没有找到,转到错误处理;该如果线性区的起始地址低于给定的地址,开始执行:判断vma的权限,调用函数handle_mm_fault(分析见后文),结果保存在fault中。当处理缺页失败需要重来并且current(引发缺页的进程)未决信号集不空,转去执行current的未决信号;未决信号集不空,重新处理缺页。处理缺页成功时候,打开读锁,返回。
上述是do_page_fault的全部逻辑过程,很容易看出处理缺页被封装在了函数handle_mm_fault中。函数handle_mm_fault代码如下(函数的三个参数分别是:引发缺页进程的mm_struct,在do_page_fault函数中找到的vma,给定的地址address和flag):

int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,unsigned long address, unsigned int flags){__set_current_state(TASK_RUNNING);...ret = __handle_mm_fault(mm, vma, address, flags);return ret;}

更新current进程的状态为就绪态(缺页异常处理的时候是等待态),进行一些标志位判断,调用函数__handle_mm_fault获得物理页框并返回。显然,我们的重点是这个函数,函数代码如下(参数分别是进程的mm_struct,在函数do_page_fault中找到的vma,指定地址和flags):

static int __handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,unsigned long address, unsigned int flags){pgd = pgd_offset(mm, address);pud = pud_alloc(mm, pgd, address);pmd = pmd_alloc(mm, pud, address);pte = pte_offset_map(pmd, address);return handle_pte_fault(mm, vma, address, pte, pmd, flags);}

获取页全局目录,申请新的页上级目录和页中间目录(不知道为什么,但是这里就是做了这些操作),得到address在页表中的对应地址,执行handle_pte_fault函数(内核在这个函数里具体分析引发缺页的原因进而执行重新创建映射或者分配新页框,很重要的函数)。函数代码如下:

static int handle_pte_fault(struct mm_struct *mm,struct vm_area_struct *vma, unsigned long address,pte_t *pte, pmd_t *pmd, unsigned int flags){if (!pte_present(entry)) {    if (pte_none(entry)) {        if (vma->vm_ops)            return do_linear_fault(mm, vma, address,pte, pmd, flags, entry);        return do_anonymous_page(mm, vma, address,pte, pmd, flags);    if (pte_file(entry))        return do_nonlinear_fault(mm, vma, address,pte, pmd, flags, entry);    return do_swap_page(mm, vma, address,pte, pmd, flags, entry);ptl = pte_lockptr(mm, pmd);spin_lock(ptl);if (flags & FAULT_FLAG_WRITE) {    if (!pte_write(entry))        return do_wp_page(mm, vma, address,pte, pmd, ptl, entry);    entry = pte_mkdirty(entry);}entry = pte_mkyoung(entry);}

如果页不在内存中并且从未被进程访问过,如果线性区的操作函数不为空执行了XXX(好像是创建一个新的映射,不懂为什么要这么做),如果为空则从未分配页框,内核执行do_anonymous_page函数(分析见后文)分配一个新页框。读页面的脏位,如果脏位被置1(此时页不在内存中),执行非线性文件映射(不是本文的重点,不分析),如果脏位为0,页面被置换出去了所以调用函数do_swap_page把该页换进内存。在解释后边代码之前来稍微提一下写时复制机制。

3、写时复制的实现

在copy_mm函数实现中我们了解了linux采用写时复制机制,所以每一个页都需要有读写保护。当有进程试图对具有写保护的页进行写操作的时候,内核会分配一个新的页框然后复制共享页框的内容。
我们接着来分析这个函数。启动自旋锁,如果进程要执行写操作但是这页有写保护(pte_write读取页的write权限)转去执行do_wp_page(实现cow,好奇怪这个函数我找不到???为什么找不到还把这个代码写上去了);如果没有写保护,置页的dirty位。最后置页的访问标志。
通过上边的分析,我们发现handle_pte_fault函数的大部分功能是查看页的标志位识别出来什么原因引起了缺页异常,然后转到各自的处理函数。下面是对各个处理函数的分析。重新建立映射(略),把外存的也交换进内存(do_swap_page,在文件那一部分分析)。do_numa_page(在内存管理那一部分会分析),所以在这里我们主要分析一个匿名分配函数。
do_anonymous_page函数(终于要到了分配页框的时候了),代码如下(函数的参数分别是,进程的mm_struct,找到的vma,指定地址,页表,页中间目录和标志位):

static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,unsigned long address, pte_t *page_table, pmd_t *pmd,unsigned int flags){if (!(flags & FAULT_FLAG_WRITE)) {     entry = pte_mkspecial(pfn_pte(my_zero_pfn(address),vma->vm_page_prot));    page_table = pte_offset_map_lock(mm, pmd, address, &ptl);    if (!pte_none(*page_table))        goto unlock;    goto setpte;}}page = alloc_zeroed_user_highpage_movable(vma, address);...entry = mk_pte(page, vma->vm_page_prot);if (vma->vm_flags & VM_WRITE)    entry = pte_mkwrite(pte_mkdirty(entry));page_table = pte_offset_map_lock(mm, pmd, address, &ptl);page_add_new_anon_rmap(page, vma, address);update_mmu_cache(vma, address, page_table);

如果进程只要求了读页,并不用分配页框,直接跳去解锁。分配新的页面,创建一个新的页表项,如果线性区(在do_page_fault函数中找到的)允许写操作,设置新页的脏位和写标志,产生页表项的线性地址。为新的匿名页增加页表映射,更新内存管理单元。
说两点:
1、源码中错误处理要比真正实现功能的代码多得多。
2、内存这部分还挺有意思的,东西也听过的,本文只是写了请页,还有地址转换、页面分配和页面回收。
3、看函数之前如果已经明白了一些比较重要的结构,代码看起来就容易多了。

1 0