一说起内存回收,就想起来以前看内存回收源码时,一脸懵逼、头脑发胀,shrink_zone那些函数看了一遍又一遍就是看不懂,自信心第一次受到严重打击。内存回收源码涉及的知识点太多:lru缓存、lru链表、page cache、脏页回写、伙伴系统、page映射、内存分配、swap页交换、逆向映射等等,基本跟内存管理扯上关系的知识点都有涉及到。需要有个循序渐进的学习方法,才能一点点搞懂内存回收的原理。
在内存紧张时,就会尝试回收文件页和匿名页page。大部分系统默认应该都没有配置swap交换,此时内存回收并不会涉及到swap页交换。内存回收大部分针对文件页page,本文主要谈谈内存回收涉及的lru缓存、lru链表、文件页内存回收这几个知识点,希望读者看完后能对内存回收有个整体框架的理解,知道它是怎么运作的,学习到内存回收的核心流程。内核源码基于3.10.96,详细源码注释见GitHub - dongzhiyan-stack/kernel-code-comment: 3.10.96 内核源代码注释。 在看本文前最好看下前一篇讲解page状态变化的文章《PageDirty、PageWriteback、PageReclaim、PageReferenced、PageUptodate等page的各个状态源码讲解》
1 lru缓存与lru链表
本节先简单介绍lru缓存与lru链表相关数据结构,然后介绍二者的联系和作用。
struct pagevec表示lru缓存,它的成员struct page *pages[]保存page指针,成员nr表示该结构目前保存了多少个page指针。
- #define PAGEVEC_SIZE 14
- struct pagevec {
- unsigned long nr;//表示当前lru缓存保存了多少个page
- unsigned long cold;
- //lru缓存中的page指针存放在该pages[]数组
- struct page *pages[PAGEVEC_SIZE];
- };
列一下常见的lru缓存成员lru_add_pvecs、lru_rotate_pvecs、lru_deactivate_pvecs,不同内核版本略有差异,定义在mm/swap.c中
- //新的page先添加到lru_add_pvecs,见__lru_cache_add()
- static DEFINE_PER_CPU(struct pagevec[NR_LRU_LISTS], lru_add_pvecs);
- //该链表page将来是被添加到inactive lru链表尾,见rotate_reclaimable_page()
- static DEFINE_PER_CPU(struct pagevec, lru_rotate_pvecs);
- //该链表page将来被添加到inactive lru链表头,见deactivate_page()
- static DEFINE_PER_CPU(struct pagevec, lru_deactivate_pvecs);
struct lruvec是lru链表结构,如下:
- struct lruvec {
- //5类lru链表:LRU_INACTIVE_ANON、LRU_ACTIVE_ANON、LRU_INACTIVE_FILE、LRU_ACTIVE_FILE、LRU_UNEVICTABLE
- struct list_head lists[NR_LRU_LISTS];
- struct zone_reclaim_stat reclaim_stat;//update_page_reclaim_stat()中增加
- #ifdef CONFIG_MEMCG
- struct zone *zone;
- #endif
- };
它的成员struct list_head lists[NR_LRU_LISTS]是个结构体指针数组,都是lru链表,依次保存LRU_INACTIVE_ANON、LRU_ACTIVE_ANON、LRU_INACTIVE_FILE、LRU_ACTIVE_FILE、LRU_UNEVICTABLE5这5类page指针。LRU_INACTIVE_ANON和LRU_ACTIVE_ANON这两个lru链表分别保存inactive/active 匿名页(应用程序堆栈、tmpfs、shmem等分配的内存页),LRU_INACTIVE_FILE和LRU_ACTIVE_FILE这两个lru链表分别保存inactive/active 文件页。
当新分配一个匿名页/文件页 page,是先添加到 inactive anon/file lru链表,然后随着page被访问,这个page将被移动到avtive anon/file lru链表。anon是匿名页的简称,file是文件页的简称。内存回收过程将page将会在inactive anon/file lru和avtive anon/file lru链表之间来回转换,比如从avtive anon/file lru链表转移page到inactive anon/file lru链表。有一点需要强调,内存回收肯定是从inactive anon/file lru链表尾取出page,看这个page能否回收。下文会详细介绍这些知识点。
struct lruvec是stuct zone和struct mem_cgroup_per_zone的成员。stuct zone就是常见的dma、normal、high内存zone,struct mem_cgroup_per_zone是memory cgroup的引入的概念。我的理解是,它用来管理memory cgroup在dma、normal、high这几个内存zone分配的内存page,在内存回收查找lruvec时会用到。这里需要说明的一点是,当向lru链表lruvec中添加page时,需要先执行spin_lock_irqsave(&zone->lru_lock, flags)加锁,因为要修改lruvec的struct list_head lists[]链表。
以内核读文件时执行的do_generic_file_read函数为例。该函数里,如果对应文件索引index的文件页还没有分配page结构,则会执行page = page_cache_alloc_cold(mapping)类似函数分配一个struct page结构,然后执行add_to_page_cache_lru(page, mapping,index, GFP_KERNEL)函数把该page添加到lru缓存。看下add_to_page_cache_lru()函数的源码:
- int add_to_page_cache_lru(struct page *page, struct address_space *mapping,
- pgoff_t offset, gfp_t gfp_mask)
- {
- int ret;
- //page按照索引index添加到radix tree
- ret = add_to_page_cache(page, mapping, offset, gfp_mask);
- if (ret == 0)
- // 把page添加到当前cpu的lru缓存page链表,LRU_INACTIVE_FILE属性的
- lru_cache_add_file(page);
- return ret;
- }
首先执行add_to_page_cache()把page按照索引添加到radix tree,然后执行lru_cache_add_file()把该page添加到lru缓存或者lru链表。
- //把page添加到当前cpu的lru缓存page链表,LRU_INACTIVE_FILE属性的
- static inline void lru_cache_add_file(struct page *page)
- {
- __lru_cache_add(page, LRU_INACTIVE_FILE);
- }
- //尝试把page添加到lru_add_pvecs[lru]这个lru缓存,lru缓存满了则把lru缓存的page添加到lru链表
- void __lru_cache_add(struct page *page, enum lru_list lru)
- {
- //得到当前cpu的lru_add_pvecs链表上lru编号指定属性的lru缓存pagevec
- struct pagevec *pvec = &get_cpu_var(lru_add_pvecs)[lru];
- page_cache_get(page);
- //如果当前cpu lru缓存已经存放了14个page,满了,先把这14个page移动到lru链表,下边再把这个新的page添加到lru缓存
- if (!pagevec_space(pvec))
- __pagevec_lru_add(pvec, lru);
- //把page添加到lru缓存链表,其实只是把page指针保存到pvec->pages[]数组而已,即pvec->pages[pvec->nr++] = page
- pagevec_add(pvec, page);
- put_cpu_var(lru_add_pvecs);
- }
__lru_cache_add()函数是重点,if (!pagevec_space(pvec))是判断lru缓存是否已经保存了14个page指针。如果是则执行__pagevec_lru_add()把lru缓存的14个page移动到lru链表。__lru_cache_add()函数最后执行pagevec_add(pvec, page)把当前的page添加到lru缓存。
到这里,lru缓存和lru链表到底是什么关系?lru缓存是per cpu变量,当内核为文件页或匿名页分配page结构,原本是要把这些page添加到inactive file/anon lru链表的(新分配的page先添加到inactive lru链表)。但实际是先把这些page指针添加到本地cpu的lru缓存,然后等本次cpu的lru缓存添加够14个page指针,才把这14 page指针一次性移动到lru链表。
lru链表是多核CPU共享的,当向lru链表添加page,需要执行spin_lock_irqsave(&zone->lru_lock, flags)加锁。如果多进程向同一个lru链表频繁添加/剔除page,加锁操作很影响性能,这种情况很常见。因此引出了lru缓存,它是个per cpu变量,每个cpu独一个。当向lru链表添加page时,先添加到本地cpu lru缓存,向lru缓存添加够14个page再一次性把这14个page移动到lru链表,这样可以避免频繁的spin_lock_irqsave(&zone->lru_lock, flags)加锁。
这里还要啰嗦一下__lru_cache_add->__pagevec_lru_add函数,将lru缓存中的14个page移动到inactive lru链表。
- void __pagevec_lru_add(struct pagevec *pvec, enum lru_list lru)
- {
- VM_BUG_ON(is_unevictable_lru(lru));
- //把pvec这个lru缓存中14个page添加到对应lru链表,增加page统计计数,lru是active anon/file
- pagevec_lru_move_fn(pvec, __pagevec_lru_add_fn, (void *)lru);
- }
- 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;
- //遍历lru缓存上的PAGEVEC_SIZE个page
- for (i = 0; i < pagevec_count(pvec); i++) {
- //依次取出lru缓存保存的page
- struct page *page = pvec->pages[i];
- struct zone *pagezone = page_zone(page);
- //上一个page和这个page不属于同一个zone,则要把上个zone的lock锁解除再把当前page所在zone加锁
- if (pagezone != zone) {
- if (zone)
- spin_unlock_irqrestore(&zone->lru_lock, flags);
- zone = pagezone;
- spin_lock_irqsave(&zone->lru_lock, flags);
- }
- //根据page返回lruvec
- lruvec = mem_cgroup_page_lruvec(page, zone);
- //设置page属性,把page添加到对应属性的lru链表(active/inactive file/anon),mem cgroup中增加page个数统计,增加lru链表中page计数。常见的move_fn函数是__pagevec_lru_add_fn/__activate_page/pagevec_move_tail_fn
- (*move_fn)(page, lruvec, arg);
- }
- if (zone)
- spin_unlock_irqrestore(&zone->lru_lock, flags);
- release_pages(pvec->pages, pvec->nr, pvec->cold);
- pagevec_reinit(pvec);
- }
在pagevec_lru_move_fn()函数里调用move_fn函数指针(我们以__pagevec_lru_add_fn为例),__pagevec_lru_add_fn函数将lru缓存中的14个page添加到inactive/active file/anon lru链表。源码如下:
- //设置page的lru和active属性,把page添加到lru编号指定属性的lru链表,mem cgroup中增加page个数统计,增加lru编号指定属性的page计数
- static void __pagevec_lru_add_fn(struct page *page, struct lruvec *lruvec,
- void *arg)//arg就是lru的active、inactive、file、anon属性编号
- {
- enum lru_list lru = (enum lru_list)arg;
- int file = is_file_lru(lru);//文件页?匿名页?
- int active = is_active_lru(lru);//active?inactive?
- //设置page的lru属性
- SetPageLRU(page);
- if (active)//设置page的active属性
- SetPageActive(page);
- //把page添加到lru编号指定属性的lru链表,mem cgroup中增加page个数统计,增加lru编号指定属性的page计数
- add_page_to_lru_list(page, lruvec, lru);
- update_page_reclaim_stat(lruvec, file, active);
- }
add_page_to_lru_list函数具体将一个page指针添加到lru链表lruvec->lists[LRU_INACTIVE_FILE]。
- //把page添加到lru编号指定属性的lru链表,mem cgroup中增加page个数统计,增加lru编号指定属性的page计数
- 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);
- //mem cgroup中增加page个数统计
- mem_cgroup_update_lru_size(lruvec, lru, nr_pages);
- //把page添加到指定属性的lru链表,如lruvec->lists[LRU_INACTIVE_FILE]
- list_add(&page->lru, &lruvec->lists[lru]);
- //增加lru编号指定属性的page计数
- __mod_zone_page_state(lruvec_zone(lruvec), NR_LRU_BASE + lru, nr_pages);
- }
如上显示的是将一个page添加到lru缓存(lru_add_pvecs)或者lru链表(lruvec->lists[])的过程。文件页或者匿名页page添加到lru缓存或者lru链表过程都是类似的,可以看下lru_cache_add_anon()和lru_cache_add_file()函数,分别演示新分配的匿名页和文件页page添加到lru缓存和lru链表。
2 内存回收分析
在内核alloc_pages直接分配内存失败,就会触发直接内存内存,也会唤醒kswapd进程进行内存回收,依据剩下的内存跟内存zone水位water+内核预留内存的关系。还有一种情况是容器场景,容器的内存消耗大于容器memory cgroup内存上限,也会触发内存回收(mem_cgroup_reclaim->try_to_free_mem_cgroup_pages->do_try_to_free_pages)。不管是何种形式的内存回收,最后都会执行shrink_zone()回收文件页和匿名页page,当然也会执行shrink_slab()回收slab内存。本文主要讲解shrink_zone()回收文件页page,相对来说会简单点。先把内存整体流程图贴下,内存回收核心函数shrink_inactive_list()的流程图随着源码讲解再贴出。
2.1 shrink_zone、shrink_lruvec、shrink_list 源码讲解
- static void shrink_zone(struct zone *zone, struct scan_control *sc)
- {
- unsigned long nr_reclaimed, nr_scanned;
- do {
- struct mem_cgroup *root = sc->target_mem_cgroup;
- struct mem_cgroup_reclaim_cookie reclaim = {
- .zone = zone,
- .priority = sc->priority,
- };
- struct mem_cgroup *memcg;
- //初始回收page数
- nr_reclaimed = sc->nr_reclaimed;
- //初始扫描page数
- nr_scanned = sc->nr_scanned;
- //非memory cgroup内存回收,返回的应该是root_mem_cgroup,管理的是内存zone所有的page。否则返回的memcg管理的page只是该memory cgroup在该内存zone分配的内存page
- memcg = mem_cgroup_iter(root, NULL, &reclaim);
- do {
- struct lruvec *lruvec;
- //返回页lru链表结构lruvec,从该lruvec的lru链表取出文件页/匿名页 进行内存回收
- lruvec = mem_cgroup_zone_lruvec(zone, memcg);
- shrink_lruvec(lruvec, sc);
- /*如果不是全局内存回收,并且回收的page数达到预期,则if成立break,否则下边mem_cgroup_iter()返回下一个memcg。如果是全局内存回收,if不成立,直接执行下边的mem_cgroup_iter()返回该zone的下一个memcg*/
- if (!global_reclaim(sc) &&
- sc->nr_reclaimed >= sc->nr_to_reclaim) {
- mem_cgroup_iter_break(root, memcg);
- break;
- }
- //返回下一个mem cgroup
- memcg = mem_cgroup_iter(root, memcg, &reclaim);
- } while (memcg);
- vmpressure(sc->gfp_mask, sc->target_mem_cgroup,
- sc->nr_scanned - nr_scanned,
- sc->nr_reclaimed - nr_reclaimed);
- //sc->nr_reclaimed - nr_reclaimed本轮循环回收的page数,sc->nr_scanned - nr_scanned是本轮循环扫描page数。这是判断内存回收是否足够,足够则结束内存回收
- } while (should_continue_reclaim(zone, sc->nr_reclaimed - nr_reclaimed,
- sc->nr_scanned - nr_scanned, sc));
- }
该函数首先找到lru链表数据结构lruvec,然后执行shrink_lruvec()开始扫描该lruvec结构inactive file、active file、active anon、inactive anon 这4个lru链表上非活动文件页page、活动文件页page、活动匿名页page、非活动匿名页page,看那个page可以被释放掉。本文主要讲解inactive file/active file 链表上的非活动文件页page、活动文件页page的内存回收。
说下struct scan_control结构,是内存回收必然要传递的一个结构。看下kswapd内存回收传递的scan_control结构。
- struct scan_control sc = {
- .gfp_mask = GFP_KERNEL,
- .may_writepage= 1,//为0则不能将page脏数据回写到磁盘,应该包含文件页和匿名页page
- .may_unmap = 0,//为0表示不能回收mmap的文件页
- .may_swap = 1,//为0不会对匿名页进行回收
- .nr_to_reclaim = ULONG_MAX,
- .order = order,//内存分配时的order
- .target_mem_cgroup = NULL,//非memory cgroup内存回收
- };
进入shrink_lruvec函数,先执行get_scan_count(lruvec, sc, nr)计算要扫描的各个lru链表(active/inactive anon/file lru链表)的page数,并把要扫描的lru链表的page数保存到nr[]数组,然后循环执行shrink_list()函数扫描、回收active/inactive anon/file lru链表的page。
- static void shrink_lruvec(struct lruvec *lruvec, struct scan_control *sc)
- {
- unsigned long nr[NR_LRU_LISTS];
- unsigned long nr_to_scan;
- enum lru_list lru;
- unsigned long nr_reclaimed = 0;
- unsigned long nr_to_reclaim = sc->nr_to_reclaim;
- struct blk_plug plug;
- //计算本次要扫描的active/inactive anon/file lru链表的page数保存到nr[]数组
- get_scan_count(lruvec, sc, nr);
- blk_start_plug(&plug);
- //一直循环,直到预期要扫描的inactive anon、active file、inactive file lru链表上的page全扫描过
- while (nr[LRU_INACTIVE_ANON] || nr[LRU_ACTIVE_FILE] ||
- nr[LRU_INACTIVE_FILE]) {
- //for循环,lru依次是:LRU_INACTIVE_ANON LRU_ACTIVE_ANON LRU_INACTIVE_FILE LRU_ACTIVE_FILE
- for_each_evictable_lru(lru) {
- //不为0表示预期的page数还没扫描完
- if (nr[lru]) {
- //本轮要扫描的page数,最多32个
- nr_to_scan = min(nr[lru], SWAP_CLUSTER_MAX);
- //nr[lru]减去扫描的page数
- nr[lru] -= nr_to_scan;
- //内存回收在这里
- nr_reclaimed += shrink_list(lru, nr_to_scan,
- lruvec, sc);
- }
- }
- //回到page数大于预期,并且设定的优先级小于DEF_PRIORITY,结束内存回收
- if (nr_reclaimed >= nr_to_reclaim &&
- sc->priority < DEF_PRIORITY)
- break;
- }
- //启动block io派发,这里才会令page页数据刷入磁盘
- blk_finish_plug(&plug);
- //sc->nr_reclaimed累加回收page数
- sc->nr_reclaimed += nr_reclaimed;
- //如果inactive anon lru链表上的page太少
- if (inactive_anon_is_low(lruvec))
- //从active anon lru链表移动page到inactive anon lru链表
- shrink_active_list(SWAP_CLUSTER_MAX, lruvec,
- sc, LRU_ACTIVE_ANON);
- //太多脏页则休眠
- throttle_vm_writeout(sc->gfp_mask);
- }
直接进入shrink_list()函数
- static unsigned long shrink_list(enum lru_list lru, unsigned long nr_to_scan,
- struct lruvec *lruvec, struct scan_control *sc)
- {
- //lru是active anon或active file时,就是说lru代表avtive lru链表时if才成立
- if (is_active_lru(lru)) {
- //inactive anon/file lru链表上的page数太少
- if (inactive_list_is_low(lruvec, lru))
- //从active anon/file lru链表向inactive anon/file lru链表头移动page
- shrink_active_list(nr_to_scan, lruvec, sc, lru);
- //注意,lru是active anon/file时,直接在这里返回0。内存回收只是针对inactive file/anon
- return 0;
- }
- //lru是inactive anon/file时,从inactive file/anon lru链表尾部扫描page,尝试回收内存
- return shrink_inactive_list(nr_to_scan, lruvec, sc, lru);
- }
注意,shrink_list()函数会在shrink_lruvec()函数中被多次调用,只是shrink_list()传入的struct lruvec *lruvec 不同,可能依次代表inactive file、active file、active anon、inactive anon 这4个lru链表。当代表active file、active anon时,if (is_active_lru(lru))成立,接着就会执行if (inactive_list_is_low(lruvec, lru))判断inactive anon/file lru链表上的page数是否太少,是的话则执行shrink_active_list(nr_to_scan, lruvec, sc, lru)尝试从active anon/file lru链表向inactive anon/file lru链表头移动一定数目的page,然后就return 0返回了。内存回收并不会尝试对active file、active anon lru链表上的page尝试回收,只会对inactive anon/file lru链表上的page进行回收,再执行shrink_inactive_list()函数完成回收。
2.2 shrink_active_list讲解
先看下shrink_active_list()函数源码:
- static void shrink_active_list(unsigned long nr_to_scan,
- struct lruvec *lruvec,
- struct scan_control *sc,
- enum lru_list lru)//此时lru肯定是LRU_ACTIVE_ANON或者LRU_ACTIVE_FILE
- {
- unsigned long nr_taken;
- unsigned long nr_scanned;
- unsigned long vm_flags;
- LIST_HEAD(l_hold); /* The pages which were snipped off */
- LIST_HEAD(l_active);
- LIST_HEAD(l_inactive);
- struct page *page;
- struct zone_reclaim_stat *reclaim_stat = &lruvec->reclaim_stat;
- unsigned long nr_rotated = 0;
- isolate_mode_t isolate_mode = 0;
- int file = is_file_lru(lru);//返回值是 LRU_INACTIVE_FILE 或 LRU_ACTIVE_FILE
- struct zone *zone = lruvec_zone(lruvec);
- //把cpu本地所有lru缓存上的page移动到 active/inactive lru链表
- lru_add_drain();
- if (!sc->may_unmap)
- isolate_mode |= ISOLATE_UNMAPPED;
- if (!sc->may_writepage)
- isolate_mode |= ISOLATE_CLEAN;
- spin_lock_irq(&zone->lru_lock);
- //根据isolate_mode隔离条件,依次尝试将active anon/file lru链表尾巴上符合隔离条件的page移动到l_hold链表,返回值是成功隔离的page数
- nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &l_hold,
- &nr_scanned, sc, isolate_mode, lru);
- if (global_reclaim(sc))
- zone->pages_scanned += nr_scanned;
- reclaim_stat->recent_scanned[file] += nr_taken;//累加隔离的page数
- __count_zone_vm_events(PGREFILL, zone, nr_scanned);
- __mod_zone_page_state(zone, NR_LRU_BASE + lru, -nr_taken);//LRU_ACTIVE_ANON或LRU_ACTIVE_FILE lru链表的page数减少nr_taken个
- __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, nr_taken);//隔离的page数增加nr_taken
- spin_unlock_irq(&zone->lru_lock);
- //遍历l_hold隔离出来的page,把符合条件的page移动l_inactive临时链表
- while (!list_empty(&l_hold)) {
- cond_resched();
- page = lru_to_page(&l_hold);
- list_del(&page->lru);
- if (unlikely(!page_evictable(page))) {//page不可回收返回0
- putback_lru_page(page);//把page添加到unevictable lru list
- continue;
- }
- //bh太多了,应该就是说文件缓存page太多了
- if (unlikely(buffer_heads_over_limit)) {
- //if成立条件 1:page有PAGE_FLAGS_PRIVATE标记 2:对page加锁成功
- if (page_has_private(page) && trylock_page(page)) {
- if (page_has_private(page))
- try_to_release_page(page, 0);//释放page
- unlock_page(page);//清理page的lock标记,并唤醒在page PG_locked等待队列的休眠的进程
- }
- }
- //如果page最近被访问过
- if (page_referenced(page, 0, sc->target_mem_cgroup,
- &vm_flags)) {
- nr_rotated += hpage_nr_pages(page);
- //该page是elv文件代码段映射
- if ((vm_flags & VM_EXEC) && page_is_file_cache(page)) {
- list_add(&page->lru, &l_active);//把page移动到临时l_active链表
- continue;
- }
- }
- //到这里,page最近没被访问过,则把page先移动到临时l_inactive
- ClearPageActive(page); /* we are de-activating */
- list_add(&page->lru, &l_inactive);
- }
- spin_lock_irq(&zone->lru_lock);
- reclaim_stat->recent_rotated[file] += nr_rotated;//最近被访问过的page
- //把l_active链表上的page移动到 active file/anon lru链表,如果page的引用计数是1,则把page移动到l_hold链表,马上就释放掉该page
- move_active_pages_to_lru(lruvec, &l_active, &l_hold, lru);
- //把l_inactive链表上的page移动到 inactive file/anon lru链表,如果page的引用计数是1,则把page移动到l_hold链表,马上就释放掉该page
- move_active_pages_to_lru(lruvec, &l_inactive, &l_hold, lru - LRU_ACTIVE);
- //不可回收page减少nr_taken
- __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, -nr_taken);
- spin_unlock_irq(&zone->lru_lock);
- //释放l_hold上的page到伙伴系统
- free_hot_cold_page_list(&l_hold, 1);
- }
总结一下该函数关键步骤:
1 调用lru_add_drain()把所有lru缓存上的page移动的lru链表
2 先调用isolate_lru_pages()把active anon/file lru链表尾巴上符合隔离条件的page移动到l_hold临时链表。
3 然后再依次判断l_hold上的这些page最近是否被访问过、是否是代码段映射的page。如果是则把page移动到l_active临时链表。如果不是则把page移动到l_inactive链表。
4 执行move_active_pages_to_lru()函数把l_active临时链表上的page移动到active file/anon lru链表头,把l_inactive临时链表上的page移动到inactive file/anon lru链表头。如果移动过程page引用计数是1,则再把page移动到l_hold临时链表。
5 执行free_hot_cold_page_list(&l_hold, 1)把l_hold临时链表上的page释放到伙伴系统。
总体来说,先遍历active file/anon lru链表上的page,先把符合条件的page隔离到l_hold链表。接着,如果这些page最近被访问过等原因,再还会再把该page移动到active file/anon lru链表头,否则把page移动到inactive file/anon lru链表头。如果这些page引用计数只有1,则会把该page释放会伙伴系统。
2.3 shrink_inactive_list函数讲解
接着看内存回收的关键函数shrink_inactive_list()函数:
- static noinline_for_stack unsigned long
- shrink_inactive_list(unsigned long nr_to_scan, struct lruvec *lruvec,
- struct scan_control *sc, enum lru_list lru)
- {
- LIST_HEAD(page_list);
- unsigned long nr_scanned;
- unsigned long nr_reclaimed = 0;
- unsigned long nr_taken;
- unsigned long nr_dirty = 0;
- unsigned long nr_writeback = 0;
- isolate_mode_t isolate_mode = 0;
- int file = is_file_lru(lru);//lru是LRU_INACTIVE_FILE 或 LRU_ACTIVE_FILE时返回1
- struct zone *zone = lruvec_zone(lruvec);
- struct zone_reclaim_stat *reclaim_stat = &lruvec->reclaim_stat;
- while (unlikely(too_many_isolated(zone, file, sc))) {
- congestion_wait(BLK_RW_ASYNC, HZ/10);
- if (fatal_signal_pending(current))
- return SWAP_CLUSTER_MAX;
- }
- //把cpu本地所有lru缓存上的page移动到 active/inactive lru链表
- lru_add_drain();
- if (!sc->may_unmap)
- isolate_mode |= ISOLATE_UNMAPPED;//只隔离没有映射过的page
- if (!sc->may_writepage)
- isolate_mode |= ISOLATE_CLEAN;//只隔离干净页,不隔离脏页或者正在脏页回写的页
- spin_lock_irq(&zone->lru_lock);
- /*根据isolate_mode隔离条件,依次尝试将inactive anon/file lru链表尾巴上符合隔离条件的page移动到page_list临时链表。返回值nr_taken是成功隔离的page数。注意,是从inactive anon/file lru链表隔离page并添加到page_list临时链表*/
- nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &page_list,
- &nr_scanned, sc, isolate_mode, lru);//nr_scanned是扫描page数
- //inactive file/anon lru链表上的page减去nr_taken个
- __mod_zone_page_state(zone, NR_LRU_BASE + lru, -nr_taken);
- //隔离的page数增加nr_taken
- __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, nr_taken);
- if (global_reclaim(sc)) {//全局内存回收
- zone->pages_scanned += nr_scanned;//累加扫描page数nr_scanned
- if (current_is_kswapd())//kswapd进程的内存回收
- __count_zone_vm_events(PGSCAN_KSWAPD, zone, nr_scanned);
- else//直接内存回收
- __count_zone_vm_events(PGSCAN_DIRECT, zone, nr_scanned);
- }
- spin_unlock_irq(&zone->lru_lock);
- if (nr_taken == 0)//隔离page数是0直接返回
- return 0;
- //在这里真正尝试内存回收
- nr_reclaimed = shrink_page_list(&page_list, zone, sc, TTU_UNMAP,
- &nr_dirty, &nr_writeback, false);
- spin_lock_irq(&zone->lru_lock);
- reclaim_stat->recent_scanned[file] += nr_taken;
- ...........
- //把page_list中的page按照条件移动到对应的lru链表,如果前边的shrink_page_list()函数对page_list链表上某些page标记Active(比如page最近被访问了),则把该page移动active lru链表头,否则移动到inactive lru链表头
- putback_inactive_pages(lruvec, &page_list);
- //更新file/anon NR_ISOLATED_ANON的page个数
- __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, -nr_taken);
- spin_unlock_irq(&zone->lru_lock);
- //这里把page_list链表还残留的page释放到伙伴系统,1表示释放到冷页,这里的page->_count应用计数应该是0
- free_hot_cold_page_list(&page_list, 1);
- //如果内存回收的page,全都在进行脏页回写,则执行wait_iff_congested(),可能会休眠
- if (nr_writeback && nr_writeback >=
- (nr_taken >> (DEF_PRIORITY - sc->priority)))
- //如果bdi和zone同时拥堵,则休眠100ms
- wait_iff_congested(zone, BLK_RW_ASYNC, HZ/10);
- ...........
- return nr_reclaimed;
- }
shrink_inactive_list()函数首先执行isolate_lru_pages()函数:根据isolate_mode隔离条件,依次尝试将inactive anon/file lru链表尾巴上符合隔离条件的page移动到page_list临时链表。到这里可能读者会有疑惑,前边执行shrink_active_list()函数,不是已经将active lru链表上的page刚执行isolate_lru_pages()隔离一次,然后把隔离出来符合条件的page移动到inactive anon/file lru链表头。现在inactive anon/file lru链表上的page也执行isolate_lru_pages()尝试隔离到page_list临时链表,此时隔离出来的page有一部分应该是刚从active lru链表隔离出来并移动到inactive anon/file lru链表的page。
接着执行shrink_inactive_list()函数尝试对page_list临时链表上的page尝试内存回收,该链表上的page并不能全部被回收,如果某各page正好又被访问了等因素,则就要令该page停留在page_list临时链表。shrink_inactive_list()函数继续执行putback_inactive_pages(lruvec, &page_list)函数,将page_list中残留的page按照条件移动到对应的lru链表,如果该page最近被访问了,则该page已经被标记了” Active”,该page就会被移动active lru链表。否则该page依然被移动到inactive lru链表。最后执行free_hot_cold_page_list(&page_list, 1)函数,把page_list链表依然还残留的page释放到伙伴系统,这种page的引用计数是0,可以被释放。
好的,shrink_inactive_list()函数大体流程已经讲解完了,接着重点讲解该函数里的shrink_page_list()函数,它才是内存回收最重要的函数。
2.4 shrink_page_list函数讲解
这个函数真的复杂,我们只讲解与文件页内存回收那部分,尽可能多举例,源码有删减。
- static unsigned long shrink_page_list(struct list_head *page_list,
- struct zone *zone,//page_list是inactive anon/file lru链表扫描出来符合内存回收条件的page
- struct scan_control *sc,
- enum ttu_flags ttu_flags,
- unsigned long *ret_nr_dirty,
- unsigned long *ret_nr_writeback,
- bool force_reclaim)
- {
- LIST_HEAD(ret_pages);
- LIST_HEAD(free_pages);
- int pgactivate = 0;
- unsigned long nr_dirty = 0;
- unsigned long nr_congested = 0;
- unsigned long nr_reclaimed = 0;
- unsigned long nr_writeback = 0;
- while (!list_empty(page_list)) {
- struct address_space *mapping;
- struct page *page;
- int may_enter_fs;//是否允许文件系统类的IO操作
- enum page_references references = PAGEREF_RECLAIM_CLEAN;
- //从page_list链表尾取出page
- page = lru_to_page(page_list);
- list_del(&page->lru);
- sc->nr_scanned++;//扫描page数加1
- //如果本次内存回收设定 不能回收mmap的page,但该page却是,则不能回收
- if (!sc->may_unmap && page_mapped(page))
- goto keep_locked;
- //如果page正在进行脏页回写
- if (PageWriteback(page)) {
- //if成立条件 1:如果是全局内存回收 2:page没有被设置Reclaim内存回收标记 3:may_enter_fs为0,即不进行IO操作
- if (global_reclaim(sc) || !PageReclaim(page) || !may_enter_fs) {
- //设置page Reclaim内存回收标记
- SetPageReclaim(page);
- //nr_writeback加1
- nr_writeback++;
- goto keep_locked;//跳到keep_locked分支,遍历下一个page
- }
- //在page PG_writeback等待队列上休眠,等待该page脏页回写完成
- wait_on_page_writeback(page);
- }
- if (!force_reclaim)//shrink_inactive_list触发的内存回收,force_reclaim是false,这是非强制内存回收
- references = page_check_references(page, sc);//检查最近是否被访问
- /*基本上只要page最近被访问过,返回PAGEREF_ACTIVATE,page将被移动到active list,或者返回PAGEREF_KEEP,本轮内存回收不回收它。如果page最近没被访问过,返回PAGEREF_RECLAIM或PAGEREF_RECLAIM_CLEAN,该page将被回收*/
- switch (references) {//非强制内存回收时成立
- case PAGEREF_ACTIVATE: //page移动到active lru链表,不回收它
- goto activate_locked;
- case PAGEREF_KEEP: //page保持在inactive lru链表,不回收它
- goto keep_locked;
- case PAGEREF_RECLAIM: //page要内存回收
- case PAGEREF_RECLAIM_CLEAN: //page要内存回收
- ; /* try to reclaim the page below */
- }
- /*有多个进程映射了此内存page,尝试解除映射。对匿名页和文件页的处理不一样,对于文件页的处理是清空进程的页表项,每清空一个进程的映射page->_count减1,这里边还牵涉到反向映射。对于文件页,进程mmap文件映射该page时,page_mapped(page)才成立。正常文件读写产生的page cache,没有进程映射吧,page_mapped(page)不成立*/
- if (page_mapped(page) && mapping) {
- switch (try_to_unmap(page, ttu_flags)) {
- case SWAP_FAIL:
- goto activate_locked;
- case SWAP_AGAIN:
- goto keep_locked;
- case SWAP_MLOCK:
- goto cull_mlocked;
- case SWAP_SUCCESS://这里成功解除映射了此page的进程
- ; /* try to free the page below */
- }
- }
- //如果page是脏页
- if (PageDirty(page)){
- nr_dirty++;//脏页数加1
- /*只有kswapd内存回收可以触发脏页刷回磁盘,目的是避免栈溢出。并且只有脏页太多kswapd进程才会触发脏页刷回磁盘*/
- if (page_is_file_cache(page) &&//如果page是文件缓存
- (!current_is_kswapd() ||//并且不是kswapd触发的内存回收
- sc->priority >= DEF_PRIORITY - 2)) {
- //增加NR_VMSCAN_IMMEDIATE的page数,优先回收的page
- inc_zone_page_state(page, NR_VMSCAN_IMMEDIATE);
- //设置page的Reclaim标记
- SetPageReclaim(page);
- /*page不参与本次内存回收,等这个page脏页回写完成,软中断回调函数里执行end_page_writeback函数才会清理Reclaim标记,然后把该page再移动inactive lru list链表尾部,等下轮内存就会释放该page*/
- goto keep_locked;
- }
- //如果前边执行page_check_references返回PAGEREF_RECLAIM_CLEAN,说明此页最近没被访问过,但page被置位PG_referenced
- if (references == PAGEREF_RECLAIM_CLEAN)
- goto keep_locked;
- //不允许文件系统类的IO操作,则该page不进行内存回收,goto keep_locked
- if (!may_enter_fs)
- goto keep_locked;
- //不能进行脏页回写操作,则该page不进行内存回收,goto keep_locked
- if (!sc->may_writepage)
- goto keep_locked;
- //把page标记Reclaim,调用文件系统write接口把page数据异步刷入磁盘,这个过程会对page标记"writeback"。只有kswsap进程能这样操作,memcg内存回收不行,直接内存回收也不行
- switch (pageout(page, mapping, sc)) {
- case PAGE_KEEP: //failed to write page out, page is locked。如果bdi拥堵则会返回PAGE_KEEP
- nr_congested++;//拥堵page数加1
- goto keep_locked;//该page本轮不能被回收,又是goto keep_locked
- case PAGE_ACTIVATE: //page不能被回写到,之后该page将被移动到active lru链表
- goto activate_locked;
- case PAGE_SUCCESS://成功调用文件系统write接口把page数据异步刷入磁盘,
- //page数据还没刷入磁盘,刷入磁盘产生中断后才会清除"writeback"标记
- if (PageWriteback(page))
- goto keep;//跟goto keep_locked处理差不多
- //page有脏页标记,应该不太可能吧??????????
- if (PageDirty(page))
- goto keep;//跟goto keep_locked处理差不多
- //尝试对page加锁,如果page之前已经其他进程被加锁则加锁失败返回0,否则当前进程对page加锁成功并返回1
- if (!trylock_page(page))
- goto keep;//跟goto keep_locked处理差不多
- //上锁成功后,再判断一次page是脏页或者正在回写的脏页,如果还是有这些标记goto keep_locked
- if (PageDirty(page) || PageWriteback(page))
- goto keep_locked;
- mapping = page_mapping(page);
- case PAGE_CLEAN:
- ; /* try to free the page below */
- }
- }
- ...........
- //如果page没有进程映射,或者成功page引用计数是2,if不成立,这个page才可以被回收,不理解!!!!!!!!!!
- if (!mapping || !__remove_mapping(mapping, page))
- goto keep_locked;
- //清除page的PG_locked标记
- __clear_page_locked(page);
- free_it:
- nr_reclaimed++;//内存回收成功的page数加1
- //把page添加到free_pages临时链表,下边就要释放free_pages链表上的page到伙伴系统,真正内存回收
- list_add(&page->lru, &free_pages);
- continue;
- cull_mlocked:
- if (PageSwapCache(page))
- try_to_free_swap(page);
- unlock_page(page);
- list_add(&page->lru, &ret_pages);
- continue;
- activate_locked:
- /* Not a candidate for swapping, so reclaim swap space. */
- if (PageSwapCache(page) && vm_swap_full())
- try_to_free_swap(page);
- VM_BUG_ON(PageActive(page));
- //设置page的Active标记,将来该page将被移动到active lru链表
- SetPageActive(page);
- pgactivate++;
- /*goto 到keep_locked这个分支的page,都不参与本轮内存回收,不符合条件。下边先把这个page移动到page_list,该函数返回后,再把page_list上的page按照实际情况移动到 active/inactive file/anon lru list。有些page是脏页,等脏页回写完成,在软中断回调函数再把该page移动inactive lru链表尾。等下次内存回收,优先回收这个page。*/
- keep_locked:
- unlock_page(page);//清除page PG_locked标记,唤醒在page PG_locked等待队列的休眠的进程
- keep:
- //把page移动到ret_pages临时链表,函数最后再把ret_pages上的page移动到page_list
- list_add(&page->lru, &ret_pages);
- }
- //标记dirty的脏页,且所在块设备都是拥堵的(nr_dirty == nr_congested),并且是全局内存回收,if成立,则标记zone拥堵。
- if (nr_dirty && nr_dirty == nr_congested && global_reclaim(sc))
- zone_set_flag(zone, ZONE_CONGESTED);
- //释放free_pages临时链表上的page到伙伴系统
- free_hot_cold_page_list(&free_pages, 1);
- //把ret_pages临时链表上的page移动到page_list链表
- list_splice(&ret_pages, page_list);
- count_vm_events(PGACTIVATE, pgactivate);
- mem_cgroup_uncharge_end();
- //脏页总数
- *ret_nr_dirty += nr_dirty;
- //正在回写的脏页数
- *ret_nr_writeback += nr_writeback;
- //返回内存回收成功的page数
- return nr_reclaimed;
- }
shrink_page_list()循环从page_list链表取出page尝试回收,该函数真的复杂,即便我们只讲解文件页的内存回收!首先,page_list链表上page是从inactive file/ lru链表上隔离出来的,符合内存回收条件的。对于文件页来说,我觉得有以下几种情况:
- 1 mmap与用户空间直接构成文件映射的文件页,这类文件页page内存回收时还得想办法解除文件映射关系。shrink_page_list()函数就有执行page_mapped(page)尝试解除进程用户空间与该page的文件映射关系。
- 2 文件缓存pagecache中的page,这些page没有直接与用户空间buf构成映射。这部分文件页可能是clean的,就是进程读过这些文件页后并没有再对对该文件页有写操作。也有可能是该page有被写过,被标记了脏页”dirty”,或者这个脏页正在回写被标记了” writeback”。可以发现,shrink_page_list()有多次执行PageWriteback(page)、PageDirty(page)判断该page是否有”dirty”标记、” writeback”标记,然后判断该page能否被回收。
继续,对文件页尝试内存回收时,会有多种处理结果
1 goto activate_locked分支。这个文件页page最近被访问过,不能被回收,则goto activate_locked跳到activate_locked分支:执行SetPageActive(page)标记该page“Active”,等从shrink_page_list()返回,执行shrink_inactive_list()函数里的putback_inactive_pages()函数,再把该page移动到active lru链表头。
2 goto keep_locked分支。该page暂时还不能回收,但是可能马上可能会被回收掉,则goto keep_locked跳到keep_locked分支,把page留在page_list链表。等从shrink_page_list()返回,执行shrink_inactive_list()函数里的putback_inactive_pages()函数,可能把该page添加到inactive lru链表头。这个page是脏页,正在被回写,则shrink_page_list()函数里会把该page标记“PageReclaim”。等回写完成,软中断回调函数最后执行end_page_writeback(),清除该page的“writeback”和"Reclaim"标记,再把该page移动到inactive lru链表尾。等下轮内存回收,会优先回收inactive lru链表尾的page。
3 goto free_it分支。该page符合内存回收条件,则goto free_it跳到free_it分支,首先nr_reclaimed++表示成功回收一个page,然后执行list_add(&page->lru, &free_pages)把该page暂时放到free_pages链表,在shrink_page_list()函数最后执行free_hot_cold_page_list(&free_pages, 1),最终把这些符合内存回收条件的page释放会伙伴系统。
内存回收还有其他情况,最后把shrink_inactive_list 和 shrink_page_list两个函数的流程图列下,希望小伙伴能快速理解。
2.5 page 在inactive lru链表和active lru链表之间的移动
在本文最后,总结一下page 在inactive lru链表和active lru链表之间的漂泊过程。
2.5.1 当page被访问触发转移
新分配的一个文件页或者匿名页page,是加入lru缓存或者inactive lru链表。当读写page文件时,其实就是访问page,最后都执行mark_page_accessed(),可能会把page从inactive lru链表移动到active lru链表。整理的几个流程如下:
- vfs_read->...... ->do_generic_file_read->mark_page_accessed
- ext4_readdir->ext4_bread->ext4_getblk->sb_getblk->__getblk->__find_get_block->touch_buffer->mark_page_accessed
还有写文件的过程vfs_write->do_sync_write->ext4_file_write->generic_file_aio_write->generic_file_buffered_write->generic_perform_write,看下源码:
- static ssize_t generic_perform_write(struct file *file,
- struct iov_iter *i, loff_t pos)
- {
- ext4_write_begin->lock_page(page);
- //把write系统调用传入的最新文件数据从用户空间buf复制到page文件页
- copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
- mark_page_accessed(page);//这里标记page最近被访问
- ext4_write_end->unlock_page(page);
- balance_dirty_pages_ratelimited(mapping);//脏页平衡
- }
mark_page_accessed源码如下:
- void mark_page_accessed(struct page *page)
- {
- //page是inactive的、page有"Referenced"标记、page可回收、page在 lru链表
- if (!PageActive(page) && !PageUnevictable(page) &&
- PageReferenced(page) && PageLRU(page)) {
- //把page从inactive lru链表移动到active lru链表
- activate_page(page);
- //清理page的"Referenced"标记
- ClearPageReferenced(page);
- } else if (!PageReferenced(page)) {//page之前没有"Referenced"标记
- SetPageReferenced(page);//设置page的"Referenced"标记
- }
- }
显然,随着page随着被访问的次数增加,page的referenced状态就会发生改变,并且page也会在inactive/active lru链表之间迁移,主要有如下3步:
- 1 page在inactive lru链表且page无Referenced标记,则设置page的Referenced标记。
- 2 page在inactive lru链表且page有Referenced标记,则把page移动到active lru链表,并清理掉Referenced标记
- 3 page在active lru链表且无referenced标记,则把仅仅标记该page的Referenced标记
Referenced标记表示该page被访问了,上边这3步表示了page的3个状态的顺序变迁。一个page在inactive lru链表并且长时间未被访问,第一次有进程访问该page,则只是把page标记Referenced。第2次进程再访问该page,则把该page移动到active lru链表,但清理掉Referenced标记。第3次再有进程访问该page,则标记该page Referenced。如下是转移过程:page在inactive lru(unreferenced)----->page在inactive lru(referenced) ----->page在active lru(unreferenced) ----->page在active lru(referenced)
显然,page被访问的次数越多,page所处的档次越来越高,从最低级的page在inactive lru链表的unreferenced状态到最高级的page在active lru链表的referenced状态。
2.5.2 从active lru链表隔离page后移动到inactive lru链表
内存回收执行到shrink_active_list,先遍历active file/anon lru链表上的page,先把符合条件的page隔离到l_hold链表。接着,如果这些page最近被访问过等原因,则执行move_active_pages_to_lru()函数再把该page移动到active file/anon lru链表头。否则执行move_active_pages_to_lru()函数把page移动到inactive file/anon lru链表头。
2.5.3 shrink_inactive_list回收的page移动到active lru链表
shrink_inactive_list->shrink_page_list函数中,回收从inactive lru链表尾取出的page,如果该page被访问过等原因,不符合内存回收条件,则标记该page “Active”。之后回到shrink_inactive_list函数,执行move_active_pages_to_lru函数将该page移动到active lru链表头。
2.5.4 shrink_inactive_list回收的page移动到inactive lru链表
shrink_inactive_list->shrink_page_list函数中,回收从inactive lru链表尾取出的page,如果该page正在被脏页回写等原因,则保持在inactive lru链表。回到shrink_inactive_list函数,执行move_active_pages_to_lru函数将该page移动到inactive lru链表头。相当于只是从inactive lru链表尾移动到inactive lru链表头。
2.5.5 end_page_writeback将page移动到inactive lru链表尾
当page脏页数据刷回磁盘,产生中断,在软中断回调函数里blk_update_request->bio_endio->ext4_end_bio->ext4_finish_bio->end_page_writeback,执行rotate_reclaimable_page将page移动到inactive lru链表尾(或lru缓存)。等下轮内存回收,优先从inactive lru链表尾取出该page回收到伙伴系统。源码如下
- void end_page_writeback(struct page *page)
- {
- //如果该page被设置了"Reclaim"标记位,
- if (TestClearPageReclaim(page))
- rotate_reclaimable_page(page);
- //清除掉page writeback标记
- if (!test_clear_page_writeback(page))
- BUG();
- smp_mb__after_clear_bit();
- //唤醒在该page的PG_writeback等待队列休眠的进程
- wake_up_page(page, PG_writeback);
- }
- /*内存回收完成后,被标记"reclaimable"的page的数据刷入了磁盘,执行rotate_reclaimable_page->end_page_writeback把该page移动到inactive lru链表尾,下轮内存回收就会释放该page到伙伴系统*/
- void rotate_reclaimable_page(struct page *page)
- {
- //page没有上PG_locked,page不是脏页,page要有acive标记,page没有设置不可回收标记,page要在lru链表
- if (!PageLocked(page) && !PageDirty(page) && !PageActive(page) &&
- !PageUnevictable(page) && PageLRU(page)) {
- struct pagevec *pvec;
- unsigned long flags;
- //page->count ++
- page_cache_get(page);
- local_irq_save(flags);
- //取出本地cpu lru缓存pagevec
- pvec = &__get_cpu_var(lru_rotate_pvecs);
- //先尝试把page添加到本地cpu lru缓存pagevec,如果添加后lru缓存pagevec满了,则把lru缓存pagevec中的所有page移动到inactive lru链表
- if (!pagevec_add(pvec, page))
- pagevec_move_tail(pvec);
- local_irq_restore(flags);
- }
- }