在刚开始看的时候感觉缺页异常(这可是异常啊,搞Java的人表示对着个比较敏感)肯定是一些进程在搞鬼,看完才发现原来是内核在搞鬼,它是故意的!!!
1、请求调页
请求调页是一种动态分配内存的策略,把页面的分配推迟到不能再迟的时候(不能再迟的时候就是进程要访问的时候)。为什么要这样呢?RAM一般情况下都是很宝贵的资源,而且进程在一段的运行时间段中一般不会访问到所有的地址空间,到时发现page不在就引发一个缺页异常。这样的话就以一个异常处理周期长度的时间换来更多的RAM去做更重要的事情。
1.0、如何引起请求调页
在发生访问一个线性地址的时候发生了错误进入do_page_faule函数,在判断出【发生在用户空间、不在中断中】进入handle_pte_fault函数(在这里判断是进行请求调页还是写时复制)。
1.1、如何分辩应该请求调页还是写时复制
先看一下在handle_pte_fault函数中是如何区分各种不同的情况从而去调用不同的处理函数的。
- 如果pte_present返回true,则pte是不在RAM中的(其实这里不一定是在RAM中的,如果设置这个pte为PROTNONE的话也是返回true,想想为什么?想想函数中的present的意思到底是什么?想想什么时候会设置PROT_NONE?)。
- 如果pte_none返回true,这个pte太干净了,进程没访问过这个页。
- 如果vm_operations_struct存在的话,还能根据它里面的值看出一些东西。
- 如果fault||nopage函数存在的话就调用do_linear_fault函数来解决。如果这两个在的话说明是这个页是映射到磁盘文件的(这涉及底层操作,如果不是因为映射到文件内核会胡乱设置值吗?)。
- 能到这里说明不是映射到磁盘文件的(磁盘文件是一种特殊情况),是不是应该调用通用的解决方法(nopfn)来解决?。
- 上面是在vm_operations_struct中寻求出路,如果找不到就(do_anonymous_page)分配一个匿名页。
- 到了这里说明这个pte上面还是有写东西的,如果通过pte_file(Present为1,Dirty为0)发现这个页是非线性磁盘文件的映射调用do_nonlinear_fault解决。
- 在这是说明Present=0&&Dirty=0,说明这个页是被临时保存在硬盘上面了,调用do_swap_page来解决。
- page在内存中那为什么会出错呢?通过write_access发现是写操作引起的。
- 如果通过pte_write判断出这个pte是不能写的(因为是共享的不是你想写就能写的),调用do_wp_page来解决(这就是写时复制)。
- 调用pte_mkdirty来弄脏这个pte。
- 好了,现在发生错误的原因只剩下了【读操作引起(这个在do_page_fault中把这种可能性干掉了)】或者【写操作并且pte可写(已被弄脏)】。这时为什么会出错呢?设置ACCESS标志然后返回(这是要放过一马?还是要重新尝试一次吗?)。
上面对在什么情况下应该调用什么样的处理函数已经比较清楚了,这只是第一步,下面要做的事情就是看看这些函数是怎么工作的:
1.2、do_linear_fault
- 取得address在文件中对应偏移的页数(vm_pgoff是vma在文件中的偏移量,然后加上address在vma中的偏移量)。
- 设置flag。如果是写操作则FAULT_FLAG_WRITE,否则为0。
- 如果是HIGHMEM就unmap(这里还是需要搞清楚这个函数到底是干嘛的!!)。
- 调用函数__do_fault。
1.3、do_nonlinear_fault
- 设置flag(这次在考虑write_access的基础上加入了非线性)。
- pte_unmap_same,同样是unmap,不过这里要比较是不是same。为什么在SMP上会出现same为0的情况?
- 检查vma的标志。
- 取得pgoff,不过这次取的方法和线性的情况下可是大不一样了。
- 调用__do_falut函数(和处理线性的情况下不同的是flag和pgoff)。
1.3、__do_fault
可以看到do_linear_fault和do_nonlinear_fault函数共同调用__do_fault来解决问题,不同的是在进入该函数时候非flag和pgoff是不同的。下面就看__do_fault的执行过程(其实现在已经能猜到了)。__do_fault函数的目的是什么呢?为的就是创建一个新的page mapping。
- 设置vm_fault(fault函数调用的一个参数类性);
- 如果fault函数存在就调用它,返回值为ret;
- 如果返回值是VM_FAULT_ERROR或VM_FAULT_NOPAGE,就返回ret(发生错误走不下去了);
- fault函数不存在就调用nopage;
- 判断VM_FAULT_SIGBUS和VM_FAULT_OOM的情况,并返回;
- 如果设置了FAULT_FLAG_WRITE标志(在上面的两个函数看出这个值在write_access=1的时候设置);
- 如果vma没有设置VM_SHARED标志(表示该区域不可以可以被多个进程共享);
- anon_vma_prepare函数如果有匿名线性区返回0,文件的MAP_PRIVATE如果是VM_SHARED的话只能在优先树中,否则只能在anon_vma列表中;
- 调用alloc_page_vma从高端内存中为vma分配一个page;
- 如果没有申请到page的话设置结果为VM_FAULT_OOM;
- 调用copy_user_highpage函数吧vmf.page的内容拷贝到page中;
- 调用__SetPageUptodate设置page的PG_uptodate标志(应该是设置page为比较新的意思吧);
- 否则的话,如果vm_operations_struct中有page_mkwrite函数就调用它;
- 如果调用失败,设置结果代码为VM_FAULT_SIGBUS,这里设置anon的作用是区释放vmf.page;
- 检查page->mapping的值,它应该是用来反向映射(address_space或者anon_vma)的,如果为null那以后处理的时候一定会出错,在这种情况下设置anon也是为了释放vmf.page;
- page_mkwrite=1;
- 调用mem_cgroup_charge函数来告诉内存控制器该页的用法是GFP_KERNEL;
- 调用pte_offset_map_lock函数取得page_table;
- 调用pte_same判断page_table和原来的orig_pte是不是相同的,如果是就执行下面的流程(什么时候是不同的呢?page_table是在上面根据address从pmd中中取到的,如果是单线程的话这个应该肯定相同的。但是这里为什么不用锁来控制?如果别的进程设置过了是不是就说明别的线程已经执行了下面的代码这里就不用重复执行了?是这个意思不?);
- 调用mk_pte函数来设置page的标志位;
- 如果设置了FAULT_FLAG_WRITE,may_mkwrite尝试设置pte为dirty&&write;
- 调用set_pte_at函数设置pte;
- 如果anon为1(什么时候anon会是1?创建一个私有映射的时候?区分出两种不同的情况从而更新不同的统计信息);
- 更新统计信息,page加入到active列表等操作
- 否则的话,如果FAULT_FLAG_WRITE,增加page的引用计数
- update_mmu_cache;
- 如果判断same失败了(这种情况比较少);
- 取消向内存控制器的通知;
- 如果设置了anon;
- 就要释放掉申请的page;
- anon=1,这样在后面就可用释放vmf.page;
- 检查anon看是否需要释放vmf.page(还有一些代码不怎么明白)。
这个函数大概的流程就在上面了,首先线从文件中读过来相应的内容到page中,接下来就看是不是写操作?如果是写操作下面考虑的是共享还是私有?如果是共享的,在这里调用mkwrite的时候是不是就会触发异常调用写时复制了?后面当然是要通知内存控制器了。接下了的工作哦基本上可用宣告大功告成了,开始设置pte,更新统计信息,从而使得进程可以访问这些空间了。
1.4、do_anonymous_page
- pte_unmap释放调page_table指向的内容(对HIGHMEM有用);
- anon_vma_prepare,在申请私有映射前的检查(或者说是准备);
- alloc_zeroed_user_highpage_movable,为vma申请page;
- __SetPageUptodate(这些在上面都是说过的,所以在这里就不重复了);
- mem_cgroup_charge;
- mk_pte;
- write&&dirty;
- 取得pte;
- 更新统计信息和位置;
- set_pte_at;
这个函数的流程看起来和__do_fault好像啊。不过少了很多的内容,比如从文件中读取页,还有就是不许要考虑early cow。
1.5、do_swap_page
这个函数负责将保存在后备存储器上的page读入内存中。其中页涉及到一些大众的想法,还是看具体代码的流程吧:
- pte_unmap_same;
- 根据pte生成swp_entry_t类型;
- 如果是在移动的话就等待移动完成;
- 从缓存查找page;
- 如果没有找到就调用swapin_readahead来读取page(这个不单单是读取一个page吧,应该是一个block的大小,并根据局部性原理);
- 如果还没有找到就宣布失败吧(没有其他方法再解决了);
- 接下来就不用说了,有点重复了(貌似取到page之后是事情都很相似)。
页面交换这部分的内容还没仔细看过,主要是对其中的结构都不是很清楚,等过几天熟悉一下之后再补充这部分的内容。请求调页的部分大概就写这么多,下面开始看看写时复制(COW):
2、写时复制
在尝试写一个shared的页的时候就会导致页面错误从而进入写时复制调用。为什么要写时复制呢?比如fork这类的函数,在创建新进程的时候会复制父进程的内容,如果这时候就完成真正的复制就太不划算了。不仅是因为会浪费大量的CPU资源,而且这些内容很多情况下子进程只是读读而已,甚至大部分的内容子进程不会区搭理,那不就傻眼了。上面已经把什么时候进入到写时复制的处理函数,这里就不再重复第说了,直接看处理流程:
2.0、do_wp_page
- 根据orig_pte取得old_page;
- 如果仅有一个线程拥有这个page那就没有必要启动复制了(在页插入到交换缓存中的时候_count也会增加),这部分还是比较复杂的,以后再仔细研究;
- 分配new_page;
- 拷贝数据;
- __SetPageUptodate(new_page);
- pte_same
- 如果有old_page的话现在可以从vma中移除了;
- mk_pte;
- write&&dirty;
- old_page引用次数减1,锁之类的,然后over了;
------------------------------------------------------
个人理解,欢迎拍砖。