轉自:http://tinylab.org/linux-swap-and-zram/
By ZhizhouTian of TinyLab.org 2016-12-23 18:04:30
1 簡介
Zram Swap 是 Linux 內核中采用時間換空間的一種技術。它通過壓縮內存(Zram)來作為交換分區,通過壓縮比來獲取更多可利用的內存空間。該技術目前在各類內存受限的嵌入式系統中,尤其是 Android 手機、電視等設備上廣泛采用,本文對此進行了詳細介紹。
為了更好地理解,首先我們介紹了內存管理基本概念,內存回收以及內存交換技術。
2 內存管理基本概念
2.1 內存管理區 struct zone
struct zone
表示一個內存管理區,用於跟蹤page用量、空閑區域、鎖等統計信息,內部含有page_high
、page_low
、page_min
三個水位線。
當小於page_low
時喚醒swapd進行回收,到達page_high
時停止。但即使到達page_min
仍然可以使用GFP_ATOMIC
分配。此時,分配器會同步調度swap,即直接回收路徑。 可以向/proc/sys/vm/min_free_kbytes
寫入以更改page_min
(以kbyte為單位)。
內存管理區共有三個:dma zone
、normal zone
及high zone
,由於 ARM 架構下,normal zone
也可以用於 DMA 操作,因此dma zone
大小為0。
normal zone
管理 1024-128 = 896M
以下的內存,high zone管理896M以上的內存。注意,ARM32 架構下,Linux-v3.2之后的版本中,對此進行了修改,請參考mail list。在這一提交中,將屬於內核的1G空間的最后264M划作三個部分:第一個8M用於隔離,第二部分的240M用於vmalloc,最后16M用於連續DMA。也就是說normal zone位於1024-264=760M
之下了(同時也是high_memory
的值)。
2.2 PFN(page frame number)
PFN是在系統初始化時,以4K為單位對所有可用內存進行編號,從0x8000_0000/PAGE_SHIFT
開始。內存管理區的(start, end)使用的就是PFN的值。
2.3 頁 struct page
該結構體用於表示一個頁框,在 ARM32 架構中,可以通過mem_map
數組與PFN進行對應。該結構體中與頁框回收比較相關的成員包括:
-
unsigned long flags
PG_active
、PG_referenced
用於表示當前頁的活躍狀態,並決定是否回收PG_unevictable
表示當前頁不可以回收PG_mlocked
表示當前頁被系統調用mlock()
鎖定了,禁止換出和釋放PG_lru
表示當前頁處於lru鏈表中PG_swapcache
表示當前頁正在被換出/換入PG_private
及PG_private_2
分別用來表示一個zspage的第一個頁和最后一個頁
-
struct
address_space
mapping末位為0時,跟蹤當前頁映射的文件;為1時,指向anon_vma(包含了1至多個vma)
-
struct list_head lru
用於將當前頁加入到某個lru的list
-
在Zram中的重新定義
許多page的屬性在zram中另有定義。比如lru用於鏈接zspage所屬的所有頁。詳細見zram的介紹。
2.4 PTE 與 VMA
-
Page Table Entry表示進程的Page Table中的一項,即最低級的頁表項,一般為4K大小。
在Linux中定義為
pte_t
(實際上就是一個unsigned 32
)。當頁駐留在內存中時,則PTE的低12位中包含PFN,且最低位(present位)為1;當頁在交換分區中的頁槽中時,則PTE的低12位中包含頁槽的地址,且最低位為0,這種情況下對這個頁訪問,硬件會rise一個缺頁中斷。 -
每個進程的虛擬內存空間都被分為許多的虛擬內存區域(VMAs),VMA是頁對齊的一段連續虛擬內存,且這些內存地址都擁有相同的訪問權限。比如,進程中,有code、data、stack、heap等VMA。
-
VMA與PTE的關系:每個VMA的虛擬地址被划分為多個頁,這些頁可以通過2級映射(ARM32 )或者3級映射(ARM64),通過缺頁中斷,內核將這些頁映射到真實的物理頁上,對應page table中的一個pte,所以每個VMA對應0至多個PTE。
3 內存回收
當系統內存緊張時即會進行內存回收。回收的辦法,或者是對文件頁進行寫回,或者是對匿名頁進行交換。
3.1 File Cache 與 Anon Page
可以被回收的頁可划分為兩種:
-
文件頁(file cache)
其特征是與外部存儲設備上的某個文件相對應,有外部的后援設備(backend),而
page->mapping
末位為0。例如用戶控件進程對某個磁盤上的文件使用mmap系統調用時分配的頁。在內存回收時,被寫過的文件頁(臟文件頁)將被寫回以保存起來。寫回之后的頁將被釋放。而沒有被寫過的頁,比如進程代碼段的頁,他們是只讀的,直接釋放就可以了 -
匿名頁(anonymous cache).
其特征是,內容不來自於外部存儲設備,
page->mapping
末位為1,例如為用戶進程進程中的malloc系統調用分配的頁即屬於匿名頁。在內存回收時,匿名頁將會被交換到交換區而保存起來。交換之后頁將被釋放。
除了一些特殊的頁面分配方法(比如在映射時即進行頁面分配,以提高性能)之外,大多用戶進程的頁(無論是文件頁還是匿名頁)都是通過page fault進行分配的。這些屬於用戶進程的頁中,除了PG_unevictable
修飾(不可回收)的頁面都是可以進行回收的(關於這個部分的介紹請見這里,比如ramfs所屬頁、mlock()
的頁等)。當頁面通過page fault被分配的時候,file page cache 被加入到非活動鏈表中(inactive list), 匿名頁(anonymous page)被加入到活動鏈表中(active list)。
3.2 LRU 算法
在內存回收時,系統會對頁加以選擇:如果選擇經常被用到的頁,即便回收了,馬上又要被用到,這樣不僅不能降低內存緊張的情形,反而會增加系統的負擔。所以應當選擇不太常用的頁(或最近沒有被用到的頁)來回收。采用的主要算法就是LRU算法。
Linux為了實現該算法,給每個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尾部的頁,將在內存回收時優先被回收(寫回或者交換)。
3.2.1 lru 緩存
每個zone都有一套lru鏈表,而zone使用一個spinlock對於LRU鏈表的訪問進行保護。在SMP系統上,各個CPU頻繁的訪問和更新lru鏈表將造成大量的競爭。因此,針對lru的四種操作,每個CPU都有四個percpu的struct page*
數組,將進行需要進行相應操作的頁先緩存在這個數組中。當數組滿或者內存回收時,則將數組中的頁更新到相應的lru上。
舉個例子:當CPU0從normal zone上分配了一個頁之后,即將這個頁放到操作1的數組中。當數組滿了之后,則將數組中的頁逐個的加入到對應的zone所屬的inactive page list或者inactive anon list(根據page可以得到對應的zone信息和該page是一個文件頁還是匿名頁的信息。新頁首次加入lru鏈表,默認狀態為inactive)。
3.2.2 lru list 的更新
除非進行頁面回收,否則內存頁在掛到lru list上之后是不移動的。對於匿名頁和文件頁,lru有着不同的更新策略:
-
對於匿名頁鏈表的更新
系統要求inactive anon lru的總量不能低於某個值。該值是一個經驗值。對於1G的系統,要求inactive anon lru上掛的內存頁總量不低於250M。當內存回收啟動時發現inactive anon不足時,則從active anon lru list尾部拿一些page(一般為32個),將他們的PTE中的ACCESSD標記清0(每次訪問這個頁面,硬件會將該位置1),放在inactive list的鏈表頭。然后遍歷inactive lru的鏈表尾部,如果此時ACCESSD的標記為1(說明最近被訪問過),則重新放到active list中,否則將交換出去。
-
對於文件頁鏈表的更新
系統要求inactive file-cache lru上頁的總量不低於active file-cache lru上頁的總量即可。當內存回收啟動時發現不滿足上述情況,則從active file-cache lru鏈表的尾部拿一些page,清空ACCESSED標記,保持
PG_referenced
,放到inactive的頭部。然后掃描inactive的尾部,並進行以下處理:如果是
PG_referenced
為1,清0,不管ACCESSSED標記是什么,都放到active file-cache lru的頭。如果映射了該文件頁的進程的PTE中有ACCESSED標記為1,則放到active file-cache lru的頭。否則,則寫回並釋放。
那么,為什么相對匿名頁,文件頁會使用一個PG_referenced
呢?這是因為一個文件頁,常常是被多個進程映射的。對這個標記的設置,在read/write等系統調用中。
3.3 文件頁與匿名頁的回收比例
內存回收時,會按照一定的比例對匿名頁與文件緩存的回收,而swapiness就是這個比值。可以通過/proc/sys/vm/swappiness
進行設置。當這個值為0時,則表示僅以釋放文件頁來回收內存,設置為100的時候,則一半來自文件頁,一半來自匿名頁。
3.4 調用內存回收接口的兩條路徑
有兩條路徑會調用內存回收接口:
- 當系統分不出內存時,在內存分配函數中同步調用內存回收接口,稱為同步回收
- 內核線程kswapd會在zone的水位下降到
page_low
時醒來並調用內存回收接口,稱為異步回收
是時候祭出這張圖了:
兩條路徑都會調用shrink_zones(),而該函數會對每個zone的inactive lru list進行回收。 對於文件頁的處理,這里不作討論。接下來討論對匿名頁的處理,即交換。
4 匿名頁的內存回收 - 交換
交換用來為匿名頁提供備份,可以分為三類:
- 屬於進程匿名線性區(如用戶態堆棧、堆)的頁
- 屬於進程私有內存映射的臟頁
- 屬於IPC共享內存的頁
就像請求調頁,交換對於程序必須是透明的。即不需要在程序中嵌入交換相關的特別指令。每個pte都包含一個present位,內核利用這個標志來通知屬於某個進程地址空間的頁已經被換出。在這個標志之外,Linux還利用pte中的其他位存放頁標識符(swapped-out page identifier)。 該標識符用於編碼換出頁在磁盤中的位置。當缺頁異常發生時,相應的異常處理程序可以檢測到該頁不在Ram中,然后換入頁。
交換子系統主要功能為:
- 在磁盤上建立交換區
- 管理交換區空間,分配與釋放頁槽
- 利用已被換出的頁的pte的換出頁標識符追蹤數據在交換區中的位置
- 提供函數從ram中把頁換出到交換區或換入到ram
交換可以用來擴展內存地址空間,使之被用戶態進程有效的使用。一個系統上運行的應用所需要的內存總量可能會超出系統中當前的物理內存總量,其原理就是將暫時不用的內存交換出去,待用到的時候再交換進來。
4.1 交換區的數據結構
從內存中換出的頁存放在交換區(swap area)中。交換區可架設在磁盤分區、大文件甚至內存型文件系統中。同時可以存在MAX_SWAPFILES
(32左右)個不同類型的交換區,而並發操作的交換區可以提高性能。
每個交換區都由一組頁槽(page slot)組成,每個頁槽大小一頁。交換區的第一個頁槽永久存放有關交換區的信息:
union swap_header {
struct {
char reserved[PAGE_SIZE - 10];
char magic[10]; /* SWAP-SPACE or SWAPSPACE2,用於標記分區或文件為交換區 */
} magic;
struct {
char bootbits[1024]; /* Space for disklabel etc.包含分區數據、磁盤標簽等 */
__u32 version; /* 交換算法的版本 */
__u32 last_page; /* 可有效使用的最后一個槽 */
__u32 nr_badpages;/* 有缺陷的頁槽的個數 */
unsigned char sws_uuid[16];
unsigned char sws_volume[16];
__u32 padding[117];/* 用於填充的字節 */
__u32 badpages[1]; /* 用來指定有缺陷的頁槽的位置 */
} info;
};
4.2 創建與激活交換區
通過mkswap
可以將某個分區設置成交換區,初始化 union swap_header
,檢查所有頁槽並確定有缺陷頁槽的位置。交換區由交換子區組成,子區由頁槽組成,由swap_extent
來表示,包含頁首索引、子區頁數及起始磁盤扇區號。當激活交換區時,組成交換區的所有子區的鏈表將創建。存放在磁盤分區中的交換區只有一個子區,但是存放在文件中的交換區可能有多個子區,這是因為文件系統可能沒有把該文件全部分配在磁盤的一組連續塊中。
4.3 交換區優先級
同時存在有多個交換區時,快速交換區(存放在快速磁盤中的交換區)可以獲得高優先級。查找頁槽時從優先級最高的交換區開始搜索。如果優先級相同,則循環使用以平衡負載。
4.4 交換區描述符
每個活動的交換區都有自己的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_list; /* entry in swap_avail_head */
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_info free_cluster_head; /* free cluster list head */
struct swap_cluster_info free_cluster_tail; /* free cluster list tail */
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 */
unsigned long *frontswap_map; /* frontswap in-use, one bit per page */
atomic_t frontswap_pages; /* frontswap pages in-use counter */
spinlock_t lock; /*
* protect map scan related fields like
* swap_map, lowest_bit, highest_bit,
* inuse_pages, cluster_next,
* cluster_nr, lowest_alloc,
* highest_alloc, free/discard cluster
* list. other fields are only changed
* at swapon/swapoff, so are protected
* by swap_lock. changing flags need
* hold this lock and swap_lock. If
* both locks need hold, hold swap_lock
* first.
*/
struct work_struct discard_work; /* discard worker */
struct swap_cluster_info discard_cluster_head; /* list head of discard clusters */
struct swap_cluster_info discard_cluster_tail; /* list tail of discard clusters */
};
** flags
字段**:包含的位的含義為:
enum {
SWP_USED = (1 << 0), /* is slot in swap_info[] used?指示該交換區是否是活動的 */
SWP_WRITEOK = (1 << 1), /* ok to write to this swap?是否可以寫入,只讀為0 */
SWP_DISCARDABLE = (1 << 2), /* blkdev support discard */
SWP_DISCARDING = (1 << 3), /* now discarding a free cluster */
SWP_SOLIDSTATE = (1 << 4), /* blkdev seeks are cheap */
SWP_CONTINUED = (1 << 5), /* swap_map has count continuation */
SWP_BLKDEV = (1 << 6), /* its a block device */
SWP_FILE = (1 << 7), /* set after swap_activate success */
SWP_AREA_DISCARD = (1 << 8), /* single-time swap area discards */
SWP_PAGE_DISCARD = (1 << 9), /* freed swap page-cluster discards */
/* add others here before... */
SWP_SCANNING = (1 << 10), /* refcount in scan_swap_map */
};
swap_map
字段:指向一個計數器數組,交換區的每個頁槽對應一個元素。如果計數器值等於0,那么頁槽就是空閑的,如果是正數,表示共享該換出頁的進程數;如果計數器值為SWAP_MAP_MAX
,那么存放這個頁槽的頁就是永久的,不能從相應的頁槽中刪除;如果計數器值為SWAP_MAP_BAD
,那么這個頁槽就是有缺陷的,不可以使用。
#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,表示頁被緩存了? */
由於一個頁可以屬於幾個進程的地址空間,所以它可能從一個進程的地址空間被換出,但仍然保留在ram中。因此可能把同一個頁換出多次。一個頁在物理上僅被換出並存儲一次,但是后來每次換出該頁都會增加swap_map計數(同時_mapcount
會減小嗎?)。其實現邏輯為swap_duplicate()
:
-
使用
swap_type(swap_entry_t)
提取所在分區及offset,通過swap_info[]
獲得struct swap_info_struct *
,通過struct swap_info_struct->swap_map[]
獲得頁槽計數值。 -
使用
swap_count(unsigned char)
來查看頁槽計數值是不是SWAP_MAP_BAD
-
增加頁槽計數值
- 如果參數為
SWAP_HAS_CACHE
,則是原有值加上它(表示當前頁被緩存了?) - 如果參數為1,則判斷是否超出
SWAP_MAP_MAX
,不超出則增加1 - 如果計數值包含COUNT_CONTINUED,則可能是用來處理vmalloc page的?
- 如果參數為
需要注意的是,首次分配頁槽時(也就是get_swap_page
調用scan_swap_map
),會將SWAP_HAS_CACHE
傳遞給頁槽計數
lowest_bit
字段:第一個空閑頁槽
highest_bit
字段:最后一個空閑頁槽
cluster_next
字段:存放下一次分配時要檢查的第一個頁槽的索引
cluster_nr
字段:存放已經分配的空閑頁槽數
swap_info
:struct swap_info_struct swap_info[]
表示所有的交換區,數組長度MAX_SWAPFILES
,用nr_swapfiles - 1
來表示數組中最后一個已經激活的交換區的index。
4.5 換出頁標識符
pte共有三種狀態:
- 當頁不屬於進程的地址空間(進程頁表下),或者頁框還沒有分配給進程時,此時是空項
- 最后一位為0,表示該頁被換出。此時pte表示為換出頁標識符。
- 最后一位為1,頁在ram中。
在換出狀態下,pte被稱為swap_entry_t(換出頁標識符):typedef struct { unsigned long val; } swp_entry_t;
該標識符由三個部分充滿一個long:最高5bit表示來自哪個swap分區,2bit表示是否來自於shmem/tempfs,24bit表示在頁槽中的offset,交換區最多有2^24個頁槽(64GB)。最后一位為0表示該頁已經換出。
4.6 激活與禁止交換區
需要注意的是,交換分區的大小設置必須在交換分區尚未激活的狀態下。4.6.1 swapon
swapon
:激活交換分區(以/dev/zram0為例)。
函數原型:SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
在使用gdb調試時,需要斷點 sys_swapon
才能斷點到該函數。下面是該函數的具體邏輯
-
struct swap_info_struct *p = alloc_swap_info()
分配一個si -
struct file *swap_file = file_open_name("/dev/zram0");
打開設備節點,獲得文件描述符。之前一直以為在kernel里不能打開文件,這里看來並非如此。 -
struct inode *inode = swap_file->f_mapping->host;
通過f_mapping
可以獲得struct file
的struct address_space
,再通過->host
來獲得所屬於的inode
。 claim_swapfile(p, inode)
:聲明/dev/zram0被p獨占- 通過
S_ISBLK
來判斷當前inode是否是塊設備 - 通過
blkdev_get(p->bdev, O_EXCL, p)
來以獨占模式打開p->bdev
,並說明獨占者為p - 通過
set_blocksize(p->bdev, PAGE_SIZE)
將設備的塊大小設置為一頁
- 通過
- 操作
swap_header
page = read_mapping_page()
,讀取swap的第一個頁;union swap_header *swap_header = kmap(page);
獲得swap的頭unsigned long maxpages = read_swap_header(p, swap_header)
返回可以分配的總頁數。取決於兩點,swap_entry_t
中swap offset的bit數,以及pte在不同架構下的長度。
-
unsinged char *swap_map = vzalloc(maxpages)
,調用vzalloc來對每一個頁槽分配計數值 -
p->cluster_next = prandom_u32() % p->highest_bit
,將cluster_next設置為一個隨機頁槽位置 -
cluster_info = vzalloc(maxpages/SWAP_CLUSTER*sizeof(*cluster_info))
:以SWAP_CLUSTER
為單位,為所有的頁槽分組。 -
p->percpu_cluster = alloc_percpu(struct percpu_cluster)
setup_swap_map_and_extents
- 遍歷
swap_header->info.nr_badpages
,為0,掠過 - 遍歷所有cluster,
i = maxpages不小於所有cluster的
,掠過 - 將第0個
struct swap_cluster_info->data
自增1,表示usage counter自增1 setup_swap_extents
創建交換子區(swap extents),由於滿足S_ISBLK(塊設備),僅有一個交換子區- 遍歷全部的cluster,並通過cluster->data將它們串起來
- 由於
p->cluster_next
是隨機的,所以cluster的index也是隨機的。這個cluster被賦值給p->free_cluster_head
。 - 從
free_cluster_head
所在的cluster開始,每個cluster的struct swap_cluster_info->data
都等於下一個cluster的index。如果已經是最后一個cluster了則會繞到第0個 - 如果是第0個則跳過(第0個cluster不被使用嗎?)
- 由於
- 遍歷
-
調用
enable_swap_info
,將當前的swap_info_struct
按照prio加入到swap_avail_head
。在該函數中,total_swap_pages += p->pages
,也就是說該變量等於所有交換分區的頁槽數之和。 - 成功返回
4.6.2 swapoff
(TODO)
4.6.3 try_to_unuse()
(TODO)
4.7 分配與釋放頁槽
struct swap_cluster_info {
unsigned int data:24; /* 如果下一個cluster是空閑的則存儲在這里 */
unsigned int flags:8; /* 參考下面的define */
};
#define CLUSTER_FLAG_FREE 1 /* This cluster is free */
#define CLUSTER_FLAG_NEXT_NULL 2 /* This cluster has no next cluster */
struct percpu_cluster {
struct swap_cluster_info index; /* Current cluster index */
unsigned int next; /* Likely next allocation offset */
};
一個cluster就是SWAPFILE_CLUSTER
(256)個page slot組合在一起的塊,所有空閑的cluster將組織在一個鏈表中。在SSD頁槽搜索算法中,為每一個CPU都分配了一個cluster,所以每個cpu都能從它自己的cluster中分配頁槽並順序的swapout,以便增加swapout的吞吐量。
搜索頁槽的函數路徑為:
kswapd --> balance_pgdat --> kswapd_shrink_zone --> shrink_zone --> shrink_lruvec --> shrink_list --> shrink_inactive_list --> shrink_page_list --> pageout --> shmem_writepage --> get_swap_page --> scan_swap_map
get_swap_page
:在該函數中,它會以plist_for_each_entry_safe
來遍歷swap_avail_head
。若僅有一個交換分區,則該list僅有一個元素,所以該函數除了一些合理性判斷外,作用就是調用了scan_swap_map
:
- 若
scan_swap_map
返回非0,則get_swap_page
返回對應的swap entry:swp_entry(si->type, offset)
- 若返回0,則遍歷其他交換分區。由於此處僅有一個交換分區,因此直接返回0。
scan_swap_map
,原型為:
static unsigned long scan_swap_map(struct swap_info_struct *si, unsigned char usage)
,執行邏輯:
-
由於采用了SSD頁槽搜索算法,因此會直接跳入
scan_swap_map_try_ssd_cluster
,為當前CPU分配cluster:percpu_cluster->index = si->free_cluster_head
,percpu_cluster->next = cluster_next(&si->free_cluster_head)
,最終通過參數返回offset = si->cluster_next
,進入check狀態 checks
狀態,對得到的頁槽(offset)進行檢查,在這里執行的邏輯有:- 如果offset所在頁槽已經有人用了(
si->swap_map[offset] != 0
),則goto scan
,進入掃描狀態 si->inuse_pages++
- 如果
si->inuse_pages == si->pages
,說明已經全部用光,此時將當前si從swap_avail_head
中刪除,get_swap_page
就不會遍歷到這個交換分區了 si->swap_map[offset] = usage
,此處usage為傳入參數SWAP_HAS_CACHE
si->cluster_next = offset + 1
,這樣下次分配時即可從當前cluster的下一個頁槽分配了。si->flags -= SWP_SCANNING
,退出分配頁槽的狀態,返回offset
- 如果offset所在頁槽已經有人用了(
scan
狀態:從當前offset開始遍歷整個交換分區- 從當前offset開始,遍歷到
si->highest_bit
,如果有空閑頁槽則進入check
狀態 - 否則會繞到
si->lowest_bit
,遍歷到剛剛offset的位置,如果有空閑頁槽就進入check
狀態 - 如果根本找不到空閑頁槽,則退出分配頁槽狀態,返回0
- 從當前offset開始,遍歷到
4.8 交換高速緩存
向交換區來回傳送頁會引發很多競爭條件,具體的說,交換子系統必須仔細處理下面的情形:
- 多重換入:兩個進程可能同時要換入同一個共享匿名頁
- 同時換入換出:一個進程可能換入正由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_cache()
,主要執行步驟為:
- 調用
get_page()
,增加該page的引用計數_mapcount(或稱_refcount)
- 置位
PG_swapcache
- 將
page->private
設置為頁槽索引 - 調用
swap_address_space()
從上面的swapper_spaces
中獲得address_space
。 - 調用
radix_tree_insert()
將頁插入到基數樹中(address_space->page_tree
)
4.9 頁換出
第一步,准備交換高速緩存。如果shrink_page_list()
函數確認某頁為匿名頁(PageAnon()
函數返回1)而且交換高速緩存中沒有相應的頁框(頁描述符的PG_swapcache
標志為0),內核就調用add_to_swap()
函數。該函數會在交換區分配一個頁槽,並把一個頁框(其頁描述符作為參數傳遞進來)插入交換高速緩存。函數主要執行步驟如下:
- 調用
get_swap_page()
分配一個新的頁槽,如果失敗則返回0 - 調用
add_to_swap_cache()
,插入基數樹
第二步,更新頁表項。通過調用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
第三步,將數據寫入交換區。在這一步里會檢查頁是否是臟頁(PG_dirty
是否置位,為什么僅針對臟頁?是因為沒寫的也可以直接被釋放嗎?)。如果是,則pageout()
將會被執行。其具體邏輯為:
-
調用
is_page_cache_freeable()
判斷該頁的引用數,除了調用者、基數樹(即swapcache)之外,還可能有某些buffer在引用該頁(此時page的PG_private
或PG_private2
必定有置位)。如果並非如此就退出pageout()
-
如果頁的mapping為空則,要么退出
pageout()
,要么該頁屬於buffer。通過page_has_private()
來判斷是否如此。如果是的話,則通過try_to_free_buffer()
來釋放緩沖區(這個緩沖區是文件系統緩沖,具體邏輯還需要研究) -
清零
PG_dirty
,pageout()
回調page->mapping->a_ops->writepage()
,而page的mapping指向全局變量swapper_spaces
數組中某元素(任一元素都相同),從而調用swap_writepage
,具體邏輯為:- 在
try_to_free_swap()
中調用page_swapcount()
檢查是否至少有一個用戶態進程引用該頁。有趣的是,這里並不檢查`page->_mapcount`,而是檢查對應的頁槽的引用計數。如果引用數為0,則將swapcache刪除(從基數樹中刪除頁框索引) - 調用
__swap_writepage
,傳入bio_end_io_t
類型的回調函數end_swap_bio_write()
- 首先檢查交換分區有無
SWP_FILE
,即是否正常開啟並運行中。zram交換分區標志為0x53,並無此標志。(具體見swap_info_struct->flags
的描述) - 調用
bdev_write_page()
,向塊設備中寫入指定頁。參數有:struct swap_info_struct->bdev
(在zram中,zram_rwpage
等函數都注冊在這個block device的opts中)、page所對應的sector、要交換的page。進入該函數時,頁被鎖住且PG_writeback
不置位,退出時狀態相反。期間通過bdev->bd_disk->fops->rw_page
回調zram_rw_page
。
- 首先檢查交換分區有無
- 在
第四步,將page釋放。取消PG_locked
。並將page->lru
加入到free_pages
。最后,數組free_pages
會被free_hot_cold_page_list()
釋放,而交換不成功的頁則要被putback
4.10 頁換入
當進程試圖對一個已被換出的頁進行尋址時,必然會發生頁的換入。在以下條件全滿足時,缺頁異常處理程序會觸發一個換入操作,關於這個部分的詳細說明,在進程地址空間的尋址中有詳細說明:
- 引起異常的地址所在的頁是一個有效的頁,也就是說,它屬於當前進程的一個線性區
- 頁不在內存中,也就是頁表項的Present標志被清除
- 與頁有關的頁表項不為空,但是
PG_dirty
位被清零,意味着頁表項乃是一個換出頁標識符
5 Zram 交換技術
zram即是上文提及的交換區的一種實現,與傳統交換區實現的不同之處在於,傳統交換區是將內存中的頁交換到磁盤中暫時保存起來,發生缺頁的時候,從磁盤中讀取出來換入。而zram則是將內存頁進行壓縮,仍然存放在內存中,發生缺頁的時候,進行解壓縮后換入。根據經驗,LZO壓縮算法一般可以將內存頁中的數據壓縮至1/3,相當於原本三個頁的數據現在一個頁就能存下了,賺到了兩個頁,從而使可用內存感覺起來變多了。
5.1 Zram 基本操作:
對zram的設置,必須在交換區未激活的狀態下執行:
echo 3 > /sys/block/zram0/max_comp_streams
echo $((400*1024*1024)) > /sys/block/zram0/disksize
創建交換區,詳細見mkswap:
mkswap /dev/block/zram0
激活交換區:
swapon /dev/block/zram0
關閉交換區:
swapoff /dev/zram0
5.2 Zram 中主要的數據結構
struct size_class及struct zspage:
Zram 使用了__alloc_page()
接口來整頁整頁的獲取內存。一般情況下,我們會將頁再次划分,以保證其內存被充分的使用盡量少的產生內存碎片。但是,由於來自用戶的頁被壓縮后,其大小在[0, 4096]范圍內是隨機的。那么將頁分配為多大都不合適。
因此,Zram將內存頁進行了不同大小的划分,大小的范圍是[32byte, 4096byte]
,間隔8byte,也就是32byte、40byte、48byte直到4096byte。對於壓縮后不足32byte的也使用32byte單位的頁來存儲。Zram使用了struct size_class
類型來表示它們。其定義為:
struct size_class {
spinlock_t lock;
struct page *fullness_list[_ZS_NR_FULLNESS_GROUPS];
/*
* Size of objects stored in this class. Must be multiple
* of ZS_ALIGN.
*/
int size;
unsigned int index;
/* Number of PAGE_SIZE sized pages to combine to form a 'zspage' */
int pages_per_zspage;
struct zs_size_stat stats;
/* huge object: pages_per_zspage == 1 && maxobj_per_zspage == 1 */
bool huge;
};
可是這種辦法還是有一個缺陷:每個頁最末尾的,不足一個單位大小的內存被浪費了。此時選擇的辦法是,將幾個頁聯合起來(最多四個頁),選擇浪費最少的方案:
struct zs_pool *zs_create_pool(const char *name, gfp_t flags) {
for (i = zs_size_classes - 1; i >= 0; i--) {
size = ZS_MIN_ALLOC_SIZE + i * ZS_SIZE_CLASS_DELTA;
pages_per_zspage = get_pages_per_zspage(size);
class->pages_per_zspage = pages_per_zspage;
}
...
}
static int get_pages_per_zspage(int class_size) {
for (i = 1; i <= ZS_MAX_PAGES_PER_ZSPAGE; i++) {
int zspage_size;
int waste, usedpc;
zspage_size = i * PAGE_SIZE;
waste = zspage_size % class_size;
usedpc = (zspage_size - waste) * 100 / zspage_size;
if (usedpc > max_usedpc) {
max_usedpc = usedpc;
max_usedpc_order = i;
}
}
return max_usedpc_order;
}
這幾個聯合起來的page,被稱為zspage。在一個zspage中,至少包含一個page,最多有4個page。 他們的特征是:
- 第一個頁的
struct page->flags
中有PG_private標記 - 最后一個頁的
struct page->flags
中有PG_private2標記 - 每個頁之間用
struct page->freelist
相互串聯起來(第一個頁的freelist指向第二個頁,第二個指向第三個…)
還有一個問題需要被考慮:當某個級別上的zspage被寫滿了,該怎么記錄他們呢?
這時在struct size_class
中,引入了fullness_list
。fullness_list
共有四個指針,分別指向完全空、基本空、基本滿和完全滿的zspage list,當分配zram存儲單位時(zs_object
),按照基本滿->基本空->完全空的順序,釋放的時候,則會遵從完全滿->基本滿->基本空->完全空的順序。在完全空的時候,zs_free()
才會調用free_zspage()
對zspage進行釋放。
zspage的內存存儲結構與zs_object
Zram會將zspage按照固定大小進行分割,而每一個單元就被稱為一個zs_object
。一個zs_object
分為兩個部分,第一個部分是一個頭,其內容是一個指向某個slab對象的指針handle;第二部分則是實際上的壓縮數據。上圖所示的,是一個zs_object
與pte之間的關系:
pte通過交換分區+頁槽值
可以找到當前這個pte被存儲到了哪個頁槽上。通過頁槽值及swap_map
數組找到在zram內存存儲的位置。其位置為某個頁的pfn+頁內的zs_object
的index。
5.3 換出到Zram交換分區
static int zram_bvec_write(struct zram *zram, struct bio_vec *bvec, u32 index, int offset)
{
struct page *page = bvec->bv_page;
zcomp_compress(zram->comp, zstrm, uncmem, &clen);
zs_malloc(meta->mem_pool, clen);
zs_get_total_pages(meta->mem_pool);
unsigned char * cmem = zs_map_object(meta->mem_pool, handle, ZS_MM_WO);
copy_page(cmem, src);
}
zram_bvec_write()
主要過程為:
- 調用
zcomp_compress
將源數據壓縮 - 調用
zs_malloc
從zram分配一塊內存(zs_object
) - 調用
zs_map_object
映射zs_object
- 調用
memcpy
把壓縮數據拷貝到zs_object
zs_malloc:
該函數的作用是根據調用者提供的大小,分配出合適大小的zs_object以便存儲壓縮數據。
unsigned long zs_malloc(struct zs_pool *pool, size_t size) {
first_page = find_get_zspage(class);
if (!first_page) {
first_page = alloc_zspage(class, pool->flags);
set_zspage_mapping(first_page, class->index, ZS_EMPTY);
}
obj = obj_malloc(first_page, class, handle);
}
具體流程如下:
- 調用
find_get_zspage
找到基本滿或者基本空的zspage - 如果沒有找到,則分配新的zspage
- 將這個zspage以指定大小划分,每個單位的頭部為i++
- 將幾個page連起來
- 從zspage中獲得第一個沒有被使用的
zs_object
並返回
猜你喜歡:
- 知識星球:獨家 Linux 實戰經驗與技巧,訂閱「Linux知識星球」
- 儒碼科技:Linux 技術咨詢、培訓與服務,聯系「儒碼科技」
- 技術交流:Linux 用戶技術交流微信群,聯系微信號:tinylab
支付寶打賞 ¥9.68元 |
微信打賞 ¥9.68元 |
|
![]() |
![]() 請作者喝杯咖啡吧 |
![]() |