各類緩存(二)

上一篇瞭解了cache,tlb,頁緩存和mmap,這篇則主要關注交換緩存和交換區。前面幾種緩存都是爲了系統能更快地讀取數據:頁緩存將文件數據緩存至內存中減小磁盤io, tlb緩存頁表數據便於地址翻譯找到物理頁面,cache則將物理頁面中的數據進行緩存便於CPU讀取。但要知足用戶的需求,或者一直知足內存密集型應用程序的需求,不管計算機上可用的物理內存有多少都是不夠的,所以內核須要將不多使用的部份內存換出到塊設備以提供更多的主內存,這種機制爲頁交換或換頁,交換區和交換緩存則是和頁交換相關的。linux

 

可換出頁算法

只有少許幾種頁能夠換出到交換區,對其餘頁來講換出到塊設備與之對應的後備存儲器便可。若是一個不多使用的頁的後備存儲器是一個塊設備(如文件),那麼就無需換出被修改的頁,而是直接與塊設備同步,騰出的頁幀能夠重用,若是再次須要該數據,能夠歷來源從新創建該頁。若是頁的後備存儲器是一個文件可是不能在內存中修改,那麼在不須要的狀況下可直接丟棄該頁,而須要交換到交換區的爲如下幾種:數組

  • 類別爲MAP_ANONYMOUS的頁,沒有關聯到文件(或屬於/dev/zero的一個映射),例如進程的棧或者是使用mmap匿名映射的內存區。
  • 進程的私有映射用於映射修改後不向底層塊設備回寫的文件,一般換出到交換區,所以此時不能從文件恢復頁的內容,內核使用MAP_PRIVATE標誌來建立此類映射。
  • 全部屬於進程堆以及使用malloc分配的頁(malloc又使用了brk系統調用或匿名映射)。
  • 用於實現某種進程間通訊機制的頁,例如用於在進程之間交換數據的共享內存頁。

須要注意的是,由內核自己使用的內存頁毫不會換出,用於將外設映射到內存空間的頁也不能換出。緩存

文件頁和匿名頁:page->mapping末位爲0時,說明爲文件映射頁,mapping指向對應文件的address_space;page->mapping末位爲1時,指向anon_vma(包含了1至多個vma),說明爲匿名映射頁。在內存回收時,匿名頁將會被交換到交換區而保存起來。交換以後頁將被釋放。匿名頁沒有後備存儲,所以須要將其寫入交換區。交換區用來爲匿名頁提供備份,匿名頁能夠分爲三類:屬於進程匿名線性區(如用戶態堆棧、堆)的頁;屬於進程私有內存映射的髒頁;屬於IPC共享內存的頁。安全

 

交換區的組織數據結構

換出的頁或者保存在一個沒有文件系統的專用分區中,或者存儲在某個現有文件系統中的一個定長文件中,能夠同時使用幾個這樣的區域,還能夠根據各個交換區的速度不一樣,爲其指定優先級。內核使用交換區時能夠根據優先級進行選擇。每一個交換區都細分爲若干連續的槽,每一個槽的長度恰好與系統的一個頁幀相同。架構

本質上,系統中的任何一頁均可以容納到交換區的任一槽中,此外內核還使用了一種彙集構造法,使得可以儘快訪問交換區。進程內存區中連續的頁將按照特定的彙集大小逐一寫到硬盤上,若是交換區中沒有更多空間可容納此長度的彙集,內核可使用其餘任何位置上的空閒槽位。併發

若是內核使用了幾個優先級相同的交換區,內核將使用一種循環進程來確保儘量均勻地利用各個交換區。若是交換區的優先級不一樣,內核首先使用高優先級的交換區,而後逐漸轉移到優先級較低的交換區。內核使用位圖用於跟蹤交換區中各槽位的使用/空閒狀態。app

有兩個用戶空間工具可用於建立和啓用交換區,分別是mkswap(用於格式化一個交換分區/文件)和swapon(用於啓用一個交換區)異步

交換子系統主要功能爲:在磁盤上創建交換區;管理交換區空間,分配與釋放頁槽;利用已被換出的頁的pte的換出頁標識符追蹤數據在交換區中的位置;提供函數從ram中把頁換出到交換區或換入到ram;

