Linux Swap 從 userspace 到 kernel詳解

簡介

Linux Swap 機制在很早之前已經出現,主要的目的是在很有限的內存大小的機器上虛擬地擴展內存,比如實際4G內存大小的手機或者PC,在配置了4G的Swap空間,那麼該機器就相當於8G內存大小。雖然內存大小間接地擴大了一倍,但與真正的8G內存大小的性能效果完全不能對等,其大致的做法是在磁盤上配置4G大小的空間,用於存放最近不再使用的內存數據,當系統需要的時候再從該磁盤上讀出來。然而磁盤的讀寫速度相較於內存的讀寫速度來說是差了一個甚至幾個數量級的,所以,如果碰到系統訪問的內存數據正好在磁盤上,就需要把數據從磁盤上讀出來,這就會導致系統變慢。

以上的例子中,我們是在磁盤上開劈了一個4G大小的空間用作swap空間,當然也可以在其它的存儲設備中開劈swap空間,比如pc上的SSD等等速度相比普通磁盤要快得多的存儲設備上,另外也可以是虛擬塊設備,比如手機系統中的ZRAM。而Linux Swap機制只是提供了一個很方便的擴充虛擬內存的方法,至於在哪些設備上擴充,完全由用戶來決定。

既然是由用戶來決定,那麼我們先來看一下開闢 linux swap 空間的方法,首先我們需要在已掛載的分區目錄裏創建一個文件,比如我們在 /data/ 這個目錄裏創建一個大小爲4G的名字爲 swapfile.swap 的文件,我們使用dd命令來創建該文件

dd if=/dev/zero of=/data/swapfile bs=1m count=4096

然後在通過 mkswap 命令把 /data/swapfile.swap 文件格式化成swap分區

mkswap /data/swapfile

通過 swapon 命令啓動 swap,這條命令裏首先會去檢查文件類型,是否是swap 分區,所以在使用swapon 命令之前必須通過 mkswap 命令來格式化swap 分區。

swapon /data/swapfile

這樣就在 /data/swapfile.swap 開闢了4G大小的swap分區。
如此類推,假如想在SSD上開闢swap分區,那麼就在掛載SSD的目錄下創建這個文件,並開啓它。
到這裏,用戶決定的swap 分區已經創建好了,接下來內存怎麼被交換到swap分區,又是怎麼從swap分區讀回來的,這些事情就交給 linux 內核完成了,也是本文重點描述的內容。

友情提示:接下來的內容與內存管理和文件系統有關,需要先掌握這兩部分的基本知識才比較容易明白。

整體架構

Linux Swap 機制的整體層級架構如下圖所示
層級架構
整套機制是基於內存管理之下建立的,由於swap會對磁盤進行讀寫,所以設計上與pagecache 相類似地建立了一個 swap cache 的緩存,提高swap的讀寫效率。這裏的swap core 指的是swap的核心層,主要完成管理各個swap分區,決策內存數據需要交換到哪個磁盤上,以及發起讀寫請求的工作。

當內存管理模塊需要回收一個匿名頁面的時候,首先先通過swap core 選擇合適的swap 分區,並修改pte,指向swap entry的pte,同時把該頁面加入到 swap cache 緩存起來,然後再通過swap core 發起回寫請求進行回寫,等待回寫結束後,內存管理模塊釋放該頁面。當用戶需要訪問處於swap分區的數據時,首先先通過內存管理模塊確定pte的swap entry,然後在swap cache 中快速地查找swap entry 對應的物理頁面,如果這時物理頁面仍末被回收,就能找到對應的頁面,則直接修改pte,指向該page,重新建立映射,如果沒找到對應的頁面,表明物理頁面已經被回收了,則在swap core 中通過 swap entry 查找對應的swap 分區和數據地址,最後申請一個page併發起讀操作,等待讀操作完成後,修改pte,指向該page,重新建立映射。

以上描述的兩個過程中,關鍵點有以下幾個:

  1. Swap entry 的數據結構,也就是說 swap core 是怎麼組織 swap entry ,又是怎麼通過swap entry找到對應在swap分區的數據的。
  2. Swap cache 存儲結構,即swap cache 是如何存儲各個匿名頁面的
  3. Page 的幾個標誌位,PG_writeback, PG_swapcache, PG_locked,這些標誌位在交換過程中有着非常重要的作用,決定了page的生命週期。
    帶着以上幾個關鍵點,我們來從細節上了解整個swap世界。

涉及的文件介紹

Swap 涉及的文件都在 kernel-sourcecode/mm/這個目錄下,查看該目錄的 Makefile 文件發現當CONFIG_SWAP 這個配置項打開時會編譯 page_io.c swap_state.c swapfile.c 和swap_ratio.c文件。以下逐一介紹一下,可大致瞭解一下,具體的調用流程在後面有描述。

  • page_io.c

    與IO相關的文件,裏面定義了所有swap機制的io操作函數,主要是以下幾個函數

end_swap_bio_write IO 寫操作完成後回調的函數
end_swap_bio_read IO 讀操作完成後回調的函數
swap_writepage IO 寫操作函數
swap_readpage IO 讀操作函數
swap_set_page_dirty 設置頁面爲髒頁函數
  • swap_state.c

    與swapcache 相關的文件,也就是構架圖中swap cache,裏面定義了 swapcache 的增,刪,查等函數,以及swap 預讀函數。

  • swap_file.c

    這個是 linux swap 機制的核心文件,也就是構架圖中swap core,裏面包含了所有swap分區的組織方式,交換策略,開啓關閉swap,用戶使用的 swapon 和swapoff 的系統調用都在該文件中定義。

  • swap_ratio.c

    與swap 分區之間的使用率相關的文件

與內存的聯繫

在內核線程中,有一個專門用來回收物理頁面的線程叫kswapd,它主要的工作之一就是尋找滿足回收條件的匿名頁面和文件頁面,然後回收它們,當然回收他們之前就需要把物理頁面的數據保存起來,這個過程稱之爲交換(以下稱swap),文件頁面自然而然地就有對應的磁盤保存位置,而對於匿名頁面來說,即其數據原本不在磁盤上的情況,我們就需要用到swap分區去保存。所以swap分區只適合交換匿名頁面的數據。

從整個 linux 系統內存回收的角度來看的話,我們可以這麼認爲,如果系統開啓了 swap 機制並且用戶通過 swapon 指定了 swap 分區,那麼內存回收時將會回收匿名頁面,否則只會回收文件頁面。
我們先初步來了解一下內存回收的入口,所有的內存回收路徑最終都會走到以下函數

shrink_page_list(struct list_head *page_list, struct pglist_data *pgdat, struct scan_control *sc, enum ttu_flags ttu_flags, struct reclaim_stat *stat, bool force_reclaim)

初步被選出來的系統認爲滿足條件的頁面放在 page_list 這個鏈表中,至於如何選擇滿足條件的頁面,是由內存回收算法LRU決定,這是內存管理方面的知識,這裏不作詳細地描述。在這個函數中會對 page_list 鏈表中所有的頁面逐一處理,若是匿名的並且沒有被 swap cache 緩存的頁面,通過 add_to_swap 函數通知 swap core 和 swap cache 把該頁面的數據交換出內存,隨後在 try_to_unmap 函數中修改該 page 對應的 pte,使其指向 swap entry 的 pte。因此 add_to_swap 函數則是 swap core 和 swap cache 與內存回收之間的橋樑。

