当前位置: 首页 > news >正文

madvise MADV_FREE对文件页统计的影响及原理

一、背景

madvise系统调用是一个与性能优化强相关的一个系统调用。madvise系统调用包括使用madvise函数,也包含使用posix_fadvise函数。如我们可以使用posix_fadvise传入POSIX_FADV_DONTNEED来清除文件页的page cache以减少内存压力。

这篇博客里,我们讲的是madvise(addr,size,MADV_FREE)这个调用,要注意,这个调用只能针对匿名内存,对于文件页的对应的内存是调用这个会报错(这一点会在下面 3.1 里讲到)。

在下面第二章里,我们贴出测试源码并说明测试方法并展示测试结果。在第三章里,我们给出相关细节的原理分析。

二、测试程序源码及效果展示

2.1 测试源码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#include <errno.h>#define PAGE_SIZE 4096ull
#define NUM_PAGES 1024*512ull // 分配 2G 的内存int main() {// 分配一块大的匿名内存size_t size = NUM_PAGES * PAGE_SIZE;void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);if (addr == MAP_FAILED) {perror("mmap");return EXIT_FAILURE;}printf("Allocated %zu bytes of anonymous memory at %p\n", size, addr);// 触发缺页异常printf("Accessing a page to trigger a page fault...\n");memset(addr, 0, size); // 访问第一页getchar();// 在这里可以观察到缺页异常的发生// 使用 madvise 将未使用的页面标记为 MADV_DONTNEEDif (madvise(addr, size, MADV_FREE) != 0) {perror("madvise MADV_FREE");munmap(addr, size);return EXIT_FAILURE;}printf("Marked memory as MADV_FREE\n");getchar();printf("Reuse the memory!\n");memset(addr, 0, size); // 访问第一页// 等待 10 秒getchar();// 再次使用 madvise 将内存标记为 MADV_DONTNEED(这在实际情况下是没有必要的,因为上面已经释放了)// 只是为了演示if (madvise(addr, size, MADV_DONTNEED) != 0) {perror("madvise MADV_DONTNEED");munmap(addr, size);return EXIT_FAILURE;}printf("Re-marked memory as MADV_DONTNEED\n");getchar();// 解除映射if (munmap(addr, size) != 0) {perror("munmap");return EXIT_FAILURE;}getchar();printf("Unmapped memory\n");return EXIT_SUCCESS;
}

2.2 编写一个内核模块用来抓取调用栈和调用信息

编写了一个内核模块,用来抓取madvise这个调用栈和调用信息。

2.2.1 内核模块源码

下面的这个代码是改写的之前在分析vdso内容时写的内核模块(vdso概念及原理,vdso_fault缺页异常,vdso符号的获取_x86架构的vdso-CSDN博客 里 2.3.1 一节里的代码),改写了一下,所以名字里包含了vdso字样。

关键的改动即注册kprobe的callback时设了lru_lazyfree_fn这个接口:

然后加入了一个pid的条件控制:

在指定的pid进程内才打印堆栈,也只打印一次,然后统计执行的次数,并打印:

完整的代码如下:

#include <linux/module.h>
#include <linux/capability.h>
#include <linux/sched.h>
#include <linux/uaccess.h>
#include <linux/proc_fs.h>
#include <linux/ctype.h>
#include <linux/seq_file.h>
#include <linux/poll.h>
#include <linux/types.h>
#include <linux/ioctl.h>
#include <linux/errno.h>
#include <linux/stddef.h>
#include <linux/lockdep.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <linux/wait.h>
#include <linux/init.h>
#include <asm/atomic.h>
#include <trace/events/workqueue.h>
#include <linux/sched/clock.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/interrupt.h>
#include <linux/tracepoint.h>
#include <trace/events/osmonitor.h>
#include <trace/events/sched.h>
#include <trace/events/irq.h>
#include <trace/events/kmem.h>
#include <linux/ptrace.h>
#include <linux/uaccess.h>
#include <asm/processor.h>
#include <linux/sched/task_stack.h>
#include <linux/nmi.h>
#include <asm/apic.h>
#include <linux/version.h>
#include <linux/sched/mm.h>
#include <asm/irq_regs.h>
#include <linux/kallsyms.h>
#include <linux/kprobes.h>
#include <linux/stop_machine.h>MODULE_LICENSE("GPL");
MODULE_AUTHOR("zhaoxin");
MODULE_DESCRIPTION("Module for vdso_fault debug.");
MODULE_VERSION("1.0");static int pid = 0;
module_param(pid, int, 0);struct kprobe _kp1;static bool _blog = false;int getfullpath(struct inode *inode,char* i_buffer,int i_len)
{struct dentry *dentry;//printk("inode = %ld\n", inode->i_ino);//spin_lock(&inode->i_lock);hlist_for_each_entry(dentry, &inode->i_dentry, d_u.d_alias) {char *buffer, *path;buffer = (char *)__get_free_page(GFP_KERNEL);if (!buffer)return -ENOMEM;path = dentry_path_raw(dentry, buffer, PAGE_SIZE);if (IS_ERR(path)){continue;   }strlcpy(i_buffer, path, i_len);//printk("dentry name = %s , path = %s", dentry->d_name.name, path);free_page((unsigned long)buffer);}//spin_unlock(&inode->i_lock);return 0;
}static bool blog = false;
static u64 runtimes = 0;int kprobecb_vdso_fault_pre(struct kprobe* i_k, struct pt_regs* i_p)
{if (current->pid == pid) {if (!blog) {blog = true;dump_stack();}runtimes ++;printk("run lru_lazyfree_fn %llu times!\n", runtimes);}return 0;
}int kprobe_register_func_vdso_fault(void)
{int ret;memset(&_kp1, 0, sizeof(_kp1));_kp1.symbol_name = "lru_lazyfree_fn";_kp1.pre_handler = kprobecb_vdso_fault_pre;_kp1.post_handler = NULL;ret = register_kprobe(&_kp1);if (ret < 0) {printk("register_kprobe fail!\n");return -1;}printk("register_kprobe success!\n");return 0;
}void kprobe_unregister_func_vdso_fault(void)
{unregister_kprobe(&_kp1);
}static int __init testvdso_init(void)
{kprobe_register_func_vdso_fault();return 0;
}static void __exit testvdso_exit(void)
{kprobe_unregister_func_vdso_fault();
}module_init(testvdso_init);
module_exit(testvdso_exit);

