
在那Linux的系统当中,文件预读的机制可是提升文件读取性能的一个很关键的办法。它就好像一个小小的助手似的,在你真正需要文件数据之前,就提前把很多有可能会用到的数据给读到内存缓存里面去。这样子当应用程序去请求数据的时候,就能够很快地从内存里头拿到,大大的减少了等待磁盘I/O的时间。在不少的场景当中,文件预读真的是让系统的性能有很明显的提升,比如说在顺序读取大文件的时候,预读能够让数据的读取变得很流畅。
但是,如同任何事情都存在两面性一样,文件预读在某些时候,也有可能从“小助手”转变成“拖油瓶”,成为系统性能的阻碍。举例来说在一些随机读写老是很频繁的场景当中,预读有可能会读取很多根本用不上的数据,这不仅浪费系统资源,还可能使得内存被大量占据着,影响别的进程正常地运行,反而降低了系统整体的性能。所以好好去了解Linux文件预读的原理,还有怎么去避免它成为性能瓶颈,对于优化Linux系统性能可是非常重要的 。
一、Linux 文件预读是什么?
面试题写作模版Linux文件预读究竟是个情况?简单来说系统的内核会去推测应用程序接下来有可能去访问哪些个文件页面。然后就提前把很多个页面从磁盘读到内存的缓存里边去,也就是pagecache。当应用程序实际去发起文件读取请求的时候,如果请求的数据在缓存里头的话,那就能够直接从内存里边去获取,这样子就大大的减少了等待磁盘I/O操作的时间。
文件进行预先读取存在着两个主要的目的。其中一个目的是减小I/O等待的时间。磁盘的读写相较于内存而言要慢上非常多,一次磁盘I/O操作有可能需要几毫秒甚至于更久的时间,而从内存读取数据的时间是以纳秒来计算的。要是应用程序每一次读取文件都去等待磁盘I/O,那么系统整体的性能就会大幅度受到影响。
通过预先读取,提前地把数据加载到内存当中,应用程序就可以快速地获取到数据,从而提升运行的效率。另外一个目的是提高磁盘的利用效率。磁盘的特性使得它更加适合大块数据的顺序读写。预读的机制会把多个小的I/O请求合并成为大的I/O请求,这样就可以减少磁盘寻道的次数,提高磁盘带宽的利用效率,从而进一步提升系统整体的吞吐量。
Linux内核对于判断顺序读以及随机读主要是依据应用程序读取文件时的偏移量情况来进行的。当应用程序在对文件进行连续读取的时候,每一次读取的偏移量是呈现出递增的状态并且是相邻的,在这个时候内核就会将其判定为顺序读。倘若读取的偏移量不存在明显的递增规律,又或者存在着较大跨度的跳跃情况,内核就会把它判断成为随机读。举例来说有一个程序按照顺序去读取文件的1 - 4KB、4 - 8KB、8 - 12KB这些区域,这就是典型的顺序读。可是要是程序先去读1 - 4KB,紧接着去读100 - 104KB,然后再去读20 - 24KB,那么这就是随机读。
当内核对其判定为是顺序读的时候,那就会启动预读的机制。预读是这么个情况:要是应用程序当下读取了文件的某一段数据,内核会按照一定的算法,算出接下来大概要读取的数据范围,然后把那部分数据提前读进到pagecache里边去。打个比方来说,应用程序读取了文件开头的4KB的数据,内核有可能就会预读接下来的16KB的数据。
在后续进行读取的时候,如果应用程序请求的数据刚好就在预读的范围里边,那就能够直接从缓存当中获取,从而达成快速响应。而对于随机读,由于很难去预测下一次会读取哪里的数据,所以内核通常就不会进行预读的操作,因为要是盲目地进行预读,可能就会读取很多没用的数据,这样就会浪费系统的资源。还不懂pagecache,可以参考这篇《不懂 page cache,别再说你懂 Linux 内核了》
下面存在着一个简单的C程序用来演示顺序读取文件的过程。这个程序每一次读取4KB的数据。它会连续地进行读取好多次。在顺序读取的时候,内核就会自动地触发预读操作,把后续的很多数据提前地加载到pagecache里边去:
#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <unistd.h>#define BUF_SIZE 4096#define READ_TIMES 100int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "Usage: %s <file>\n", argv[0]); return 1; } int fd = open(argv[1], O_RDONLY); if (fd == -1) { perror("open"); return 1; } char *buf = malloc(BUF_SIZE); if (!buf) { perror("malloc"); close(fd); return 1; } ssize_t n; for (int i = 0; i < READ_TIMES; i++) { n = read(fd, buf, BUF_SIZE); if (n <= 0) break; /* 处理读取到的数据 */ } free(buf); close(fd); return 0;}程序运用open这个操作来打开文件。之后在循环之中每一次调用read去读取4096字节。由于读取的偏移量是连续递增的,内核就会判定为是顺序读并且触发预读。第一次进行read的时候,内核不只是读取所请求的4KB,还会预先读取后面若干KB的数据到pagecache里。后续的read请求要是命中了预读的范围,就直接从内存当中返回,不需要去等待磁盘I/O。
二、预读如何成为性能瓶颈?
预读如何成为性能瓶颈
(1)内存占用问题——预读能够使得文件读取变快,但是要是不对其进行控制的话,就会占据很多内存。
在Linux系统当中,预读的数据是存放在pagecache里面的。要是预读量过大的话,pagecache就会被大量的预读数据给占满,如此一来其他进程能够使用的内存就变少了。举例来说在一个8GB内存的服务器上,要是预读机制不断地把大量的文件数据读进pagecache,占据了6GB甚至更多的内存,那么其他需要内存来运行的程序,像是数据库服务、Web服务器程序等等,就有可能因为内存不够而频繁地进行磁盘交换操作。
这如同一家餐厅,原本有足够的座位供各种各样的顾客使用,可是预读这个贪吃的顾客一下子占据了大部分座位,其他顾客没有地方坐,只能在外面等着,这严重影响了餐厅的运营效率。频繁的磁盘交换操作会让系统的I/O负担大大增加,因为内存和磁盘之间的数据交换速度比内存里面的数据处理速度要慢得多,最终就会使得整个系统的性能下降。
下面是查看 pagecache 内存占用的命令示例,可以用来判断预读数据是否过度占用了内存:
# 查看内存详细信息,重点关注 Cached、Buffers、SwapCachedcat /proc/meminfo | grep -E "Cached|Buffers|Swap|MemTotal|MemFree"# 输出示例:# MemTotal: 8167848 kB# MemFree: 524100 kB# Buffers: 12300 kB# Cached: 6892400 kB# SwapCached: 0 kB# SwapTotal: 2097148 kB# SwapFree: 2097148 kB# 用 free 命令查看整体内存使用free -h# 输出示例:# total used free shared buff/cache available# Mem: 7.8Gi 7.1Gi 512Mi 10Mi 6.6Gi 480Mi# Swap: 2.0Gi 0B 2.0Gi上面示例当中,系统总的内存是8GB,buff/cache占据了6.6GB,available就仅仅只有480MB。这就意味着pagecache占据了不少的内存,要是这时候有应用程序需要去分配内存,那么或许就会触发内存回收或者swap操作。
Cached字段所对应的是pagecache的大小,Buffers所对应的是块设备缓存,SwapCached说的是被换出到swap之后又换回内存的页面的数量。当available内存一直处于偏低的状态而且swap使用量上升的时候,很有可能就是预读数据过度占用了pagecache。
(2)错误预测的后果——预读是依托内核的预测算法来进行的。但是这个算法并不是百分百准确的。一旦预测出现错误,那就会产生问题。要是内核错误地把应用程序不会去访问的数据预读进内存,那这些没用的的数据就会占据内存空间,而且还会浪费I/O资源。
比如说在频繁随机读写的数据库应用场景当中,由于数据访问模式比较复杂,很难精准地预测下一回要读数据。要是内核老是按照顺序读的方式来预读,那就会读取好多数据库后续用不到的数据。这就好像你去超市买东西,本来打算买苹果、香蕉和牛奶,可是超市工作人员按照自己猜测的,给你准备一堆你不需要的橙子、葡萄和面包,占据超市库存空间(内存),工作人员搬运这些东西(I/O操作)就是白费劲,真正需要的东西可能就因为库存不够而没法及时提供(影响数据库性能)。而且这些没用的预读操作还会占据磁盘带宽,使得真正要读的数据不能够及时从磁盘读到内存,进一步降低系统I/O性能。
下面用一个 C 程序随机读取文件的场景。程序通过 lseek 随机跳转到文件的不同位置读取数据,这种访问模式会导致内核预读失效,产生大量无效 I/O:
#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <unistd.h>#include <time.h>#define BUF_SIZE 4096#define READ_TIMES 1000int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "Usage: %s <file>\n", argv[0]); return 1; } int fd = open(argv[1], O_RDONLY); if (fd == -1) { perror("open"); return 1; } /* 获取文件大小 */ off_t file_size = lseek(fd, 0, SEEK_END); if (file_size <= 0) { fprintf(stderr, "File too small\n"); close(fd); return 1; } char *buf = malloc(BUF_SIZE); if (!buf) { perror("malloc"); close(fd); return 1; } srand(time(NULL)); ssize_t n; for (int i = 0; i < READ_TIMES; i++) { /* 随机生成读取偏移量,模拟随机访问模式 */ off_t offset = (rand() % (file_size / BUF_SIZE)) * BUF_SIZE; lseek(fd, offset, SEEK_SET); n = read(fd, buf, BUF_SIZE); if (n <= 0) break; } free(buf); close(fd); return 0;}程序于循环之中每一次都运用 rand 来生成随机的偏移量。接着通过 lseek 跳转到那个位置去读取 4 千字节的数据。由于偏移量是随机进行跳转的,内核没有办法去判断这是顺序读取。预读的机制就会频繁地启动然后又频繁地失效。
内核有可能在某一次读取之后预读了后续连续的数据,可是下一次读取又跳到了完全不一样的位置。这样一来预读的数据就永远都不会被访问到。这既浪费了 I/O 带宽又占用了 pagecache 内存 。
对于这种确定的随机访问场景,可以在打开文件后用 posix_fadvise 告知内核关闭预读,避免无效 I/O:
/* 告知内核访问模式为随机读,内核会减少或关闭预读 */posix_fadvise(fd, 0, file_size, POSIX_FADV_RANDOM);添加上这一行代码之后,内核便不会对于这个文件去进行预读。每一次进行读取的时候就仅仅只是读取所请求的4KB数据。如此这般就避免了由于预读失效而致使的I/O方面的浪费以及内存占用的状况。在那种主要是属于随机读写的数据库应用当中,这种优化所产生的效果那是格外明显。
(3)多进程竞争——在多进程的情境之中,倘若有好几个进程同时开展文件预读工作,那么便会出现I/O资源竞争这样的问题。
每一个进程都期望获取到足够的I/O带宽来达成自身的预读操作,可是磁盘的I/O带宽是有限的。举例而言在一个服务器之上同时运行好几个数据处理程序,每一个程序都在处理大文件并且进行预读。这些进程会同时向磁盘发出大量的预读请求,如同众多车辆同时拥挤在一条狭窄的道路之上,都想要快点通过(获取I/O带宽),但是道路的容量是有限的,最终就导致了交通堵塞(I/O资源竞争)。
这样的竞争会使得每一个进程的预读操作都不能够高效地完成,增加了每一个进程的I/O等待时间。就好像大家都在排队等候公交车,人太多车太少,每一个人都得等待挺长的时间才能上车,从而降低了整个系统的性能。而且为了协调这些进程对I/O资源的访问,内核得耗费额外的时间和资源来进行调度,这也会进一步消耗系统资源,降低系统整体的运行效率。
下面用一个 C 程序多进程同时读取大文件的场景。程序通过 fork 创建多个子进程,每个子进程独立读取同一个大文件,多个数据处理程序同时运行时的 I/O 竞争:
#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <unistd.h>#include <sys/wait.h>#include <time.h>#define BUF_SIZE 4096#define PROCESS_COUNT 4#define READ_PER_PROC 500void read_file(const char *path, int proc_id) { int fd = open(path, O_RDONLY); if (fd == -1) { perror("open"); return; } char *buf = malloc(BUF_SIZE); if (!buf) { perror("malloc"); close(fd); return; } ssize_t n; for (int i = 0; i < READ_PER_PROC; i++) { n = read(fd, buf, BUF_SIZE); if (n <= 0) break; } printf("Process %d finished reading\n", proc_id); free(buf); close(fd);}int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "Usage: %s <file>\n", argv[0]); return 1; } for (int i = 0; i < PROCESS_COUNT; i++) { pid_t pid = fork(); if (pid == 0) { /* 子进程:读取文件 */ read_file(argv[1], i); return 0; } else if (pid < 0) { perror("fork"); return 1; } } /* 父进程:等待所有子进程结束 */ for (int i = 0; i < PROCESS_COUNT; i++) { wait(NULL); } printf("All processes finished\n"); return 0;}程序通过 fork 创建 4 个子进程,每个子进程独立打开同一个文件并顺序读取 500 次、每次 4KB。4 个进程同时运行时,每个进程都会触发内核预读,内核需要同时为 4 个文件描述符维护预读状态并发出预读 I/O 请求。磁盘 I/O 带宽有限,4 个进程的预读请求会相互竞争,导致每个进程的实际读取速度都低于单独运行时的速度。可以用 time 命令对比单进程和多进程的运行时间:
# 单进程读取(将 PROCESS_COUNT 改为 1 后编译运行)time ./single_read large_file.bin# 输出示例:real 0m2.1s user 0m0.0s sys 0m0.3s# 4 进程同时读取time ./multi_process_read large_file.bin# 输出示例:real 0m5.8s user 0m0.0s sys 0m0.8s4个进程同时进行读取操作所花费的总时间是5.8秒,这个总耗时比单进程所花费的2.1秒要高出来不少,并非是那种理想中的接近2.1秒的情况。这是因为多个进程的预读请求在磁盘的层面上存在着竞争的状况,进而使得磁盘寻道的次数变多了,I/O调度方面的开销也变大了。在实际的服务器环境当中,如果同时运行好几个大文件处理的任务的话,那么可以采用任务排队、限制并发进程的数量或者运用cgroups来限制I/O带宽这样的方式来缓解这种竞争的情况。
三、判断预读是否成瓶颈的方法
在Linux系统之中,有着许许多多实用的系统监控工具。这些工具可以协助我们去判断文件预读是否成为性能方面的瓶颈。iostat以及vmstat便是较为常用的两个工具。
iostat它是属于sysstat这个工具包里面的东西。它主要的作用就是用来对系统磁盘I/O性能进行监控。我们可以借助它来获取磁盘的读写速率、I/O操作的次数、设备利用率等等这些关键的信息。比如说使用命令iostat -x 2,这里面的 -x参数就是用来显示扩展的统计信息的,2就代表着每2秒钟去刷新数据,这样子就能够实时地看到磁盘I/O的动态变化。
vmstat乃是一种虚拟内存统计方面的工具。它可以去汇报内存使用的状况。也能够去展现磁盘活动以及CPU利用情况等方面的信息。这些个方面和虚拟内存活动是紧密相关联的,彼此之间是有影响的。在使用的时候,输入vmstat 5,也就是每5秒钟就输出一次系统整体性能的数据。我们能够从输出的结果当中去分析内存、磁盘等资源的使用状态。
下面是 iostat -x 2 的实际输出示例,重点关注 await 和 %util 两列:
Linux 5.15.0 (server) 08/26/2026 _x86_64_ (8 CPU)avg-cpu: %user %nice %system %iowait %steal %idle 5.23 0.00 2.15 18.47 0.00 74.15Device r/s w/s rkB/s wkB/s await r_await w_await %utilsda 156.00 12.00 6400.00 48.00 25.30 26.10 15.20 85.40sdb 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00I/O等待的时间为25.30毫秒,已经超出了20毫秒,这就显示出I/O等待的时间是比较高的。磁盘的%util为85.40%,也就意味着磁盘快要达到饱和的状态了。CPU的%iowait是18.47%,这就表明CPU有相当多的时间在等待磁盘的I/O。这些指标一同出现的时候,很有可能是预读产生了过多的无效I/O,从而将磁盘的带宽给占据了。
下面是 vmstat 5 的输出示例:
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 2 3 0 524100 12300 6892400 0 0 8200 120 520 890 5 2 74 19 0 1 4 0 498200 11800 6918500 0 0 9100 90 540 920 4 2 73 21 0wa 列(CPU 等待 I/O 比例)为 19% 和 21%,持续高于 10%。b 列(等待 I/O 的进程数)为 3 和 4,说明有多个进程被 I/O 阻塞。bi 列(每秒读入块数)较高,说明磁盘读操作频繁。cache 列占用了约 6.9GB 内存,如果系统总内存为 8GB,说明 pagecache 占用过大,可能存在预读数据挤占应用内存的情况。
依靠着这一些监控方面的工具,我们可以去关注一些关键的指标,通过这样的方式来判别预读到底是不是转化成为了瓶颈状况。
高I/O等待时间乃是一个较为重要的信号。在iostat的输出内容当中,await指的是平均每一次I/O操作的等待时间,其单位为毫秒。倘若这个数值一直处于比较高的状态,比如说超过20毫秒乃至更高,那么就说明I/O等待的情况是比较严重的,很有可能是预读产生了许多无用的I/O操作,占据了磁盘的带宽,使得真正需要读取的数据不能够及时地完成I/O操作,从而就成为了性能方面的瓶颈。在vmstat当中,wa代表的是CPU等待磁盘I/O的时间所占的比例,要是wa值经常大于10%,也就意味着文件系统的活动对CPU有较大的阻碍,有可能存在预读方面的问题。
低缓存命中率需要引起重视。缓存命中率指的是数据请求被缓存直接返回的比率。在Linux系统之中,文件预读的数据存储在pagecache里面。要是缓存命中率比较低,那就代表着有很多的数据请求没有命中缓存,得从磁盘进行读取。这有可能是因为预读的数据不准确,没有命中应用实际所需要的数据,使得缓存没有发挥出作用,进而影响到系统性能。
虽然没有像iostat、vmstat那样直接的命令来直观地显示缓存命中率,但是我们可以通过分析文件读取操作中从缓存获取数据和从磁盘获取数据的次数来大致进行估算。比如说在一些数据库应用当中,查看数据库日志记录,统计数据读取请求里从缓存获取数据的次数和总的读取次数,用前者除以后者就可以得到缓存命中率。
四、避免预读成性能瓶颈的策略
(1)优化预读算法——对预读算法进行优化,能够使得预读变得更为精准,进而减少无效的预读情况,防止它成为性能方面的瓶颈问题。当下Linux内核主要是依据文件读取的偏移量来判别到底是顺序读还是随机读。这样的方式比较简单,但是在复杂的场景之下就不够精准了。举例来说在一些大数据处理的应用当中,数据读取的模式看上去像是随机的,可实际上是存在内在规律的,传统的判别方法就难以精准地进行识别,从而使得预读的效果不太好。
我们需要去考量改进预测的逻辑。不只是偏移量,还得综合文件访问的历史、应用程序的行为等多个维度的信息来判断读的模式。比如说记录应用程序在某一段时间内对不同文件区域的访问频率以及顺序,利用这些历史的数据,用机器学习的算法去训练模型,预测下一次有可能访问的数据区域,这样子能够提升预读的命中率。
增加算法的自适应能力那也是很关键的。系统的负载、应用程序的工作负载一直在发生变化,预读的算法得能够实时察觉到这些变化并且去调整预读的策略。当系统负载高的时候,就得适当地降低预读量,不要过多地去占用资源。当应用程序从顺序读切换到随机读的时候,要及时地停止预读,减少无效的操作。
(2)合理配置参数——合理地去进行配置以及预读相关的参数,能够使得预读机制可以更好地去适应不同的工作负载情况,而不会成为性能方面的瓶颈。预读窗口的大小是一个关键的参数,它决定了每一次预读的数据量。要是窗口过大的话,就会占用很多的内存以及I/O资源,使得其他进程的资源不够用。要是窗口过小的话,预读的效果就不明显。在处理大文件顺序读取的时候,比如说视频转码这类应用,就可以适当地去增大预读窗口,充分地利用磁盘的带宽,从而提升数据读取的速度。默认的预读窗口是128KB,这类应用可以尝试着增大到512KB甚至于1MB,通过多次的测试来找到最优的数值。
内存的分配比例是极为关键的。它决定了有多少内存会被用于文件预读缓存。在内存比较有限的系统当中,必须要合理地给预读缓存以及其他进程来进行内存的分配。举例来说在一个运行着数据库和文件处理程序的服务器上面,如果给文件预读缓存分配了过多的内存,那么数据库就有可能因为内存不够而使得性能变差。
我们可以通过对系统参数进行调整,像是修改/etc/sysctl.conf文件里面的相关配置,来对内存分配比例进行合理的设置,从而确保各个进程都能够有足够的内存来运行。另外不同的存储设备有着不一样的特点,对于预读参数的要求也是不一样的。固态硬盘(SSD)有着较快的读写速度,随机读写性能比较强,预读参数就能够相对地保守一些。机械硬盘(HDD)在顺序读写方面有着优势,但是随机读写比较慢,就可以适当地增大预读参数来提高顺序读取的性能。比如说对于SSD,把预读扇区数设置成256就可以了,而对于HDD,可以尝试设置成512甚至更高,具体的数值需要结合实际的应用场景以及测试结果来确定。
下面是使用 blockdev 命令查看和设置块设备预读窗口的示例:
# 查看当前预读窗口大小(单位:512字节扇区)blockdev --getra /dev/sda# 输出示例:256(即 256 * 512 = 128KB)# 设置预读窗口为 1024 个扇区(即 512KB)blockdev --setra 1024 /dev/sda# 验证设置结果blockdev --getra /dev/sda# 输出:1024--getra这个指令是用来读取当前所设置的预读扇区的数量,--setra这个指令是用来对预读扇区的数量进行设置的。
每一个扇区的大小是512字节,所以256所对应的就是128KB,1024所对应的就是512KB。在大文件进行顺序读取这样的场景当中,像是视频转码、日志分析这类情形下,可以适当地将这个数值往大了调整。在以随机读写作为主要方式的场景里,比如数据库这类状况下,可以适当地将这个数值往小了调整以此来减少无效的预读。
下面是通过 /etc/sysctl.conf 调整与内存和 I/O 相关参数的示例:
# 编辑配置文件vi /etc/sysctl.conf# 脏页写入磁盘的比例阈值(占总内存百分比)vm.dirty_ratio = 10# 脏页后台写入的比例阈值vm.dirty_background_ratio = 5# 脏页可停留的最长时间(单位:1/100秒)vm.dirty_expire_centisecs = 3000# 系统保留的最低空闲内存(单位:KB)vm.min_free_kbytes = 65536# 使配置生效sysctl -p当脏页在总内存中所占的比例达到了dirty_ratio所设定的比例的时候,进程就会被阻塞,随后进行同步写入磁盘的操作。把这个dirty_ratio的值降低能够减少脏页的堆积情况,以此来避免突然出现大量的I/O情况。dirty_background_ratio是用来对后台刷写线程什么时候开始工作进行控制的。min_free_kbytes需要去保证系统保留一定数量的空闲内存,防止pagecache过度占用从而使得应用程序的内存不够用。
(3)应用层优化——从应用层面去进行优化,能够有效地减少预读所带来的不良影响,不会让预读成为性能方面的瓶颈。
优化文件的访问模式是关键所在。应用程序应当尽可能地采用顺序访问模式,减少随机读操作。在数据库查询之中,要合理地设计查询语句,借助索引来优化查询,使得数据能够按照顺序进行读取。举个例子来说,在存储用户信息的数据库表当中,如果经常需要按照用户 ID 来查询信息,那么就给用户 ID 字段建立一个索引,这样在查询的时候就可以通过索引迅速地找到数据,达成顺序读取,减少随机读,从而降低预读的无目的性,提高系统的性能。
降低不必要的文件读取是十分关键的事情。应用程序在对文件进行读取之前,得先去判断一下是不是真的要去读取整个文件,能不能依靠缓存或者别的办法来获取部分数据。在内容管理系统当中,对于很多经常被访问的文件,可以在应用层设置缓存机制。
当用户请求文件的时候,先去检查缓存里面有没有该文件的数据,如果有的话那就直接进行返回,不用再重复地从磁盘去进行读取,这样就减少了预读操作,从而节省了系统资源。另外应用程序可以根据自身业务的特点,主动地给内核提供文件访问的提示,比如说使用posix_fadvise函数来告诉内核文件的访问模式是顺序读还是随机读,帮助内核更加精准地去做预读的决策,进而提升预读的效率。
下面是使用 posix_fadvise 函数向内核提示文件访问模式的 C 代码示例:
#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <unistd.h>#include <sys/types.h>#define BUF_SIZE 4096int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "Usage: %s <file>\n", argv[0]); return 1; } int fd = open(argv[1], O_RDONLY); if (fd == -1) { perror("open"); return 1; } /* 获取文件大小 */ off_t file_size = lseek(fd, 0, SEEK_END); lseek(fd, 0, SEEK_SET); /* 告知内核:将顺序读取整个文件,内核会增大预读窗口 */ int ret = posix_fadvise(fd, 0, file_size, POSIX_FADV_SEQUENTIAL); if (ret != 0) { fprintf(stderr, "posix_fadvise failed: %d\n", ret); } char *buf = malloc(BUF_SIZE); if (!buf) { perror("malloc"); close(fd); return 1; } ssize_t n; while ((n = read(fd, buf, BUF_SIZE)) > 0) { /* 处理数据 */ } /* 读取完成后,告知内核释放已访问的缓存 */ posix_fadvise(fd, 0, file_size, POSIX_FADV_DONTNEED); free(buf); close(fd); return 0;}posix_fadvise函数当中的第三个参数advice是用于指定访问模式的。POSIX_FADV_SEQUENTIAL这个模式,是告知内核应用将会顺序地读取文件。内核会因为这个情况而增大预读窗口,这样子就能够提升预读的效率。POSIX_FADV_RANDOM这个模式,是告知内核访问模式是随机的。内核会因为这个而减小或者关闭预读,来避免无效的I/O。POSIX_FADV_DONTNEED这个模式,是告知内核指定范围的数据不再需要。这样子就可以释放对应的pagecache,进而回收内存。POSIX_FADV_WILLNEED这个模式,是主动提示内核去预读指定范围的数据。
如果访问模式是随机的,可以使用 POSIX_FADV_RANDOM 来关闭预读:
/* 告知内核:将随机读取文件,内核会减少预读 */posix_fadvise(fd, 0, file_size, POSIX_FADV_RANDOM);对于已经确定需要的数据范围,可以用 POSIX_FADV_WILLNEED 主动触发预读,在处理当前数据时让内核并行加载后续数据:
/* 主动提示内核预读接下来的 1MB 数据 */posix_fadvise(fd, current_offset, 1024 * 1024, POSIX_FADV_WILLNEED);Linux 之中的文件预读机制可以起到提升文件读取性能的作用。其会预先将有可能会用到的数据读取到内存缓存当中去。如此一来便减少了 I/O 等待的时间,同时还提高了磁盘的利用效率。可是在某些时候预读也会变成性能方面的瓶颈。比如说内存占用过高、预读出现错误还有多进程对于 I/O 资源进行竞争这类问题。
end
如果这篇文章对你有所启发,欢迎点赞、在看,转发三连。星标⭐账号,还可以第一时间收到推送,感谢你的收看,我们下期再见~
往期干货推荐