Linux學習總結—缺頁中斷和交換技術【轉】


三、Linux缺頁中斷處理
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 。如果 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()拷貝一份該頁面;

釋放頁鎖和頁面緩存等,並返回結果。 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM