一、背景
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的統計。