一、背景
内存回收是一个非常大的话题,可以认为是内存管理的高级课题。虽然内存回收的主体逻辑并不是那么复杂,但是具体代码实现里涉及到大量的与内存管理相关基础概念有关的非常多的细节逻辑,这些细节逻辑有些就相当复杂,有些也并没有什么很好的资料可以查阅,有些知识点甚至在某些网上查阅的资料里的解释还是错的。
本文算是内存回收相关主题里的基础概念篇里的第一个部分,讲的是MIGRATE_TYPE里的类型,这些类型其实从字面意思上来说都感觉上很好理解,但是如果不结合实际实验,真正在问到一些细节时,你又不能确保地回答上来,所以需要实验的配合,另外,实验也能便于大家记忆概念。
下面大部分的实验项为自行编写,不少理论概念在经过阅读多份资料和代码求证以及反复思考之后进行了总结。
二、内存管理从大到小的全局的概念
内存管理从大到小的概念:
node:一般系统都是1个node,服务器可能会有多个node,cpu和内存如果属于不同的node访问内存的访问效率会低于node内的cpu的内存访问,一般cpu按照不同的node分开去访问不同node内的内存
zone:目前的x64系统,默认是一个node里有DMA/DMA32/NORMAL这三个zone
free_area:一个zone里按照order去管理空闲页
migrate_type:migrate_type是已经确定了上述的这些,比如,node已经确定是nodeA,zone也已经确定是zoneNORMAL,free_area是哪一个也已经确定比如是order 3,这些都已经确定的情况下,才去分不同的migratetype。一般系统上分为MIGRATE_MOVABLE/MIGRATE_UNMOVABLE/MIGRATE_RECLAIMABLE这三个类型
可以通过下图:
cat /proc/pagetypeinfo来阐述这个矩阵关系
注意:/proc/pagetypeinfo节点只展示了free_area的pages,并不是展示系统里的所有的pages
三、MIGRATE_TYPE的若干实验
page cache的使用在linux系统里是一个大课题,因为与之相关联的有很多系统的行为,也有不少问题与之相关,与硬盘有关的操作默认我们都会用到page cache(O_DIRECT方式除外),我们实验确认一下page cache属于哪个MIGRATE_TYPE。
然后,我们针对内核代码里使用kmalloc带上常用的GFP_KERNEL标志位进行分配的内存进行实验,确认其MIGRRATE_TYPE。
继而,我们在kmalloc分配内存时增加一些其他标志位,如__GFP_RECLAIMABLE等,再做类似的实验。
然后在第四章,我们根据实验的情况,回到代码里去理一下逻辑,因为有了实验的直观的感受,再去看代码会更好的记住逻辑。
3.1 实验对比确认page cache属于哪个MIGRATE_TYPE
我们实验用cp方式备份一个大文件(500G)来分析page cache的使用是属于哪个MIGRATE_TYPE。
在做实验前,我们先说明一下:
对于RECLAIMABLE类型的MIGRATE_TYPE,我们可以通过cat /proc/meminfo | grep Reclaimable来观测
关于KReclaimable和SReclaimable
可以简单的理解KReclaimable和SReclaimable他们俩是一样的,KReclaimable除了包含可回收的slab以外,还包含其他也用到shrinker去配合做内存紧张时回收的这部分内存。
3.1.2 在cp前,先用dd命令构造一个大文件来作为cp的src,用vmtouch -e把该大文件从page cache里去除
构造一个大文件:
dd if=/dev/zero of=/bigvolume/test8 bs=1G count=10
然后删除该大文件对应的page cache:
vmtouch -e test8
vmtouch -e test9
vmtouch工具可用于查看系统里的pagecache的使用情况,主要是针对具体的某个文件来查看使用了多少page cache,并且可以指定清除某个文件所用到的page cache
3.1.3 观察实验前后cat /proc/meminfo | grep Reclaimable的情况
拷贝前:
cat /proc/meminfo | grep Reclaimable
同时也cat看一下slab的情况:
cat /proc/slabinfo | grep -E "kmalloc-512"
执行拷贝:
cp test8 test9
拷贝后:
3.1.4 实验得出的推论
这个对比实验可以有三个推论:
1)page cache是属于reclaimable的migrate type
2)page cache的分配的kmalloc-512这个type,512对应的是当前磁盘的逻辑blocksize,一般都是512
3)KReclaimable的统计包含了SReclaimable的统计(SReclaimable统计的是slab里的可回收内存)
另外,需要提示的一点是,kmalloc-512所分配的大小并不是完全是文件的大小,它是远小于实际使用page cache的大小的,上面拷贝用了10G,但是对应的SReclaimable的增大只有200多M,这里面除了kmalloc-512的部分,还有buffer_head部分的slab,这里就不展开了,但是可以肯定的是,page cache部分的内存是可以进行回收的。
3.2 实验对比确认常用的kmalloc GFP_KERNEL分配的内存属于哪个MIGRATE_TYPE
如果是kmalloc GFP_KERNEL分配的内存,如下实验可以得出是MIGRATE_UNMOVABLE类型
我们是这么做实验的,分配前观察cat /proc/pagetypeinfo的情况,看是否系统目前是否保持稳定,再insmod一个测试的ko,用kzalloc来分配一块指定order的内存,观察对应order的free_page的变化情况,来确定是哪个MIGRATE_TYPE
分配前,稳定着unmovable order 5 也就是4k*32的大小,23个free
insmod的ko里执行的逻辑:
可以看到上图中的逻辑,是分配的order5的内存
分配后:
发现order5的UNMOVABLE的内存减少了程序代码里设的4个
3.3 实验对比确认使用__GFP_RECLAIMABLE标志属于哪个MIGRATE_TYPE
通过下面的对比实验可以确认使用__GFP_RECLAIMABLE标志进行分配的内存属于MIGRATE_RECLAIMABLE类型
kmalloc之前:
kmalloc了102400*4k=400M后
换个方式,用pagetypeinfo来对比看。
分配前:
分配了10个order为5的物理内存块后:
上图里里面有2个order5的page块拆分成小的page了。
对于alloc_pages的通用的传入GFP_KERNEL进行分配,也做了实验,确认了也是UNMOVABLE内存:
分配前:
分配后:
3.3.1 如果用kmem_cache_create分配,需要用reclaimable的MIGRATE_TYPE则需要用SLAB_RECLAIM_ACCOUNT标志位
在用kmem_cache_create时,带上SLAB_RECLAIM_ACCOUNT标志,让其
在__kmem_cache_create里会根据这个标志,置上__GFP_RECLAIMABLE
3.3.2 memcgroup统计和控制相关的SLAB_RECLAIM_ACCOUNT和GFP_KERNEL_ACCOUNT
上面的和SLAB_RECLAIM_ACCOUNT一同设的SLAB_ACCOUNT是用于内存统计,与memory cgroup功能有关,不是非常关键的分配路径一般都最好带上它,来收memory cgroup的统计和控制,关键路径或者不希望被memory cgroup管控的分配可以不带上它,SLAB_ACCOUNT是用于kmem_cache_create,与之相同意思的kmalloc分配时带的标志是GFP_KERNEL_ACCOUNT,这个标志比GFP_KERNEL多了收memory cgroup统计和限制这部分功能。
3.4 其他内存分配函数及实验
vmalloc是分配物理地址上不连续的但逻辑地址上连续的内存,gfp是GFP_KERNEL
__vmalloc是可以设置gfp的vmalloc
kvmalloc是优先分配物理地址上连续的,如果分配失败,则变成vmalloc方式:
使用vmalloc来分配的分配前后的对比实验:
在分配10个4M内存前,有大量的UNMOVABLE内存的free pages
分配代码:
在分配之后,unmovable内存一下子被占用了:
kvmalloc若使用默认的GFP_KERNEL标志进行分配,如果设置的size超过4M,则计算在/proc/meminfo里的Vmalloc项里,但如果不超过4M(意思就是能用kmalloc分配出来的话),则计算在/proc/meminfo里的Slab和SUnreclaim两项里。
3.5 实验相关的一些注意事项
1)虽然kmalloc如果分配4M远远超过了kmalloc使用slab进行分配的上限大小8k,但是它仍然被算在了/proc/meminfo的Slab里。
2)经实验发现如果kmalloc的size超过了slab的管理上限(一般是8k),就算以GFP_KERNEL_ACCOUNT | __GFP_RECLAIMABLE标志位来申请内存,无论是alloc_pages还是kmalloc进行分配的内存都不会统计到/proc/meminfo里的SReclaimable里,而小于8k,包括8k时,它是会被计算到SReclaimable里的。
3)可以借助shrinker来做一些内存吃紧时的回收的事情,内核在内存紧张时会触发调用所有的注册的shrinker,这个和交换到swap分区进行reclaim不是一件事情。
4)已做了实验,确认了如果用slab来分配,带上SLAB_RECLAIM_ACCOUNT是可以被计算进KReclaimable和SReclaimable的
这个4)这一项的验证,见下面代码:
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/types.h>
#define TEST_COUNT 1000000 // 定义要创建的对象个数
/* 定义一个简单的结构体 */
struct my_struct {
int data;
char name[1000];
};
/* 定义 slab 缓存 */
static struct kmem_cache *my_cache;
/* 定义指针数组来存储分配的对象 */
static struct my_struct *objects[TEST_COUNT];
/* 模块初始化 */
static int __init my_module_init(void) {
int i;
/* 创建一个 slab 缓存,使用 SLAB_RECLAIM_ACCOUNT 标志 */
my_cache = kmem_cache_create("my_cache", sizeof(struct my_struct), 0,
SLAB_HWCACHE_ALIGN | SLAB_RECLAIM_ACCOUNT, NULL);
if (!my_cache) {
pr_err("Failed to create slab cache\n");
return -ENOMEM;
}
/* 分配多个对象 */
for (i = 0; i < TEST_COUNT; i++) {
objects[i] = kmem_cache_alloc(my_cache, GFP_KERNEL);
if (!objects[i]) {
pr_err("Failed to allocate object %d from slab cache\n", i);
/* 释放已分配的对象 */
while (i > 0) {
kmem_cache_free(my_cache, objects[--i]);
}
kmem_cache_destroy(my_cache);
return -ENOMEM;
}
/* 初始化对象 */
objects[i]->data = i * 10; // 用一些测试数据
snprintf(objects[i]->name, sizeof(objects[i]->name), "Object %d", i);
pr_info("Allocated object %d: data = %d, name = %s\n", i, objects[i]->data, objects[i]->name);
}
return 0;
}
/* 模块退出 */
static void __exit my_module_exit(void) {
int i;
/* 释放分配的对象 */
for (i = 0; i < TEST_COUNT; i++) {
if (objects[i]) {
kmem_cache_free(my_cache, objects[i]);
pr_info("Freed object %d\n", i);
}
}
/* 销毁 slab 缓存 */
if (my_cache) {
kmem_cache_destroy(my_cache);
pr_info("Slab cache destroyed\n");
}
}
/* 模块信息 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example of slab allocation with SLAB_RECLAIM_ACCOUNT");
MODULE_VERSION("1.0");
module_init(my_module_init);
module_exit(my_module_exit);
insmod前:
insmod后:
四、内存分配时的标志位逻辑
在内存分配时,根据gfp来进行MIGRATE_TYPE的设置
根据gfp选择MIGRATE_TYPE的在分配内存时的调用链:
__alloc_pages->prepare_alloc_pages->gfp_migratetype
我们看一下gfp_migratetype的逻辑:
上图中的VM_WARN_ON宏是运行时检查,GFP_MOVABLE_MASK是__GFP_MOVABLE或上__GFP_RECLAIMABLE,这句检查的意思就是传入的gfp不能同时包含__GFP_MOVABLE和__GFP_RECLAIMABLE
事实上,MIGRATE_TYPE在内核里的定义就是,什么标志都不设是:
return (__force unsigned long)(gfp_flags & GFP_MOVABLE_MASK) >> GFP_MOVABLE_SHIFT;
是0,对应于MIGRATE_UNMOVABLE,MIGRATE_UNMOVABLE表示不可移动且不可回收
4.1 关于__GFP_FS和__GFP_IO
关于__GFP_FS和__GFP_IO的解释,在内核文档里有说明:
换句话说,gfp里有__GFP_FS标志表示在内存分配路径上可以使用或者访问文件系统
相应的,gfp里有__GFP_IO标志表示在内存分配路径上可以使用或者访问磁盘
从下图中可以看到,常用的内存分配标志位是带上这两个标志的,意思就是说,一般来说都是可以在内存分配路径上访问文件系统和磁盘的
而在文件系统/磁盘里的进行内存分配时,就会把对应的__GFP_FS/__GFP_IO剔除
下图是一些剔除动作的截图:
五、关于MIGRATE_TYPE的fallback概念
migrate_type在上面的章节里也提到了是去分类管理确定order的free_area。按照申请内存分配时传入的要求(gfp标志)来决定先从对应node对应zone对应order的哪个MIGRATE_TYPE的free_area里去要,但是如果要不到,会从zone里的更大的order里去找,直到MAX_ORDER。如果还是找不到,会从zone里的其他migrate类型的free_area里去偷,也就是这里说的fallback的概念。与之有关的有非常多的细节,这些会在后面的内存管理的章节里详细展开。