本文爲原創,轉載請註明:http://www.cnblogs.com/tolimit/node
如前面所說,lru鏈表組織的頁包括:能夠存放到swap分區中的頁,映射了文件的頁,以及被鎖在內存中禁止換出的進程頁。全部屬於這些狀況的頁都必須加入到lru鏈表中,無一例外,而剩下那些沒有加入到lru鏈表中的頁,基本也就剩內核使用的頁框了。算法
struct zone { ...... /* lru鏈表使用的自旋鎖 * 當須要修改lru鏈表描述符中任何一個鏈表時,都須要持有此鎖,也就是說,不會有兩個不一樣的lru鏈表同時進行修改 */ spinlock_t lru_lock; /* lru鏈表描述符 */ struct lruvec lruvec; ...... }
/* 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鏈表。併發
對於此zone中全部映射了具體磁盤文件頁而且沒有被鎖在內存中的頁(映射了內核映像的頁除外),lru鏈表描述符會使用下面兩個鏈表組織:app
這兩個鏈表咱們統稱爲文件頁lru鏈表。less
而對於此zone中那些鎖在內存中的頁,lru鏈表描述符會使用這個鏈表進行組織:異步
爲了方便對於LRU_INACTIVE_ANON和LRU_ACTIVE_ANON這兩個鏈表,統稱爲匿名頁lru鏈表,而LRU_INACTIVE_FILE和LRU_ACTIVE_FILE統稱爲文件頁lru鏈表。當進程運行過程當中,經過調用mlock()將一些內存頁鎖在內存中時,這些內存頁就會被加入到它們鎖在的zone的LRU_UNEVICTABLE鏈表中,在LRU_UNEVICTABLE鏈表中的頁多是文件頁也多是匿名頁。函數
以前說了內核主要是將對應頁的頁描述符加入到上述幾個鏈表中的某個,好比我一個頁映射了磁盤文件,那麼這個頁就加入到文件頁lru鏈表中,內核主要經過頁描述符的lru和flags標誌描述一個加入到了lru鏈表中的頁。this
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鏈表時,必定要佔有zone中的lru_lock這個鎖,在多核的硬件環境中,在同時須要對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鏈表中,此時必須先加入到當前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鏈表中,主要經過三個標誌位:
而對於文件頁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_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鏈表中,這個操做通常會調用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鏈表纔會有大的變更。
這樣就會涉及到一個問題,因爲頁被訪問時,訪問了此頁的進程對應此頁的頁表項中的Accessed會置位,表面此頁被訪問了,而lru鏈表只有在進行內存回收時纔會進行判斷,那就會有一種狀況,在一個小時以內,內存空閒頁富足,這一個小時中都沒有發生內存回收,而這一個小時中,全部進程使用的內存頁都進行過了訪問,也就是每一個頁反向映射到進程頁表項中總能找到有進程訪問過此頁,這時候內存回收開始了,lru鏈表如何將這些頁判斷爲活動頁仍是非活動頁?能夠說,在這種狀況,第一輪內存回收基本上顆粒無收,由於全部頁都會被斷定爲活動頁,可是當第二輪內存回收時,就能夠正常判斷了,由於每一輪內存回收後,都會清除全部訪問了此頁的頁表項的Accessed標誌,在第二輪內存回收時,只有在第一輪內存回收後與第二輪內存回收開始前被訪問過的頁,纔會被判斷爲最近被訪問過的頁。以匿名頁lru鏈表進行說明,以下圖:
開始內存回收前,全部加入的頁都標記了被訪問。
第一輪內存回收後,清空全部頁的被訪問標記,這樣全部頁都被算做最近沒有被訪問過的。只有在全部頁框都被標記了Accessed的狀況下才會出現這種特殊狀況,實際的真實狀況也並非這樣,並不必定Accessed=1的頁就會移動到活動匿名頁lru鏈表中,下面咱們會細說。
下面咱們就會詳細說明lru鏈表是怎麼進行更新的,這裏咱們必須分開說明匿名頁lru鏈表的更新以及文件頁lru鏈表的更新操做,雖然它們的更新操做是同時發生的,可是它們的不少判斷是很不同的,這裏咱們先說匿名頁lru鏈表的更新,在說明時,默認頁不會在此期間被mlock()鎖在內存中(由於這樣此頁就必須拿出本來的lru鏈表,加入到LRU_UNEVICTABLE鏈表中)。須要明確,以前說了,活動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個,而後對掃描的頁的狀態進行相應的處理,對於不一樣狀態的頁進行不一樣的處理,處理標準以下:
圖示以下:
這當中還有一件很巧妙的事,以前說lru緩存時有一種專門處理是將非活動匿名頁lru鏈表中的頁移動到非活動匿名頁lru鏈表末尾的,這個的使用狀況就是針對那些正在回寫的頁的,從上圖能夠看到,正在回寫的頁被移動到了非活動匿名頁lru鏈表,而且會在頁描述符中置位PG_reclaim,當塊層回寫完成後,若是此頁的PG_reclaim置位了,則將此頁移動到非活動匿名頁lru鏈表的末尾,這樣在下次一輪內存回收時,這些頁將會優先獲得掃描,而且更容易釋放回收。這裏正在回寫的頁都是正在回寫到swap分區的頁,由於在回收過程當中,只有回寫完成的頁纔可以釋放。
文件頁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鏈表中的頁的處理以下:
這裏須要注意,當文件頁從活動文件頁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鏈表時會不一樣,以下就是最近總結出來的:
因爲能力有限,沒能總結出直接加入到活動文件頁lru鏈表中的新頁,可是這種頁是存在的。
須要注意,這些頁並非在建立的時候就會生成,須要考慮寫時複製。
遺留問題:
1.當一個新進程裝載到內存中時,活動文件頁lru鏈表與非活動文件頁lru鏈表都會增長,經過pmap -d查看應該都是映射了可執行文件的頁,可是這些頁中哪些頁加入活動文件頁lru鏈表,哪些頁加入非活動lru鏈表