简单记录下最近的 Linux 下统计缺页次数的实验。

方法大致有两种:

  1. 读取 /proc/stat 文件中 intr 中断字段后的第 16 项内容,该项的值为系统启动以来的缺页中断次数;

  2. 修改内核,加入缺页中断函数 do_page_fault 的统计变量,通过动态加载模块,利用 proc 文件系统来获取该统计变量。

关于第一种方法,结果不一定准确,在我的 Ubuntu(17.04 和 18.04)下一直显示为 0,CentOS 下虽然不为 0,但结果与第二种方法的相去甚远,所以下面主要的讲的是第二种方法。

网上有一些方法,但都是基于 3.10 之前的内核,和后来的相比,相关的代码有比较大的变化,故不再拿老内核做例。由于我的 CentOS7 是比较老的 3.10 内核,所以下面的方法都是基于稍微新一点 3.16,更新的 4.xx 内核当然也没问题。

实验环境

  • CentOS 7 x64

  • kernel : 3.10.0

修改内核源代码实现缺页统计

前期准备

下载安装必要的开发工具

yum groupinstall "development tools"

里面包括 gcc 等编译内核需要的工具

下载内核源代码

Linux 内核源代码网址公布在 kernel.org

wget https://cdn.kernel.org/pub/linux/kernel/v3.x/linux-3.16.56.tar.xz

创建目录并解压

xz -d linux-3.16.56.tar.xz
tar -xvf linux-3.16.56.tar 
cd linux-3.16.56/

修改内核源代码

总计需要修改 3 个文件

fault.c

根据自己主机的架构进行选择,可以用uname -a查看,一般都是 Intel x86,定位到 arch/**x86**/mm/fault.c 文件。

vim 打开定位到 do_page_fault 函数,修改如下注释两处:

unsigned long volatile pfcount;	//1.添加统计缺页次数的全局变量pfcount
dotraplinkage void notrace
do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
        unsigned long address = read_cr2(); /* Get the faulting address */
        enum ctx_state prev_state;

        /*
         * We must have this function tagged with __kprobes, notrace and call
         * read_cr2() before calling anything else. To avoid calling any kind
         * of tracing machinery before we've observed the CR2 value.
         *
         * exception_{enter,exit}() contain all sorts of tracepoints.
         */

        prev_state = exception_enter();
        __do_page_fault(regs, error_code, address);
        pfcount++;	// 2.每调用一次该函数,pfcount 加一
        exception_exit(prev_state);
}
NOKPROBE_SYMBOL(do_page_fault);

系统每发生一次缺页错误便会调用该函数,所以我们可以认为该函数的调用次数就是系统缺页的次数。

mm.h

include/linux/mm.h 文件中加入全局变量 pfcount 的声明

extern unsigned long totalram_pages;
extern void * high_memory;
extern int page_cluster;
extern unsigned long volatile pfcount;	//声明全局变量pfcount

kallsyms.c

kernel/kallsyms.c 文件中导出全局变量 pfcount,使内核及模块可以访问

device_initcall(kallsyms_init);
EXPORT_SYMBOL(pfcount);	//导出全局变量pfcount

编译安装内核

拷贝原内核的配置文件

cp /boot/config-3.10.0-514.21.1.el7.x86_64 .config

开始编译

make -j

可以在参数 -j 后指定并行编译的任务数,比如说 -j8 是 8 线程,具体视自己 cpu 核心数而定。这一步耗时比较长,我选的编译方式会有一大堆配置选项,基本上直接回车继续,不影响最终结果。 

安装新内核模块和新内核

将新内核模块安装到系统的标准模块目录中

make modules_install

将新内核安装到系统中

make install

设置新内核为默认内核并重启

查看一下系统当前有几个内核:cat /boot/grub2/grub.cfg | grep menuentry

menuentry 'CentOS Linux (3.16.56) 7 (Core)' --class centos --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-3.10.0-514.21.1.el7.x86_64-advanced-58ebcc18-06a4-454a-a772-4f583085b996' {
menuentry 'CentOS Linux (3.10.0-514.21.1.el7.x86_64) 7 (Core)' --class centos --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-3.10.0-514.21.1.el7.x86_64-advanced-58ebcc18-06a4-454a-a772-4f583085b996' {
menuentry 'CentOS Linux (3.10.0-514.el7.x86_64) 7 (Core)' --class centos --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-3.10.0-514.el7.x86_64-advanced-58ebcc18-06a4-454a-a772-4f583085b996' {
menuentry 'CentOS Linux (0-rescue-8d13a50988cc5c4972347415eddf7d47) 7 (Core)' --class centos --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-0-rescue-8d13a50988cc5c4972347415eddf7d47-advanced-58ebcc18-06a4-454a-a772-4f583085b996' {

根据版本号可以知道,第一个是系统原来的内核,第一个是新生成的内核,

修改默认内核:grub2-set-default "CentOS Linux (3.16.56) 7 (Core)"

重启:reboot

查看内核版本号:uname -r 检验是否切换成功

读取统计结果

编写读取 pfcount 值的模块代码

先在主目录创建一个文件夹,用来放置相关文件

cd ~
mkdir test_page_fault
cd test_page_fault/
vim pf.c

需要注意的是从 Linux 3.10 开始,create_proc_read_entry 函数改成了proc_create,所以网上的方法对于 3.10 后的内核已经不适用。对于 proc 文件系统的读写,我参考了其他 proc 文件读写的源代码,用到了 seq_file.h 头文件。

#include<linux/init.h>
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/mm.h>
#include<linux/proc_fs.h>
#include<linux/fs.h>
#include<linux/seq_file.h>
#include<linux/string.h>
#include<asm/uaccess.h>

struct proc_dir_entry *proc_pf;
struct proc_dir_entry *proc_pfcount;
extern unsigned long volatile pfcount;

static int show_pfcount(struct seq_file *p, void *v)
{
        seq_printf(p, "%ld\n", pfcount);
        return 0;
}

static int pfcount_open(struct inode *inode, struct file *file)
{
        return single_open(file, show_pfcount, NULL);
}

struct file_operations fops = {
        .open = pfcount_open,	//自定义函数 pfcount_open 实现内置 open 方法
        .read = seq_read,
};

static int pf_init(void)
{
        proc_pf = proc_mkdir("pf", 0);  //在 proc 目录下创建名为 pf 的文件夹        
        proc_create("pfcount", 0777, proc_pf, &fops);   //在 pf 文件夹下创建名为 pfcount 的文件,并将内核模块中的 pfcount 值写入该文件中
        return 0;
}

static void pf_exit(void)
{
        remove_proc_entry("pfcount", proc_pf);
        remove_proc_entry("pf", 0);
}

module_init(pf_init);
module_exit(pf_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("violettime");

编译链接生成 pf.ko 模块

pf.c 同级目录下创建 Makefile 来编译构建内核模块,文件内容如下:

obj-m := pf.o
ifneq ($(KERNELRELEASE),)
    obj-m:=pf.o
else
    KDIR:=/lib/modules/$(shell uname -r)/build
    PWD:=$(shell pwd)
default:
        $(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
        $(MAKE) -C $(KDIR) M=$(PWD) clean
endif

使用make命令生成 pf.ko 模块

加载 pf.ko 模块

insmod pf.ko

使用模块的好处就是如果你不想用它,可以使用rmmod pf.ko命令卸载。

查看缺页次数

cat /proc/pf/pfcount

相隔一段时间的缺页次数