而另外的橋樑則是發生在缺頁中斷中,當進程訪問的數據不在物理內存時會進入到缺頁中斷中,在該中斷裏,先去判斷產生缺頁中斷的 pte 是否是 swap entry,如果是則調用 do_swap_page 函數把數據從磁盤上交換回內存中。

因此swap core 和 swap cache 與 memory management 之間的橋樑主要有兩個,如下表所述

函數 功能 調用時機
add_to_swap 把一個page放入swap cache 的緩存中,並分配一個 swp_entry_t 類型的swap entry,然後綁定該page 內存回收時
do_swap_page 在 swap cache 緩存中尋找對應 swap entry 的page,如果找不到則從swap分區中讀出數據,並與虛擬內存地址進行映射 缺頁中斷時

與IO的聯繫

接着上一節,在內存回收時,通過add_to_swap 函數把 swap entry 和 page 綁定之後,需要通過 io 操作把page 裏面的數據回寫到swap entry 指定的swap 分區中,具體的操作是在pageout函數裏。

static pageout_t pageout(struct page *page, struct address_space *mapping, struct scan_control *sc)

首先我們先來看一下 pageout 函數中的第二個參數 struct address_space *mapping,該指針是指向 swapper_spaces[][]其中一個元素的地址,該變量是一個全局變量,當用戶通過 swapon 命令增加一個swap 分區時,會分配N個 struct address_space 變量,並加到全局數組變量swapper_spaces中,一個struct address_space 管理的swap 空間大小是64M,即一個swap分區分成N個大小爲64M的struct address_space,比如192M的swap 分區,在創建時被拆出了3個struct address_space,而全局數組變量swapper_spaces的第一維元素是代表第幾個swap分區,第二維存儲的是該swap分區所有的64M大小的struct address_space,其結構如下圖所示
address_spaces
因此pageout函數傳入的mapping 參數就是swap分區中的其中一個address_space,那麼接下來的流程就和普通文件的回寫流程就基本一樣了,通過address_space->a_ops->writepage回調的方式回寫page 的數據到磁盤上,實際上是調用了 page_io.c 文件中的 swap_writepage 函數。

我們再來看一下 swap 分區 的 address_space 結構,其實 swap 分區的 address_space 精減了很多,address_space->host 指向的是NULL,address_space->a_ops 指向的是全局變量 swap_aops,在這個struct address_space_operations IO操作集中只實現了writepage, set_page_dirty, 和migratepage三個接口函數。

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

再來看 swap_writepage 函數實現,此處省略掉非關鍵代碼

int __swap_writepage(struct page *page, struct writeback_control *wbc,
		bio_end_io_t end_write_func)
{
	struct bio *bio;
	int ret;
	struct swap_info_struct *sis = page_swap_info(page);
    ......
	ret = 0;
	bio = get_swap_bio(GFP_NOIO, page, end_write_func);
    ......
	bio->bi_opf = REQ_OP_WRITE | wbc_to_write_flags(wbc);
    ......
	submit_bio(bio);
out:
	return ret;
}

通過 get_swap_bio 函數來獲取一個bio,然後再通過 submit_bio 來提交一個 bio,剩下的事情就交給block層去完成了,讀函數 swap_readpage 也類型於寫函數一樣,至於 bio 是怎麼獲取?又怎麼跟 swapfile 建立聯繫的?會在下一節講數據結構時詳細說明。

主要的數據結構

swap_info_struct

這個結構體描述的是一個 swap 分區的具體信息(以下稱之爲 swap 分區描述符),一個 swap 分區對應一個 swap_info_struct 結構體,該結構體中具體的內容如下:

struct swap_info_struct {
	unsigned long	flags;		/* SWP_USED etc: see above */
	signed short	prio;		/* swap priority of this type */
	struct plist_node list;		/* entry in swap_active_head */
	struct plist_node avail_lists[MAX_NUMNODES];/* entry in swap_avail_heads */
	signed char	type;		/* strange name for an index */
	unsigned int	max;		/* extent of the swap_map */
	unsigned char *swap_map;	/* vmalloc'ed array of usage counts */
	struct swap_cluster_info *cluster_info; /* cluster info. Only for SSD */
	struct swap_cluster_list free_clusters; /* free clusters list */
	unsigned int lowest_bit;	/* index of first free in swap_map */
	unsigned int highest_bit;	/* index of last free in swap_map */
	unsigned int pages;		/* total of usable pages of swap */
	unsigned int inuse_pages;	/* number of those currently in use */
	unsigned int cluster_next;	/* likely index for next allocation */
	unsigned int cluster_nr;	/* countdown to next cluster search */
	struct percpu_cluster __percpu *percpu_cluster; /* per cpu's swap location */
	struct swap_extent *curr_swap_extent;
	struct swap_extent first_swap_extent;
	struct block_device *bdev;	/* swap device or bdev of swap file */
	struct file *swap_file;		/* seldom referenced */
	unsigned int old_block_size;	/* seldom referenced */
#ifdef CONFIG_FRONTSWAP
	unsigned long *frontswap_map;	/* frontswap in-use, one bit per page */
	atomic_t frontswap_pages;	/* frontswap pages in-use counter */
#endif
	spinlock_t lock;		
	spinlock_t cont_lock;		
	struct work_struct discard_work; /* discard worker */
	struct swap_cluster_list discard_clusters; /* discard clusters list */
	unsigned int write_pending;
	unsigned int max_writes;
};

我們關注一下幾個關鍵的成員變量

  • list
    swap_active_head 鏈表中的節點
  • prio
    該 swap 分區的優先級
  • type
    該變量指的是第幾個 swap 分區,系統一共只允許創建 MAX_SWAPFILES 個 swap 分區,如果該swap 分區是第一個創建的,那麼type 就是0,該變量同時也是 swap_info[] 全局變量的索引,即swap_info[0] 就是第一個 swap 分區的 swap_info_struct 結構體。swap_info[] 數組保存着系統中所有swap 分區描述符,即 swap_info_struct 結構體。
  • swap_map
    頁槽數組。該變量是 char 型的數組,長度是該 swap 分區的頁面個數,即如果該 swap 分區創建時是4G,其長度就是1048576 (4G/4K),swap_map 中每一元素都是一個頁槽,linux 內核把 swap 分區以4K爲單位,劃分出n個存儲塊,即一個存儲塊正好能填充一個物理頁面的數據,所以一個頁槽就代表着一個存儲塊使用情況,0代表空閒狀態,仍沒被使用,由於一個物理頁面有可能被多個進程 map,所以該值大於0則代表的是有多少個使用了該頁面的進程被 unmap,swap_count 函數就是通過swap_map[n] 來得到,值得注意一下該變量有幾個特殊值,如下
        #define SWAP_MAP_MAX	0x3e	/* Max duplication count, in first swap_map */
        #define SWAP_MAP_BAD	0x3f	/* Note pageblock is bad, in first swap_map */
        #define SWAP_HAS_CACHE	0x40	/* Flag page is cached, in first swap_map */
    
    SWAP_MAP_MAX 是最大的計數,即頁槽的計數不能超過該值,SWAP_MAP_BAD 標明瞭該存儲塊爲壞塊,不能使用,SWAP_HAS_CACHE 其實就是頁槽的第6位,如果頁槽的第6位被置位,證明該存儲塊的物理頁面在 swap cache 中,否則不在 swap cache 中,即該位應與進出 swap cache 同步。
  • curr_swap_extent
    是一個存儲塊鏈表的遊標指針
  • first_swap_extent
    存儲塊鏈表頭,描述連續的存儲塊的結構體是 struct swap_extent,該結構體中記錄着存儲塊的個數(nr_pages),和該連續的存儲塊對應的扇區的開始位置(start_block),這裏需要注意的是 start_block 需要轉換成以512字節爲單位的真正扇區號,block io 層才能真正找到數據存儲的位置,轉換方式也非常簡單,即把 start_block 左移( PAGE_SHIFT - 9)位。
  • pages
    代表的是去除掉壞塊之後可用存儲塊的個數
  • inuse_pages
    代表的是已經使用的存儲塊個數
  • cluster_next
    指向下一個可用存儲塊的編號
  • bdev
    指向該 swap 分區所處於的塊設備 struct block_device 結構體
  • swap_file
    指向 swap 分區的 struct file 結構體