交換區能夠用來擴展內存地址空間,使之被用戶態進程有效的使用。一個系統上運行的應用所須要的內存總量可能會超出系統中當前的物理內存總量,其原理就是將暫時不用的內存交換出去,待用到的時候再交換進來。從內存中換出的頁存放在交換區(swap area)中。交換區可架設在磁盤分區、大文件甚至內存型文件系統中。每一個交換區都由一組頁槽(page slot)組成,每一個頁槽大小一頁。交換區的第一個頁槽永久存放有關交換區的信息,使用結構swap_header表示。每一個活動的交換區都有本身的swap_info_struct。

pte共有三種狀態:1)當頁不屬於進程的地址空間(進程頁表下),或者頁框尚未分配給進程時,此時是空項;2)最後一位爲0,表示該頁被換出,此時pte表示爲換出頁標識符(swap_entry_t),該標識符由三個部分充滿一個long:最高5bit表示來自哪一個swap分區,2bit表示是否來自於shmem/tempfs,24bit表示在頁槽中的offset,交換區最多有2^24個頁槽(64GB);3)最後一位爲1,表示頁在ram中。

 

交換緩存

交換緩存在選擇換出頁的操做和實際執行頁交換的機制之間充當協調者,即在頁面選擇策略和用於在內存和交換區之間傳輸數據的機制之間,交換緩存充當代理人的角色,這兩個部分經過交換緩存交互。交換緩存用於如下目的,具體取決於頁交換請求的方向(讀入內存或寫入交換區):

  • 在換出頁時,頁面選擇邏輯首先選擇一個適當的、不多使用的頁幀。該頁幀緩衝在頁緩存中,而後將其轉移到交換緩存。
  • 若是換出頁由幾個進程在同時使用,內核必須設置進程頁目錄中的對應頁表項,使之指向交換文件中相關的位置。在其中某個進程訪問該頁的數據時,該頁將再次換入,該進程對應此頁的頁表項將設置爲該頁當前的內存地址。可是這樣會致使一個問題:其餘進程仍然指向交換文件中的位置。所以在換入共享頁時,他們將停留在交換緩存中,直到全部進程都已經從交換區請求該頁,並都知道了該頁在內存中新的位置位置。沒有交換緩存,內核沒法肯定一個共享的內存頁是否已經換入內存,將不可避免地致使對數據的冗餘讀取。可是在引入逆向映射機制後,經過rmap可找到引用該頁數據的全部進程,這意味着引用該頁的全部進程中的相關頁表項均可以更新,指向交換區中對應的位置。這意味着該頁的數據能夠當即換出而無須在交換緩存中保持很長一段時間。

換出頁在頁表中經過一種專門的頁表項來標記,其中會存儲:1)一個標誌,表示頁已經換出;2)該頁所在交換區的編號;3)對應槽位的偏移量,用於在交換區中查找該頁所在的槽位。一個pte_t實例可經過pte_to_swap_entry函數轉換爲一個swap_entry_t實例,該實例存儲了交換分區的標識和該交換分區內部的偏移量,以便惟一肯定一頁。

就數據結構而言,交換緩存就是一個頁緩存,swapper_space中表示了相關函數及結構:

struct address_space swapper_spaces[MAX_SWAPFILES] = {
    [0 ... MAX_SWAPFILES - 1] = { .page_tree = RADIX_TREE_INIT(GFP_ATOMIC|__GFP_NOWARN), .a_ops = &swap_aops, .backing_dev_info = &swap_backing_dev_info, } };

其中經過swap_ops來處理經過交換緩存提供的地址空間,這些函數是交換緩存與系統交換區進行數據傳輸的接口:

static const struct address_space_operations swap_aops = {
    .writepage    = swap_writepage, .set_page_dirty = swap_set_page_dirty, .migratepage = migrate_page, };

swap_writepage將髒頁與底層塊設備同步,其目的並不是用來維護物理內存和塊設備之間的一致性。其目的是將頁從交換緩存移除,將其數據傳輸到交換區。

swap_set_page_dirty用於將頁標記爲髒。

 

向交換區來回傳送頁會引起不少競爭條件,具體的說,交換子系統必須仔細處理下面的情形:

1)多重換入:兩個進程可能同時要換入同一個共享匿名頁;

2)同時換入換出:一個進程可能換入正由PFRA(頁框回收機制)換出的頁;

交換緩存(swap cache)的引入就是爲了解決這類同步問題的。關鍵的原則是:沒有檢查交換緩存是否已包含了所涉及的頁,就不能進行換入或換出操做。有了交換緩存,涉及同一頁的併發交換操做老是做用於同一個頁框的。所以,內核能夠安全的依賴頁描述符的PG_locked標誌,以免任何競爭條件。

