LRU(Least Recently Used,最近未使用)

一、LRU的由来

lru的引入主要是和内存回收有关。

属于内核的大部分page是不能够进行回收的,比如内核栈、内核代码段、内核数据段以及大部分内核使用的page,它们都是不能够进行回收的;

相反,进程使用的page,比如进程代码段、进程数据段、进程堆栈、进程访问文件时映射的文件页、进程间共享内存使用的页,这些页框都是可以进行回收的。

那么,可回收的page里面选择那些进行回收呢?有以下几个维度的考虑:

  1. 时间维度:假设一个页很久没有被访问到了,那么就假设在下一段时间中,这个页也可能不会被访问到,就可以释放掉内存。
  2. 频次维度:假设一个页面被疯狂频繁的使用,它肯定是一个热页,但是这个页面最近的一次访问时间离现在稍微久了一点点,此时进来大量的页面,这些页面的特点是只会使用一两次,以后将再也不会用到。在这种情况下,如果只从时间维度考虑,这个之前频繁地被疯狂访问的页面就会被置换出去了(本来应该将这些大量一次性访问的页面置换出去的),当这个页面在不久之后要被访问时,此时已经不在内存中了,还需要在重新置换进来,造成性能的损耗。这种现象也叫 Page Thrashing(页面颠簸)。
     

实际上,上面两种思路在算法上分别对应:

  1. LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
  2. LFU(Least Frequently Used)最不经常使用。算法根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。

因此,内核考虑了上述两种因素的统计,引入了 active 链表和 inactive 两类链表。工作原理如下:

  1. 首先 inactive 链表的尾部存放的是访问频率最低并且最少访问的页面,在内存紧张的时候,这些页面被置换出去的优先级是最大的。

  2. 对于文件页来说,当它被第一次读取的时候,内核会将它放置在 inactive 链表的头部,如果它继续被访问,则会提升至 active 链表的尾部。如果它没有继续被访问,则会随着新文件页的进入,内核会将它慢慢的推到 inactive 链表的尾部,如果此时再次被访问则会直接被提升到 active 链表的头部。大家可以看出此时页面的使用频率这个因素已经被考量了进来。

  3. 对于匿名页来说,当它被第一次读取的时候,内核会直接将它放置在 active 链表的尾部,注意不是 inactive 链表的头部,这里和文件页不同。因为匿名页的换出 Swap Out 成本会更大,内核会对匿名页更加优待。当匿名页再次被访问的时候就会被被提升到 active 链表的头部。

  4. 当遇到内存紧张的情况需要换页时,内核会从 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来保证同步,这么做的原因:

  1. 在kernel-2.x/3.x版本的时代,64bit的cpu还没有问世,⼤部分设备的cpu都是32bit,由于32bit地址支持访问的内存空间有限,对于⼤内存设备,系统存在⼤量的⾼端内存(HighZONE), 并且Normal ZONE中的区间有限,为了⽅便内存管理,LRU是按照zone来划分的。

  2. 通过ZONE来管理LRU存在⼀个弊端,即每个zone上⾯的page⽼化程度⽆法保持⼀致,例如⼀个进程从不同的zone中分配了内存,从High ZONE中分配的内存在⼀定时间周期内被回收了,⽽Normal ZONE中的内存有可能还在LRU中;理想情况应该是它们能够在同⼀段时间内被回收,保证各个LRU的⽼化程度趋于一致。开源社区针对该问题,已经做了⼤量优化,但是效果不达预期。

  3. ⽬前主流的设备很少使⽤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博客 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/17996.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

网盘聚合搜索项目Aipan(爱盼)

本文软件由网友 刘源 推荐&#xff1b; 简介 什么是 Aipan&#xff08;爱盼&#xff09; ? Aipan&#xff08;爱盼&#xff09;是一个基于 Vue 和 Nuxt.js 技术构建的开源网盘搜索项目。其主要目标是为用户提供一个能够自主拥有和管理的网盘搜索网站。该项目持续维护和更新&a…

i春秋-Hash(__wakeup沉默、序列化)

练习平台地址 竞赛中心 题目描述 题目内容 啥也没有就一个标签跳转 点击后的确发生了跳转 观察到url中有key和hash两个值&#xff0c;猜测hash是key的hash 查看源代码发现确实是 $hashmd5($sign.$key);the length of $sign is 8 解密得到$sign应该为kkkkkk01 构造122的hash i…

【含开题报告+文档+PPT+源码】基于SpringBoot+Vue的智能蒙绮英语学习系统设计与实现

开题报告 随着全球化的不断深入&#xff0c;英语作为国际通用语言&#xff0c;其重要性日益凸显。掌握英语不仅可以拓宽个人的视野和知识面&#xff0c;还可以增强个人竞争力&#xff0c;为个人职业发展带来更多机会。因此&#xff0c;开发一款能够帮助用户更高效地学习英文单…

Blender vs 3dMax谁才是3D软件的未来?

在探讨Blender与3ds Max谁才是3D软件的未来时&#xff0c;我们需要从多个维度对这两款软件进行详细分析。 Blender的优势 开源免费&#xff1a;Blender是一款完全免费的开源3D创作套件&#xff0c;这使得它成为独立艺术家、业余爱好者和小型工作室的首选。用户无需承担巨额的…

ubuntu安装cuda、cudnn和TensoRT【分步安装】

我说实话&#xff0c;这个环境安装真的要了老命&#xff0c;太tm恶心了。 因为遇到了很多问题&#xff0c;非常之恶心 问题一&#xff1a;版本对应 问题二&#xff1a;下载具慢 问题三&#xff1a;上NVIDIA有时候上不去 问题四&#xff1a;找到对应版本点不了 问题五&…

