读取文件的时候,会触发多少次page fault 中断呢? 这影响性能呢。
这取决于 用户读取文件的方式。linux内核对用户读文件,建模为两种方式:顺序读 与 随机读。接下来的两组实验以 两种读取方式为参照变量,进行观察。
page fault 分为两种minor fault 和 major fault。
# 生成128m文件
dd if=/dev/zero of=1g.img bs=1M count=128
观察并分析如下结果,
# 实验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
[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中的顺序读写时,只发生一次major fault,而不是大量的。难道只需一次major page fault就可以预读整个文件?显然一次中断是不够的。除了第一次触发的page fault中断的预读,后续的预读都是发生在 read()系统调用中。read()系统调用中,会检查当前页是否设置了PG_readahead标志,标识到达 预读窗口末端,是则 触发 Linux内核预读机制中的page_cache_async_readahead()进行预读。
此机制下,实验1中的连续读场景 只触发一次major page fault,将与文件大小呈现 线性增加的中断开销,降低为常数级别的中断开销。需要注意的是,磁盘io的开销是不变的,只是转移到read()系统调用中。
这揭示了程序世界约定俗成的局部性友好原理。用户行为的局部性模型,方便程序设计者进行可预测性的优化。

而在实验2中的随机读场景中,用户读行为的不可预测,难以命中 内核的文件预读cache,此时的预读反而成为一种负担,无用的cache充斥着内存。major page fault的中断开销不可避免。
泛化而言,这有点类似linux内核网络设备中的napi收包。都是利用局部性原理。
连续读的场景,接近 产销模型,内核磁盘IO生产cache,进程消费cache。不同的是,这里 由消费者来 触发 生产者的生产(经典反馈系统),且其磁盘IO速度是有限的,明显小于消费者速度。
产销模型就是一阶反馈系统。
预读窗口的末端不仅打上PG_readahead标记,同时打上时间戳。当当前page有PG_readahead标记时,根据时间戳计算距离上次预读的时间interval。interval较小,则增大预读窗口。
这种异步方式,仍会出现生产-消费速度不一致的情况,且增加编码难度(如何进程同步,如何处理临界区)。同步方式的好处 就是能协调 生产-消费的速度,编码容易清晰、易维护。
多次read()中,会有一次需要磁盘文件预读,会增大此次系统调用的时间。通过调整PG_readahead的位置

weird-major-page-fault-number-when-reading-sequentially-randomly-in-mmap-regio
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;
}