如兩個進程共享同一換出頁,當第一個進程試圖訪問頁時,內核開始換入頁操做,第一步就是檢查頁框是否在交換緩存中,假定頁框不在交換緩存中,那麼內核就分配一個新頁框並把它插入到交換緩存,而後開始I/O操做,從交換區讀入頁的數據;同時,第二個進程訪問該共享匿名頁,與上面相同,內核開始換入操做,檢查涉及的頁框是否在交換緩存中。如今頁框在交換緩存,所以內核只是訪問頁框描述符,在PG_locked標誌清0以前(即I/O數據傳輸完畢以前),讓當前進程睡眠。

當換入換出操做同時出現時,交換緩存起着相當重要的做用。shrink_list()函數要開始換出一個匿名頁,就必須當try_to_unmap()從進程(全部擁有該頁的進程)的用戶態頁表中成功刪除了該頁後才能夠。可是當換出的頁寫操做還在執行的時候,這些進程中可能有某個進程要訪問該頁,而產生換入操做。在寫入磁盤前,待換出的頁由shrink_list()存放在交換緩存。考慮頁由兩個進程(A和B)共享的狀況。最初,兩個進程的頁表項都引用該頁框,該頁有兩個擁有者。當PFRA選擇回收頁時,shrink_list()把頁框插入交換緩存。而後PFRA調用try_to_unmap()從這兩個進程的頁表項中刪除對該頁框的引用。一旦這個函數結束,該頁框就只有交換緩存引用它,而引用頁槽的有這兩個進程和交換緩存。假如正當頁中的數據寫入磁盤時,進程B又訪問該頁,即它要用該頁內部的線性地址訪問它,那麼缺頁異常處理程序會發現頁框正在交換緩存中,並把物理地址放回進程B的頁表項。若是上面併發的換入操做沒發生,換出操做結束,則shrink_list()會從交換緩存刪除該頁框並把它釋放到夥伴系統。

能夠認爲交換緩存是一個臨時區域,該區域存有正在被換入或換出的匿名頁描述符。當換入或換出結束時(對於共享匿名頁,換入換出操做必須對共享該頁的全部進程進行),匿名頁描述符就能夠從交換緩存刪除。

交換緩存由頁緩存數據結構和過程實現。頁緩存的核心就是一組基數樹,基數樹算法能夠從address_space對象地址(即該頁的擁有者)和偏移量值推算出頁描述符的地址。在交換緩存中頁的存放方式是隔頁存放,並有以下特徵:頁描述符的mapping字段爲null;頁描述符的PG_swapcache標誌置位;private字段存放於該頁有關的換出頁標識符;此外當頁被放入交換緩存時,頁描述符的count字段和頁槽引用計數器的值都會增長,由於交換緩存既要使用頁框,也要使用頁槽。最後,交換緩存中的全部頁只使用struct address_space swapper_spaces[MAX_SWAPFILES],所以一個交換分區的交換緩存對應一個基數樹(由struct address_space.page_tree指向),換出頁標識符中有對所屬交換分區的標識,根據基數樹對交換緩存中的頁進行尋址。struct address_space.nrpages則用來存放交換緩存中的頁數。

 

添加新頁

可以使用下面兩個內核方法向交換緩存中添加頁:

  • 在內核想要主動換出一頁時會調用add_to_swap,即當策略算法肯定可用內存不足時。該例程不只將頁添加到交換緩存中(在頁數據寫出到磁盤以前,會一直停留在其中),還在某個交換區中爲該頁分配一個槽位,儘管數據不會在此時複製到硬盤,但內核仍然必須考慮爲該頁選擇交換區和對應的槽位。
  • 當從交換區讀入由幾個進程共享的一頁(可根據交換區中的使用計數器斷定)時,該頁將同時保持在交換區和交換緩存中,直至被再次換出,或被全部共享該頁的進程換入。內核經過add_to_swap_cache函數實現該行爲,該函數將一頁添加到交換緩存中而不對交換區進行操做。

使用函數get_swap_page在交換區中分配槽位,以後將須要換出的page實例設置PG_swapcache標誌並將交換標識符swap_entry_t保存在page的private成員中,在頁的內容實際換出時還需構造一個體繫結構相關的頁表項,而後將全局變量total_swapcache_pages加1來更新統計信息,還需將頁插入到由swapper_space創建的基數樹。最後,SetPageUpdate和SetPageDirty修改頁的標誌,由於頁的內容還沒有包含在交換區。對於交換頁來講,對於的底層塊設備是交換區,於是同步(幾乎)就等價於頁換出,將數據從內存傳輸到交換區是由與swapper_space關聯的特定於地址空間的操做完成的,最後更新頁表。