該結構體包含的內容非常多,這裏總結一下,不管 swap 分區是一整個塊設備還是一個在塊設備上創建的文件,主要是把它劃分成多個以一個頁面爲單位的存儲塊,並用 swap_map 來描述各個存儲塊的使用情況。然後以高低優先級的順序保存在 swap_active_head 鏈表中。

swp_entry_t

到這裏,請讀者思考一個問題,如果現在把一個物理頁面的數據回寫到 swap 分區中的其中一個存儲塊,然後釋放該物理頁面,在下次應用進程再需要讀回剛剛被回寫的數據時,如何快速地尋找到那個存儲塊?

對內存管理比較瞭解的讀者會第一時間想到 pte,那麼 linux swap 機制就需要在回收物理頁面之前修改該虛擬地址對應的 pte,使之指向一個 swp_entry_t 類型的變量,在下次訪問該虛擬地址時進入缺頁中斷,然後通過 swp_entry_t 來找到 swap 分區中的存儲塊位置,並從中讀回數據,最後再修改 pte 重新指向物理頁面。
swap_entry_t 變量就是在整體架構章節中提到的 swap entry 的 pte。以下是該變量的定義

typedef struct {
	unsigned long val;
} swp_entry_t;

該變量可通過 set_page_private 函數保存在 page-> private 中,跟隨 page 進行傳遞,所以如果需要該 page 的swap entry,直接訪問該 page 的 private 就能得到,其實它就是一個 unsigned long 類型的值,在這個數值中包含了兩個信息,一個是 swap 分區的 type(以下稱 swap type),另一個是存儲塊的編號,即 offset (以下稱 swap offset),第2到第7位存放 swap type,第8位到第57位存放 swap offset,最低兩位保留不用,第58位必須等於0,因爲該位置1是代表無效的pte,可參見源碼註釋

 *	bits 0-1:	present (must be zero)
 *	bits 2-7:	swap type
 *	bits 8-57: swap offset
 *	bit  58:	PTE_PROT_NONE (must be zero)

內核裏有以下api,可以很方便地在 swp_entry_t 變量和 swap typeswap offsetpte 之間轉換:

__swp_type(swp_entry_t) 從swp_entry_t中得到swap type
__swp_offset(swp_entry_t) 從swp_entry_t中得到swap offset
__swp_entry(type,offset) 把swap type 和 swap offset 合併成 swp_entry_t
__pte_to_swp_entry(pte) 把pte 格式化成 swp_entry_t
__swp_entry_to_pte(swp) 把swp_entry_t格式化成pte

得到 swap typeswap offset 之後,我們就可以通過全局數組變量 swap_info[swap type] 來得到swap 分區描述符 swap_info_struct,再通過 swap offset 找到對應的存儲塊描述符 swap_extent,再把swap_extent->start_block 轉換成 bio 所需的扇區號,然後通過 submit_bio 函數來發起讀請求,最後等待block io層完成讀操作。這一過程的關鍵在於bio的組建,即如何得到扇區號。

swap_extent

存儲塊描述符,用於描述多個連續的存儲塊,以及描述與塊設備中扇區的映射關係。其數據結構如下

struct swap_extent {
	struct list_head list;
	pgoff_t start_page;
	pgoff_t nr_pages;
	sector_t start_block;
};
  • list
    鏈表節點,其鏈表頭是 swap 分區描述符的 first_swap_extent 變量
  • start_page
    描述的第一個存儲塊編號
  • nr_pages
    描述存儲塊個數
  • start_block
    描述該第一個存儲塊對應的扇區編號

因此這裏涉及到了兩個比較關鍵的概念,一個是存儲塊,另一個是扇區編號。
存儲塊的大小爲 4KB,扇區的大小爲 512Bytes,所以一個存儲塊中包含 8個 連續的扇區。
存儲塊的編號是虛擬的,是連續遞增的,swp_entry_t中的swap offset 變量所指的就是存儲塊編號。
扇區編號是真實地指向塊設備的扇區號,是 block io 層需要的變量,所以在給定 swap offset 時,可以通過該結構體找到對應的塊設備扇區號,從而發起 io 操作

以下通過圖解來描述一下這兩者的映射關係

swap_extent
黃色小方塊代表真正 block 設備的一個扇區,紅色方塊代表一個 4KB 大小的存儲塊,一個存儲塊中包含有 8個 連續的扇區,多個存儲塊之間如果是連續的,可以合併成一個 swap_extent 結構來描述,如1號2號存儲塊,其中 start_page 的變量描述的第一個存儲塊編號,而nr_pages是描述了有多少個連續的存儲塊,start_block 經過左移(PAGE_SHIFT-9)位得到該存儲塊對應 block 設備的開始扇區編號。舉例說明一下,如果 swp_entry_tswap offset 是2,也就是 2號存儲塊,那麼找到 start_page 小於等於2並且(start_page + nr_pages)大於等於2的 swap_extent,然後通過(start_block+ offset-start_page) 找到真正的扇區編號。這樣 bio 就可以組建起來,併發給 block io 層去進行讀寫操作了。

swap_cluster_info

爲了快速地尋找一個空閒的頁槽,在ssd等非旋轉類磁盤(即非機械硬盤)作爲swap分區時,就不能只是簡單地全局遍歷 swap_map[] 數組了,需要增加一種機制(以下稱之爲ssd算法)來輔助查找頁槽,我們先來看一下如果直接遍歷 swap_map[] 數組來的查找空閒頁槽會有什麼樣的問題,如果當前系統有很多進程需要去交換內存,那麼就會同時訪問 swap_map[] 數組,從而產生資源競爭,就需要全局的 spin_lock 來保護,在這種情況下如果系統中有 n個 cpu 同時訪問這個數組,那麼只能一個一個地訪問,從而阻塞了其它 cpu 的執行,就好比如n個人走一條獨木橋一樣,他們都走完獨木橋就需要花很多時間。解決這個問題的方法也很簡單,架設多條獨木橋,一次可以通過n個人,其實ssd算法就類似於這種架設多條獨木橋的方法。具體的做法是,把所有的頁槽組織成以 256個 頁槽爲一個簇的鏈表,如下圖所示
swap_cluster
藍色小方塊是一個頁槽,白色大方塊是一個簇(cluster),每個簇中都有一個單獨的 spin_lock,用於保護這個簇對應的 256個 頁槽。如果有兩個 cpu 同時訪問 swap 分區不同簇的頁槽就不會有競爭關係,比如 cpu0 訪問第2個頁槽,cpu1 訪問第260個頁槽,這個時候兩個cpu申請的 spin_lock 不是同一個,所以可以順利申請得到,不會產生 spin_lock 競爭,這樣就提高了訪問效率。
我們先來看一下一個簇的數據結構 struct swap_cluster_info

