三、Linux缺页中断处理
转自:http://blog.csdn.net/cxylaf/article/details/1626534
1.请求调页中断:
进程线性地址空间里的页面不必常驻内存,例如进程的分配请求被理解满足,空间仅仅保留vm_area_struct的空间,页面可能被交换到后援存储器,或者写一个只读页面(COW)。Linux采用请求调页技术来解决硬件的缺页中断异常,并且通过预约式换页策略。
主缺页中断和次缺页中断,费时的需要从磁盘读取数据时就会产生主缺页中断。
每种CPU结构提供一个do_page_fault (struct pt_regs *regs, error_code) 处理缺页中断,该函数提供了大量信息,如发生异常地址,是页面没找到还是页面保护错误,是读异常还是写异常,来自用户空间还是内核空间。它负责确定异常类型及异常如何被体系结构无关的代码处理。下图是Linux缺页中断处理流程:
图 Linux缺页中断处理
一旦异常处理程序确定异常是有效内存区域中的有效缺页中断,将调用体系结构无关的函数handle_mm_fault()。如果请求页表项不存在,就分配请求的页表项,并调用handle_pte_fault()。
第一步调用pte_present检查PTE标志位,确定其是否在内存中,然后调用pte_none()检查PTE是否分配。如果PTE还没有分配的话,将调用do_no_page()处理请求页面的分配,否则说明该页已经交换到磁盘,也是调用do_swap_page()处理请求换页。如果换出的页面属于虚拟文件则由do_no_page()处理。
第二步确定是否写页面。如果PTE写保护,就调用do_swap_page(),因为这个页面是写时复制页面。COW页面识别方法:页面所在VMA标志位可写,但相应的PTE确不是可写的。如果不是COW页面,通常将之标志为脏,因为它已经被写过了。
第三步确定页面是否已经读取及是否在内存中,但还会发生异常。这是因为在某些体系结构中没有3级页表,在这种情况下建立PTE并标志为新即可。
2.请求页面分配:
第一次访问页面,首先分配页面,一般由do_no_page()填充数据。如果父VMA的vm_area_struct->vm_ops提供了nopage()函数,则用它填充数据;否则调用do_anonymous_page()匿名函数来填充数据。如果被文件或设备映射,如果时文件映射,filemap_nopage()将替代nopage()函数,如果由虚拟文件映射而来,则shmem_nopage()。每种设备驱动将提供不同的nopage()函数,该函数返回struct page结构。
3.请求换页:
将页面交换至后援存储器后,函数do_swap_page()负责将页面读入内存,将在后面讲述。通过PTE的信息就足够查找到交换的页面。页面交换出去时,一般先放到交换高速缓存中。
缺页中断时如果页面在高速缓存中,则只要简单增加页面计数,然后把它放到进程页表中并计数次缺页中断发生的次数。
如果页面仅存在磁盘中,Linux将调用swapin_readahead()读取它及后续的若干页面。
4.页面帧回收
除了slab分配器,系统中所有正在使用的页面都存放在页面高速缓存中,并由page->lru链接在一起。Slab页面不存放到高速缓存中因为基于被slab使用的对象对页面计数很困难。除了查找每个进程页表外没有其他方法能把struct page映射为PTE,查找页表代价很大。如果页面高速缓存中存在大量的进程映射页面,系统将会遍历进程页表,通过swap_out()函数交换出页面直到有足够的页面空闲,而共享页会给swap_out()带来问题。如果一个页面是共享的,同时一个交换项已经被分配,PTE就会填写所需信息以便在交换分区里重新找到该页并将引用计数减1。只有引用计数为0时该页才能被替换出去。
内存和磁盘缓存申请越来越多的页面但确无法判断如何释放进程页面,请求分页机制在进程页面缺失时申请新页面,但它却不能强制释放进程不再使用的页面。
The Page Frame Reclaiming Algorithm(PFRA)页面回收算法用于从用户进程和内核cache中回收页面放到伙伴系统的空闲块列表中。PFRA必须在系统空闲内存达到某个最低限度时进行页面回收,回收的对象必须是非空闲页面。
可将系统页面划分为四种:
1) Unreclaimable 不可回收的,包括空闲页面、保留页面设置了PG_reserved标志、内核动态分配的页面、进程内核栈的页面、设置了PG_locked标志的临时锁住的页面、设置了VM_LOCKED标志的内存页面。
2) Swappable 可交换的页面,用户进程空间的匿名页面(用户堆栈)、tmpfs文件系统的映射页面(入IPC共享内存页面),页面存放到交换空间。
3) Syncable 可同步的页面,入用户态地址空间的映射页面、保护磁盘数据的页面缓存的页面、块设备缓冲页、磁盘缓存的页面(入inode cache),如果有必要的话,需同步磁盘映像上的数据。
4) Discardable 可丢弃的页面,入内存缓存中的无用页面(slab分配器中的页面)、dentry cache的页面。
PFRA 算法是基于经验而非理论的算法,它的设计原则如下:
1) 首先释放无损坏的页面。进程不再引用的磁盘和内存缓存应该先于用户态地址空间的页面释放。
2) 标志所有进程态进程的页面为可回收的。
3) 多进程共享页面的回收,要先清除引用该页面的进程页表项,然后再回收。
4) 回收“不在使用的”页面。PFRA用LRU链表把进程划分为in-use和unused两种,PFRA仅回收unused状态的页面。Linux使用PTE中的Accessed比特位实现非严格的LRU算法。
页面回收通常在三种情况下执行:
1) 系统可用内存比较低时进行回收(通常发生在申请内存失败)。
2) 内核进入suspend-to-disk状态时进行回收。
3) 周期性回收,内核线程周期性激活并在必要时进行页面回收。
Low on memory回收有以下几种情形:
1) _ _getblk( ) 调用的grow_buffers( )函数分配新缓存页失败;
2) create_empty_buffers( ) 调用的alloc_page_buffers( )函数为页面分配临时的buffer head失败;
3) _ _alloc_pages( ) 函数在给定内存区里分配一组连续的页面帧失败。
周期性回收涉及的两种内核线程:
1) Kswapd 内核线程在内存区中检测空闲页面是否低于 p ages_high 的门槛值;
2) 预定义工作队列中的事件内核线程,PFRA周期性调度该工作队列中的task回收slab分配器中所有空闲的slab;
所有用户空间进程和页面缓存的页面被分为活动链表和非活动链表,统称LRU链表。每个区描述符中包括active_list和inactive_list两个链表分别将这些页面链接起来。nr_active和nr_inactive分别表示这两种页面数量,lru_lock用于同步。页描述符中的PG_lru用于标志一个页面是否属于LRU链表,PG_active用于标志页面是否属于活动链表,lru字段用于把LRU中的链表串起来。活动链表和非活动链表的页面根据最近的访问情况进行动态调整。PG_referenced标志就是此用途。
处理LRU链表的函数有:
add_page_to_active_list() 、add_page_to_inactive_list()、activate_page()、lru_cache_add()、lru_cache_add_active()等,这些函数比较简单。
shrink_active_list ( )用于将页表从活动链表移到非活动链表。该函数在shrink_zone()函数执行用户地址空间的页面回收时执行。
5.交换分区:
系统可以有MAX_SWAPFILES的交换分区,每个分区可放在磁盘分区上或者普通文件里。每个交换区由一系列页槽组成。每个交换区有个swap_header结构描述交换区版本等信息。每个交换区有若干个swap_extent组成,每个swap_extent是连续的物理区域。对于磁盘交换区只有一个swap_extent,对于文件交换区则由多个swap_extent组成,因为文件并不是放在连续的磁盘块上的。mkswap命令可以创建交换分区。
图 交换分区结构
图 交换页结构
swp_type() 和 swp_offset() 函数根据页槽索引和交换区号得到type和offset值,函数 swp_entry(type,offset)得到交换槽。最后一位总是清0表示页不在RAM上。槽最大
224(64G)。第一个可用槽索引为1。槽索引不能全为0。
一个页面可能被多个进程共用,它可能被从一个进程地址空间换出但仍然在物理内存上,因此一个页面可能被多次换出。但物理上仅第一次被换出并存储在交换区上,接下来的换出操作只是增加swap_map的引用计数。swap_duplicate(swp_entry_t entry)的功能正是用户尝试换出一个已经换出的页面。
6.交换缓存:
多个进程同时换进一个共享匿名页时或者一个进程换入一个正被PFRA换出的页时存在竞争条件,引入交换缓存解决这种同步问题。通过PG_locked标志可以保证对一个页的并发的交换操作只作用在一个页面上,从而避免竞争条件。
7. 页面回收算法描述:
下图是各种情况下进行页面回收时的函数调用关系图。可以看出最终调用函数为cache_reap()、shrink_slab()和shrink_list()。cache_reap()用于周期性回收slab分配器中的无用slab。shrink_slab()用于回收磁盘缓存的页面。shrink_list()是页面回收的核心函数,在最新代码中该函数名改为shrink_page_list()。下面会重点讲解。
图中shrink_caches()最新函数名为shrink_zones()、shrink_cache()最新函数名为shrink_inactive_list()。其他函数不变。
图 PFRA函数结构调用关系
低内存回收页面:
如上图所示,当内存分配失败时,内核调用free_more_memory(),该函数首先调用wakeup_bdflush( )唤醒pdflush内核线程触发写操作,从磁盘页面缓冲中写1024个dirty页面到磁盘上以释放包含缓冲、缓冲头和VFS的数据结构所占用的页表;然后进行系统调用sched_yield( ),以使pdflush线程能够有机会运行;最后该函数循环遍历系统节点,对每个节点上的低内存区(ZONE_DMA 和 ZONE_NORMAL)调用try_to_free_pages( )函数。
try_to_free_pages(struct zone **zones, gfp_t gfp_mask) 函数的目标是通过循环调用shrink_slab()和shrink_zones()释放至少32个页帧,每次调用增加优先级参数,初始优先级是12,最高为0。如果循环13次,仍然没有释放掉32个页面,则PFRA进行内存异出保护: 调用out_of_memory()函数选择一个进程回收它所有的页面。
shrink_zones(int priority, struct zone **zones, struct scan_control *sc) 函数对zones列表中每个区调用shrink_zone()函数。调用shrink_zone()前,先用sc->priority的值更新zone描述符中的prev_priority,如果zone->all_unreclaimable字段不为0且优先级不是12,则不对该区进行页面回收。
shrink_zone(int priority, struct zone *zone, struct scan_control *sc) 函数尝试回收32个页面。该函数循环进行shrink_active_list()和shrink_inactive_list的操作以达到目标。该函数流程如下:
1) atomic_inc(&zone->reclaim_in_progress) 增加区的回收计数;
2) 增加zone->nr_scan_active,根据优先级,增加范围是zone->nr_active/2
12 to zone->nr_active/2
0 。如果 zone->nr_scan_active >= 32 则赋给nr_active变量,同时zone->nr_scan_active设为0,否则nr_active=0;
3) zone->nr_scan_inactive 和nr_inactive做同样处理;
4) 如果nr_active和nr_inactive不同时为空,则进行while循环进行5、6步操作:
5) 如果nr_active非0,则从active链表移动某些页面到inactive链表:
nr_to_scan = min(nr_active,(unsigned long)sc->swap_cluster_max);
nr_active -= nr_to_scan;
shrink_active_list(nr_to_scan, zone, sc, priority);
6) 如果nr_inactive非0,则回收inactive链表中的页面:
nr_to_scan = min(nr_inactive,(unsigned long)sc->swap_cluster_max);
nr_inactive -= nr_to_scan;
nr_reclaimed += shrink_inactive_list(nr_to_scan, zone, sc);
7) atomic_dec(&zone->reclaim_in_progress) 减小回收计数,并返回回收页面数nr_reclaimed;
shrink_inactive_list(unsigned long max_scan, struct zone *zone, struct scan_control *sc) 函数从区的inactive链表中抽取一组页面,放到临时链表中,调用shrink_page_list()对链表中的每个页面进行回收。下面是 shrink_inactive_list() 主要步骤:
1) 调用lru_add_drain()将当前CPU上pagevec结构的lru_add_pvecs和lru_add_active_pvecs中的页面分别移到活动链表和非活动链表中;
2) 获取LRU锁spin_lock_irq(&zone->lru_lock);
3) 最多扫描max_scan个页面,对每个页面增加使用计数,检查该页面是否正被释放到伙伴系统中,将该页面移动一个临时链表中;
4) 从zone->nr_inactive中减去移到临时链表中的页面数;
5) 增加zone->pages_scanned计数;
6) 释放LRU锁:spin_unlock_irq(&zone->lru_lock);
7) 对临时链表调用shrink_page_list(&page_list, sc)回收页面;
8) 增加nr_reclaimed计数;
9) 获取LRU锁spin_lock(&zone->lru_lock);
10) 将shrink_page_list(&page_list, sc)没有回收掉的页面重新添加到active链表和inactive链表中。该函数在回收过程中可能会设置PG_active标志,所以也要考虑往active链表中添加。
11) 如果扫描页面数nr_scanned小于max_scan则循环进行3~10的操作;
12) 返回回收的页面数;
shrink_page_list(struct list_head *page_list, struct scan_control *sc) 做真正的页面回收工作,该函数流程如下:
图 shrink_page_list()页面回收逻辑处理流程
1) 调用cond_resched()进行条件调度;
2) 循环遍历page_list中每个页面,从列表中移出该页面描述符并回收该页面,如果回收失败,则把该页面插入一个局部链表中;该步流程参见流程图。
l 调用cond_resched() 进行条件调度;
l 从LRU链表中取出第一个页面并从LRU链表中删除;
l 如果页面被锁定,这调过该页面,该页加到临时链表中;
l 如果页面不能部分回收并且页面是进程页表的映射,这跳过该页;
l 如果进程是回写的dirty页面,则跳过;
l 如果页面被引用并且页面映射在使用,这跳过并激活该页面,以便后面放入active列表;
l 如果是匿名页面且不在交换区中,这调用add_to_swap()为该页面分配交换区空间并把该页加到交换缓存中;
l 如果页面是进程空间映射并且页面映射地址非空,则调用try_to_unmap()移除该页面的页表映射;
l 如果页面为dirty页面并且无引用、交换可写、且是fs文件系统映射,调用pageout()写出该页面。
3) 循环结束,把局部链表中的页面移回到page_list链表中;
4) 返回回收页面数。
每个页面帧处理后只有三种结果:
1) 通过调用free_code_page()页面被释放到伙伴系统中,页面被有效回收;
2) 页面没有回收,被重新插入到page_list链表中,并且认为该页面在将来可能会被再次回收,因而清除PG _active标志,以便在后面加入到inactive链表中;
3) 页面没有回收,被重新插入到page_list链表中,并且认为该页面在可预见的将来不会被再次回收,因而设置PG _active标志,以便在后面加入到active链表中
回收一个匿名页面时,该页面必须添加到交换缓存中,交换区中必须为其预留一个新页槽。如果页面在某些进程的用户态地址空间中,shrink_page_list()调用try_to_unmap定位所有执向该页面帧的进程PTE项,只有返回成功时才继续回收;如果页面是dirty状态,必须要写到磁盘上才能回收,这需要调用pageout()函数,回收只有在pageout()很快完成写操作或者不必进行写操作时才继续进行;如果页面保护VFS buffers,则调用try_to_release_page()释放buffer heads。
最后如果上面都进展顺利的话, shrink_page_list()函数检查页的引用计数:如果值正好为2,则一个为页面缓存或交换缓存,另一个是PFRA自身(shrink_inactive_page()函数中增加该值)。这种情况下,该页面可以回收,并且它不为dirty。根据页面PG _swapcache标志,页面从页面缓存或交换缓存中移除;然后调用free_code_page()。
换出页面
add_to_swap(struct page * page, gfp_t gfp_mask)换出操作首先是为页面分配交换页槽,并分配交换缓存;步骤如下:
1) get_swap_page()为换出页面预留交换槽位;
2) 调用__add_to_swap_cache()传入槽索引、页描述符和gfp标志将页面加到交换缓存中,并标记为dirty;
3) 设置页面PG _uptodate和PG_dirty标志,以便shrink_inactive_page()能够强制将页面写到磁盘上;
4) 返回;
try_to_unmap(struct page *page, int migration),换出操作第二步,在add_to_swap后面调用,该函数查找所有用户页表中指向该匿名页帧的页表项,并在PTE中设置换出标志。
Page_out()换出操作第三步将dirty页面写到磁盘:
1) 检查页面缓存或交换缓存中的页,并查看该页面是否近被页面缓存或交换缓存占有;如果失败,返回PAGE_KEEP。
2) 检查address_space对象的writepage方法是否定义,如没有返还PAGE_ACTIVATE;
3) 检查当前进程是否可以发送写请求到当前映射地址空间对象对应的块设备上请求队列上。
4) SetPageReclaim(page)设置页面回收标志;
5) 调用mapping->a_ops->writepage(page, &wbc)进行写操作,如果失败则清除回收标志;
6) 如果PageWriteback(page)失败,页面没有写回,则清除回收标志ClearPageReclaim(page);
7) 返回成功;
对于交换分区,writepage的实现函数是swap_writepage(),该函数流程如下:
1) 检查是否有其他进程引用该页面,如果没有,从交换缓存中移除该页面返回0;
2) get_swap_bio()分配初始化bio描述符,该函数从交换页标志中找到交换区,然后遍历交换扩展链表找到页槽的起始磁盘分区。bio描述符包含对一个页面的请求,并设置完成方法为end_swap_bio_write()。
3) set_page_writeback(page)设置页面writeback标志,unlock_page()该页面解锁;
4) submit_bio(rw, bio)向块设备提交bio描述符进行写操作;
5) 返回;
一旦写操作完成,end_swap_bio_write()被执行。该函数唤醒等待页面PG_writeback标志清除的进程,清除PG_writeback标志,是否bio描述符。
换入页面
换入页面操作发生在一个进程访问被换出到磁盘上的页面时。当下列条件发生时页面出错处理程序会进行换入操作:
1) 包含引发异常的地址的页面是一个当前进程内存区域的有效页面;
2) 该页面不在内存中,PTE的页面present表示被清0;
3) 与页面相关的pte不为null,Dirty位被清0,这意味着该pte包含换出页的标志;
当上述条件同时满足时,hand_pte_fault()调用do_swap_page()函数换入请求页面。
do_swap_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
int write_access, pte_t orig_pte)
该函数处理流程如下:
1) entry = pte_to_swp_entry(orig_pte)得到交换槽位信息;
2) page = lookup_swap_cache(entry)查看交换槽对应的页面是否存在于交换缓存中,如果是则跳到第6步;
3) 调用swapin_readahead(entry, address, vma)从交换区中读取一组页面,对每个页面调用read_swap_cache_async()读取该页面;
4) 对于进程访问异常的页面再次调用read_swap_cache_async()。因为swapin_readahead调用可能失败,在它成功的情况下read_swap_cache_async()发现该页面在交换缓存里,很快返回;
5) 如果页面还是没有在交换缓存中,可能存在其他内核控制路径已经把该页面换入。比较page_table对应的pte内容与orig_pte是否相同,如果不同,说明页面已经换入。函数跳出返回。
6) 如果页面在交换缓存中,调用mark_page_accessed并锁住该页面;
7) pte_offset_map_lock(mm, pmd, address, &ptl)获取page_table对应的pte内容,与orig_pte比较,判断是否有其他内核控制路径进行换入操作;
8) 测试PG_uptodate标志,如果未设置,则出错返回;
9) 增加mm->anon_rss的计数;
10) mk_pte(page, vma->vm_page_prot)创建PTE并设置标志,插入到进程页表中;
11) page_add_anon_rmap()为该匿名页插入反向映射数据结构的内容;
12) swap_free(entry)释放页槽;
13) 检查交换缓存负载是否达到50%,如果是,并且该页面仅被触发页面访问异常的进程占有,则从交换缓存中释放该页。
14) 如果write_access标志为1,说明是COW写时复制,调用do_wp_page()拷贝一份该页面;
释放页锁和页面缓存等,并返回结果。