page cache 的管理,是内核内存管理里最复杂的一块,也是容器混部场景下,问题最多的地方
我们这里只关注读 cache 的处理,脏页的控制单独讲。所以这篇文章里,无特殊说明 page cache 默认不包括脏页部分
当我们谈到 page cache 时, 我们会关注什么?
有以下几个关键的点
- 什么时机会触发 page cache 回收?
- 回收过程是什么样的
- 不可回收的页面有哪些?
- 不容易回收的页面有哪些?
- 回收力度如何控制
接下来,我们就这几点,来讲一讲 page cache 的一些内核实现内幕。以及混部场景下,可能会遇到的一些坑
实际上,不同的回收方式,其时机、回收的页面范围、力度、算法都稍有不同,所以下面我们将按照不同的回收方式来详细讲
1. 整机 drop_caches 回收
内核接口 /proc/sys/vm/drop_caches
内核的代码实现入口在 fs/drop_caches.c 里面
这个接口支持3种方式:
- echo 1,清理 page cache
- echo 2,清理 slab,比如 dentry cache 通常也很消耗内存
- echo 3,两种都清理
我们这里只讨论方式1
1.1. 回收时机、力度、算法
只有人为的 echo xx > /proc/sys/vm/drop_caches 时,才会触发 page cache 回收
每次触发 drop_caches,基本上都会把系统能回收的 clean page 一次性全部回收回来,注意,是全部能回收的
所以,这里其实也没有什么的特殊的回收算法了,简单全遍历就完了
1.2. 回收过程
内核代码 fs/drop_caches.c
简单来说,就是
- 遍历所有的超级块,super_block
- 遍历每个超级块上的所有 inode 对象
- 根据 inode->i_mapping 找到每个 inode 的 address_space 空间
- 遍历 address_space 下的所有 page
- 将 page 从 radix tree 上删除
- 调用文件系统的 releasepage 函数释放文件系统资源。这个可以忽略,我看 fs/* 几乎所有文件系统都不实现这个函数了
- 释放所有能释放的 page 内存(引用计数为0)
核心逻辑的调用栈如下:
- drop_caches_sysctl_handler
- iterate_supers(drop_pagecache_sb, NULL)
- drop_pagecache_sb, list_for_each_entry(inode, &sb->s_inodes, i_sb_list)
- invalidate_mapping_pages(inode->i_mapping, 0, -1) // 这个函数的实现在 mm/truncate.c 文件里,Invalidate all the unlocked pages of one inode
- invalidate_inode_page(page) for page in pagevec_lookup_entries(&pvec)
- invalidate_complete_page() 删除page的mapping,并从 page cache 的radix-tree 里面剔除,因为下一步就直接 free 内存了
- pagevec_release(&pvec) // 释放所有的 page 内存空间
1.3. 回收范围
drop_caches 是一个非常轻量级的回收过程,只回收能够立即释放的 page
从 invalidate_inode_page() 我们可以看到,有3种页面,是不会被回收的:
- 脏页
- 正在回写的页
- mmap + MAP_SHARED 方式映射到 page table 的页
- PG_SyncReadahead 需要多次drop才能回收
int invalidate_inode_page(struct page *page)
{
struct address_space *mapping = page_mapping(page);
if (!mapping)
return 0;
if (PageDirty(page) || PageWriteback(page))
return 0;
if (page_mapped(page))
return 0;
return invalidate_complete_page(mapping, page);
}
注意,page_mapping() 和 page_mapped() 不是一个东西。另外,!mapping 这段代码我没看懂是过滤的啥?
page_mapping() 返回 page 的 address_space,读的是 page->mapping 信息
(1)返回 NULL,说明该页要么是 slab cache,要么是 anon
(2)返回非空,可能是 swap_address_space(),或者就是正常页所在的一个 address_space
struct address_space *page_mapping(struct page *page)
{
struct address_space *mapping;
page = compound_head(page);
/* This happens if someone calls flush_dcache_page on slab page */
if (unlikely(PageSlab(page)))
return NULL;
if (unlikely(PageSwapCache(page))) {
swp_entry_t entry;
entry.val = page_private(page);
return swap_address_space(entry);
}
mapping = page->mapping;
if ((unsigned long)mapping & PAGE_MAPPING_ANON)
return NULL;
return (void *)((unsigned long)mapping & ~PAGE_MAPPING_FLAGS);
}
而 page_mapped 是用来判断 page 是否在 page table 里面。这里用 page_mapped() 主要是用来判断当前 page 是否是一个 mmap + MAP_SHARED 产生的页(因为 MAP_PRIVATE 产生的页不会填充到 page table 里面,具体可以自己看下代码)