当前位置: 首页 > 科技观察

Linux文件读写机制及优化方法

时间:2023-03-19 00:45:17 科技观察

本文只讨论Linux下文件的读写机制,不涉及read、fread、cin等不同读取方式的比较,这些读取方式是本质上是调用系统apiread,只是制作了一个不同的包。以下所有测试都使用打开、读取、写入一组系统api缓存。缓存是用来减少高速设备访问低速设备所需平均时间的组件。文件读写涉及计算机内存和磁盘,内存的运行速度比磁盘快很多,如果每次调用read和write都直接操作磁盘,一方面会限制速度,另一方面会降低磁盘的使用寿命,所以不管是读操作还是对磁盘进行写操作时,操作系统会缓存数据PageCache页缓存(PageCache)是内存和文件之间的缓冲区。它实际上是一个内存区域。所有文件IO(包括网络文件)直接与页面缓存交互。操作系统通过inode、address_space、structpage等一系列数据结构,实现文件到页级别的映射。我们暂时不讨论这些具体的数据结构以及它们之间的关系。我们只需要知道页面缓存的存在及其在文件IO中的重要作用即可。作用,很大程度上,文件读写的优化,就是针对文件中pagecache对应的一块区域,优化DirtyPagepagecache。如果页面缓存的内容与对应的文件区不一致,则页面缓存称为脏页(DirtyPage)。修改页面缓存或创建新的页面缓存。只要不刷新磁盘,就会产生脏页。检查页面缓存的大小在Linux中有两种方法可以检查页面缓存的大小。50034419970496Swap:000cached这一列是pagecache的大小,单位是Byte,另外一个直接查看/proc/meminfo。这里我们只关注两个字段Cached:1202872kBDirty:52kBCached是页面缓存的大小,Dirty是脏页大小和脏页写回参数Linux有一些参数可以改变操作系统对脏页的写回行为。.dirty_background_ratio是可以被脏页填充的内存百分比。当脏页总大小达到这个比例时,系统后台进程会开始将脏页刷新到磁盘(类似于vm.dirty_background_bytes,但是是由字节数设置的)vm。dirty_ratio是绝对的脏数据限制,脏数据在内存中的百分比不能超过这个值。如果脏数据超过这个数量,新的IO请求将被阻塞,直到脏数据写入磁盘。vm.dirty_writeback_centisecs指定多久做一次脏数据回写操作,单位是百分之一秒。vm.dirty_expire_centisecs指定脏数据可以存活的时间,单位是百分之一秒,比如这里设置为30秒,当操作系统进行回写操作时,如果脏数据在内存超过30秒,将被写回磁盘。这些参数可以使用命令sudosysctl-wvm.dirty_background_ratio=5修改。需要根权限。也可以在root用户下执行echo5>/proc/sys/vm/dirty_background_ratio来修改文件读写流程。有了pagecache和dirtypages的概念之后,我们再来看看读写文件的过程。读取文件时,用户发起读取操作。操作系统找到页面缓存。如果安装了页面缓存,则读取的内容将直接从页面缓存中返回。用户调用read完成文件写入。用户发起写操作。操作系统搜索页面缓存。将用户传入的内容写入页面缓存。如果***,则将用户传入的内容直接写入页面缓存。用户写调用完成后,页面被修改,成为脏页。操作系统有两种机制将脏页写回磁盘用户手动调用fsync(),pdflush进程定时将脏页写回磁盘。页面缓存和磁盘文件之间存在对应关系。这种关系由操作系统维护。对pagecache的读写操作都是在内核态完成的。对于用户来说,优化文件读写的思路是透明的。不同的优化方案适用于不同的使用场景,比如文件大小、读写频率等,这里不考虑修改系统参数的方案。修改系统参数总是有益的。丢失,需要选择一个平衡点,与业务关系太密切,比如是否要求数据强一致性,是否容忍数据丢失等。优化的思路有以下两方面的考虑:1.最大限度地利用pagecache2.减少系统API的调用次数。它会快得多。第二点提到的系统API,主要是读写。由于系统调用会从用户态进入内核态,有的还伴随着内存数据的拷贝,所以在某些场景下减少系统调用也会提高性能readaheadreadahead是一种非阻塞的系统调用,会触发操作系统将文件内容预读入页面缓存并立即返回。函数原型如下ssize_treadahead(intfd,off64_toffset,size_tcount);一般情况下,调用readahead后立即调用read并不会提高读取速度。我们通常在批量读取或者读取前一段时间调用readahead。假设如下场景,我们需要连续读取1000个1M的文件。解决方法如下,伪代码直接调用读取函数char*buf=(char*)malloc(10*1024*1024);for(inti=0;i<1000;++i){intfd=open_file();intsize=stat_file_size();read(fd,buf,size);//dosomethingwithbufclose(fd);}先批量调用readahead再调用readint*fds=(int*)malloc(sizeof(int)*1000);int*fd_size=(int*)malloc(sizeof(int)*1000);for(inti=0;i<1000;++i){intfd=open_file();intsize=stat_file_size();预读(fd,0,大小);fds[i]=fd;fd_size[i]=大小;}char*buf=(char*)malloc(10*1024*1024);for(inti=0;i<1000;++i){read(fds[i],buf,fd_size[i]);//dosomethingwithbufclose(fds[i]);}有兴趣的可以自己写代码实际测试一下。需要注意的是,在测试前必须将脏页写回并清除页缓存。执行以下命令sync&&sudosysctl-wvm。drop_caches=3可以通过检查/proc/meminfo中的Cached和Dirty项目来确认它是否有效。通过测试发现第二种方法比第一种方法快10%-20%左右。在这个场景中,它是分批执行的。readahead后立即执行read,优化空间有限。如果有场景可以在read之前的某个时间调用readahead,会大大提高read本身的读取速度。这个方案实际上是利用了操作系统的pagecache,即提前触发操作系统将文件读入pagecache,操作系统有完整的pagefault处理、缓存激活、缓存清除机制.缓存比例差别不大,会增加维护成本。mmapmmap是一种内存映射文件的方法,即将一个文件或其他对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中的一个虚拟地址的组合。一对一的映射关系,函数原型如下:void*mmap(void*addr,size_tlength,intprot,intflags,intfd,off_toffset);它会自动将脏页回写到对应的文件磁盘,即完成对文件的操作,而无需调用read、write等系统调用函数。如下图,mmap不仅可以减少read、write等系统调用,还可以减少内存的拷贝次数,比如调用read时,一个完整的过程就是操作系统读取磁盘文件到pagecache,然后将pagecache中的数据复制到read传递过来的buffer中,而如果使用mmap,操作系统只需要从复制磁盘读取pagecache,然后用户就可以直接操作mmap-通过指针映射内存,减少了从内核态到用户态的数据拷贝。mmap适用于频繁读写同一区域,比如一个64M的文件,存储了一些索引信息,我们需要经常修改,持久化到磁盘,这样文件就可以映射到用户的虚拟内存中了mmap,然后可以通过指针修改内存区域,修改的部分会被操作系统自动刷新回磁盘。您可以调用msync来手动刷写磁盘。