2.2.2 抓到的madvise的调用栈和调用信息

抓到的堆栈:

如下图可以看到执行了524280次,是0x7FFF8,离2G的0x80000个4k page的0x80000的个数差了8:

这个差值来自于下图里的红色框出逻辑的批处理逻辑的判断:

2.3 看/proc/meminfo和free -h的测试结果

我们关注运行测试程序期间及之前和之后的/proc/meminfo和free -h的状态变化。

2.3.1 对比观察free -h的变化

程序运行前,buff/cache是14G,free是106G:

执行程序之后,并触发2G的缺页异常之后:

看free -h的变化是,buff/cache不变,free减少2G,从106G到104G:

然后再运行madvise(addr, size, MADV_FREE):

从free -h看是没有变化的:

所以,madvise(addr, size, MADV_FREE)的执行对free -h的统计是不产生变化的。

2.3.2 对比观察/proc/meminfo的变化

观察的脚本是:

watch -n 0.1 "cat /proc/meminfo | grep -E 'MemFree|Buffers|Cached|Active|Inactive|\(anon\)|\(file\)|AnonPages|Mapped'"

我们只观察我们需要重点关注这几项。

执行程序前是:

触发2G的缺页异常之后:

可以看到MemFree如预期减少2G,Active统计增加2G,Active(anon)统计增加2G,Active(file)统计不增加。对于AnonPages统计项,是增加了2G,对于Mapped统计项,未变动。

再继续运行程序,调用madvise(addr, size, MADV_FREE)之后:

可以如上图看到,在这个调用的前后情况来看,MemFree无变化,Buffers/Cached都无变化,Active里减少2G到了Inactive里,即从Active(anon)里减少了2G到了Inactive(file)里。

而对于AnonPages统计项和Mapped统计项,这个madvise(addr, size, MADV_FREE)调用无变动。

三、原理分析

3.1 madvise(addr, size, MADV_FREE)不能用于文件页

我们把 2.1 里的源码修改一下,修改过后的源码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>#define PAGE_SIZE 4096ull
#define NUM_PAGES 1024*512ull // 分配 2G 的内存int main() {size_t size = NUM_PAGES * PAGE_SIZE;int fd = open("temp_file.bin", O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);if (fd == -1) {perror("open");return EXIT_FAILURE;}if (ftruncate64(fd, size) == -1) {perror("ftruncate");close(fd);return EXIT_FAILURE;}// 分配一块大的匿名内存void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (addr == MAP_FAILED) {perror("mmap");return EXIT_FAILURE;}printf("Allocated %zu bytes of anonymous memory at %p\n", size, addr);// 触发缺页异常printf("Accessing a page to trigger a page fault...\n");memset(addr, 0, size); // 访问第一页getchar();// 在这里可以观察到缺页异常的发生// 使用 madvise 将未使用的页面标记为 MADV_DONTNEEDif (madvise(addr, size, MADV_FREE) != 0) {perror("madvise MADV_FREE");munmap(addr, size);return EXIT_FAILURE;}//posix_fadvise(fd, 0, size, POSIX_FADV_DONTNEED);printf("Marked memory as MADV_FREE\n");getchar();printf("Reuse the memory!\n");memset(addr, 0, size); // 访问第一页// 等待 10 秒getchar();// 再次使用 madvise 将内存标记为 MADV_DONTNEED(这在实际情况下是没有必要的,因为上面已经释放了)// 只是为了演示if (madvise(addr, size, MADV_DONTNEED) != 0) {perror("madvise MADV_DONTNEED");munmap(addr, size);return EXIT_FAILURE;}printf("Re-marked memory as MADV_DONTNEED\n");getchar();// 解除映射if (munmap(addr, size) != 0) {perror("munmap");return EXIT_FAILURE;}getchar();printf("Unmapped memory\n");return EXIT_SUCCESS;
}