插入交換緩存的函數爲__add_to_swap_cache(),主要執行步驟爲:1)調用get_page(),增長該page的引用計數_mapcount(或稱_refcount);2)置位PG_swapcache;3)將page->private設置爲頁槽索引;4)調用swap_address_space()從上面的swapper_spaces中得到address_space;5)調用radix_tree_insert()將頁插入到基數樹中(address_space->page_tree)

 

數據回寫(頁換出)

頁換出的過程爲:

1)準備交換緩存。若是shrink_page_list()函數確認某頁爲匿名頁(PageAnon()函數返回1)並且交換緩存中沒有相應的頁框(頁描述符的PG_swapcache標誌爲0),內核就調用add_to_swap()函數。該函數會在交換區分配一個頁槽,並把一個頁框(其頁描述符做爲參數傳遞進來)插入交換緩存。函數主要執行步驟以下:調用get_swap_page()分配一個新的頁槽,若是失敗則返回0;調用add_to_swap_cache(),插入基數樹。

2)更新頁表項。經過調用try_to_unmap()來肯定引用了該匿名頁的每一個用戶態頁表項的地址,而後將換出頁標識符寫入其中。大概調用過程爲:try_to_unmap()->remap_walk()->remap_walk_anon() –> rwc->remap_one()->try_to_unmap_one,經過page->private得到entry,構造出一個swp_pte->set_pte_at(),將swp_pte設置給pte

3)將數據寫入交換區。1)檢查頁是不是髒頁,若是是則pageout()將會被執行。其具體邏輯爲:調用is_page_cache_freeable()判斷該頁的引用數,除了調用者、基數樹(即swapcache)以外,還可能有某些buffer在引用該頁(此時page的PG_private或PG_private2一定有置位);2)若是頁的mapping爲空則要麼退出pageout(),要麼該頁屬於buffer。經過page_has_private()來判斷是否如此。若是是的話,則經過try_to_free_buffer()來釋放緩衝區(這個緩衝區是文件系統緩衝);3)清零PG_dirty,pageout()回調page->mapping->a_ops->writepage(),而page的mapping指向全局變量swapper_spaces數組中某元素,從而調用swap_writepage,具體邏輯爲:

在try_to_free_swap()中調用page_swapcount()檢查是否至少有一個用戶態進程引用該頁。這裏並不檢查page->_mapcount,而是檢查對應的頁槽的引用計數。若是引用數爲0,則從基數樹中刪除頁框索引;

調用__swap_writepage,傳入bio_end_io_t類型的回調函數end_swap_bio_write(),首先檢查交換分區有無SWP_FILE,便是否正常開啓並運行中。

調用bdev_write_page(),向塊設備中寫入指定頁。參數有:struct swap_info_struct->bdev、page所對應的sector、要交換的page。進入該函數時,頁被鎖住且PG_writeback不置位,退出時狀態相反。

最後將page釋放。取消PG_locked。並將page->lru加入到free_pages。最後,數組free_pages會被free_hot_cold_page_list()釋放,而交換不成功的頁則要被putback。

數據回寫由swap_writepage完成,內核首先調用remove_exclusive_swap_cache檢查相關頁是否只由交換緩存使用而內核其餘部分都再也不使用,是的話則能夠換出,而後填充struct bio實例,包括塊層須要的全部參數,而後使用setpagewriteback設置PG_writeback標誌,經過submit_bio將寫請求發送至塊層,在寫請求執行時,塊層會將PG_writeback標誌清除。將頁的內容寫入到交換區對應的槽位後,還需更新頁表。一方面頁表項須要指定該頁不在內存(_PAGE_PRESENT標誌清除表示該頁已經換出,_PAGE_FILE標誌位清除表示該頁在交換緩存中,用於非線性映射的頁表項也不會設置_PAGE_PRESENT,但能夠經過_PAGE_FILE標誌位與換出頁相區分),另外一方面還需指向對應槽位在交換區中的位置。(進行頁面回收時,在頁寫回交換區後,若是頁保存在交換緩存中則能夠用__delete_from_swap_cache將該頁從交換緩存刪除,若是頁不在交換緩存中,則使用__remove_from_page_cache將其從通常的頁緩存刪除)

 

頁面回收