struct swap_cluster_info {
	spinlock_t lock;	
	unsigned int data:24;
	unsigned int flags:8;
};

dataflags 兩個位域組成一個 unsigned int 的類型的變量,低8位爲flags,高24位是data。

  • data:如果該簇是空閒的,這個變量代表的是下一個空閒的簇編號,如果該簇不是空閒的,那麼該變量代表是已經分配的頁槽數
  • flags:有三種flag,如下:
#define CLUSTER_FLAG_FREE 1 /* This cluster is free */
#define CLUSTER_FLAG_NEXT_NULL 2 /* This cluster has no next cluster */
#define CLUSTER_FLAG_HUGE 4 /* This cluster is backing a transparent huge page */
第三種flag先本文不作介紹,需要開啓CONFIG_THP_SWAP這個配置纔用到。

在 swap 分區描述符中有一個數組保存所有簇,該數組變量是 cluster_info,其數組的索引值就是空閒簇的 data,如下圖所示,data 變量指向的是下一個空閒簇在 cluster_info 數組中的索引值,最後一個空閒簇的data等於自身的索引值。
swap_cluster2
swap_info_struct 結構中還有另一個變量,是 free_clusters,它是一個所有空閒簇的鏈表,但這個鏈表結構與常規的結構不同,如上圖所示,free_clusters 中只有兩個 struct swap_cluster_info 類型的變量,一個保存的是第一個空閒簇,另一個保存的是最後一個空閒簇,這個鏈表是通過 data 變量來連接起來,並不是指針的方式。由於 data 變量是指向下一個空閒簇的編號,所以通過 data 變量就可以把所有空閒簇都連接起來。這樣做的目的是可以節省變量的存儲空間,因爲swap分區會比較大,所需要記錄的頁槽太多。
以下列出ssd算法的幾個關鍵操作函數,如向 clusters 鏈表中增加一個簇,刪除一個簇,判斷鏈表是否爲空,判斷簇的狀態等等

cluster_list_add_tail(struct swap_cluster_list *list, struct swap_cluster_info *ci, unsigned int idx) ci指向swap_info_struct->cluster_info,idx 代表的是全局簇數組cluster_info的索引號,即往list中插入一個swap_info_struct->cluster_info[idx]簇
cluster_list_del_first(struct swap_cluster_list *list, struct swap_cluster_info *ci) ci指向swap_info_struct->cluster_info。從list鏈表裏刪除第一個簇
struct swap_cluster_info * lock_cluster(struct swap_info_struct *si,unsigned long offset) 獲取offset頁槽對應的簇的spin_lock,並返回該簇指針
unlock_cluster(struct swap_cluster_info *ci) 解鎖ci 簇的cispin_lock,需要與lock_cluster成對使用
cluster_list_empty(struct swap_cluster_list *list) 判斷該list 是否爲空
cluster_list_first(struct swap_cluster_list *list) 返回該list的第一個簇索引號
alloc_cluster(struct swap_info_struct *si, unsigned long idx) 分配簇索引號爲idx的簇,即把簇從free_clusters的鏈表中取出
free_cluster(struct swap_info_struct *si, unsigned long idx) 釋放一個簇索引號爲idx的簇,即把簇放回free_clusters的鏈表中

swapper_spaces

swapper_spaces 在整體架構章節中有提到過,它是一個二維數組,其元素是 struct address_space 類型的結構體變量,第一維代表第幾個 swap 分區,即與 swap_info_struct 中的type 同步,至於第二維的描述可回顧整體架構章節中的與IO的聯繫一節。所以在給定 swp_entry_t 之後就可以通過以下宏得到 struct address_space

#define swap_address_space(entry)			    \
	(&swapper_spaces[swp_type(entry)][swp_offset(entry) \
		>> SWAP_ADDRESS_SPACE_SHIFT])

struct address_space 這個結構體的作用是管理一個最大爲 64MBswap cache,以及提供一套操作函數集(a_ops)給io回寫使用。以下分析一下swapper_spaces初始化的代碼

int init_swap_address_space(unsigned int type, unsigned long nr_pages)
{
	struct address_space *spaces, *space;
	unsigned int i, nr;

	nr = DIV_ROUND_UP(nr_pages, SWAP_ADDRESS_SPACE_PAGES); //求出該swap分區需要多少個64M大小的address_space
	spaces = kvzalloc(sizeof(struct address_space) * nr, GFP_KERNEL); //爲該swap分區分配空間用於存儲address_space
	if (!spaces)
		return -ENOMEM;
	for (i = 0; i < nr; i++) { //遍歷所有的address_space 並初始化它
		space = spaces + i;
		INIT_RADIX_TREE(&space->page_tree, GFP_ATOMIC|__GFP_NOWARN); //初始化swap cache 的樹
		atomic_set(&space->i_mmap_writable, 0);
		space->a_ops = &swap_aops;  // 初始化a_ops,使其指向swap_aops 的操作集
		/* swap cache doesn't use writeback related tags */
		mapping_set_no_writeback_tags(space);
		spin_lock_init(&space->tree_lock);
	}
	nr_swapper_spaces[type] = nr;
	rcu_assign_pointer(swapper_spaces[type], spaces); //把該swap 分區的所有address_spaces 存入swapper_spaces中

	return 0;
}

swap cache

swap cache 指的是交換緩存區,它的作用類似 page cache,主要是提高讀寫效率。swap cache 的數據結構是基數樹,其根節點是 address_space->page_tree,基數樹的數據結構在這裏不展開描述,以下列出與 page cache 增,刪,查的操作函數,以及什麼時候會加入到 swap cache,什麼時候會從中刪除,即增加和刪除的時機。

__add_to_swap_cache(struct page *page, swp_entry_t entry) 增加一個page 到swap cache 中,並把entry 保存到page->private變量中
__delete_from_swap_cache(struct page *page) 從page cache 中刪掉page
lookup_swap_cache(swp_entry_t entry, struct vm_area_struct *vma, unsigned long addr) 從swap cache查找entry 指定的page

把page加入交換緩存區的時機:

  1. 本來在物理內存沒有數據,需要從 swap 分區中把數據讀回時,即 swapin,需要加入到 swap cache 中。
  2. 系統回收內存時,選擇一個匿名頁面進行回收,則先把該page 放入swap cache

把 page 從交換緩存區中刪除掉的時機:

  1. 該 page 已經沒有進程需要了,根據 page_swapped(page) 來判斷。
  2. 該 page 的 PG_writebackPG_dirty 都爲 0 時,並且系統急需回收內存時。
  3. 該 page 發生了寫時複製,或者發生寫訪問異常時,並且只一本進程使用該 page, 即寫訪問的方式調用 do_swap_page

Swapout/swapin 流程分析

瞭解了關鍵數據結構以及swap分區的組織方式之後,接下來通過代碼分析swapout 與 swapin 的流程

Swapout

