持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第20天,点击查看活动详情
在Linux操作系统中,当内存充足时,内核会尽量多地使用内存作为文件缓存(page cache),从而提高系统的性能。文件缓存页面会添加到文件类型的LRU链表中;当内存紧张时,文件缓存页面会被丢弃,或者把修改的文件缓存页面回写到存储设备中,与块设备同步之后便可释放出物理内存。现在的应用程序转向内存密集型,无论系统中有多少物理内存都是不够用的,因此Linux操作系统会使用存储设备作为交换分区,内核将很少使用的内存换出到交换分区,以便释放出物理内存,这个机制称为页交换(swapping),这些处理机制统称为页面回收(page reclaim)。
LRU链表
在最近几十年操作系统的发展过程中,出现了很多页面交换算法,其中每个算法都有各自的优点和缺点。Linux内核中采用的页交换算法主要是经典LRU链表算法和第二次机会(second chance)法。 LRU是Least Recently Used的缩写,意为最近最少使用。根据局部性原理,LRU假定最近不使用的页面在较短的时间内也不会频繁使用。在内存不足时,这些页面将成为被换出的候选者。内核使用双向链表来定义LRU链表,并且根据页面的类型将LRU链表分为LRU_ANON和LRU_FILE。每种类型根据页面的活跃性分为活跃LRU链表和不活跃LRU链表,所以内核中一共有如下5个LRU链表。
- 不活跃匿名页面链表LRU_INACTIVE_ANON。
- 活跃匿名页面链表LRU_ACTIVE_ANON。
- 不活跃文件映射页面链表LRU_INACTIVE_FILE。
- 活跃文件映射页面链表LRU_ACTIVE_FILE。
- 不可回收页面链表LRU_UNEVICTABLE。
LRU链表之所以要分成这样,是因为当内存紧缺时总是优先换出文件映射的文件缓存页面(LRU_FILE链表中的页面),而不是匿名页面。因为大多数情况下,文件缓存页面不需要被回写到磁盘,除非页面内容修改了(称为脏页),而匿名页面总是要在写入交换分区之后,才能被换出。LRU链表按照内存节点配置[2],也就是说,每个内存节点中都有一整套LRU链表,因此内存节点的描述符数据结构(pglist_data)中有一个成员lruvec指向这些链表。枚举类型变量lru_list列举出上述各种LRU链表的类型,lruvec数据结构中定义了上述各种LRU类型的链表。
<include/linux/mmzone.h>
#define LRU_BASE 0
#define LRU_ACTIVE 1
#define LRU_FILE 2
enum 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
};
struct lruvec {
struct list_head lists[NR_LRU_LISTS];
struct zone_reclaim_stat reclaim_stat;
...
};
struct pglist_data {
...
struct lruvec lruvec;
...
};
在Linux 4.8内核之前,LRU链表是按照zone来配置的,即每个zone有一套LRU链表。因为在设计之初,64位的CPU还没有面世,设计了基于zone的页面回收策略(zone-based reclaim)。32位CPU中通常会有大量的高端内存,高端内存所在的zone称为ZONE_HIGHMEM。页面回收策略从基于zone的策略迁移到基于内存节点的策略的一个主要原因是在同一个内存节点的不同zone中存在着不同的页面老化速度(page age speed),这会导致很多问题。如一个应用程序在不同的zone中分配了内存,在高端zone(ZONE_HIGH)中分配的页面可能已经被回收了,而在低端zone(ZONE_NORMAL)中分配的页面还在LRU链表中,理想情况下它们应该在同一个时间周期内被回收。从另外一个角度来看,zone的各个LRU链表的扫描覆盖率应该趋于一致。也就是说,在给定的时间内,一个LRU链表被充分扫描了,另外的LRU链表也应该如此。其原因在于,页面回收内核线程kswapd和页面分配内核代码路径之间复杂的扫描逻辑,长期以来内核社区一直在添加各种“诡异”的补丁来解决各种问题,试图维护一个公平的扫描率,以解决zone老化速度不一致的问题,但是依然没有从根本上解决。基于内存节点的页面回收机制可以有效解决这个问题,并且去掉基于zone页面回收的一些“诡异”和难以理解的代码逻辑。 目前,具有大内存的计算机已经很少继续使用32位的Linux内核,64位Linux内核已经没有高端内存的概念。另外,在NUMA计算机上,每个内存节点上的内存布局不同,导致每个内存节点的页面回收的行为可能会不同。 因此,基于内存节点的LRU页面回收机制更容易让人理解,页面分配机制可以去掉“诡异”的补丁,并且在NUMA计算机上各个内存节点的行为比较一致。Linux 4.8内核合并了社区专家Mel Gorman的改动。 LRU链表是如何实现页面老化的呢? 这需要从页面如何加入LRU链表以及LRU链表如何摘取页面说起。加入LRU链表的常用接口函数是lru_cache_add()。
<lru_cache_add()->__lru_cache_add()>
static void __lru_cache_add(struct page *page)
{
struct pagevec *pvec = &get_cpu_var(lru_add_pvec);
get_page(page);
if (!pagevec_add(pvec, page) || PageCompound(page))
__pagevec_lru_add(pvec);
put_cpu_var(lru_add_pvec);
}
这里使用了页向量(pagevec)数据结构,借助一个数组来保存特定数目的页,可以对这些页面执行同样的操作。页向量会以“批处理的方式”执行,比单独处理一个页面的方式效率要高。pagevec数据结构的定义如下。
#define PAGEVEC_SIZE 15
struct pagevec {
unsigned char nr;
bool percpu_pvec_drained;
struct page *pages[PAGEVEC_SIZE];
};
pagevec_add()函数首先往pvec->pages[]数组里添加页面,如果没有空间了,则调用__pagevec_lru_add()函数把原有的页面添加到LRU链表中。