頁面回收在兩個地方觸發:

  • 若是內核檢測到在某個操做期間內存嚴重不足,將調用try_to_free_pages檢查當前內存域中全部頁,並釋放最不經常使用的那些
  • 一個後臺守護進程kswapd會按期檢查內存使用狀況,並檢測即將發生的內存不足。

Linux使用LRU算法進行內存回收,並給每一個zone都提供了5個LRU鏈表:Active Anon Page,活躍的匿名頁,page->flags帶有PG_active;Inactive Anon Page,不活躍的匿名頁,page->flags不帶有PG_active;Active File Cache,活躍的文件緩存,page->flags帶有PG_active;Inactive File Cache,不活躍的文件緩存,page->flags不帶有PG_active;unevictable,不可回收頁,page->flags帶有PG_unevictable;

共包含四種操做:將新分配的頁加入到lru鏈表;將inactive的頁從放到inactive list的鏈表尾部;將active的頁轉移到inactive list;將inactive的頁移到active list;

而inactive list尾部的頁,將在內存回收時優先被回收(寫回或者交換)。

 

處理交換缺頁異常(頁換入)

頁換入的過程:

當進程試圖對一個已被換出的頁進行尋址時,必然會發生頁的換入。在如下條件全知足時,缺頁異常處理程序會觸發一個換入操做:1)引發異常的地址所在的頁是一個有效的頁,也就是說,它屬於當前進程的一個線性區;2)頁不在內存中,也就是頁表項的Present標誌被清除;3)與頁有關的頁表項不爲空,可是PG_dirty位被清零,意味着頁表項乃是一個換出頁標識符。

換入時首先檢查該頁是否在交換緩存中,如果則直接返回,若沒有,則須要根據pte的換出頁標識符從對應的交換區的頁槽中讀取該頁。首先須要分配一個新的內存頁容納從交換區讀取的數據,若是頁分配成功,內核將添加該page實例到交換緩存,並將其添加到活動頁的LRU緩存,而後與換出頁相似,經過swap_readpage發起從硬盤到物理內存的數據傳輸(如交換區是一個文件,則其file描述符保存在swap_info_struct->swap_file中,以後讀取頁面與讀取文件相似。):get_swap_bio產生一個適當的bio請求,而submit_bio將該請求發送到塊層。其中add_page_to_swap_cache自動鎖定頁,swap_readpage通知塊層在頁已經徹底讀入後調用end_swap_bio_read。若是順利會對該頁設置PG_uptodate標誌並解鎖。由於讀操做是異步的,但在頁標記爲PG_uptodate並解鎖時,內核能夠確認其中已經填充了所需的數據。

 

訪問換出頁致使的缺頁異常,由mm/memory.c中的do_swap_page處理,代碼流程以下:

內核不只要檢查所請求的頁是否仍然或已經在交換緩存中,它還使用一種簡單的預讀方法一次性從交換區讀入幾頁,預防將來可能出現的缺頁異常。

換出頁所在的交換區和槽位信息都保持在頁表項中,內核首先使用pte_to_swp_entry將頁表項轉換爲一個swp_entry_t實例,而後使用lookup_swap_cache檢查所需的頁是否在交換緩存中,若在交換緩存中則直接返回,若是該頁的數據還沒有寫出,或該頁是共享的,此前已經由另外一個進程讀入那麼就有可能在交換緩存中找到。若是不在交換緩存中,內核不只必需要讀取該頁,還必須發起一個預讀操做讀取幾個預期可能使用的頁。若是未在交換緩存中找到該頁,內核則分配一個新的內存頁,容納從交換區讀取的數據,若是頁分配成功,內核將添加該page實例到交換緩存,並將其添加到活動頁的LRU緩存,而後經過swap_readpage發起從硬盤到物理內存的數據傳輸。

在頁已經換入後,須要用mark_page_accessed標記該頁,使內核認定其已經訪問過,而後將該頁插入進程頁表,此後調用page_add_anon_rmap加入逆向映射,而後檢查是否能夠釋放交換區中對應的槽位。

若是該頁是以讀/寫模式訪問,內核必須用過調用d0_wp_page來結束操做,這將建立該頁的一個副本,並將其添加到致使異常的進程的頁表中,將原始頁的使用計數器減1.

 

本篇只簡述了交換區和交換緩存的相關概念及操做流程,對於具體的數據結構和函數實現未做分析,內容參考《深刻Linux內核架構》及linux swap與zram詳解

相關文章
相關標籤/搜索