Swapout 的入口是在shrink_page_list, 即當系統需要回收物理內存時發生swapout 的動作,先來看一下一個匿名頁面回收的整個過程框圖
swapout
整個回收過程分爲兩次 shrink,這裏面的原因是因爲IO的寫速度會很慢,不能阻塞內存回收的進程,所以第一次shrink只是發起了一個回寫請求然後就返回了,等待IO的回寫操作完成後,第二次回收該匿名頁面時,再把它回收掉。
先來看一下第一次shrink的過程,以及page的狀態變化過程

static unsigned long shrink_page_list(struct list_head *page_list,
				      struct pglist_data *pgdat,
				      struct scan_control *sc,
				      enum ttu_flags ttu_flags,
				      struct reclaim_stat *stat,
				      bool force_reclaim)
{
	LIST_HEAD(ret_pages); //初始化返回的鏈表,即把此次shrink無法回收的頁面放入該鏈表中
	LIST_HEAD(free_pages); //初始化回收的鏈表,即把此次shrink 可以回收的頁面放入該鏈表中
…
	while (!list_empty(page_list)) {
…
		page = lru_to_page(page_list); 
		list_del(&page->lru); // 從 page_list 中取出一個 page,page_list 需要回收的page鏈表

		if (!trylock_page(page))  //先判斷是用否有別的進程在使用該頁面,如果沒有則設置PG_lock,並返回1, 這個flag多用於io讀, 但此時第一次shrink時大多數情況下是沒有別的進程在使用該頁面的,所以接着往下走
			goto keep;

		may_enter_fs = (sc->gfp_mask & __GFP_FS) ||
			(PageSwapCache(page) && (sc->gfp_mask & __GFP_IO));

		if (PageAnon(page) && PageSwapBacked(page)) { //判斷是否是匿名頁面並且不是lazyfree的頁面,顯然這個條件是滿足的
			if (!PageSwapCache(page)) { //判斷該匿名頁面是否是 swapcache ,即通過page的 PG_swapcache 的flag 來判斷,此時該頁面第一次 shrink,所以這裏是否,進入if裏面的流程
                  …
				if (!add_to_swap(page)) //爲該匿名頁面創建swp_entry_t,並存放到page->private變量中,把page放入 swap cache,設置page的PG_swapcache和PG_dirty的flag,並更新swap_info_struct的頁槽信息,該函數是通往 swap core 和swap cache的接口函數,下面會分析
				{
                            …
						goto activate_locked; // 失敗後返回
				}
…
				/* Adding to swap updated mapping */
				mapping = page_mapping(page); // 根據page中的swp_entry_t獲取對應的swapper_spaces[type][offset],這裏可回顧一下數據結構章節中的swapper_spaces的介紹。
			}
		} else if (unlikely(PageTransHuge(page))) {
			/* Split file THP */
			if (split_huge_page_to_list(page, page_list))
				goto keep_locked;
		}

		/*
		 * The page is mapped into the page tables of one or more
		 * processes. Try to unmap it here.
		 */
		if (page_mapped(page)) {
			enum ttu_flags flags = ttu_flags | TTU_BATCH_FLUSH;

			if (unlikely(PageTransHuge(page)))
				flags |= TTU_SPLIT_HUGE_PMD;
			if (!try_to_unmap(page, flags, sc->target_vma)) { // unmap, 即與上層的虛擬地址解除映射關係,並修改pte,使其值等於 page->private,即swp_entry_t變量,等到swapin 時就直接把pte強制類型轉換成swp_entry_t 類型的值,就可以得到entry了。
				nr_unmap_fail++;
				goto activate_locked;
			}
		}

		if (PageDirty(page)) { //由於add_to_swap 函數最後把該頁面設置爲髒頁面,所以該if成立,進入if裏面
			…
						/*
			 * Page is dirty. Flush the TLB if a writable entry
			 * potentially exists to avoid CPU writes after IO
			 * starts and then write it out here.
			 */
			try_to_unmap_flush_dirty();
			switch (pageout(page, mapping, sc)) { // 發起 io 回寫請求,並把該page 的flag 設置爲PG_writeback,然後把PG_dirty清除掉
		    ……
			case PAGE_SUCCESS: //如果請求成功,返回 PAGE_SUCCESS
				if (PageWriteback(page))  //該條件成立,跳轉到 keep
					goto keep;
                   ……
			}
		}
……
keep:
		list_add(&page->lru, &ret_pages); //把該頁面放到 ret_pages鏈表裏,返回時會把該鏈表中的所有頁面都放回收lru 鏈表中,即不回收頁面
		VM_BUG_ON_PAGE(PageLRU(page) || PageUnevictable(page), page);
	}
……

	list_splice(&ret_pages, page_list);
……
	return nr_reclaimed;
}
接下來再看一下add_to_swap函數實現
int add_to_swap(struct page *page)
{
	swp_entry_t entry;
	int err;
     ……
	entry = get_swap_page(page); //爲該頁面分配一個swp_entry_t,並更新swap_info_struct的頁槽信息
	if (!entry.val)
		return 0;
    ……
	err = add_to_swap_cache(page, entry,
			__GFP_HIGH|__GFP_NOMEMALLOC|__GFP_NOWARN); //把頁面加入到swap cache 中,設置PG_swapcache,並把entry 保存到page->private變量中,跟隨page傳遞
	/* -ENOMEM radix-tree allocation failure */
	
	set_page_dirty(page); // 設置該頁面爲髒頁

	return 1;
     ……
}

再來看一下 get_swap_page 函數,分析一下 swp_entry_t 是如何分配出來的,頁槽信息怎麼更新。kernel 4.14的內核在實現該函數時因爲增加了槽緩存的機制,會比較複雜,其實即是預先分配好幾個swp_entry_t 緩存起來,需要時從緩存分配,不過我們抓住關鍵幾個步驟就可以了,先不考慮槽緩存

swp_entry_t get_swap_page(struct page *page)
{
	swp_entry_t entry, *pentry;
	struct swap_slots_cache *cache;

	entry.val = 0;
     ……
	get_swap_pages(1, false, &entry); // 1代表需要分配一個存儲塊,false 代表

	return entry;
}

直接跳到 get_swap_pages 函數,該函數的目的有兩個,一個是找出一個合適的 swap 分區,另一個是從 swap 分區中快速地找到合適的存儲塊,即 swap offset。

