• 如何制造更多的 major page fault


    初衷

    读取文件的时候,会触发多少次page fault 中断呢? 这影响性能呢。
    这取决于 用户读取文件的方式。linux内核对用户读文件,建模为两种方式:顺序读 与 随机读。接下来的两组实验以 两种读取方式为参照变量,进行观察。

    前置知识

    page fault 分为两种minor fault 和 major fault。

    • minor fault 对应页表缺失的page fault,此中断中 只需进行 建立MMU页表映射。
    • major fault 对应 页表缺失+文件cache缺失。此中断中还需进行磁盘IO,将磁盘中的数据读取到 内存中。

    实验

    环境

    centos8

    # 生成128m文件
    dd if=/dev/zero of=1g.img bs=1M count=128
    
    • 1
    • 2

    观测进程page fault中断次数

    实验1:顺序读下的page fault 次数

    观察并分析如下结果,

    # 实验1:顺序读的page fault 次数
    [root@ct8test88 z]# perf stat -e cpu-clock,page-faults,minor-faults,major-faults -I 1000 ./test 128m.img seq
    running
    sequence read
    #           time             counts unit events
         0.001381612               0.62 msec cpu-clock                 #    0.001 CPUs utilized
         0.001381612                 50      page-faults               #    0.081 M/sec
         0.001381612                 49      minor-faults              #    0.080 M/sec
         0.001381612                  1      major-faults              #    0.002 M/sec
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    实验2:随机读下的page fault 次数

    [root@ct8test88 z]# perf stat -e cpu-clock,page-faults,minor-faults,major-faults -I 1000 ./test 128m.img randon
    running
    randon read
    #           time             counts unit events
         1.001084681             881.64 msec cpu-clock                 #    0.882 CPUs utilized
         1.001084681              2,179      page-faults               #    0.002 M/sec
         1.001084681              2,150      minor-faults              #    0.002 M/sec
         1.001084681                 29      major-faults              #    0.033 K/sec
         1.929977246             928.73 msec cpu-clock                 #    0.929 CPUs utilized
         1.929977246                  1      page-faults               #    0.001 K/sec
         1.929977246                  1      minor-faults              #    0.001 K/sec
         1.929977246                  0      major-faults              #    0.000 K/sec
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    实验1现象解读

    实验1中的顺序读写时,只发生一次major fault,而不是大量的。难道只需一次major page fault就可以预读整个文件?显然一次中断是不够的。除了第一次触发的page fault中断的预读,后续的预读都是发生在 read()系统调用中。read()系统调用中,会检查当前页是否设置了PG_readahead标志,标识到达 预读窗口末端,是则 触发 Linux内核预读机制中的page_cache_async_readahead()进行预读。

    中断开销的复杂度分析

    此机制下,实验1中的连续读场景 只触发一次major page fault,将与文件大小呈现 线性增加的中断开销,降低为常数级别的中断开销。需要注意的是,磁盘io的开销是不变的,只是转移到read()系统调用中。
    这揭示了程序世界约定俗成的局部性友好原理。用户行为的局部性模型,方便程序设计者进行可预测性的优化。
    实验1的连续读场景

    实验2现象解读

    而在实验2中的随机读场景中,用户读行为的不可预测,难以命中 内核的文件预读cache,此时的预读反而成为一种负担,无用的cache充斥着内存。major page fault的中断开销不可避免。

    类比与联想

    泛化而言,这有点类似linux内核网络设备中的napi收包。都是利用局部性原理。
    连续读的场景,接近 产销模型,内核磁盘IO生产cache,进程消费cache。不同的是,这里 由消费者来 触发 生产者的生产(经典反馈系统),且其磁盘IO速度是有限的,明显小于消费者速度。
    产销模型就是一阶反馈系统。

    优化

    预读窗口的动态调整

    预读窗口的末端不仅打上PG_readahead标记,同时打上时间戳。当当前page有PG_readahead标记时,根据时间戳计算距离上次预读的时间interval。interval较小,则增大预读窗口。

    预读过程 卸载给 专门的线程

    这种异步方式,仍会出现生产-消费速度不一致的情况,且增加编码难度(如何进程同步,如何处理临界区)。同步方式的好处 就是能协调 生产-消费的速度,编码容易清晰、易维护。

    降低read()的时间抖动

    多次read()中,会有一次需要磁盘文件预读,会增大此次系统调用的时间。通过调整PG_readahead的位置
    在这里插入图片描述

    参考

    weird-major-page-fault-number-when-reading-sequentially-randomly-in-mmap-regio

    复现

    test.c源代码

    int main(int argc, char ** argv) {
        // int fd = open(argv[1], O_RDONLY | O_DIRECT);
        int fd = open(argv[1], O_RDONLY | O_DIRECT);
        struct stat stats;
        fstat(fd, &stats);
        posix_fadvise(fd, 0, stats.st_size, POSIX_FADV_DONTNEED);
        char * map = (char *) mmap(NULL, stats.st_size, PROT_READ, MAP_SHARED, fd, 0);
        if (map == MAP_FAILED) {
          perror("Failed to mmap");
          return 1;
        }
        int result = 0;
        int i;
        printf("running\n");
        if(!strcmp("seq", argv[2])) {
            printf("sequence read\n");
            for (i = 0; i < stats.st_size; i++) {
              result += map[i];
            }
        } else { // randon read
            // hopefully this won't trigger extra page faults
            unsigned int idx = 0;
            printf("randon read\n");
            for (i = 0; i < stats.st_size; i++) {
              result += map[idx % stats.st_size];
              idx += i;
            }
        }
    
        munmap(map, stats.st_size);
        return result;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
  • 相关阅读:
    手写Spring——bean的扫描、加载和实例化
    LeetCode-N 皇后(C++)
    PEDOT:PSS/甘油酸胆碱([Ch][Glyce])离子液体混合材料
    【数据结构教程】线性表及其逻辑结构
    018-Java类与对象案例分析
    Vue3中diff算法比对新老节点孩子数组
    OpenCV-Python实战(4) —— OpenCV 五角星各点在坐标系上面的坐标计算(以重心为原点)
    Vue 汉字转拼音;根据拼音首字母排序转二维数组;提取拼音首字母排序。
    CentOS 系统如何在防火墙开启端口
    Nacos 认识和安装-1
  • 原文地址:https://blog.csdn.net/qq_20679687/article/details/126547491