一、LRU的由来
lru的引入主要是和内存回收有关。
属于内核的大部分page是不能够进行回收的,比如内核栈、内核代码段、内核数据段以及大部分内核使用的page,它们都是不能够进行回收的;
相反,进程使用的page,比如进程代码段、进程数据段、进程堆栈、进程访问文件时映射的文件页、进程间共享内存使用的页,这些页框都是可以进行回收的。
那么,可回收的page里面选择那些进行回收呢?有以下几个维度的考虑:
- 时间维度:假设一个页很久没有被访问到了,那么就假设在下一段时间中,这个页也可能不会被访问到,就可以释放掉内存。
- 频次维度:假设一个页面被疯狂频繁的使用,它肯定是一个热页,但是这个页面最近的一次访问时间离现在稍微久了一点点,此时进来大量的页面,这些页面的特点是只会使用一两次,以后将再也不会用到。在这种情况下,如果只从时间维度考虑,这个之前频繁地被疯狂访问的页面就会被置换出去了(本来应该将这些大量一次性访问的页面置换出去的),当这个页面在不久之后要被访问时,此时已经不在内存中了,还需要在重新置换进来,造成性能的损耗。这种现象也叫 Page Thrashing(页面颠簸)。
实际上,上面两种思路在算法上分别对应:
- LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
- LFU(Least Frequently Used)最不经常使用。算法根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
因此,内核考虑了上述两种因素的统计,引入了 active 链表和 inactive 两类链表。工作原理如下:
-
首先 inactive 链表的尾部存放的是访问频率最低并且最少访问的页面,在内存紧张的时候,这些页面被置换出去的优先级是最大的。
-
对于文件页来说,当它被第一次读取的时候,内核会将它放置在 inactive 链表的头部,如果它继续被访问,则会提升至 active 链表的尾部。如果它没有继续被访问,则会随着新文件页的进入,内核会将它慢慢的推到 inactive 链表的尾部,如果此时再次被访问则会直接被提升到 active 链表的头部。大家可以看出此时页面的使用频率这个因素已经被考量了进来。
-
对于匿名页来说,当它被第一次读取的时候,内核会直接将它放置在 active 链表的尾部,注意不是 inactive 链表的头部,这里和文件页不同。因为匿名页的换出 Swap Out 成本会更大,内核会对匿名页更加优待。当匿名页再次被访问的时候就会被被提升到 active 链表的头部。
-
当遇到内存紧张的情况需要换页时,内核会从 active 链表的尾部开始扫描,将一定量的页面降级到 inactive 链表头部,这样一来原来位于 inactive 链表尾部的页面就会被置换出去。
内核在回收内存的时候,这两个列表中的回收优先级为:inactive 链表尾部 > inactive 链表头部 > active 链表尾部 > active 链表头部。
内核主要对进程使用的页进行回收,而回收操作,主要是两个方面:一.直接将一些页释放。二.将页回写保存到磁盘,然后再释放。对于第一种,最明显的就是进程代码段的页,这些页都是只读的,因为代码段是禁止修改的,对于这些页,直接释放掉就好,因为磁盘上对应的数据与页中的数据是一致的。那么对于进程需要回写的页,内核主要将这些页放到磁盘的两个地方,当进程使用的页中的数据是映射于具体文件的,那么只需要将此页中的数据回写到对应文件所在磁盘位置就可以了。而对于那些没有映射磁盘对应文件的页,内核则将它们存放到swap分区中。根据这个,整理出下面这些情况的页:
- 进程堆、栈、数据段使用的匿名页:存放到swap分区中
- 进程代码段映射的可执行文件的文件页:直接释放
- 打开文件进行读写使用的文件页:如果页中数据与文件数据不一致,则进行回写到磁盘对应文件中,如果一致,则直接释放
- 进行文件映射mmap共享内存时使用的页:如果页中数据与文件数据不一致,则进行回写到磁盘对应文件中,如果一致,则直接释放
- 进行匿名mmap共享内存时使用的页:存放到swap分区中
- 进行shmem共享内存时使用的页:存放到swap分区中
也就是整个lru链表主要组织上面三种情况的页:
- 可以存放到swap分区中的页
- 映射了磁盘文件的文件页
- 被锁在内存中禁止换出的进程页(包括以上两种页)
二、LRU的相关数据结构
1、lru链表:
每个node有一个
mmzone.h - include/linux/mmzone.h - Linux source code v5.4.285 - Bootlin Elixir Cross Referencer
include/linux/mmzone.htypedef struct pglist_data {/* lru链表使用的自旋锁 * 当需要修改lru链表描述符中任何一个链表时,都需要持有此锁,也就是说,不会有两个不同的lru链表同时进行修改*/spinlock_t lru_lock;.../* Fields commonly accessed by the page reclaim scanner */struct lruvec lruvec;...
} pg_data_t;
struct lruvec结构体如下所示:
struct lruvec {struct list_head lists[NR_LRU_LISTS];struct zone_reclaim_stat reclaim_stat;/* Evictions & activations on the inactive file list */atomic_long_t inactive_age;/* Refaults at the time of last reclaim cycle */unsigned long refaults;
#ifdef CONFIG_MEMCGstruct pglist_data *pgdat;
#endif
};/** We do arithmetic on the LRU lists in various places in the code,* so it is important to keep the active lists LRU_ACTIVE higher in* the array than the corresponding inactive lists, and to keep* the *_FILE lists LRU_FILE higher than the corresponding _ANON lists.** This has to be kept in sync with the statistics in zone_stat_item* above and the descriptions in vmstat_text in mm/vmstat.c*/
#define LRU_BASE 0
#define LRU_ACTIVE 1
#define LRU_FILE 2enum lru_list {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,NR_LRU_LISTS
};
可以看到,一个lru链表描述符中总共有5个双向链表头,它们分别描述五中不同类型的链表。由于每个页有自己的页描述符,而内核主要就是将对应的页的页描述符加入到这些链表中。
- LRU_INACTIVE_ANON:称为非活动匿名页lru链表,此链表中保存的是此zone中所有最近没被访问过的并且可以存放到swap分区的页描述符,在此链表中的页描述符的PG_active标志为0。
- LRU_ACTIVE_ANON:称为活动匿名页lru链表,此链表中保存的是此zone中所有最近被访问过的并且可以存放到swap分区的页描述符,此链表中的页描述符的PG_active标志为1。
- LRU_INACTIVE_FILE:称为非活动文件页lru链表,此链表中保存的是此zone中所有最近没被访问过的文件页的页描述符,此链表中的页描述符的PG_active标志为0。
- LRU_ACTIVE_FILE:称为活动文件页lru链表,此链表中保存的是此zone中所有最近被访问过的文件页的页描述符,此链表中的页描述符的PG_active标志为1。
- LRU_UNEVICTABLE:此链表中保存的是此zone中所有锁在内存中的页,禁止换出的页。当进程运行过程中,通过调用mlock()将一些内存页锁在内存中时,这些内存页就会被加入到它们锁在的zone的LRU_UNEVICTABLE链表中,在LRU_UNEVICTABLE链表中的页可能是文件页也可能是匿名页。
为什么会把 active 链表和 inactive 链表分成两类,一类是匿名页,一类是文件页?
swappiness 用于表示 Swap 机制的积极程度,数值越大,Swap 的积极程度,越高越倾向于回收匿名页。数值越小,Swap 的积极程度越低,越倾向于回收文件页。
(通过
cat /proc/sys/vm/swappiness
命令查看,swappiness 选项的取值范围为 0 到 100,默认为 60。)因为回收匿名页和回收文件页的代价是不一样的,回收匿名页代价会更高一点,所以引入 swappiness 来控制内核回收的倾向。
注意: swappiness 只是表示 Swap 积极的程度,当内存非常紧张的时候,即使将 swappiness 设置为 0 ,也还是会发生 Swap 的。
假设我们现在只有 active 链表和 inactive 链表,不对这两个链表进行匿名页和文件页的归类,在需要页面置换的时候,内核会先从 active 链表尾部开始扫描,当 swappiness 被设置为 0 时,内核只会置换文件页,不会置换匿名页。
由于 active 链表和 inactive 链表没有进行物理页面类型的归类,所以链表中既会有匿名页也会有文件页,如果链表中有大量的匿名页的话,内核就会不断的跳过这些匿名页去寻找文件页,并将文件页替换出去,这样从性能上来说肯定是低效的。
因此内核将 active 链表和 inactive 链表按照匿名页和文件页进行了归类,当 swappiness 被设置为 0 时,内核只需要去 nr_zone_active_file 和 nr_zone_inactive_file 链表中扫描即可,提升了性能。
其实除了以上笔者介绍的四种 LRU 链表(匿名页的 active 链表,inactive 链表和文件页的active 链表, inactive 链表)之外,内核还有一种链表,比如进程可以通过 mlock() 等系统调用把内存页锁定在内存里,保证该内存页无论如何不会被置换出去,比如出于安全或者性能的考虑,页面中可能会包含一些敏感的信息不想被 swap 到磁盘上导致泄密,或者一些频繁访问的内存页必须一直贮存在内存中。
当这些被锁定在内存中的页面很多时,内核在扫描 active 链表的时候也不得不跳过这些页面,所以内核又将这些被锁定的页面单独拎出来放在一个独立的链表中。
在kernel-4.8之前,所有的lru是按照zone的粒度管理的,即每个zone都有5个LRU,通过zone->lru_lock来保证同步;从kernel-4.8开始,所有的lru都是统计在node上⾯的,通过pgdat->lru_lock来保证同步,这么做的原因:
在kernel-2.x/3.x版本的时代,64bit的cpu还没有问世,⼤部分设备的cpu都是32bit,由于32bit地址支持访问的内存空间有限,对于⼤内存设备,系统存在⼤量的⾼端内存(HighZONE), 并且Normal ZONE中的区间有限,为了⽅便内存管理,LRU是按照zone来划分的。
通过ZONE来管理LRU存在⼀个弊端,即每个zone上⾯的page⽼化程度⽆法保持⼀致,例如⼀个进程从不同的zone中分配了内存,从High ZONE中分配的内存在⼀定时间周期内被回收了,⽽Normal ZONE中的内存有可能还在LRU中;理想情况应该是它们能够在同⼀段时间内被回收,保证各个LRU的⽼化程度趋于一致。开源社区针对该问题,已经做了⼤量优化,但是效果不达预期。
⽬前主流的设备很少使⽤32bit的CPU,⽽64bit地址⼏乎可以直接访问所有的物理内 存,已经不存在High ZONE,可以在node的维度来管理所有的LRU。
基于上述原因,现在kswapd在回收内存⽅向与kernel-4.8之前有差异,在之前的版本中,kswapd是从DMA→Normal→High的⽅向来进⾏回收内存,恰好与alloc_pages分配内存的⽅向相反,这样可以减少zone->lru_lock锁的竞争,降低cpu负载。
2、lru缓存:
当需要修改lru链表时,一定要占有zone中的lru_lock这个锁。在多核的硬件环境中,在同时需要对lru链表进行修改时,锁的竞争会非常的频繁,所以内核提供了一个lru缓存的机制,这种机制能够减少锁的竞争频率。
lru缓存相当于将一些需要相同处理的页集合起来,当达到一定数量时再对它们进行一批次的处理,这样做可以让对锁的需求集中在这个处理的时间点。为了更好的说明lru缓存,先对lru链表进行操作主要有以下几种:
- 将不处于lru链表的新页放入到lru链表中(新增)
- 将页从lru链表中移除(删除)
- 将inactive lru链表中的页移动到inactive lru链表尾部(活动页不需要这样做,后面说明)
- 将处于active lru链表的页移动到inactive lru链表(跨链表操作)
- 将处于inactive lru链表的页移动到active lru链表(跨链表操作)
除了最后一项移除操作外,其他四样操作除非在特殊情况下, 都需要依赖于lru缓存。可以看到上面的5种操作,并不是完整的一套操作集(比如没有将活动lru链表中的页移动到活动lru链表尾部),原因是因为lru链表并不是供于整个系统所有模块使用的,可以说lru链表的出现,就是专门用于进行内存回收,所以这里的操作集只实现了满足于内存回收所需要使用的操作。
大部分在内存回收路径中对lru链表的操作,都不需要用到lru缓存,只有非内存回收路径中需要对页进行lru链表的操作时,才会使用到lru缓存。为了对应这四种操作,内核为每个CPU提供了四种lru缓存,当页要进行lru的处理时,就要先加入到lru缓存,当lru缓存满了或者系统主要要求将lru缓存中所有的页进行处理,才会将lru缓存中的页放入到页想放入的lru链表中。每种lru缓存使用struct pagevec进行描述:
pagevec.h - include/linux/pagevec.h - Linux source code v5.4.285 - Bootlin Elixir Cross Referencer
/* 15 pointers + header align the pagevec structure to a power of two */
#define PAGEVEC_SIZE 15struct pagevec {unsigned char nr; //记录当前 pagevec 中存放的page 数bool percpu_pvec_drained; //pagevec_release()时进行标记保护struct page *pages[PAGEVEC_SIZE]; //最多存放PAGEVEC_SIZE个,当达到上限时,就会统一存到LRU 中
};
每个cpu 定义了6个全局pagevec 变量如下:
//mm/swap.cstatic DEFINE_PER_CPU(struct pagevec, lru_add_pvec);
static DEFINE_PER_CPU(struct pagevec, lru_rotate_pvecs);
static DEFINE_PER_CPU(struct pagevec, lru_deactivate_file_pvecs);
static DEFINE_PER_CPU(struct pagevec, lru_deactivate_pvecs);
static DEFINE_PER_CPU(struct pagevec, lru_lazyfree_pvecs);
#ifdef CONFIG_SMP
static DEFINE_PER_CPU(struct pagevec, activate_page_pvecs);
#endif
- lru_add_pvec:添加page 到LRU 缓存中,当lru_add_pvec 数组达到上限,会统一把该页向量中的pages 存放到 LRU 链表中,注意此时缓存pages 不属于 LRU list
- lru_rotate_pvecs :缓存已经在 LRU_INACTIVE 链表中的非活动页,将这些页添加到LRU_INACTIVE 尾部
- lru_deactivate_file_pvecs :缓存已经存在于 LRU_ACTIVE_FILE 链表中,清除掉PG_active 和 PG_referenced 标记后,将这些页移到 LRU_INACTIVE_FILE 链表中
- lru_deactivate_pvecs:和lru_deactivate_file_pvecs 类似,缓存将被移到 LRU_INACTIVE_ANON 链表中
- lru_lazyfree_pvecs:缓存匿名页。清除掉 PG_active、PG_referenced、PG_swapbacked 标志后,将这些页添加到 LRU_INACTIVE_FILE 链表中
- activate_page_pvecs:是将LRU 链表中的页加入到 LRU_ACTIVE
3、struct page中相关的lru成员
struct page {unsigned long flags;......union {/* 页处于不同情况时,加入的链表不同* 1.是一个进程正在使用的页,加入到对应lru链表和lru缓存中* 2.如果为空闲页框,并且是空闲块的第一个页,加入到伙伴系统的空闲块链表中(只有空闲块的第一个页需要加入)* 3.如果是一个slab的第一个页,则将其加入到slab链表中(比如slab的满slab链表,slub的部分空slab链表)* 4.将页隔离时用于加入隔离链表*/struct list_head lru; ......};......}
flag:flags 字段的高 8 位用来表示 struct page 的定位信息,剩余低位表示特定的标志位。
物理内存页的这些标志位定义在内核 /include/linux/page-flags.h
文件中:
page-flags.h - include/linux/page-flags.h - Linux source code v5.4.285 - Bootlin Elixir Cross Referencer
-
PG_locked 表示该物理页面已经被锁定,如果该标志位置位,说明有使用者正在操作该 page , 则内核的其他部分不允许访问该页, 这可以防止内存管理出现竞态条件,例如:在从硬盘读取数据到 page 时。
-
PG_mlocked 表示该物理内存页被进程通过 mlock 系统调用锁定常驻在内存中,不会被置换出去。
-
PG_referenced 表示该物理页面刚刚被访问过。
-
PG_active 表示该物理页位于 active list 链表中。PG_referenced 和 PG_active 共同控制了系统使用该内存页的活跃程度,在内存回收的时候这两个信息非常重要。
-
PG_uptodate 表示该物理页的数据已经从块设备中读取到内存中,并且期间没有出错。
-
PG_readahead 当进程在顺序访问文件的时候,内核会预读若干相邻的文件页数据到 page 中,物理页 page 结构设置了该标志位,表示它是一个正在被内核预读的页。
-
PG_dirty 物理内存页的脏页标识,表示该物理内存页中的数据已经被进程修改,但还没有同步会磁盘中。
-
PG_lru 表示该物理内存页现在被放置在哪个 lru 链表上,比如:是在 active list 链表中 ? 还是在 inactive list 链表中 ?
-
PG_highmem 表示该物理内存页是在高端内存中。
-
PG_writeback 表示该物理内存页正在被内核的 pdflush 线程回写到磁盘中。
-
PG_slab 表示该物理内存页属于 slab 分配器所管理的一部分。
-
PG_swapcache 表示该物理内存页处于 swap cache 中。 struct page 中的 private 指针这时指向 swap_entry_t 。
-
PG_reclaim 表示该物理内存页已经被内核选中即将要进行回收。
-
PG_buddy 表示该物理内存页是空闲的并且被伙伴系统所管理。
-
PG_compound 表示物理内存页属于复合页的其中一部分。
-
PG_private 标志被置位的时候表示该 struct page 结构中的 private 指针指向了具体的对象。不同场景指向的对象不同。
除此之外内核还定义了一些标准宏,用来检查某个物理内存页 page 是否设置了特定的标志位,以及对这些标志位的操作,这些宏在内核中的实现都是原子的,命名格式如下:
-
PageXXX(page):检查 page 是否设置了 PG_XXX 标志位
-
SetPageXXX(page):设置 page 的 PG_XXX 标志位
-
ClearPageXXX(page):清除 page 的 PG_XXX 标志位
-
TestSetPageXXX(page):设置 page 的 PG_XXX 标志位,并返回原值
另外在很多情况下,内核通常需要等待物理页 page 的某个状态改变,才能继续恢复工作,内核提供了如下两个辅助函数,来实现在特定状态的阻塞等待:
static inline void wait_on_page_locked(struct page *page)
static inline void wait_on_page_writeback(struct page *page)
当物理页面在锁定的状态下,进程调用了 wait_on_page_locked 函数,那么进程就会阻塞等待知道页面解锁。
当物理页面正在被内核回写到磁盘的过程中,进程调用了 wait_on_page_writeback 函数就会进入阻塞状态直到脏页数据被回写到磁盘之后被唤醒。
三、源码解析
3.1、几个关键的函数
PageUnevictable
用于检查一个page是否不可回收。不可回收的页面通常是那些不能被交换到磁盘上的页面,例如内存映射的文件、内核内存等。
static inline int PageUnevictable(struct page *page)
{return test_bit(PG_unevictable, &page->flags);
}
page_evictable
用于检查一个page是否可以回收。它帮助内核决定哪些页面可以被交换到磁盘上,以释放内存
int page_evictable(struct page *page)
{int ret;/* Prevent address_space of inode and swap cache from being freed */rcu_read_lock();ret = !mapping_unevictable(page_mapping(page)) && !PageMlocked(page);rcu_read_unlock();return ret;
}//如果page有做映射,那么就检查对应的虚拟内存的AS_UNEVICTABLE标志位
//如果page没有被映射,输入的mapping就是null,最终的返回值就是false。表示不可回收
static inline int mapping_unevictable(struct address_space *mapping)
{if (mapping)return test_bit(AS_UNEVICTABLE, &mapping->flags);return !!mapping;
}
总结:
- 页面没有被锁住
- 页面在虚拟内存空间上可回收(有被映射,且AS_UNEVICTABLE没有置位)
page_lru
判断一个page在哪一个lru链表或者即将加入哪一个lru链表
#define LRU_BASE 0
#define LRU_ACTIVE 1
#define LRU_FILE 2enum lru_list {LRU_INACTIVE_ANON = LRU_BASE, //0LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE, //1LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE, //2LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE, //3LRU_UNEVICTABLE, //4NR_LRU_LISTS
};/*** page_lru - which LRU list should a page be on?* @page: the page to test** Returns the LRU list a page should be on, as an index* into the array of LRU lists.*/
static __always_inline enum lru_list page_lru(struct page *page)
{enum lru_list lru;if (PageUnevictable(page))lru = LRU_UNEVICTABLE;//4 不可回收else {lru = page_lru_base_type(page);if (PageActive(page))//检查page的flags字段中的PG_active位lru += LRU_ACTIVE;}return lru;
}//先统一放在inactive链表上
static inline enum lru_list page_lru_base_type(struct page *page)
{if (page_is_file_cache(page))return LRU_INACTIVE_FILE;//2return LRU_INACTIVE_ANON; //0
}
pagevec_lru_move_fn
swap.c - mm/swap.c - Linux source code v5.4.285 - Bootlin Elixir Cross Referencer
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 pglist_data *pgdat = NULL;struct lruvec *lruvec;unsigned long flags = 0;//遍历缓存中的所有页for (i = 0; i < pagevec_count(pvec); i++) {struct page *page = pvec->pages[i];struct pglist_data *pagepgdat = page_pgdat(page);//判断是否为同一个node,同一个node不需要加锁,否则需要加锁处理if (pagepgdat != pgdat) {if (pgdat)spin_unlock_irqrestore(&pgdat->lru_lock, flags);pgdat = pagepgdat;spin_lock_irqsave(&pgdat->lru_lock, flags);}//找到目标lruvec,最终页转移到该结构中的LRU链表中lruvec = mem_cgroup_page_lruvec(page, pgdat);(*move_fn)(page, lruvec, arg);//根据传入的函数进行回调}if (pgdat)spin_unlock_irqrestore(&pgdat->lru_lock, flags);//减少page的引用值,当引用值为0时,从LRU链表中移除页表并释放掉release_pages(pvec->pages, pvec->nr);//重置pvec结构pagevec_reinit(pvec);
}
3.1、将某一个page添加到lru缓存:
void lru_cache_add(struct page *page)
{//确保页是有效的,并且没有被其他进程锁定或使用。VM_BUG_ON_PAGE(PageActive(page) && PageUnevictable(page), page);//检查页是否已经在 LRU 链表中。如果页已经在 LRU 链表中,则不需要再次添加VM_BUG_ON_PAGE(PageLRU(page), page);__lru_cache_add(page);//核心函数
}static void __lru_cache_add(struct page *page)
{//获取当前 CPU 的 lru_add_pvec 变量。lru_add_pvec 是一个 pagevec 结构体,用于暂存要添加到 LRU 链表的页。struct pagevec *pvec = &get_cpu_var(lru_add_pvec);get_page(page);//pagevec_add(pvec, page) 尝试将页添加到 pagevec 中。如果 pagevec 已满,则返回 falseif (!pagevec_add(pvec, page) || PageCompound(page))__pagevec_lru_add(pvec);// 将 pagevec 中的所有页添加到 LRU 链表中put_cpu_var(lru_add_pvec);//释放当前 CPU 的 lru_add_pvec 变量
}/** Add a page to a pagevec. Returns the number of slots still available.*/
static inline unsigned pagevec_add(struct pagevec *pvec, struct page *page)
{/* 将page加入到此cpu的lru缓存中,注意,加入pagevec实际上只是将pagevec中的pages数组中的某个指针指向此页,如果此页原本属于lru链表,那么现在实际还是在原来的lru链表中 */pvec->pages[pvec->nr++] = page;return pagevec_space(pvec);//检查 pagevec 是否已满
}
3.2、将缓存的页加入lru链表
/** Add the passed pages to the LRU, then drop the caller's refcount* on them. Reinitialises the caller's pagevec.*/
void __pagevec_lru_add(struct pagevec *pvec)
{pagevec_lru_move_fn(pvec, __pagevec_lru_add_fn, NULL);//pagevec_lru_move_fn是个通用函数,后续分析
}static void __pagevec_lru_add_fn(struct page *page, struct lruvec *lruvec,void *arg)
{enum lru_list lru;int was_unevictable = TestClearPageUnevictable(page);VM_BUG_ON_PAGE(PageLRU(page), page);SetPageLRU(page);//设置页的 LRU 标志smp_mb();if (page_evictable(page)) {//判断页是不是被锁住lru = page_lru(page);//要加入到哪个lru链表update_page_reclaim_stat(lruvec, page_is_file_cache(page),PageActive(page));if (was_unevictable)count_vm_event(UNEVICTABLE_PGRESCUED);} else {lru = LRU_UNEVICTABLE;ClearPageActive(page);SetPageUnevictable(page);if (!was_unevictable)count_vm_event(UNEVICTABLE_PGCULLED);}add_page_to_lru_list(page, lruvec, lru);//将页添加到适当的 LRU 链表(活跃或非活跃)trace_mm_lru_insertion(page, lru);
}
3.3、将inactive lru链表中的页移动到inactive lru链表尾部
主要通过rotate_reclaimable_page()函数实现,这种操作主要使用在:当一个脏页需要进行回收时,系统首先会将页异步回写到磁盘中(swap分区或者对应的磁盘文件),然后通过这种操作将页移动到非活动lru链表尾部。这样这些页在下次内存回收时会优先得到回收。
/** Writeback is about to end against a page which has been marked for immediate* reclaim. If it still appears to be reclaimable, move it to the tail of the* inactive list.*/
void rotate_reclaimable_page(struct page *page)
{if (!PageLocked(page) && !PageDirty(page) &&!PageUnevictable(page) && PageLRU(page)) {struct pagevec *pvec;unsigned long flags;get_page(page);local_irq_save(flags);pvec = this_cpu_ptr(&lru_rotate_pvecs);if (!pagevec_add(pvec, page) || PageCompound(page))pagevec_move_tail(pvec);//核心函数local_irq_restore(flags);}
}/** pagevec_move_tail() must be called with IRQ disabled.* Otherwise this may cause nasty races.*/
static void pagevec_move_tail(struct pagevec *pvec)
{int pgmoved = 0;//pagevec_move_tail_fn核心函数pagevec_lru_move_fn(pvec, pagevec_move_tail_fn, &pgmoved);__count_vm_events(PGROTATED, pgmoved);
}static void pagevec_move_tail_fn(struct page *page, struct lruvec *lruvec,void *arg)
{int *pgmoved = arg;/* 页属于非活动页 */if (PageLRU(page) && !PageUnevictable(page)) {del_page_from_lru_list(page, lruvec, page_lru(page));ClearPageActive(page);add_page_to_lru_list_tail(page, lruvec, page_lru(page));(*pgmoved)++;}
}
ref:
lru缓存、lru链表、内存回收内核源码讲解_shrink lru-CSDN博客
linux内存源码分析 - 内存回收(lru链表) - tolimit - 博客园
Linux 内存管理窥探(16):页面回收 (LRU)_linux protect lru-CSDN博客
https://justinwei.blog.csdn.net/article/details/126533273
一步一图带你深入理解 Linux 物理内存管理 - bin的技术小屋 - 博客园
【原创】(十)Linux内存管理 - zoned page frame allocator - 5 - LoyenWang - 博客园
struct page介绍_page 中的idle和young标志位-CSDN博客