int get_swap_pages(int n_goal, bool cluster, swp_entry_t swp_entries[])
{
…

start_over:
	node = numa_node_id(); //獲取numa 節點號
	plist_for_each_entry_safe(si, next, &swap_avail_heads[node], avail_lists[node]) { //以優先級高到低遍歷所有swap分區
         ……
start:
		spin_lock(&si->lock);
		if (!si->highest_bit || !(si->flags & SWP_WRITEOK)) { //判斷該swap分區是否還有空閒頁槽,以及判斷該swap分區是否可寫的,SWP_WRITEOK這個flag是在swapon 的時候設置的,一般都帶這個flag
			spin_lock(&swap_avail_lock);
			if (plist_node_empty(&si->avail_lists[node])) {
				spin_unlock(&si->lock);
				goto nextsi;
			}
			WARN(!si->highest_bit,
			     "swap_info %d in list but !highest_bit\n",
			     si->type);
			WARN(!(si->flags & SWP_WRITEOK),
			     "swap_info %d in list but !SWP_WRITEOK\n",
			     si->type);
			__del_from_avail_list(si);  //如果沒有空閒頁槽,則直接從swap_avail_heads鏈表中刪除掉
			spin_unlock(&si->lock);
			goto nextsi; //跳到下一個swap分區
		}
		if (cluster) { // 這裏是false ,因此走else路徑,當內核的CONFIG_THP_SWAP的功能打開時,這裏纔是true
			if (!(si->flags & SWP_FILE))
				n_ret = swap_alloc_cluster(si, swp_entries);
		} else
			n_ret = scan_swap_map_slots(si, SWAP_HAS_CACHE,
						    n_goal, swp_entries);  //走到這裏,說明已經找到合適的swap分區,即第一個目的已經完成,接下來通過該函數去尋找合適的 offset,最終把兩者合併後返回swp_entries。
		……
nextsi:
		if (plist_node_empty(&next->avail_lists[node])) 
			goto start_over;
	}

	spin_unlock(&swap_avail_lock);

check_out:
	if (n_ret < n_goal)
		atomic_long_add((long)(n_goal - n_ret) * nr_pages,
				&nr_swap_pages);
noswap:
	return n_ret;
}

scan_swap_map_slots 比較複雜,其做法是通過 ssd算法 快速地查找到可用頁槽編號。

static int scan_swap_map_slots(struct swap_info_struct *si,
			       unsigned char usage, int nr,
			       swp_entry_t slots[])
{
     ……
	scan_base = offset = si->cluster_next; //獲取下一個可用存儲塊的編號,即空閒頁槽的編號

	/* SSD algorithm */
	if (si->cluster_info) {
		if (scan_swap_map_try_ssd_cluster(si, &offset, &scan_base)) //通過簇來輔助查找空閒頁槽的編號,並重新設置offset變量
			goto checks; //如果找到則跳到下面的 checks
		else
			goto scan; //如果找不到,則跳到scan,全局地遍歷 swap_map 看還有沒有空閒的頁槽
	}
     ……
checks:
	if (si->cluster_info) {  //這個if代碼段中主要的功能是確保 offset 所處屬的簇必須是空閒簇鏈表free_cluster的第一個簇
		while (scan_swap_map_ssd_cluster_conflict(si, offset)) {
		/* take a break if we already got some slots */
			if (n_ret)
				goto done;
			if (!scan_swap_map_try_ssd_cluster(si, &offset,
							&scan_base))
				goto scan;
		}
	}
	
	ci = lock_cluster(si, offset);   //獲取簇的spin_lock
	/* reuse swap entry of cache-only swap if not busy. */
	if (vm_swap_full() && si->swap_map[offset] == SWAP_HAS_CACHE) { //如果該swap分區已經滿了,則嘗試回收部分頁槽
		int swap_was_freed;
		unlock_cluster(ci);
		spin_unlock(&si->lock);
		swap_was_freed = __try_to_reclaim_swap(si, offset); //回收頁槽
		spin_lock(&si->lock);
		/* entry was freed successfully, try to use this again */
		if (swap_was_freed)
			goto checks;  //如果回收到了頁槽,則返回check 繼續分配頁槽
		goto scan; /* check next one */
	}
     ……
	si->swap_map[offset] = usage;  //更新頁槽的第6位,即SWAP_HAS_CACHE
	inc_cluster_info_page(si, si->cluster_info, offset);  //把該頁槽對應的簇從free_clusters的鏈表中取出來,並更新其data字段,即爲1
	unlock_cluster(ci);  //解鎖 spin_lock

	swap_range_alloc(si, offset, 1);  //更新swap_info_struct中的inuse_pages 變量,即加1
	si->cluster_next = offset + 1;  //更新下一個可用頁槽的編號,即往後移一位
	slots[n_ret++] = swp_entry(si->type, offset);  //把 swap分區的type 和頁槽篇號offset合併成swp_entry_t保存在slots[]中,返回給該函數調用者

	/* got enough slots or reach max slots? */
	if ((n_ret == nr) || (offset >= si->highest_bit))
		goto done;  //slot[] 填滿後直接返回,這裏在不考慮頁槽緩存的情況下,slot[]數組中只需要一個swp_entry_t就可以了
……

done:
	si->flags -= SWP_SCANNING;
	return n_ret;

scan:
	spin_unlock(&si->lock);
	while (++offset <= si->highest_bit) {  //從當前的頁槽號往後查找
		if (!si->swap_map[offset]) {  //頁槽值爲0,代表該頁槽是空閒狀態
			spin_lock(&si->lock);
			goto checks;   //找到後跳到 checks 更新頁槽信息後直接返回
		}
		if (vm_swap_full() && si->swap_map[offset] == SWAP_HAS_CACHE) {
			spin_lock(&si->lock);
			goto checks;  //如果swap 分區滿了,則跳回checks 回收頁槽
		}
          ……
	}
	offset = si->lowest_bit;
	while (offset < scan_base) {  //從當前的頁槽號往前查找,下面的邏輯與上面的while 邏輯一樣
		if (!si->swap_map[offset]) {
			spin_lock(&si->lock);
			goto checks;
		}
		if (vm_swap_full() && si->swap_map[offset] == SWAP_HAS_CACHE) {
			spin_lock(&si->lock);
			goto checks;
		}
		if (unlikely(--latency_ration < 0)) {
			cond_resched();
			latency_ration = LATENCY_LIMIT;
		}
		offset++;
	}
	spin_lock(&si->lock);
    // 兩個while循環已經代表了全局地掃描了swap_map數組,如果程序走到這,說明該swap分區裏已經沒有了可用頁槽
no_page:
	si->flags -= SWP_SCANNING;
	return n_ret;
}

我們再來看一下ssd算法的核心函數 scan_swap_map_try_ssd_cluster,先來看一下它的參數

  • struct swap_info_struct *si
    指向swap分區描述符
  • unsigned long *offset
    ssd算法查找到的空閒頁槽編號
  • unsigned long *scan_base
    ssd算法查找到的空閒頁槽編號

函數的返回值是個bool類型,true代表已經找到空閒頁槽編號,false 代表沒有找到。

static bool scan_swap_map_try_ssd_cluster(struct swap_info_struct *si,
	unsigned long *offset, unsigned long *scan_base)
{
    ……
new_cluster:
	cluster = this_cpu_ptr(si->percpu_cluster);
	if (cluster_is_null(&cluster->index)) {  //這個if代碼段主要是從free_clusters鏈表中獲取第一個空閒簇,並賦值到per_cpu變量si->percpu_cluster,之後會對這個空閒簇進行可用頁槽的查找
		if (!cluster_list_empty(&si->free_clusters)) {
			cluster->index = si->free_clusters.head;
			cluster->next = cluster_next(&cluster->index) *
					SWAPFILE_CLUSTER;
		} else if (!cluster_list_empty(&si->discard_clusters)) {
			/*
			 * we don't have free cluster but have some clusters in
			 * discarding, do discard now and reclaim them
			 */
			swap_do_scheduled_discard(si);
			*scan_base = *offset = si->cluster_next;
			goto new_cluster;
		} else
			return false;
	}

	found_free = false;

	/*
	 * Other CPUs can use our cluster if they can't find a free cluster,
	 * check if there is still free entry in the cluster
	 */
	tmp = cluster->next;  //獲取當前可分配的簇
	max = min_t(unsigned long, si->max,
		    (cluster_next(&cluster->index) + 1) * SWAPFILE_CLUSTER);  //獲取該簇的最大頁槽索引
	if (tmp >= max) {
		cluster_set_null(&cluster->index);
		goto new_cluster;
	}
	ci = lock_cluster(si, tmp);  //加鎖,保護該簇對應的256個頁槽
	while (tmp < max) {
		if (!si->swap_map[tmp]) {
			found_free = true;  //找到一個空閒的頁槽,跳出循環
			break;
		}
		tmp++;
	}
	unlock_cluster(ci);  //解鎖
	if (!found_free) {
		cluster_set_null(&cluster->index);
		goto new_cluster;  //如果仍然沒找到頁槽,則返回new_cluster,從free_clusters鏈表中取出新的空閒簇再進行查找
	}
	cluster->next = tmp + 1;  //更新下一個可分配的簇
	*offset = tmp;  //把已經找到的頁槽更新到offset 變量
	*scan_base = tmp; //把已經找到的頁槽更新到scan_base 變量
	return found_free;
}

