ITPub博客

首页 > 数据库 > 数据库开发技术 > Linux的文件读写机制和优化方式

Linux的文件读写机制和优化方式

原创 数据库开发技术 作者:步步运维步步坑 时间:2024-06-17 11:30:31 0 删除 编辑

Linux 是一个广泛使用的操作系统,以其强大的控制能力、出色的安全性和高效性能而著称。在Linux环境下,文件的读写机制是操作系统内核提供的核心功能之一,它使得用户可以方便地访问和管理文件系统中的数据。

在Linux中,文件的读写通常通过一组系统调用API来完成,其中最基本的就是open、read和write。这些系统调用为应用程序提供了与文件系统交互的接口,使得应用程序能够打开文件、读取文件内容以及向文件写入数据。

open系统调用用于打开或创建一个文件,并返回一个文件描述符,这个描述符在后续的文件操作中用于标识该文件。read系统调用则用于从已打开的文件中读取数据,它接受文件描述符、缓冲区指针和要读取的字节数作为参数,并将读取到的数据存储在提供的缓冲区中。write系统调用则用于向已打开的文件中写入数据,它接受文件描述符、数据指针和要写入的字节数作为参数,并将数据写入文件中。

需要注意的是,虽然不同的读取方式(如read、fread、cin等)在表面上看起来不同,但它们本质上都是基于这些基本的系统调用API来实现的。这些不同的读取方式只是对系统调用的不同封装和抽象,以适应不同编程语言和框架的需求。

因此,在Linux下讨论文件的读写机制时,我们可以聚焦于这些基本的系统调用API。通过理解这些API的工作原理和使用方法,我们可以更好地掌握Linux环境下的文件读写操作,并编写出高效、安全的代码。

最后,关于测试方面,本文提到所有测试均使用open、read、write这一套系统API。因为它能够直接反映底层文件系统的性能和行为,有助于我们更准确地评估和优化文件读写操作的性能。


缓存

缓存是一种优化机制,旨在显著提高高速设备访问低速设备时的效率。在计算机系统中,内存(高速设备)和磁盘(低速设备)之间的读写操作差异显著:内存的操作速度远超过磁盘。直接对磁盘进行频繁的读写操作不仅会限制整体性能,还可能缩短磁盘的使用寿命。

因此,为了缓解这种速度差异带来的性能瓶颈,操作系统引入了缓存机制。缓存的作用就像一个临时的数据存储区域,用于存储最近访问或即将访问的数据。当应用程序需要读取或写入数据时,操作系统会首先检查缓存中是否已经有这些数据。如果缓存中存在所需数据(缓存命中),则直接从缓存中读取或写入,避免了访问慢速磁盘的过程,从而大大提高了访问速度。如果缓存中不存在所需数据(缓存不命中),则操作系统会从磁盘中读取或写入数据,并将这些数据存储在缓存中,以备后续使用。

通过这种方式,缓存显著减少了高速设备(如内存)直接访问低速设备(如磁盘)的次数,从而降低了平均访问时间,提高了整体性能。同时,由于减少了磁盘的读写操作,也延长了磁盘的使用寿命。因此,缓存是操作系统中至关重要的一个组件,对于提升计算机系统的性能和稳定性起到了关键作用。


Page Cache

页缓存(Page Cache)在内存与文件之间充当着一个关键的缓冲角色,本质上,它是一片专用于此目的的内存空间。无论是本地文件还是网络文件的I/O操作,都是直接通过与页缓存的交互来实现的。操作系统借助一系列复杂但高效的数据结构,如inode、address_space以及struct page等,将文件内容映射到具体的页面级别,从而实现对文件的精细管理。对我们来说,重要的是要理解页缓存的存在及其核心作用。在文件I/O操作中,页缓存扮演着至关重要的角色。实际上,许多针对文件读写性能的优化措施都是围绕如何更有效地利用页缓存展开的。通过合理地利用页缓存,我们可以显著减少直接对磁盘的访问次数,从而提高文件读写的效率,同时也有助于延长磁盘的使用寿命