基于树莓派的边缘端 AI 目标检测、目标跟踪、姿态估计 视频分析推理 加速方案:Hailo with ultralytics YOLOv8 YOLOv11

文件大纲 加速原理硬件安装软件安装基本设置系统升级docker 方案Demo 测试目标检测姿态估计视频分析参考文献前序树莓派文章hailo加速原理 Hailo 发布的 Raspberry Pi AI kit 加速原理,有几篇文章介绍的不错 https://ubuntu.com/blog/hackers-guide-to-the-raspberry-pi-ai-ki…

网络工程实验一:静态路由的配置

#实验仅供参考&#xff0c;勿直接粘贴复制&#xff0c;用以学习交流# 1、实验目的&#xff1a; 熟悉eNSP网络仿真工具平台。 掌握静态路由配置方法。 能够配置路由器接口地址。 2、实验内容&#xff1a; 2.1、安装对应的软件&#xff1a; &#xff08;1&#xff09;Virtual …

基于java的宠物用品交易平台

一、作品包含 源码数据库设计文档万字PPT全套环境和工具资源部署教程 二、项目技术 前端技术&#xff1a;Html、Css、Js、Vue、Element-ui 数据库&#xff1a;MySQL 后端技术&#xff1a;Java、Spring Boot、MyBatis 三、运行环境 开发工具&#xff1a;IDEA/eclipse 数据…

华为VPN技术

1.启动设备 2.配置IP地址 [FW1]int g1/0/0 [FW1-GigabitEthernet1/0/0]ip add 192.168.1.254 24 [FW1-GigabitEthernet1/0/0]int g1/0/1 [FW1-GigabitEthernet1/0/1]ip add 100.1.1.1 24 [FW1-GigabitEthernet1/0/1]service-manage ping permit [FW2]int g1/0/0 [FW2-Gi…

鸿蒙进阶篇-数组常用方法(一)

大家好&#xff0c;这里是鸿蒙开天组&#xff0c;今天咱们来学习一下数组的常用方法。 数组在编程中是非常常见的数据类型&#xff0c;毕竟需要用到的场合太多了&#xff0c;所以我们今天来使劲学习一下常用方法&#xff0c;在这里也没法一一列全&#xff0c;但是大家也可以根…

整数唯一分解定理

整数唯一分解定理&#xff0c;也称为算术基本定理&#xff0c;是由德国数学家高斯在其著作《算术研究》中首次提出的。本文回顾整数唯一分解定理以及对应的几个重要结论。 一、整数唯一分解定理 整数唯一分解定理&#xff0c;也称为算术基本定理&#xff0c;是数论中的一个重…

对Pod做一个详细了解

文章目录 01创建一个pod02删除pod03镜像拉取策略04pod的标签05pod资源限制方法06pod的重启策略 07pod中运行多个容器08对pod内的容器执行命令09 验证多个pod中多个容器网络共享10 pod的创建流程和调度的约束方式pod的声明周期介绍pod 的健康检查健康检查的方式probe的探测方式案…

LinkedHashMap实现LRU

LRU 环境&#xff1a;JDK11 最近接触LRU(Least Recently Used)&#xff0c;即最近最少使用&#xff0c;也称淘汰算法&#xff0c;在JDK中LinkedHashMap有相关实现 LRU的LinkedHashMap实现 LinkedHashMap继承HashMap。所以内存的存储结构和HashMap一样&#xff0c;但是LinkedH…

基于rk356x u-boot版本功能分析及编译相关(三)Makefile分析

🎏技术驱动源于热爱,祝各位学有所成。 文章目录 一、Makefile简要概述二、简要流程图三、Makefile文件具体分析大家好哈,这次因工作比较忙,文章更新拖的有些久了。哈哈,话不多说,咱们接着上次继续说u-boot的Makefile。 一、Makefile简要概述 一般要了解u-boot源码的编译…

shell(1)脚本创建执行与变量使用

shell&#xff08;1&#xff09;脚本创建执行与变量使用 声明&#xff01; 学习视频来自B站up主 泷羽sec 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章 笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&…

第5章总体设计-5.4 硬件可行性分析

5.4 硬件可行性分析 5.4.1 硬件方案评估1. 框式产品硬件可行性分析&#xff08;1&#xff09;机框设计可行性。&#xff08;2&#xff09;单板设计可行性。&#xff08;3&#xff09;核心功能器件选型。&#xff08;4&#xff09;数据流。 2. 盒式产品硬件可行性分析3. 终端产品…

TOIS24|推荐公平性的反事实解释

论文&#xff1a;https://arxiv.org/pdf/2307.04386 代码&#xff1a;https://anonymous.4open.science/r/CFairER-anony/. 关键词&#xff1a;可解释推荐;公平;反事实的解释;强化学习 1 动机 现有推荐系统存在的公平性问题&#xff0c;例如性别歧视和种族偏见等&#xff0c;…

week 3 - Assembly Language

Important Instructions and Syntax 此内容是以MASM编写的&#xff0c;你将使用Visual C/C内联汇编来编程&#xff0c;因此数据元素的声明有所不同&#xff0c;但概念和指令集&#xff08;instruction sets)相同。 一、General-Purpose Registers 寄存器是CPU内的命名存储单元…

6.C操作符详解,深入探索操作符与字符串处理

C操作符详解&#xff0c;深入探索操作符与字符串处理 C语言往期系列文章目录 往期回顾&#xff1a; C语言是什么&#xff1f;编程界的‘常青树’&#xff0c;它的辉煌你不可不知VS 2022 社区版C语言的安装教程&#xff0c;不要再卡在下载0B/s啦C语言入门&#xff1a;解锁基础…