到此,基本的ssd算法已經分析完畢,讀者可能會感覺到有些吃力,但只要抓住減少 spin_lock 競爭這一條主要目的去閱讀這部分的代碼,是比較好理解的。後面內核版本中還有一些針對提高 swap 頁槽查找以及分配效率的修改,有興趣的讀可以看一下附錄文章1 ,相信可以加深對本文的理解。
以上是第一次 shrink 的流程,至此,page 只是發起了 io 回寫申請,然後就直接返回到 lru 鏈表中,並沒有真正地回收頁面。
在第一次 shrink 流程執行完後,其 pageflag 狀態如下

PG_writeback PG_swapcache PG_dirty PG_active
1 1 0 0

真正的回收頁面發生在第二次shrink到該page時,即io回寫完成之後,我們先來看一下 bio->bi_end_io 回調函數,即io回寫完成後的回調函數,請注意pageflag狀態變換

void end_swap_bio_write(struct bio *bio)
{
	struct page *page = bio->bi_io_vec[0].bv_page;  //從bio中獲取page,即發起io回寫的page

	if (bio->bi_status) {  //bio發生錯誤會進入該if代碼段,在些分析中省略
		…
	}
	end_page_writeback(page);  //核心函數
	bio_put(bio);
}

再來看 end_page_writeback 函數

void end_page_writeback(struct page *page)
{
	/*
	 * TestClearPageReclaim could be used here but it is an atomic
	 * operation and overkill in this particular case. Failing to
	 * shuffle a page marked for immediate reclaim is too mild to
	 * justify taking an atomic operation penalty at the end of
	 * ever page writeback.
	 */
	if (PageReclaim(page)) {
		ClearPageReclaim(page);  //清除PG_reclaim標記位
		rotate_reclaimable_page(page);
	}

	if (!test_clear_page_writeback(page))  //清除PG_writeback標記位
		BUG();

	smp_mb__after_atomic();
	wake_up_page(page, PG_writeback);  //喚醒正在等該page的PG_writeback標記位的進程
}

所以,當page的io回寫完成後,其PG_writeback和PG_reclaim兩個標記位會被清除掉,那麼當page 第二次shrink時,其flag狀態應該就是如下表所示

PG_writeback PG_swapcache PG_dirty PG_active
0 1 0 0

我們再來看一下第二次shrink的流程,其實可以跳過很多if判斷,直接進入到回收流程

static unsigned long shrink_page_list(struct list_head *page_list,
				      struct pglist_data *pgdat,
				      struct scan_control *sc,
				      enum ttu_flags ttu_flags,
				      struct reclaim_stat *stat,
				      bool force_reclaim)
{
	……
	while (!list_empty(page_list)) {
	     ……
		if (PageWriteback(page)) {  //跳過該判斷
			……
		}

		
		if (PageAnon(page) && PageSwapBacked(page)) { 
			if (!PageSwapCache(page)) {   //跳過該判斷
				
			}
		} 

		if (page_mapped(page)) {  //由於在第一次shrink的時候unmap了,所以此時page_mapped等於0,跳過該判斷
             ……
		}

		if (PageDirty(page)) {  //跳過該判斷
			……
		}
		……
		if (PageAnon(page) && !PageSwapBacked(page)) {   //非lazyfree的頁面,走else
	        ……
		} else if (!mapping || !__remove_mapping(mapping, page, true)) //這裏進入到__remove_mapping中,把page從swapcache 中移除掉,並清除頁槽的第6位,即SWAP_HAS_CACHE
			goto keep_locked;
		……
free_it:
    ……
		list_add(&page->lru, &free_pages);  //把該page加入到free_pages 鏈表中等待回收
    ……  //接下來的事情就是回收free_pages鏈表中的所有page,
	return nr_reclaimed;
}

以上是整個swapout的流程,這裏總結一下,第一次去shrink匿名頁面時,會先爲page裏的數據分配一塊swap分區的存儲塊,然後發起一個io寫請求,請求把page的數據寫入到這塊存儲塊中,同時把該page放入swap cache 中,接着返回,並等待io寫操作完成,第二次去shrink該page時,從swap cache 中刪除該page,並回收它。

Swapin

swapin 的入口是do_swap_page,由於物理頁面被回收了,所以進程再次訪問一塊虛擬地址時,就會產生缺頁中斷,最終進入到 do_swap_page,在這個函數中會重新分配新的頁面,然後再從swap分區讀回這塊虛擬地址對應的數據。具體請看以下代碼分析

int do_swap_page(struct vm_fault *vmf)
{
	……
	entry = pte_to_swp_entry(vmf->orig_pte);  //從pte中獲取swap entry,即把orig_pte 強制類型轉換成swp_entry_t類型
    ……
	page = lookup_swap_cache(entry, vma, vmf->address);  //在 swap cache 中查找entry 對應的page
	swapcache = page;

	if (!page) {  //如果在swap cache中沒找到,則進入if代碼段
		struct swap_info_struct *si = swp_swap_info(entry);  //獲取swap分區描述符

          ……
			page = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE, vmf);  //分配一個page,並從swap分區中讀出數據填充到page中,再把page放入swap cache中緩存,此時page的PG_lock被置位了,需要等待IO讀操作完成才清零,即page被lock住,如果別人想lock該page,則需要等待該page被unlock
			swapcache = page;
		……
	}

	locked = lock_page_or_retry(page, vma->vm_mm, vmf->flags);  //此時嘗試去lock該page,成功則返回1,失敗則返回0
    ….
	if (!locked) {  //顯然此時返回是0,即page的IO讀操作仍末完成。
		ret |= VM_FAULT_RETRY;  //設置返回標記爲retry
		goto out_release; //返回重新嘗試 do_swap_page,但在重新嘗試do_swap_page時則可以從page cache 中直接獲取到該page,不需要再從swap分區中讀數據了
	}
     //程序走到這表明該page的IO讀操作已經完成

	……
	pte = mk_pte(page, vmf->vma_page_prot);  //根據page的物理地址,以及該page的保護位生成pte
	if ((vmf->flags & FAULT_FLAG_WRITE) && reuse_swap_page(page, NULL)) {  //如果該缺頁中斷爲寫訪問異常時,並且page只有一個進程使用,則把該page從swap cache 中刪除,並清除對應在swap分區中的數據,下面會分析reuse_swap_page函數
		pte = maybe_mkwrite(pte_mkdirty(pte), vmf->vma_flags);  //設置pte中的可寫保護位和PTE_DIRTY位
		vmf->flags &= ~FAULT_FLAG_WRITE;
		ret |= VM_FAULT_WRITE;
		exclusive = RMAP_EXCLUSIVE;
	}
	……
	set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);  更新該虛擬地址對應的pte
	……
		do_page_add_anon_rmap(page, vma, vmf->address, exclusive);  //建立新的匿名映射
		mem_cgroup_commit_charge(page, memcg, true, false);
		activate_page(page);  //把該page放入active anonymouns lru鏈表中
	……

	swap_free(entry);  //更新頁槽的counter,即減一,如果counter等於0,說明需要該存儲塊數據的人已經全部讀回到內存,並且該page也不在swap cache 中,那麼直接清除存儲塊數據,即回收頁槽,釋放更多的swap空間
	if (mem_cgroup_swap_full(page) ||
	    (vmf->vma_flags & VM_LOCKED) || PageMlocked(page))
		try_to_free_swap(page);  //如果swap分區滿了,則嘗試回收無用頁槽
	unlock_page(page);  //解鎖 PG_lock
	……