# 查看页缓存[root@VM-0-16-opencloudos ~]# free                total        used        free      shared  buff/cache   availableMem:         1755144      558856      147424       39500     1275432     1196288Swap:        1049596           0     1049596[root@VM-0-16-opencloudos ~]## cached那一列就是页缓存大小,单位Byte# 另一种是直接查看/proc/meminfo[root@VM-0-16-opencloudos ~]# cat /proc/meminfo ...Cached:          1000968 kB...Dirty:               112 kB...[root@VM-0-16-opencloudos ~]# # Cached是页缓存大小,Dirty是脏页大小



Dirty Page

页缓存对应文件中的一块区域,如果页缓存和对应的文件区域内容不一致,则该页缓存叫做脏页(Dirty Page)。对页缓存进行修改或者新建页缓存,只要没有刷磁盘,都会产生脏页

Linux有一些参数可以改变操作系统对脏页的回写行为

# 脏页回写参数[root@VM-0-16-opencloudos ~]#  sysctl -a 2>/dev/null | grep dirtyvm.dirty_background_bytes = 0vm.dirty_background_ratio = 10vm.dirty_bytes = 0vm.dirty_expire_centisecs = 3000vm.dirty_ratio = 40vm.dirty_writeback_centisecs = 500vm.dirtytime_expire_seconds = 43200vm.pagecache_limit_ignore_dirty = 1[root@VM-0-16-opencloudos ~]#
  • vm.dirty_background_ratio :是一个百分比阈值,用于触发系统后台进程开始将内存中的脏页(即尚未写入磁盘但已被修改过的页面)刷回磁盘的操作。当内存中的脏页所占的比例达到这个阈值时,系统会在后台异步地将这些脏页写回磁盘,以确保数据的持久性。这个设置可以帮助平衡系统的I/O负载,避免因为过多的脏页写入而导致性能下降。


  • vm.dirty_background_bytes:与vm.dirty_background_ratio类似,是通过设置具体的字节数来触发后台脏页写回操作。当内存中的脏页总大小达到这个字节数阈值时,后台进程会开始工作。

  • vm.dirty_ratio 是一个更为严格的限制,它定义了一个绝对的脏页比例上限。当内存中的脏页所占的比例超过这个值时,系统会阻止新的写操作,直到有足够的脏页被写回磁盘,以确保系统的稳定性和数据的完整性。

  • vm.dirty_writeback_centisecs 设置了一个时间间隔,用于控制脏页写回操作的频率。单位是百分之一秒,即每多少个百分之一秒执行一次脏页写回。这个设置可以帮助系统根据负载情况调整脏页写回的速度。

  • vm.dirty_expire_centisecs 定义了一个脏页在内存中能够存活的最长时间。当脏页在内存中超过这个时间限制时,无论其是否达到其他脏页比例或大小的阈值,都会被强制写回磁盘。这个设置有助于确保数据的及时持久化,防止因系统崩溃或电源故障而导致的数据丢失。

这些参数可以通过 sudo sysctl -w vm.dirty_background_ratio=5 这样的命令来修改,需要root权限,也可以在root用户下执行来修改

 echo 5 > /proc/sys/vm/dirty_background_ratio 


文件读写流程

读文件

  1. 用户发起读取文件的操作请求。

  2. 操作系统接收到请求后,首先会检查页缓存中是否已存在所需数据。

  3. 如果页缓存中没有找到所需数据(即缓存未命中),操作系统会触发一个缺页异常。接着,系统会分配新的页缓存空间,并从磁盘中读取相应的数据块来填充这个页缓存。

  4. 如果页缓存中已经包含了所需数据(即缓存命中),操作系统会直接从页缓存中提

  5. 无论缓存命中还是未命中,一旦数据准备好,操作系统就会将其返回给用户,完成read调用过程。这样,用户就可以继续处理读取到的数据。



