文章目录
1. 简介
Buddy 的内存分配和释放算法还是比较简单明晰的。分配的时候优先找 order 相等的空闲内存链表,找不到的话就去找 order 更大的空闲内存链表;释放的话先释放到对应 order 的空闲内存链表,然后尝试和 buddy 内存进行合并,尽量合并成更大的空闲内存块。
但是内存管理加入 缺页(PageFault)
和 回收(Reclaim)
以后,情况就变得异常复杂了。Linux 为了用同样多的内存养活更多的进程和服务操碎了心:
- PageFault:对新创建的进程,除了内核态的内存不得不马上分配,对用户态的内存严格遵循
lazy
的延后分配策略,对私有数据设置COW(Copy On Write)
策略只有在更改的时候才会触发 PageFault 重新分配物理页面,对新的文件映射也只是简单的分配VMA只有在实际访问的时候才会触发 PageFault 分配物理页面。 - Reclaim:在内存不够用的情况下,Linux 尝试回收一些内存。对于从文件映射的内存 (FileMap),因为文件中有备份所以先丢弃掉内存,需要访问时再重新触发 PageFault 从文件中加载;对于没有文件映射的内存 (AnonMap),可以先把它交换到 Swap 分区上去,需要访问时再重新触发 PageFault 从 Swap 中加载。
本篇文章就来详细的分析上述的 缺页(PageFault)
和 回收(Reclaim)
过程。
2. LRU 组织
用户态的内存(FileMap + AnonMap)的内存回收的主要来源。为了尽量减少内存回收锁引起的震荡,刚刚回收的内存马上又被访问需要重新分配。Linux 设计了 LRU (Last Recent Use) 链表,来优先回收最长时间没有访问的内存。
2.1 LRU 链表
设计了 5 组 LRU 链表:
enum lru_list {
LRU_INACTIVE_ANON = LRU_BASE, // inactive 匿名
LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE, // active 匿名
LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE, // inactive 文件
LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE, // active 文件
LRU_UNEVICTABLE, // 不可回收内存
NR_LRU_LISTS
};
在 4.8 版本以前是每个 zone 拥有独立的 lru 链表,在 4.8 版本以后改成了每个 node 一个 lru 链表:
typedef struct pglist_data {
struct lruvec lruvec;
...
}
struct lruvec {
struct list_head lists[NR_LRU_LISTS]; // lru 链表
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_MEMCG
struct pglist_data *pgdat;
#endif
};
2.2 LRU Cache
为了减少多个CPU在操作LRU链表时的拿锁冲突,系统设计了 PerCPU 的 lru cache。每个 cache 能容纳 14 个 page,一共定义了以下几类 cache:
static DEFINE_PER_CPU(struct pagevec, lru_add_pvec); // 将不处于lru链表的新页放入到lru链表中
static DEFINE_PER_CPU(struct pagevec, lru_rotate_pvecs); // 将非活动lru链表中的页移动到非活动lru链表尾部
static DEFINE_PER_CPU(struct pagevec, lru_deactivate_file_pvecs); // 将处于活动lru链表的页移动到非活动lru链表
static DEFINE_PER_CPU(struct pagevec, lru_lazyfree_pvecs);
#ifdef CONFIG_SMP
static DEFINE_PER_CPU(struct pagevec, activate_page_pvecs); // 将处于非活动lru链表的页移动到活动lru链表
#endif
/* 14 pointers + two long’s align the pagevec structure to a power of two */
#define PAGEVEC_SIZE 14
struct pagevec {
unsigned long nr;
bool percpu_pvec_drained;
struct page *pages[PAGEVEC_SIZE];
};
2.3 LRU 移动操作
page 可以加入到 lru 链表,并且根据条件在 active/inactive 链表间移动。
2.3.1 page 加入 LRU
关于 lru 的操作,其中最重要的是把 新分配的 page 加入到 lru 中。这部分工作一般由 do_page_fault() 来处理。
do_page_fault() 分配内存 page,以及把新 page 加入到 lru 的典型场景如下:
场景 | 入口函数 | LRU函数调用关系 | 条件 | LRU链表 | LRU list | page flags |
---|---|---|---|---|---|---|
匿名内存第一次发生缺页 | do_anonymous_page() | lru_cache_add_active_or_unevictable() → lru_cache_add() → __lru_cache_add() → __pagevec_lru_add() → pagevec_lru_move_fn() → __pagevec_lru_add_fn() | !(vma->vm_flags & VM_LOCKED) | 匿名 active lru | pglist_data->lruvec.lists[LRU_ACTIVE_ANON] | PG_swapbacked + PG_active + PG_lru |
↑ | ↑ | lru_cache_add_active_or_unevictable() → add_page_to_unevictable_list() | (vma->vm_flags & VM_LOCKED) | 不可回收 lru | pglist_data->lruvec.lists[LRU_UNEVICTABLE] | PG_swapbacked + PG_unevictable + PG_lru |
匿名内存被swap出去后发生缺页 | do_swap_page() | lru_cache_add_active_or_unevictable() | ↑ | ↑ | ↑ | ↑ |
私有文件内存写操作缺页 | do_cow_fault() | finish_fault() → alloc_set_pte() → lru_cache_add_active_or_unevictable() | !(vma->vm_flags & VM_LOCKED) | 匿名 active lru | pglist_data->lruvec.lists[LRU_ACTIVE_ANON] | PG_swapbacked + PG_active + PG_lru |
↑ | ↑ | finish_fault() → alloc_set_pte() → lru_cache_add_active_or_unevictable() | (vma->vm_flags & VM_LOCKED) | 不可回收 lru | pglist_data->lruvec.lists[LRU_UNEVICTABLE] | PG_swapbacked + PG_unevictable + PG_lru |
私有内存写操作缺页 | do_wp_page() | wp_page_copy() → lru_cache_add_active_or_unevictable() | ↑ | ↑ | ↑ | ↑ |
文件内存读操作缺页 | do_read_fault() | pagecache_get_page() → add_to_page_cache_lru() → lru_cache_add() | - | 文件 lru | pglist_data->lruvec.lists[LRU_INACTIVE_FILE / LRU_ACTIVE_FILE] | PG_active + PG_lru |
共享文件内存写操作缺页 | do_shared_fault() | - | ↑ | ↑ | ↑ | ↑ |
其中核心部分的代码分析如下:
static void __lru_cache_add(struct page *page)
{
struct pagevec *pvec = &get_cpu_var(lru_add_pvec);
get_page(page); /* (1) 首先把 page 加入到 lru cache 中 */ if (!pagevec_add(pvec, page) || PageCompound(page)) /* (2) 如果 lru cache 空间已满,把page加入到各自对应的 lru 链表中 */ __pagevec_lru_add(pvec); put_cpu_var(lru_add_pvec);
}
↓
void __pagevec_lru_add(struct pagevec pvec)
{
/ (2.1) 遍历 lru cache 中的 page,根据 page 的标志把 page 加入到不同类型的 lru 链表 */
pagevec_lru_move_fn(pvec, __pagevec_lru_add_fn, NULL);
}
↓
static void __pagevec_lru_add_fn(struct page *page, struct lruvec *lruvec,
void arg)
{
int file = page_is_file_cache(page);
int active = PageActive(page);
/ (2.1.1) 根据 page 中的标志,获取到 page 想要加入的 lru 类型 */
enum lru_list lru = page_lru(page);
VM_BUG_ON_PAGE(PageLRU(page), page);
/* (2.1.2) 加入 lru 链表的 page 设置 PG_lru 标志 */
SetPageLRU(page);
/* (2.1.3) 加入 lru 链表 */
add_page_to_lru_list(page, lruvec, lru);
update_page_reclaim_stat(lruvec, file, active);
trace_mm_lru_insertion(page, lru);
}
↓
static __always_inline enum lru_list page_lru(struct page *page)
{
enum lru_list lru;
/* (2.1.1.1) page 标志设置了 PG_unevictable,lru = LRU_UNEVICTABLE */
if (PageUnevictable(page))
lru = LRU_UNEVICTABLE;
/* (2.1.1.2) page 标志设置:
PG_swapbacked,lru = LRU_INACTIVE_ANON
PG_swapbacked + PG_active,lru = LRU_ACTIVE_ANON
,lru = LRU_INACTIVE_FILE
PG_active,lru = LRU_ACTIVE_FILE
*/
else {
lru = page_lru_base_type(page);
if (PageActive(page))
lru += LRU_ACTIVE;
}
return lru;
}
static inline enum lru_list page_lru_base_type(struct page *page)
{
if (page_is_file_cache(page))
return LRU_INACTIVE_FILE;
return LRU_INACTIVE_ANON;
}
static inline int page_is_file_cache(struct page *page)
{
return !PageSwapBacked(page);
}
2.3.2 其他 LRU 移动操作
action | function |
---|---|
将处于非活动链表中的页移动到非活动链表尾部 | rotate_reclaimable_page() → pagevec_move_tail() → pagevec_move_tail_fn() |
将活动lru链表中的页加入到非活动lru链表中 | deactivate_page() → lru_deactivate_file_fn() |
将非活动lru链表的页加入到活动lru链表 | activate_page() → __activate_page() |
3. LRU 回收
使用 LRU 回收内存的大概流程如下所示:
3.1 LRU 更新
为了减少对性能的影响,系统把加入到 LRU 的内存 page 分为 active 和 inactive,从 inactive 链表中回收内存。page 近期被访问过即 active,近期没有被访问即 inactive。
系统并不会设计一个定时器,而是通过判断两次扫描之间 PTE 中的 Accessed
bit 没有被置位,从而来判断对应 page 有没有被访问过。每次扫描完会清理 Accessed
bit:
一个 page 可能会被多个 vma 锁映射,系统通过 反向映射 找到所有 vma ,并统计 有多少个 vma 的 pte 被访问过 accessed。
page_referenced()
- 1、在 active 链表回收扫描函数 shrink_active_list() 中的处理:
shrink_node() → shrink_node_memcg() → shrink_list() → shrink_active_list():
shrink_active_list()
{
…
/* (1) page 对于的任一 vma 的 pte Accessed bit 有被置位,且是代码段