out:
	return ret;
   ……
}

接下來分析一下兩個核心函數 swapin_readaheadreuse_swap_page
swapin_readahead 最終會進入swap_cluster_readahead函數或者swap_vma_readahead函數,兩者的區別在於預讀的方式,前者是預讀 page 對應的物理地址前後範圍的數據,後者是預讀虛擬地址前後範圍的數據,其實現上相差不大,所以我們先來分析 swap_cluster_readahead,至於另一個函數有興趣的讀者可參考此分析自行查閱代碼。

struct page *swap_cluster_readahead(swp_entry_t entry, gfp_t gfp_mask,
				struct vm_fault *vmf)
{
	……
	mask = swapin_nr_pages(offset) - 1;  // 獲取需要預讀的頁槽個數
	if (!mask) 
		goto skip;  //如果不需要預讀,則只讀當前entry指向的頁槽,跳到skip

	do_poll = false;
	/* Read a page_cluster sized and aligned cluster around offset. */
	start_offset = offset & ~mask;  //得到預讀開始的頁槽
	end_offset = offset | mask;  //得到預讀結束的頁槽
     ……
	for (offset = start_offset; offset <= end_offset ; offset++) { 從開始的頁槽一個個地預讀
		/* Ok, do the async read-ahead now */
		page = __read_swap_cache_async(
			swp_entry(swp_type(entry), offset),
			gfp_mask, vma, addr, &page_allocated);  //分配新的page,並放入swap cache,設置PG_swapcache,把entry 保存到page->private變量中,跟隨page傳遞,置位頁槽的第6位,即SWAP_HAS_CACHE
……
		if (page_allocated) { //該變量爲true,代表page cache中沒有頁槽對應的page,是重新分配的page,則需要把頁槽所指向的swap分區中的數據讀出來放到page中。
			swap_readpage(page, false);  //發起io讀請求,該實現與swapout時調用的swap_writepage類似,這裏不再分析,這裏再提醒一下,等待io讀操作完成後該page的PG_lock纔會被清零,具體的清零操作是在bio的回調函數end_swap_bio_read中完成。讀者可回憶一下io寫操作完成後清零page的PG_writeback位。
			……
		}
		put_page(page);
	}
     ……
skip:
	return read_swap_cache_async(entry, gfp_mask, vma, addr, do_poll);  //這裏只讀一個頁槽的數據,其實現與上面預讀多個頁槽的邏輯基本一樣,這裏不再分析
}

__read_swap_cache_async 這個函數主要完成以下幾個工作

  1. 再次從 swap cache 查找頁槽對應的 page,若找到,則直接返回
  2. 若仍沒有找到,則分配一個新的 page,並設置頁槽第6位,即 SWAP_HAS_CACHE
  3. 設置 pagePG_lockPG_swapback,並把page加入到 swap cache
  4. page存入 inactive anonymouns lru 鏈表中
  5. 返回page的指針
struct page *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
			struct vm_area_struct *vma, unsigned long addr,
			bool *new_page_allocated)
{
	do {
		found_page = find_get_page(swapper_space, swp_offset(entry));  //第一步
		if (found_page)
			break;
         ……
		if (!new_page) {
			new_page = alloc_page_vma(gfp_mask, vma, addr);   //第二步中分配新的page
			if (!new_page)
				break;		/* Out of memory */
		}

		……
		err = swapcache_prepare(entry);  //第二步中的設置頁槽第6位
		……
		/* May fail (-ENOMEM) if radix-tree node allocation failed. */
		__SetPageLocked(new_page);   //第三步
		__SetPageSwapBacked(new_page);  //第三步
		err = __add_to_swap_cache(new_page, entry);  //第三步中的加入page到swap cache中
		if (likely(!err)) {
              ……
			lru_cache_add_anon(new_page);  //第四步
			*new_page_allocated = true;
			return new_page;  //第5步,返回page指針
		}
		……
	} while (err != -ENOMEM);

	if (new_page)
		put_page(new_page);
	return found_page;
}

接下來看另一個核心函數 reuse_swap_page,該函數的作用是嘗試把只有一個進程使用的頁槽回收掉。

bool reuse_swap_page(struct page *page, int *total_map_swapcount)
{
	……
	count = page_trans_huge_map_swapcount(page, &total_mapcount, &total_swapcount); //獲取所有使用該paga的進程數量,返回count爲總數量,total_mapcount爲已經與虛擬地址map 的進程數量,total_swapcount 爲還沒與虛擬地址map 的數量
	if (total_map_swapcount)
		*total_map_swapcount = total_mapcount + total_swapcount;  //更新形參total_map_swapcount
	if (count == 1 && PageSwapCache(page) &&
	    (likely(!PageTransCompound(page)) ||
	     /* The remaining swap count will be freed soon */
	     total_swapcount == page_swapcount(page))) {  //這裏的判斷有三個,分別是使用該page的進程總數爲1,即只有一個進程使用; 該page是在swap cache 中; 當前進程的虛擬地址仍沒有與該page建立映射,即仍未map。當這三個條件都滿足時則認爲該頁槽指向的存儲塊數據再也不需要了,可以清除掉。
		if (!PageWriteback(page)) {  //該page是否正在回寫磁盤,如果已經回寫完成則回收頁槽,否則走else,返回false,表明頁槽沒回收成功。
			page = compound_head(page); 
			delete_from_swap_cache(page);  //從swap cache中刪除,並回收頁槽
			SetPageDirty(page);  //把page的PG_dirty位置1
		} else {
			swp_entry_t entry;
			struct swap_info_struct *p;

			entry.val = page_private(page);
			p = swap_info_get(entry);
			if (p->flags & SWP_STABLE_WRITES) {
				spin_unlock(&p->lock);
				return false;
			}
			spin_unlock(&p->lock);
		}
	}

	return count <= 1;
}

delete_from_swap_cache 這個函數其實有兩部分組成,__delete_from_swap_cache和put_swap_page,__delete_from_swap_cache函數是從swap cache 中刪除一個page的核心函數,put_swap_page功能是把頁槽所指向的存儲塊數據清除。

附錄


  1. https://lwn.net/Articles/704478/ ↩︎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章