写文件

  1. 用户发起一个写入文件的操作请求。

  2. 操作系统开始查找页缓存来存储待写入的数据。

  3. 如果页缓存中没有找到相应的页面(即缓存未命中),系统会触发一个缺页异常。随后,它会创建一个新的页缓存页面,并将用户希望写入的内容直接填充到这个新创建的页缓存中。

  4. 如果页缓存中已经存在相应的页面(即缓存命中),操作系统会直接将用户传入的内容写入到这个已存在的页缓存页面中。

  5. 一旦数据被成功写入页缓存,用户的write调用即告完成。此时,用户认为写入操作已经完成,但实际上数据还驻留在内存中,尚未持久化到磁盘。

  6. 当页缓存中的页面被修改后,这些页面就被标记为“脏页”,因为它们包含了尚未写回磁盘的更改。为了确保数据的持久性,操作系统有两种主要的机制来将这些脏页写回到磁盘:

7.用户可以主动调用fsync()函数来强制将页缓存中的脏页立即写回磁盘。这是一个同步操作,会阻塞调用线程,直到所有相关的脏页都被成功写回磁盘。

8.另一方面,操作系统内部有一个名为pdflush(或其他类似名称)的后台进程,它会定期扫描页缓存中的脏页,并将它们异步写回磁盘。这种机制是自动的,不需要用户干预,但它可能不如fsync()那样即时或可靠,因为它受到系统调度和其他因素的影响。

页缓存和磁盘文件是有对应关系的,这种关系由操作系统维护,对页缓存的读写操作是在内核态完成,对用户来说是透明的

文件读写的优化思路

不同的业务需求和工作负载决定了采用何种优化策略最为合适。在不考虑修改系统参数的前提下,我们可以从最大化利用页缓存和减少系统API调用次数这两个方面来优化性能。

首先,关于最大化利用页缓存,其核心思想在于尽可能地让数据读写操作在页缓存中完成,而不是直接涉及磁盘操作。这是因为页缓存位于内存中,其访问速度远快于磁盘。当数据被读取到页缓存后,后续的读操作就可以直接从缓存中获取,而无需再次访问磁盘。同样地,当数据被写入页缓存后,操作系统会在合适的时机将其异步地写回磁盘,从而减少了频繁的磁盘I/O操作。

其次,减少系统API调用次数也是提升性能的一个重要途径。系统API调用,如read和write,涉及到从用户态切换到内核态的开销,以及可能的内存数据拷贝。在某些高并发的场景中,减少这些系统调用的次数可以有效降低CPU的使用率和内存消耗,从而提升整体性能。

总的来说,优化的思路可以表述为:

优化数据访问模式,以最大化利用页缓存,减少磁盘I/O次数,从而提升数据读写的性能。

通过减少系统API调用的次数,降低用户态和内核态切换的开销,减少内存数据的拷贝,进一步提升系统性能。

这两种优化策略并非相互独立,而是相辅相成的。在实际应用中,可以根据具体的使用场景和需求来综合运用这两种策略,以达到最佳的性能提升效果。


readhead

readahead 是一个非阻塞的系统调用,它的主要功能是请求操作系统预先读取文件内容至页缓存中,而无需等待读取操作完成就立即返回。这允许应用程序在后台进行文件内容的读取操作,同时继续执行其他任务,从而提高了程序的响应性和效率。通过预先读取文件内容到页缓存,当程序真正需要访问这些数据时,它们已经位于内存中,减少了因等待磁盘I/O而导致的延迟。函数原型如下


ssize_t readahead(int fd, off64_t offset, size_t count);

在通常情况下,调用readahead后立马调用read并不会提高读取速度,我们通常在批量读取或在读取之前一段时间调用readahead,假设如下场景,我们需要连续读取1000个1M的文件,有如下两个方案,伪代码如下