运行后看到madvise(addr, size, MADV_FREE)这句话调用出错:

所以madvise(addr, size, MADV_FREE)的这个调用只能用于匿名内存。

3.2 madvise(addr, size, MADV_FREE)会将该匿名内存挪到Inactive(file)里

有关这个统计项的迁移的核心逻辑即调用madvise(addr, size, MADV_FREE)时最终调用到:

folio_mark_lazyfree调用了lru_lazyfree_fn

在lru_lazyfree_fn里完成了统计上的迁移。

这个迁移从folio_mark_lazyfree函数的注释里也可以清晰的看到描述:

可以看到,这个迁移的原因之一就是加速回收逻辑。因为我们系统里的大部分内存回收都是回收的inactive file里的页。

我们再来看一下lru_lazyfree_fn函数的实现:

可以从上图里看到,红色框出的注释里清楚地写到,Lazyfree的这部分folio需要清楚swapbacked的flag,为的是和普通的匿名页相区别。怎么理解呢,因为普通的匿名页都是指已经完成了物理页的分配并会继续使用的部分,而这部分调用madvise MADV_FREE则是不再继续使用的部分,所以内核并不需要在内存紧张时把它们交换到swap分区里去,因为这部分page上面的数据用户已经标记了不再使用了。

关于这个folio是不是file的lru的判断,内核的函数folio_is_file_lru如下:

3.3 /proc/meminfo里的Mapped和cgroup的memory.stat里的file一样都不会统计到该MADV_FREE出来的内存

在上面的实验里,我们也看到了/proc/meminfo里的Mapped统计项是不会统计到该madvise MADV_FREE出来的内存的。

同样的对于memory.stat里的file项的统计也是一样的,也是不统计到该madvise MADV_FREE出来的内存的:

其实/proc/meminfo里的Mapped这个名字更加贴切,也不容易产生误解。即产生过文件映射的部分。但要注意,shm_open创建出来的共享内存,由于有tmpfs文件系统下的文件映射,所以也要包含到/proc/meminfo里的Mapped的统计,也同样的包含到memory.stat里的file的统计。

相关文章:

  • Java求职面试:从Spring Boot到微服务架构的全面解析
  • NGINX upstream、stream、四/七层负载均衡以及案例示例
  • qt编译报错error: ‘VideoSrcCtrl‘ does not name a type
  • vue中将html2canvas转成的图片传递给后台java
  • idea软件配置移动到D盘
  • 20250427在ubuntu16.04.7系统上编译NanoPi NEO开发板的FriendlyCore系统解决问题mkimage not found
  • Jetpack Compose多布局实现:状态驱动与自适应UI设计全解析
  • 数字巴别塔:全栈多模态开发框架如何用自然语言重构软件生产关系?
  • 基于单片机的智能药盒系统
  • 树莓派超全系列教程文档--(43)树莓派内核简介及更新
  • django admin AttributeError: ‘UserResorce‘ object has no attribute ‘ID‘
  • 《数据结构初阶》【顺序表 + 单链表 + 双向链表】
  • 利用人工智能和快速工程增强 API 测试
  • docker打开滚动日志
  • Missashe考研日记-day28
  • python合并一个word段落中的run
  • 如何优雅地解决AI生成内容粘贴到Word排版混乱的问题?
  • 解决两个技术问题后小有感触-QZ Tray使用经验小总结
  • 「浏览器即OS」:WebVM技术栈如何用Wasm字节码重构冯·诺依曼体系?
  • .aar中申请权限时使用了android:maxSdkVersion导致主App的权限组找不到对应的权限
  • 中公教育薪酬透视:董监高合计涨薪122万,员工精简近三成
  • 国家发改委:建立实施育儿补贴制度
  • 2025厦门体育产业采风活动圆满举行
  • 如何做大中国拳击产业的蛋糕?这项赛事给出办赛新思考
  • 马上评丨喷淋头全是摆设,酒店消防岂能“零设防”
  • 国家市监总局:民生无小事,严打民生领域侵权假冒违法行为