本文為原創,轉載請注明:http://www.cnblogs.com/tolimit/
概述
- 進程堆、棧、數據段使用的匿名頁:存放到swap分區中
- 進程代碼段映射的可執行文件的文件頁:直接釋放
- 打開文件進行讀寫使用的文件頁:如果頁中數據與文件數據不一致,則進行回寫到磁盤對應文件中,如果一致,則直接釋放
- 進行文件映射mmap共享內存時使用的頁:如果頁中數據與文件數據不一致,則進行回寫到磁盤對應文件中,如果一致,則直接釋放
- 進行匿名mmap共享內存時使用的頁:存放到swap分區中
- 進行shmem共享內存時使用的頁:存放到swap分區中
lru鏈表描述符
如前面所說,lru鏈表組織的頁包括:可以存放到swap分區中的頁,映射了文件的頁,以及被鎖在內存中禁止換出的進程頁。所有屬於這些情況的頁都必須加入到lru鏈表中,無一例外,而剩下那些沒有加入到lru鏈表中的頁,基本也就剩內核使用的頁框了。
struct zone { ...... /* lru鏈表使用的自旋鎖 * 當需要修改lru鏈表描述符中任何一個鏈表時,都需要持有此鎖,也就是說,不會有兩個不同的lru鏈表同時進行修改 */ spinlock_t lru_lock; /* lru鏈表描述符 */ struct lruvec lruvec; ...... }
- 可以存放到swap分區中的頁
- 映射了磁盤文件的文件頁
- 被鎖在內存中禁止換出的進程頁(包括以上兩種頁)
/* lru鏈表描述符,主要有5個雙向鏈表 * LRU_INACTIVE_ANON = LRU_BASE, * LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE, * LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE, * LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE, * LRU_UNEVICTABLE, */ struct lruvec { /* 5個lru雙向鏈表頭 */ struct list_head lists[NR_LRU_LISTS]; struct zone_reclaim_stat reclaim_stat; #ifdef CONFIG_MEMCG /* 所屬zone */ struct zone *zone; #endif };
可以看到,一個lru鏈表描述符中總共有5個雙向鏈表頭,它們分別描述五中不同類型的鏈表。由於每個頁有自己的頁描述符,而內核主要就是將對應的頁的頁描述符加入到這些鏈表中。
對於此zone中所有可以存放到swap分區中並且沒被鎖在內存中的頁(進程堆、棧、數據段使用的頁,匿名mmap共享內存使用的頁,shmem共享內存使用的頁),lru鏈表描述符會使用下面兩個鏈表進行組織:
- LRU_INACTIVE_ANON:稱為非活動匿名頁lru鏈表,此鏈表中保存的是此zone中所有最近沒被訪問過的並且可以存放到swap分區的頁描述符,在此鏈表中的頁描述符的PG_active標志為0。
- LRU_ACTIVE_ANON:稱為活動匿名頁lru鏈表,此鏈表中保存的是此zone中所有最近被訪問過的並且可以存放到swap分區的頁描述符,此鏈表中的頁描述符的PG_active標志為1。
這兩個鏈表我們統稱為匿名頁lru鏈表。
對於此zone中所有映射了具體磁盤文件頁並且沒有被鎖在內存中的頁(映射了內核映像的頁除外),lru鏈表描述符會使用下面兩個鏈表組織:
- LRU_INACTIVE_FILE:稱為非活動文件頁lru鏈表,此鏈表中保存的是此zone中所有最近沒被訪問過的文件頁的頁描述符,此鏈表中的頁描述符的PG_active標志為0。
- LRU_ACTIVE_FILE:稱為活動文件頁lru鏈表,此鏈表中保存的是此zone中所有最近被訪問過的文件頁的頁描述符,此鏈表中的頁描述符的PG_active標志為1。
這兩個鏈表我們統稱為文件頁lru鏈表。
而對於此zone中那些鎖在內存中的頁,lru鏈表描述符會使用這個鏈表進行組織:
- LRU_UNEVICTABLE:此鏈表中保存的是此zone中所有禁止換出的頁的描述符。
為了方便對於LRU_INACTIVE_ANON和LRU_ACTIVE_ANON這兩個鏈表,統稱為匿名頁lru鏈表,而LRU_INACTIVE_FILE和LRU_ACTIVE_FILE統稱為文件頁lru鏈表。當進程運行過程中,通過調用mlock()將一些內存頁鎖在內存中時,這些內存頁就會被加入到它們鎖在的zone的LRU_UNEVICTABLE鏈表中,在LRU_UNEVICTABLE鏈表中的頁可能是文件頁也可能是匿名頁。
之前說了內核主要是將對應頁的頁描述符加入到上述幾個鏈表中的某個,比如我一個頁映射了磁盤文件,那么這個頁就加入到文件頁lru鏈表中,內核主要通過頁描述符的lru和flags標志描述一個加入到了lru鏈表中的頁。
struct page { /* 用於頁描述符,一組標志(如PG_locked、PG_error),同時頁框所在的管理區和node的編號也保存在當中 */ /* 在lru算法中主要用到的標志 * PG_active: 表示此頁當前是否活躍,當放到或者准備放到活動lru鏈表時,被置位 * PG_referenced: 表示此頁最近是否被訪問,每次頁面訪問都會被置位 * PG_lru: 表示此頁是處於lru鏈表中的 * PG_mlocked: 表示此頁被mlock()鎖在內存中,禁止換出和釋放 * PG_swapbacked: 表示此頁依靠swap,可能是進程的匿名頁(堆、棧、數據段),匿名mmap共享內存映射,shmem共享內存映射 */ unsigned long flags; ...... union { /* 頁處於不同情況時,加入的鏈表不同 * 1.是一個進程正在使用的頁,加入到對應lru鏈表和lru緩存中 * 2.如果為空閑頁框,並且是空閑塊的第一個頁,加入到伙伴系統的空閑塊鏈表中(只有空閑塊的第一個頁需要加入) * 3.如果是一個slab的第一個頁,則將其加入到slab鏈表中(比如slab的滿slab鏈表,slub的部分空slab鏈表) * 4.將頁隔離時用於加入隔離鏈表 */ struct list_head lru; ...... }; ...... }
由於struct page是一個復合結構,當page用於不同情況時,lru變量加入的鏈表不同(如注釋),這里我們只討論頁是進程正在使用的頁時的情況。這時候,頁通過頁描述符的lru加入到對應的zone的lru鏈表中,然后會置位flags中的PG_lru標志,表明此頁是在lru鏈表中的。而如果flags的PG_lru和PG_mlocked都置位,說明此頁是處於lru鏈表中的LRU_UNEVICTABLE鏈表上。如下圖:
需要注意,此zone中所有可以存放於swap分區的頁加入到匿名頁lru鏈表,並不代表這些頁現在就在swap分區中,而是未來內存不足時,可以將這些頁數據放到swap分區中,以此來回收這些頁。
lru緩存
上面說到,當需要修改lru鏈表時,一定要占有zone中的lru_lock這個鎖,在多核的硬件環境中,在同時需要對lru鏈表進行修改時,鎖的競爭會非常的頻繁,所以內核提供了一個lru緩存的機制,這種機制能夠減少鎖的競爭頻率。其實這種機制非常簡單,lru緩存相當於將一些需要相同處理的頁集合起來,當達到一定數量時再對它們進行一批次的處理,這樣做可以讓對鎖的需求集中在這個處理的時間點,而沒有lru緩存的情況下,則是當一個頁需要處理時則立即進行處理,對鎖的需求的時間點就會比較離散。首先為了更好的說明lru緩存,先對lru鏈表進行操作主要有以下幾種:
- 將不處於lru鏈表的新頁放入到lru鏈表中
- 將非活動lru鏈表中的頁移動到非活動lru鏈表尾部(活動頁不需要這樣做,后面說明)
- 將處於活動lru鏈表的頁移動到非活動lru鏈表
- 將處於非活動lru鏈表的頁移動到活動lru鏈表
- 將頁從lru鏈表中移除
除了最后一項移除操作外,其他四樣操作除非在特殊情況下, 否則都需要依賴於lru緩存。可以看到上面的5種操作,並不是完整的一套操作集(比如沒有將活動lru鏈表中的頁移動到活動lru鏈表尾部),原因是因為lru鏈表並不是供於整個系統所有模塊使用的,可以說lru鏈表的出現,就是專門用於進行內存回收,所以這里的操作集只實現了滿足於內存回收所需要使用的操作。
大部分在內存回收路徑中對lru鏈表的操作,都不需要用到lru緩存,只有非內存回收路徑中需要對頁進行lru鏈表的操作時,才會使用到lru緩存。為了對應這四種操作,內核為每個CPU提供了四種lru緩存,當頁要進行lru的處理時,就要先加入到lru緩存,當lru緩存滿了或者系統主要要求將lru緩存中所有的頁進行處理,才會將lru緩存中的頁放入到頁想放入的lru鏈表中。每種lru緩存使用struct pagevec進行描述:
/* LRU緩存 * PAGEVEC_SIZE默認為14 */ struct pagevec { /* 當前數量 */ unsigned long nr; unsigned long cold; /* 指針數組,每一項都可以指向一個頁描述符,默認大小是14 */ struct page *pages[PAGEVEC_SIZE]; };
一個lru緩存的大小為14,也就是一個lru緩存中最多能存放14個即將處理的頁。
nr代表的是此lru緩存中保存的頁數量,而加入到了lru緩存中的頁,lru緩存中的pages指針數組中的某一項就會指向此頁的頁描述符,也就是當lru緩存滿時,pages數組中每一項都會指向一個頁描述符。
上面說了內核為每個CPU提供四種緩存,這四種lru緩存如下:
/* 這部分的lru緩存是用於那些原來不屬於lru鏈表的,新加入進來的頁 */ static DEFINE_PER_CPU(struct pagevec, lru_add_pvec); /* 在這個lru_rotate_pvecs中的頁都是非活動頁並且在非活動lru鏈表中,將這些頁移動到非活動lru鏈表的末尾 */ static DEFINE_PER_CPU(struct pagevec, lru_rotate_pvecs); /* 在這個lru緩存的頁原本應屬於活動lru鏈表中的頁,會強制清除PG_activate和PG_referenced,並加入到非活動lru鏈表的鏈表表頭中 * 這些頁一般從活動lru鏈表中的尾部拿出來的 */ static DEFINE_PER_CPU(struct pagevec, lru_deactivate_pvecs); #ifdef CONFIG_SMP /* 將此lru緩存中的頁放到活動頁lru鏈表頭中,這些頁原本屬於非活動lru鏈表的頁 */ static DEFINE_PER_CPU(struct pagevec, activate_page_pvecs); #endif
如注釋所說,CPU的每一個lru緩存處理的頁是不同的,當一個新頁需要加入lru鏈表時,就會加入到cpu的lru_add_pvec緩存;當一個非活動lru鏈表的頁需要被移動到非活動頁lru鏈表末尾時,就會被加入cpu的lru_rotate_pvecs緩存;當一個活動lru鏈表的頁需要移動到非活動lru鏈表中時,就會加入到cpu的lru_deactivate_pvecs緩存;當一個非活動lru鏈表的頁被轉移到活動lru鏈表中時,就會加入到cpu的activate_page_pvecs緩存。
注意,內核是為每個CPU提供四種lru緩存,而不是每個zone,並且也不是為每種lru鏈表提供四種lru緩存,也就是說,只要是新頁,所有應該放入lru鏈表的新頁都會加入到當前CPU的lru_add_pvec這個lru緩存中,比如同時有兩個新頁,一個將加入到zone0的活動匿名頁lru鏈表,另一個將加入到zone1的非活動文件頁lru鏈表,這兩個新頁都會先加入到此CPU的lru_add_pvec這個lru緩存中。用以下圖進行說明更好理解,當前CPU的lru緩存中有page1,page2和page3這3個頁,這時候page4加入了進來:
當page4加入后,當前CPU的lru_add_pvec緩存中有4個頁待處理的頁,而此時,如果當前CPU的lru_add_pvec緩存大小為4,或者一些情況需要當前CPU立即對lru_add_pvec緩存進行處理,那么這些頁就會被放入到它們需要放入的lru鏈表中,如下:
這些頁加入完后,當前CPU的lru_add_pvec緩存為空,又等待新一輪要被加入的新頁。
對於CPU的lru_add_pvec緩存的處理,如上,而其他類型的lru緩存處理也是相同。只需要記住,要對頁實現什么操作,就放到CPU對應的lru緩存中,而CPU的lru緩存滿或者需要立即將lru緩存中的頁放入lru鏈表時,就會將lru緩存中的頁放到它們需要放入的lru鏈表中。同時,對於lru緩存來說,它們只負責將頁放到頁應該放到的lru鏈表中,所以,在一個頁加入lru緩存前,就必須設置好此頁的一些屬性,這樣才能配合lru緩存進行工作。
加入lru鏈表
將上面的所有結構說完,已經明確了幾點:
- 不同類型的頁需要加入的lru鏈表不同
- 在smp中,加入lru鏈表前需要先加入到當前CPU的lru緩存中
- 需要不同處理的頁加入的當前CPU的lru緩存不同。
接下來我們看看不同操作的實現代碼。
實現代碼
新頁加入lru鏈表
當需要將一個新頁需要加入到lru鏈表中,此時必須先加入到當前CPU的lru_add_pvec緩存中,一般通過__lru_cache_add()函數進行加入,如下:
/* 加入到lru_add_pvec緩存中 */ static void __lru_cache_add(struct page *page) { /* 獲取此CPU的lru緩存, */ struct pagevec *pvec = &get_cpu_var(lru_add_pvec); /* page->_count++ * 在頁從lru緩存移動到lru鏈表時,這些頁的page->_count會-- */ page_cache_get(page); /* 檢查LRU緩存是否已滿,如果滿則將此lru緩存中的頁放到lru鏈表中 */ if (!pagevec_space(pvec)) __pagevec_lru_add(pvec); /* 將page加入到此cpu的lru緩存中,注意,加入pagevec實際上只是將pagevec中的pages數組中的某個指針指向此頁,如果此頁原本屬於lru鏈表,那么現在實際還是在原來的lru鏈表中 */ pagevec_add(pvec, page); put_cpu_var(lru_add_pvec); }
注意在此函數中加入的頁的page->_count會++,也就是新加入lru緩存的頁它的page->_count會++,而之后我們會看到,當頁從lru緩存中移動到lru鏈表后,此頁的page->_count就會--了。
pagevec_space()用於判斷這個lru緩存是否已滿,判斷方法很簡單:
static inline unsigned pagevec_space(struct pagevec *pvec) { return PAGEVEC_SIZE - pvec->nr; }
如果lru緩存已滿的情況下,就必須先把lru緩存中的頁先放入它們需要放入的lru鏈表中,之后再將這個新頁放入到lru緩存中,通過調用pagevec_add()將頁加入到lru緩存中,如下:
/* 將page加入到lru緩存pvec中 */ static inline unsigned pagevec_add(struct pagevec *pvec, struct page *page) { /* lru緩存pvec的pages[]中的pvec->nr項指針指向此頁 */ pvec->pages[pvec->nr++] = page; /* 返回此lru緩存剩余的空間 */ return pagevec_space(pvec); }
在一些特殊情況或者lru緩存已滿的情況下,都會將lru緩存中的頁放入到它們對應的lru鏈表中,這個可通過__pagevec_lru_add()函數進行實現,在__pagevec_lru_add()函數中,主要根據lru緩存的nr遍歷緩存中已經保存的頁,在期間會對這些頁所在的zone的lru_lock上鎖,因為不能同時有2個CPU並發地修改同一個lru鏈表,之后會調用相應的回調函數,對遍歷的頁進行處理:
/* 將pagevec中的頁加入到lru鏈表中,並且會將pvec->nr設置為0 */ void __pagevec_lru_add(struct pagevec *pvec) { /* __pagevec_lru_add_fn為回調函數 */ pagevec_lru_move_fn(pvec, __pagevec_lru_add_fn, NULL); }
實際上不同的lru鏈表操作,很大一部分不同就是這個回調函數的不同,回調函數決定了遍歷的每個頁應該進行怎么樣的處理,而不同lru鏈表操作它們遍歷lru緩存中的頁的函數都是pagevec_lru_move_fn,我們先看看所有lru鏈表操作都共同使用的pagevec_lru_move_fn:
/* 將緩存中的頁做move_fn處理,然后對頁進行page->_count-- * 當所有頁加入到lru緩存中時,都要page->_count++ */ static void pagevec_lru_move_fn(struct pagevec *pvec, void (*move_fn)(struct page *page, struct lruvec *lruvec, void *arg), void *arg) { int i; struct zone *zone = NULL; struct lruvec *lruvec; unsigned long flags = 0; /* 遍歷pagevec中的所有頁 * pagevec_count()返回lru緩存pvec中已經加入的頁的數量 */ for (i = 0; i < pagevec_count(pvec); i++) { struct page *page = pvec->pages[i]; /* 獲取頁所在的zone */ struct zone *pagezone = page_zone(page); /* 由於不同頁可能加入到的zone不同,這樣就是判斷是否是同一個zone,是的話就不需要上鎖了 * 不是的話要先把之前上鎖的zone解鎖,再對此zone的lru_lock上鎖 */ if (pagezone != zone) { /* 對之前的zone進行解鎖,如果是第一次循環則不需要 */ if (zone) spin_unlock_irqrestore(&zone->lru_lock, flags); /* 設置上次訪問的zone */ zone = pagezone; /* 這里會上鎖,因為當前zone沒有上鎖,后面加入lru的時候就不需要上鎖 */ spin_lock_irqsave(&zone->lru_lock, flags); } /* 獲取zone的lru鏈表 */ lruvec = mem_cgroup_page_lruvec(page, zone); /* 將page加入到zone的lru鏈表中 */ (*move_fn)(page, lruvec, arg); } /* 遍歷結束,對zone解鎖 */ if (zone) spin_unlock_irqrestore(&zone->lru_lock, flags); /* 對pagevec中所有頁的page->_count-- */ release_pages(pvec->pages, pvec->nr, pvec->cold); /* pvec->nr = 0 */ pagevec_reinit(pvec);
可以看到,這里最核心的操作,實際上就是遍歷lru緩存pvec中每個指向的頁,如果該頁所在zone的lru_lock沒有進行上鎖,則上鎖,然后對每個頁進行傳入的回調函數的操作,當所有頁都使用回調函數move_fn處理完成后,就對lru緩存中的所有頁進行page->_count--操作。
從之前的代碼可以看到,這個move_fn就是傳入的回調函數,對於新頁加入到lru鏈表中的情況,這個move_fn就是__pagevec_lru_add_fn():
/* 將lru_add緩存中的頁加入到lru鏈表中 */ static void __pagevec_lru_add_fn(struct page *page, struct lruvec *lruvec, void *arg) { /* 判斷此頁是否是page cache頁(映射文件的頁) */ int file = page_is_file_cache(page); /* 是否是活躍的頁 * 主要判斷page的PG_active標志 * 如果此標志置位了,則將此頁加入到活動lru鏈表中 * 如果沒置位,則加入到非活動lru鏈表中 */ int active = PageActive(page); /* 獲取page所在的lru鏈表,里面會檢測是映射頁還是文件頁,並且檢查PG_active,最后能得出該page應該放到哪個lru鏈表中 * 里面就可以判斷出此頁需要加入到哪個lru鏈表中 * 如果PG_active置位,則加入到活動lru鏈表,否則加入到非活動lru鏈表 * 如果PG_swapbacked置位,則加入到匿名頁lru鏈表,否則加入到文件頁lru鏈表
* 如果PG_unevictable置位,則加入到LRU_UNEVICTABLE鏈表中 */ enum lru_list lru = page_lru(page); VM_BUG_ON_PAGE(PageLRU(page), page); SetPageLRU(page); /* 將page加入到lru中 */ add_page_to_lru_list(page, lruvec, lru); /* 更新lruvec中的reclaim_stat */ update_page_reclaim_stat(lruvec, file, active); trace_mm_lru_insertion(page, lru); }
如注釋所說,判斷頁需要加入到哪個lru鏈表中,主要通過三個標志位:
- PG_active::此標志置位,表示此頁需要加入或者處於頁所在zone的活動lru鏈表中,當此頁已經在lru鏈表中時,此標志可以讓系統判斷此頁是在活動lru鏈表還是非活動lru鏈表中。
- PG_swapbacked:此標志置位,表示此頁可以回寫到swap分區,那么此頁需要加入或者處於頁所在zone的匿名頁lru鏈表中。
- PG_unevictable:置位表示此頁被鎖在內存中禁止換出,表示此頁需要加入或者處於頁所在zone的LRU_UNEVICTABLE鏈表中。
而對於文件頁lru鏈表來說,實際上還有一個PG_referenced標志,這里先提一下,后面會細說。
好的,通過這三個標志就能過清楚判斷頁需要加入到所屬zone的哪個lru鏈表中了,到這里,也能說明,在加入lru緩存前,頁必須設置好這三個標志位,表明自己想加入到所屬zone的哪個lru鏈表中。接下來我們看看add_page_to_lru_list()函數,這個函數就很簡單了,如下:
/* 將頁加入到lruvec中的lru類型的鏈表頭部 */ static __always_inline void add_page_to_lru_list(struct page *page, struct lruvec *lruvec, enum lru_list lru) { /* 獲取頁的數量,因為可能是透明大頁的情況,會是多個頁 */ int nr_pages = hpage_nr_pages(page); /* 更新lruvec中lru類型的鏈表的頁數量 */ mem_cgroup_update_lru_size(lruvec, lru, nr_pages); /* 加入到對應LRU鏈表頭部,這里不上鎖,所以在調用此函數前需要上鎖 */ list_add(&page->lru, &lruvec->lists[lru]); /* 更新統計 */ __mod_zone_page_state(lruvec_zone(lruvec), NR_LRU_BASE + lru, nr_pages); }
這樣一個新頁加入lru緩存以及加入到lru鏈表中的代碼就已經說完了,切記,並不是只有lru緩存滿了,才會將其中的頁加入到對應的lru鏈表中,一些特殊情況會要求lru緩存立即把存着的頁加入到lru鏈表中。
將處於非活動鏈表中的頁移動到非活動鏈表尾部
主要通過rotate_reclaimable_page()函數實現,這種操作主要使用在:當一個臟頁需要進行回收時,系統首先會將頁異步回寫到磁盤中(swap分區或者對應的磁盤文件),然后通過這種操作將頁移動到非活動lru鏈表尾部。這樣這些頁在下次內存回收時會優先得到回收。
rotate_reclaimable_page()函數如下:
/* 將處於非活動lru鏈表中的頁移動到非活動lru鏈表尾部 * 如果頁是處於非活動匿名頁lru鏈表,那么就加入到非活動匿名頁lru鏈表尾部 * 如果頁是處於非活動文件頁lru鏈表,那么就加入到非活動文件頁lru鏈表尾部 */ void rotate_reclaimable_page(struct page *page) { /* 此頁加入到非活動lru鏈表尾部的條件 * 頁當前不能被上鎖(並不是鎖在內存,而是每個頁自己的鎖PG_locked) * 頁必須不能是臟頁(這里應該也不會是臟頁) * 頁必須非活動的(如果頁是活動的,那頁如果在lru鏈表中,那肯定是在活動lru鏈表) * 頁沒有被鎖在內存中 * 頁處於lru鏈表中 */ if (!PageLocked(page) && !PageDirty(page) && !PageActive(page) && !PageUnevictable(page) && PageLRU(page)) { struct pagevec *pvec; unsigned long flags; /* page->_count++,因為這里會加入到lru_rotate_pvecs這個lru緩存中 * lru緩存中的頁移動到lru時,會對移動的頁page->_count-- */ page_cache_get(page); /* 禁止中斷 */ local_irq_save(flags); /* 獲取當前CPU的lru_rotate_pvecs緩存 */ pvec = this_cpu_ptr(&lru_rotate_pvecs); if (!pagevec_add(pvec, page)) /* lru_rotate_pvecs緩存已滿,將當前緩存中的頁加入到非活動lru鏈表尾部 */ pagevec_move_tail(pvec); /* 重新開啟中斷 */ local_irq_restore(flags); } }
實際上實現方式與之前新頁加入lru鏈表的操作差不多,簡單看一下pagevec_move_tail()函數和它的回調函數:
/* 將lru緩存pvec中的頁移動到非活動lru鏈表尾部 * 這些頁原本就屬於非活動lru鏈表 */ static void pagevec_move_tail(struct pagevec *pvec) { int pgmoved = 0; pagevec_lru_move_fn(pvec, pagevec_move_tail_fn, &pgmoved); __count_vm_events(PGROTATED, pgmoved); } /* 將lru緩存pvec中的頁移動到非活動lru鏈表尾部操作的回調函數 * 這些頁原本就屬於非活動lru鏈表 */ static void pagevec_move_tail_fn(struct page *page, struct lruvec *lruvec, void *arg) { int *pgmoved = arg; /* 頁屬於非活動頁 */ if (PageLRU(page) && !PageActive(page) && !PageUnevictable(page)) { /* 獲取頁應該放入匿名頁lru鏈表還是文件頁lru鏈表,通過頁的PG_swapbacked標志判斷 */ enum lru_list lru = page_lru_base_type(page); /* 加入到對應的非活動lru鏈表尾部 */ list_move_tail(&page->lru, &lruvec->lists[lru]); (*pgmoved)++; } }
可以看到與新頁加入lru鏈表操作一樣,都是使用pagevec_lru_move_fn()函數進行遍歷lru緩存中的頁,只是回調函數不同。
將活動lru鏈表中的頁加入到非活動lru鏈表中
這個操作使用的場景是文件系統主動將一些沒有被進程映射的頁進行釋放時使用,就會將一些活動lru鏈表的頁移動到非活動lru鏈表中,在內存回收過程中並不會使用這種方式。注意,在這種操作中只會移動那些沒有被進程映射的頁。並且將活動lru鏈表中的頁移動到非活動lru鏈表中,有兩種方式,一種是移動到非活動lru鏈表的頭部,一種是移動到非活動lru鏈表的尾部,由於內存回收是從非活動lru鏈表尾部開始掃描頁框的,所以加入到非活動lru鏈表尾部的頁框更容易被釋放,而在這種操作中,只會將干凈的,不需要回寫的頁放入到非活動lru鏈表尾部。
主要是將活動lru鏈表中的頁加入到lru_deactivate_pvecs這個CPU的lru緩存實現,而加入函數,是deactivate_page():
/* 將頁移動到非活動lru鏈表中 * 此頁應該屬於活動lru鏈表中的頁 */ void deactivate_page(struct page *page) { /* 如果頁被鎖在內存中禁止換出,則跳出 */ if (PageUnevictable(page)) return; /* page->_count == 1才會進入if語句 * 說明此頁已經沒有進程進行映射了 */ if (likely(get_page_unless_zero(page))) { struct pagevec *pvec = &get_cpu_var(lru_deactivate_pvecs); if (!pagevec_add(pvec, page)) pagevec_lru_move_fn(pvec, lru_deactivate_fn, NULL); put_cpu_var(lru_deactivate_pvecs); } }
主要看回調函數lru_deactivate_fn():
/* 將處於活動lru鏈表中的page移動到非活動lru鏈表中 * 此頁只有不被鎖在內存中,並且沒有進程映射了此頁的情況下才會移動 */ static void lru_deactivate_fn(struct page *page, struct lruvec *lruvec, void *arg) { int lru, file; bool active; /* 此頁不在lru中,則不處理此頁 */ if (!PageLRU(page)) return; /* 如果此頁被鎖在內存中禁止換出,則不處理此頁 */ if (PageUnevictable(page)) return; /* Some processes are using the page */ /* 有進程映射了此頁,也不處理此頁 */ if (page_mapped(page)) return; /* 獲取頁的活動標志,PG_active */ active = PageActive(page); /* 根據頁的PG_swapbacked判斷此頁是否需要依賴swap分區 */ file = page_is_file_cache(page); /* 獲取此頁需要加入匿名頁或者文件頁lru鏈表,也是通過PG_swapbacked標志判斷 */ lru = page_lru_base_type(page); /* 從活動lru鏈表中刪除 */ del_page_from_lru_list(page, lruvec, lru + active); /* 清除PG_active和PG_referenced */ ClearPageActive(page); ClearPageReferenced(page); /* 加到非活動頁lru鏈表頭部 */ add_page_to_lru_list(page, lruvec, lru); /* 如果此頁當前正在回寫或者是臟頁 */ if (PageWriteback(page) || PageDirty(page)) { /* 則設置此頁需要回收 */ SetPageReclaim(page); } else { /* 如果此頁是干凈的,並且非活動的,則將此頁移動到非活動lru鏈表尾部 * 因為此頁回收起來更簡單,不用回寫 */ list_move_tail(&page->lru, &lruvec->lists[lru]); __count_vm_event(PGROTATED); } /* 統計 */ if (active) __count_vm_event(PGDEACTIVATE); update_page_reclaim_stat(lruvec, file, 0); }
可以看到3個重點:1.只處理沒有被進程映射的頁。2.干凈的頁放入到非活動lru鏈表尾部,其他頁放入到非活動lru鏈表頭部。3.如果頁是臟頁或者正在回寫的頁,則設置頁回收標志。
將非活動lru鏈表的頁加入到活動lru鏈表
還有最后一個操作,將活動lru鏈表的頁加入到非活動lru鏈表中,這種操作主要在一些頁是非活動的,之后被標記為活動頁了,這時候就需要將這些頁加入到活動lru鏈表中,這個操作一般會調用activate_page()實現:
/* smp下使用,設置頁為活動頁,並加入到對應的活動頁lru鏈表中 */ void activate_page(struct page *page) { if (PageLRU(page) && !PageActive(page) && !PageUnevictable(page)) { struct pagevec *pvec = &get_cpu_var(activate_page_pvecs); page_cache_get(page); if (!pagevec_add(pvec, page)) pagevec_lru_move_fn(pvec, __activate_page, NULL); put_cpu_var(activate_page_pvecs); } }
我們直接看回調函數__activate_page():
/* 設置頁為活動頁,並加入到對應的活動頁lru鏈表中 */ static void __activate_page(struct page *page, struct lruvec *lruvec, void *arg) { if (PageLRU(page) && !PageActive(page) && !PageUnevictable(page)) { /* 是否為文件頁 */ int file = page_is_file_cache(page); /* 獲取lru類型 */ int lru = page_lru_base_type(page); /* 將此頁從lru鏈表中移除 */ del_page_from_lru_list(page, lruvec, lru); /* 設置page的PG_active標志,此標志說明此頁在活動頁的lru鏈表中 */ SetPageActive(page); /* 獲取類型,lru在這里一般是lru_inactive_file或者lru_inactive_anon * 加上LRU_ACTIVE就變成了lru_active_file或者lru_active_anon */ lru += LRU_ACTIVE; /* 將此頁加入到活動頁lru鏈表頭 */ add_page_to_lru_list(page, lruvec, lru); trace_mm_lru_activate(page); __count_vm_event(PGACTIVATE); /* 更新lruvec中zone_reclaim_stat->recent_scanned[file]++和zone_reclaim_stat->recent_rotated[file]++ */ update_page_reclaim_stat(lruvec, file, 1); } }
到這里所有對lru鏈表中頁的操作就說完了,對於移除操作,則直接移除,並且清除頁的PG_lru標志就可以了。需要切記,只有非內存回收的情況下對lru鏈表進行操作,才需要使用到這些lru緩存,而而內存回收時對lru鏈表的操作,大部分操作是不需要使用這些lru緩存的(只有將隔離的頁重新加入lru鏈表時會使用)。
lru鏈表的更新
我們知道,lru鏈表是將相同類型的頁分為兩個部分,一部分是活動頁,一部分是非活動頁,而具體的划分方法,就是看頁最近是否被訪問過,被訪問過則是活動頁,沒被訪問過則是非活動頁(實際上這種說法並不准確,后面會細說),這樣看來,每當一個頁被訪問了,是不是都要判斷這個頁是否需要移動到活動lru鏈表?一個頁久不被訪問了,是不是要將這個頁移動到非活動lru鏈表?實際上不是的,之前也說了很多遍,lru鏈表是專門為內存回收服務的,在內存回收沒有進行之前,lru鏈表可以說是休眠的,系統可以將頁加入到lru鏈表中,也可以將頁從lru鏈表中移除,但是lru鏈表不會更新哪些沒被訪問的頁需要移動到非活動lru鏈表,哪些經常被訪問的頁移動到活動lru鏈表。只有當進行內存回收時,lru鏈表才會開始干這件事。也就是說,在沒有進程內存回收時,lru鏈表基本不會有大的變動,變動只有新頁加入,一些頁移除,只有在內存回收過程中,lru鏈表才會有大的變動。
這樣就會涉及到一個問題,由於頁被訪問時,訪問了此頁的進程對應此頁的頁表項中的Accessed會置位,表面此頁被訪問了,而lru鏈表只有在進行內存回收時才會進行判斷,那就會有一種情況,在一個小時之內,內存空閑頁富足,這一個小時中都沒有發生內存回收,而這一個小時中,所有進程使用的內存頁都進行過了訪問,也就是每個頁反向映射到進程頁表項中總能找到有進程訪問過此頁,這時候內存回收開始了,lru鏈表如何將這些頁判斷為活動頁還是非活動頁?可以說,在這種情況,第一輪內存回收基本上顆粒無收,因為所有頁都會被判定為活動頁,但是當第二輪內存回收時,就可以正常判斷了,因為每一輪內存回收后,都會清除所有訪問了此頁的頁表項的Accessed標志,在第二輪內存回收時,只有在第一輪內存回收后與第二輪內存回收開始前被訪問過的頁,才會被判斷為最近被訪問過的頁。以匿名頁lru鏈表進行說明,如下圖:
開始內存回收前,所有加入的頁都標記了被訪問。
第一輪內存回收后,清空所有頁的被訪問標記,這樣所有頁都被算作最近沒有被訪問過的。只有在所有頁框都被標記了Accessed的情況下才會出現這種特殊情況,實際的真實情況也並不是這樣,並不一定Accessed=1的頁就會移動到活動匿名頁lru鏈表中,下面我們會細說。
下面我們就會詳細說明lru鏈表是怎么進行更新的,這里我們必須分開說明匿名頁lru鏈表的更新以及文件頁lru鏈表的更新操作,雖然它們的更新操作是同時發生的,但是它們的很多判斷是很不一樣的,這里我們先說匿名頁lru鏈表的更新,在說明時,默認頁不會在此期間被mlock()鎖在內存中(因為這樣此頁就必須拿出原本的lru鏈表,加入到LRU_UNEVICTABLE鏈表中)。需要明確,之前說了,活動lru鏈表中存放的是最近訪問過的頁,非活動lru鏈表中存放的是最近沒被訪問過的頁,實際上這種說法是不准確的,很久沒被訪問的頁也有可能在活動lru鏈表中,而經常被訪問的頁也有可能出現在非活動lru鏈表中,下面我們就會細說。
匿名頁lru鏈表的更新
匿名頁lru鏈表是專門存放那些在內存回收時可以回寫到swap分區中的頁,這些頁有進程的堆、棧、數據段,shmem共享內存使用的頁,匿名mmap共享內存使用的頁。之前說了,活動lru鏈表中存放的是最近訪問過的頁,非活動lru鏈表中存放的是最近沒被訪問過的頁,實際上這種說法是不准確的,在內存回收過程中,活動匿名頁lru鏈表是否進行更新,取決於非活動匿名頁lru鏈表長度是否達到標准,也就是說,當非活動匿名頁lru鏈表長度在內存回收過程中一直符合標准,即使活動匿名頁lru鏈表中所有的頁都一直沒被訪問過,也不會將這些頁移動到非活動匿名頁lru鏈表中,以下就是內核中關於非活動匿名頁lru鏈表長度的經驗標准:
/* * zone中 非活動匿名頁lru鏈表 * 總內存大小 需要包含的所有頁的總大小 * ------------------------------------- * 10MB 5MB * 100MB 50MB * 1GB 250MB * 10GB 0.9GB * 100GB 3GB * 1TB 10GB * 10TB 32GB */
也如之前所說,lru鏈表在沒有進行內存回收時,幾乎是休眠的,也就是說,當沒有進行內存回收時,鏈表中頁的數量低於以上要求的lru鏈表長度都沒有問題,以匿名頁lru鏈表為例,當前zone管理着1GB的內存,根據經驗公式,此zone的非活動匿名頁lru鏈表中頁的總內存量最多為250MB,當前此zone的非活動匿名頁lru鏈表包含的頁的總內存量為270MB,這時候一個進程取消了100MB的匿名mmap共享內存映射,這100MB全部來自於此zone,這時候這些頁被移除出了此zone的非活動匿名頁lru鏈表,此時,此zone的非活動匿名頁包含的頁的總內存量為170MB,低於了經驗公式的250MB,但是這並不會造成匿名頁lru鏈表的調整,只有當內存不足時導致內存回收了,在內存回收中才會進行匿名頁lru鏈表的調整,讓非活動匿名頁lru鏈表包含的頁提高,總內存量保持到250MB以上。同理,對於文件頁lru鏈表也是一樣。總之就是一句話,只有在內存回收進行中,才會調整lru鏈表中各個鏈表長度(除LRU_UNEVICTABLE鏈表外)。
當進程內存回收時,非活動匿名頁lru鏈表長度未達到標准,就會先從活動匿名頁lru鏈表尾部向頭部進行掃描(一般每次掃描32個頁),然后會將所有掃描到的頁移動到非活動匿名頁lru鏈表中,注意,這里是所有掃描到的頁,並不會判斷此頁有沒有被訪問,即使被訪問了,也移動到非活動匿名頁lru鏈表,我們假設所有在匿名頁lru鏈表中的頁在描述過程中都不會被進程鎖在內存中禁止換出,而使用Accessed標志表示此頁最近是否被進程訪問過(實際這個標志在進程頁表項中),整體如下:
可以看到,每次從活動匿名頁lru鏈表尾部拿出一些頁,移動到非活動匿名頁lru鏈表頭部。這些頁中即使有最近被訪問的頁,也必須移動到非活動匿名頁lru鏈表中。並且只要被掃描到的頁,所有映射了此頁的進程頁表項的Accessed標志都會被清除。
當對活動匿名頁lru鏈表進行一次移動后,就會立即對非活動匿名頁lru鏈表進行一次更新操作,同樣,也是從非活動匿名頁lru鏈表尾部開始向頭部掃描,最多一次掃描32個,然后對掃描的頁的狀態進行相應的處理,對於不同狀態的頁進行不同的處理,處理標准如下:
- 對於最近訪問過的頁(一個或多個映射了此頁的進程頁表項的Accessed被置位),將頁移動到活動匿名頁lru鏈表尾部中。
- 對於正在回寫的頁,將頁移動到非活動匿名頁lru鏈表頭部,並標記這些頁的PG_reclaim。
- 其他頁,嘗試對它們進行回收,回收失敗的頁則將它們移動到非活動匿名頁lru鏈表頭部。
圖示如下:
這當中還有一件很巧妙的事,之前說lru緩存時有一種專門處理是將非活動匿名頁lru鏈表中的頁移動到非活動匿名頁lru鏈表末尾的,這個的使用情況就是針對那些正在回寫的頁的,從上圖可以看到,正在回寫的頁被移動到了非活動匿名頁lru鏈表,並且會在頁描述符中置位PG_reclaim,當塊層回寫完成后,如果此頁的PG_reclaim置位了,則將此頁移動到非活動匿名頁lru鏈表的末尾,這樣在下次一輪內存回收時,這些頁將會優先得到掃描,並且更容易釋放回收。這里正在回寫的頁都是正在回寫到swap分區的頁,因為在回收過程中,只有回寫完成的頁才能夠釋放。
文件頁lru鏈表的更新
文件頁lru鏈表中存放的是映射了具體磁盤文件數據的頁,這些頁包括:進程讀寫的文件,進程代碼段使用的頁(這部分映射了可執行文件),文件mmap共享內存映射使用的頁。一個zone中的這些頁在沒有被鎖在內存中時,都會存放到文件頁lru鏈表中。實際上文件頁lru鏈表的更新流程與匿名頁lru鏈表的更新流程是一樣的,首先,進行內存回收時,當非活動文件頁lru鏈表長度不滿足系統要求時,就會先從活動文件頁lru鏈表末尾拿出一些頁,加入到非活動文件頁lru鏈表頭部,然后再從非活動文件頁lru鏈表尾部向頭部進行一定數量頁的掃描,對掃描的頁進行一些相應的處理。在這個過程中,判斷非活動文件頁lru鏈表長度的經驗公式是與匿名頁lru鏈表不一樣的,並且對不同的頁處理頁不一樣。
之前有稍微提及到一個頁描述符中的PG_referenced標志,這里進行一個詳細說明,這個標志可以說專門用於文件頁的,置位了說明此文件頁最近被訪問過,沒置位說明此文件頁最近沒有被訪問過。可能到這里大家會覺得很奇怪,在進程頁表項中有一個Accessed位用於標記此頁表項映射的頁被訪問過(這個是CPU自動完成的),而在頁描述符中又有一個PG_referenced標志,用於描述一個頁是否被訪問過,這是因為文件頁的特殊性。對於匿名頁來說,進程訪問匿名頁只需要通過一個地址就可以直接訪問了,而對於訪問文件頁,由於文件頁建立時並不像匿名頁那么便捷,對於匿名頁,在缺頁異常中直接分配一個頁作為匿名頁使用就行了(進程頁幾乎都是在缺頁異常中分配的,跟寫時復制和延時分配有關),文件頁還需要將磁盤中的數據讀入文件頁中。並且大部分情況下文件頁是通過write()和read()進行訪問(除了文件映射方式的mmap共享內存可以直接通過地址訪問),所以內核可以在一些操作文件頁的代碼路徑上,顯式去置位文件頁描述符的PG_referenced,這樣也可以表明此頁最近有被訪問過,而對於非文件頁來說,這個PG_referenced在大多數情況下就沒什么意義了,因為這些頁可以直接通過地址去訪問(比如malloc()了一段內存,就可以直接地址訪問)。
在文件頁lru鏈表中,內核要求非活動文件頁lru鏈表中保存的頁數量必須要多於活動頁lru鏈表中保存的頁,如果低於,那么就必須將活動文件頁lru鏈表尾部的一部分頁移動到非活動文件頁lru鏈表頭中,但是這部分並不是像匿名頁lru鏈表這樣所有掃描到的頁都直接進行移動,這里,活動文件頁lru鏈表會對大部分頁進行移動,但是當掃描到的頁是進程代碼段的頁,並且此頁的PG_referenced置位,會將這種頁移動到活動文件頁lru鏈表頭部,而不是移動到非活動文件頁lru鏈表頭部,對於其他的文件頁,無論是否最近被訪問過,都移動到非活動文件頁lru鏈表頭部。可以從這里看出來,代碼段的頁的回收優先級是比較低的,內核不太希望回收這部分的內存頁,除非這部分的頁一直都沒被訪問,就會被移動到非活動文件頁lru鏈表中。,如下圖:
注意與匿名頁lru鏈表掃描一樣,被掃描到的頁,所有映射了此文件頁的進程頁表項的Accessed標志會被清除,但是不會清除PG_referenced標志。
而對於非活動文件頁lru鏈表的更新,情況比非活動匿名頁lru鏈表復雜得多,對於掃描到的非活動文件頁lru鏈表中的頁的處理如下:
- 此文件頁最近被多個進程訪問(多個映射此頁的進程頁表項Accessed被置位),則將頁移動到活動文件頁lru鏈表頭部。
- 此頁的PG_referenced被置位,則將頁移動到活動文件頁lru鏈表頭部。
- 對於最近被訪問過的代碼段文件頁,移動到活動文件頁lru鏈表頭部。
- 最近只被一個進程訪問過的頁,並且頁的PG_referenced沒有置位,則將頁移動到非活動文件頁lru鏈表頭部。
- 正在回寫的頁則將頁的PG_relaim置位,然后頁移動到非活動文件頁lru鏈表頭部。
- 其他頁嘗試釋放回收。
- 回收失敗的頁移動到非活動文件頁lru鏈表頭部。
這里需要注意,當文件頁從活動文件頁lru鏈表移動到非活動文件頁lru鏈表時,是不會對頁的PG_referenced進行清除操作的,從非活動文件頁lru鏈表移動到活動文件頁lru鏈表時,如果發現此文件頁最近被訪問過,則會置位此頁的PG_referenced標志。
到這里整個在內存回收中匿名頁lru鏈表的整理和文件頁lru鏈表的整理就已經描述完了,之后的文章會配合內存回收流程更詳細地去說明整個流程。
這里只需要記住一點,lru鏈表的掃描只有在內存回收時進行,對於匿名頁lru鏈表和文件頁lru鏈表,在非活動鏈表長度不足的時候,才會從尾向頭去掃描活動lru鏈表,將部分活動lru鏈表的頁移動非活動lru鏈表中,對於不同類型的頁,內核有不同的判斷標准和處理方式。可以說,這個最近最少使用頁鏈表,我個人認為更明確的叫法應該算是內存回收時最近最少使用頁鏈表。
不同類型的頁加入
活動lru鏈表是存放最近被訪問的頁框,而進程剛申請的一個新頁,按理來說最近肯定是被訪問過了,應該加入到活動lru鏈表中,但是情況並不是這樣,之前也說過,lru鏈表是專為內存回收服務的,系統希望在內存回收過程中不同類型的頁應該有不同的回收優先級,有些類型的頁,系統希望優先回收,而有些類型的頁,系統希望慢點回收。而我們知道,內核的內存回收是從非活動lru鏈表末尾開始向前掃描其中的每一個頁,它並不會去掃描活動lru鏈表,只有當非活動lru鏈表中的頁數量不滿足要求時,會從活動lru鏈表中移動一些頁到非活動lru鏈表中,也就是,加入到非活動lru鏈表的頁,是更有可能優先被內核進行回收的。因此,由於不同類型的頁在內存回收中有不同的優先級,導致不同類型的新頁加入到lru鏈表時會不同,如下就是最近總結出來的:
- 進程堆、棧、數據段中使用的新匿名頁:加入到對應zone的 活動匿名頁lru鏈表
- shmem共享內存使用的新頁:加入到對應zone的 非活動匿名頁lru鏈表
- 匿名mmap共享內存映射使用的新頁:加入到對應zone的 非活動匿名頁lru鏈表
- 新的映射磁盤文件數據的文件頁:加入到對應zone的 非活動文件頁lru鏈表
- 使用文件映射mmap共享內存使用的新頁:加入到對於zone的 非活動文件頁lru鏈表
由於能力有限,沒能總結出直接加入到活動文件頁lru鏈表中的新頁,但是這種頁是存在的。
需要注意,這些頁並不是在創建的時候就會生成,需要考慮寫時復制。
遺留問題:
1.當一個新進程裝載到內存中時,活動文件頁lru鏈表與非活動文件頁lru鏈表都會增加,通過pmap -d查看應該都是映射了可執行文件的頁,但是這些頁中哪些頁加入活動文件頁lru鏈表,哪些頁加入非活動lru鏈表