直接调用

char* buf = (char*)malloc(10*1024*1024);for (int i = 0; i < 1000; ++i){   int fd = open_file();   int size = stat_file_size();   read(fd, buf, size);   // do something with buf   close(fd);}


先批量调用readahead再调用read

int* fds = (int*)malloc(sizeof(int)*1000);int* fd_size = (int*)malloc(sizeof(int)*1000);for (int i = 0; i < 1000; ++i){   int fd = open_file();   int size = stat_file_size();   readahead(fd, 0, size);   fds[i] = fd;   fd_size[i] = size;}char* buf = (char*)malloc(10*1024*1024);for (int i = 0; i < 1000; ++i){   read(fds[i], buf, fd_size[i]);   // do something with buf   close(fds[i]);}


感兴趣的可以写代码实际测试一下,需要注意的是在测试前必须先回写脏页和清空页缓存,执行如下命令

sync && sudo sysctl -w vm.drop_caches=3


可通过查看/proc/meminfo中的Cached及Dirty项确认是否生效

通过测试发现,第二种方法比第一种读取速度大约提高10%-20%,这种场景下是批量执行readahead后立马执行read,优化空间有限,如果有一种场景可以在read之前一段时间调用readahead,那将大大提高read本身的读取速度

这种方案实际上是利用了操作系统的页缓存,即提前触发操作系统将文件读取到页缓存,并且操作系统对缺页处理、缓存命中、缓存淘汰都由一套完善的机制,虽然用户也可以针对自己的数据做缓存管理,但和直接使用页缓存比并没有多大差别,而且会增加维护代价

mmap

mmap 函数是一种用于在进程的地址空间中创建文件映射的方法。它允许你将一个文件或对象的内容直接映射到进程的虚拟内存区域,从而实现对文件的内存式访问。通过这个函数,你可以指定映射的起始地址、映射的长度、所需的保护级别(读/写/执行)、映射的类型和标志,以及用于映射的文件描述符和偏移量。

一旦映射成功,你就可以像操作内存一样操作映射区域中的数据,无需再进行传统的文件读写操作。这种机制可以显著提高大文件的处理效率,特别是在需要频繁访问文件内容的情况下

函数原型:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数

mmap 系统调用在减少系统调用次数的同时,也显著降低了内存拷贝的次数,从而进一步提升了性能。传统的 read 系统调用涉及两个主要步骤:首先,操作系统从磁盘读取文件内容到页缓存;接着,操作系统再将页缓存中的数据拷贝到用户态提供的缓冲区中。这种数据拷贝不仅增加了开销,还可能成为性能瓶颈。

然而,使用 mmap 后,情况发生了改变。mmap 允许用户进程直接将文件或文件的一部分映射到其虚拟地址空间中。这意味着,操作系统只需将文件内容读取到页缓存中,而无需再将其拷贝到用户态的缓冲区。用户进程可以直接通过指针访问这些映射到虚拟内存中的数据,就像操作普通内存一样。

对于频繁读写同一块数据区域的情况,mmap 尤其适用。例如,假设有一个64MB大小的文件,其中存储了大量的索引信息,并且这些索引信息需要频繁地修改并持久化到磁盘。通过使用 mmap,我们可以将这个文件映射到用户进程的虚拟内存空间中。然后,通过直接修改映射区域的内存内容,我们可以实现对文件的修改。操作系统会负责将修改的部分异步地写回到磁盘,或者在必要时,我们也可以通过调用 msync 系统调用来手动触发写回操作。

这种方式不仅减少了系统调用的次数,还避免了不必要的数据拷贝,从而显著提高了对同一块数据区域的频繁读写操作的性能。



THE

END

期待你的

分享

点赞

在看

添加博主

微信号|yfq940-



原文作者:步步运维步步坑;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2022-10-08

  • 博文量
    100
  • 访问量
    51741