当前位置: 首页 > Linux

linux文件读写分析

时间:2023-04-06 18:55:02 Linux

转载Linux内核响应一个块设备文件读写层次结构如图(来自ULK3):1.VFS,虚拟文件??系统。2.DiskCaches,磁盘缓存。将磁盘上的数据缓存在内存中,以加快文件读写速度。其实一般情况下,read/write只和缓存打交道。(有特殊情况)read直接从缓存中读取数据。如果要读取的数据还没有在缓存中,则触发一次磁盘读取操作,然后等待磁盘上的数据更新到磁盘缓存中;write也是直接写入缓存,以后不用管了。随后的内核负责将数据写回磁盘。为了实现这样的缓存,在每个文件的inode中嵌入了一个address_space结构,通过inode->i_mapping访问。address_space结构中维护了一棵基数树,用于磁盘缓存的内存页都挂在这棵树上。由于磁盘缓存与文件的索引节点相关联,因此打开文件的每个进程共享相同的缓存。通过要读写的文件的pos,可以转换出要读写的页(pos以字节为单位,只需要除以每页的字节数即可)。当一个inode被加载到内存中时,相应的磁盘缓存是空的(基数树中没有页面)。随着文件的读写,磁盘上的数据被加载到内存中,相应的内存页挂到基数树的相应位置。如果写入文件,只是更新inode对应的radixtree上对应页的内容,不会直接写回磁盘。以这种方式写入但尚未更新到磁盘的页面称为脏页。内核线程pdflush定期将每个inode上的脏页更新到磁盘,也会适时回收radix上的页。(可以参考《linux页面回收浅析》)。当需要读写的文件内容还没有加载到相应的radixtree中时,read/write的执行过程会向底层的“通用块层”发起读请求,以读取数据。而如果在文件打开时指定了O_DIRECT选项,则意味着绕过磁盘缓存,直接与“通用块层”打交道。既然磁盘缓存提供了有利于提高读写效率的缓存机制,为什么还要使用O_DIRECT选项来绕过呢?一般来说,这样做的应用程序会在用户态维护一套更利于应用程序的专用缓存机制,取代内核提供的磁盘缓存的通用缓存机制。(这是数据库程序通常做的。)由于使用O_DIRECT选项后文件缓存从内核提供的磁盘缓存变为用户态缓存,打开同一个文件的不同进程将无法共享这些缓存(除非这些进程创建共享内存或某物)。如果某些进程对同一个文件使用O_DIRECT选项,但有些进程不使用怎么办?不使用O_DIRECT选项的进程在读写文件时,会在磁盘缓存中留下相应的内容;当使用O_DIRECT选项的进程读写这个文件时,需要先在磁盘缓存中存放相应的内容,才能进行此次读写。脏数据被写回磁盘,然后直接读取和写入磁盘。关于O_DIRECT选项带来的direct_IO的具体实现细节,说来话长,这里就不介绍了。可以参考《linux异步IO浅析》。3.GenericBlockLayer,通用块层。Linux内核对块设备抽象出一个统一的模型,把块设备看成是一个由若干扇区组成的数组空间。扇区是磁盘设备读写的最小单位,可以通过扇区号指定要访问的磁盘的扇区。上层的读写请求在通用块层构造成一个或多个bio结构,描述一个请求——访问的起始扇区号?访问了多少扇区?是阅读还是写作?对应的内存页、页偏移量和数据长度分别是多少?等等……这里主要有两个问题:要访问的扇区号从哪里来?记忆是如何组织的?上面提到,上层的读写请求可以通过文件pos定位到对应的磁盘缓存的页,通过页索引可以知道要访问的文件所在的扇区。获取扇区的索引。但是文件中的扇区数与磁盘上的扇区数并不相同,需要通过特定文件系统提供的函数将得到的扇区索引转换为磁盘的扇区号。文件系统会记录当前磁盘上的扇区使用情况,并针对每个inode,依次使用哪些扇区。(见《linux文件系统实现浅析》)因此,通过文件系统提供的具体功能,最终将上层请求的文件pos映射到磁盘上的扇区号。可以看出,来自上层的请求可能跨越多个扇区,并且可能形成多个不连续的扇区段。对应每一个扇区,构建一个bio结构。由于块设备一般都支持一次性访问多个连续的扇区,所以一个扇区段(一个以上的扇区)可以包含在一个bio结构中代表一次块设备IO请求。接下来说一下内存的组织。由于来自上层的读写请求可能跨越多个扇区,因此也可能跨越磁盘缓存上的多个页面。因此,bio中包含的一个扇区请求可能对应一组内存页。但是,这些页是分开分配的,内存地址很可能不连续。那么,既然bio描述的是块设备请求,那么块设备一次可以访问一组连续的扇区,但是是否可以一次访问一组不连续的内存地址呢?块设备一般使用DMA将块设备上一组连续扇区上的数据复制到一组连续内存页上(或者将一组连续内存页上的数据复制到一组连续扇区上),DMA本身一般做不支持一次性访问非连续内存页。但是一些架构包括io-mmu。正如mmu可以将一组不连续的物理页映射成连续的虚拟地址一样,编程io-mmu可以让DMA将一组不连续的物理内存视为连续的。因此,即使一个bio中包含不连续的多段内存,也有可能在一个DMA中完成。当然,并不是所有的架构都支持io-mmu,所以一个bio在后续的设备驱动中也可能被拆分成多个设备请求。每个构造好的bio结构体都会单独提交,提交给底层IO调度器。4.I/OSchedulerLayer,IO调度器。我们知道磁盘通过磁头读写数据,而磁头在定位扇区的过程中需要进行机械运动。与电和磁的传递相比,机械运动是非常缓慢的,这也是磁盘如此缓慢的主要原因。IO调度器要做的就是在完成已有请求的前提下,尽可能少的移动磁头,从而提高磁盘的读写效率。最著名的是“电梯算法”。在IO调度器中,将上层提交的bio构造成一个request结构,一个request结构包含一组顺序bios。而每一个物理设备都会对应一个request_queue,依次存储相关的请求。新的bio可以合并到request_queue中已有的request结构中(甚至可以合并到已有的bio中),也可以生成一个新的request结构插入到request_queue中合适的位置。如何组合和插入取决于设备驱动选择的IO调度算法。总的来说,IO调度算法可以想象成“电梯算法”,虽然实际的IO调度算法有所改进。除了类似“电梯算法”的IO调度算法,还有“none”算法,其实就是没有算法,或者可以说是“先到先得”的算法。因为很多块设备已经可以很好地支持随机访问(比如固态硬盘、闪存),所以对它们使用“电梯算法”是没有意义的。除了改变请求的顺序,IO调度器还可能延迟触发请求的处理。因为只有当请求队列有一定数量的请求时,“电梯算法”才能发挥作用,否则在极端情况下会退化为“先来先服务算法”。这是通过request_queue的plug/unplug实现的,plug相当于去激活,unplug相当于recovery。当请求很少时,request_queue将被停用。当请求数达到一定数量,或者request_queue中最早的请求已经等待了很长时间,request_queue就会恢复。当request_queue恢复时,会调用driver提供的回调函数,于是driver开始处理request_queue。一般来说,read/write系统调用在这里返回。返回后,它可能会等待(同步)或继续做其他事情(异步)。返回前会在任务队列中加入一个任务,处理任务队列的内核线程以后会执行request_queue的拔出操作,触发driver处理请求。5.DeviceDriver,设备驱动程序。此时,设备驱动要做的就是从request_queue中取出请求,然后操作硬件设备将这些请求一一执行。除了处理请求,设备驱动还需要选择一种IO调度算法,因为设备驱动最了解设备的属性,知道什么样的IO调度算法最适合。甚至,设备驱动可以屏蔽IO调度器,直接处理上层bio。(当然,设备驱动程序也可以实现自己的IO调度算法。)可以说,IO调度器就是内核提供给设备驱动程序的一组方法。使用与否,使用什么方法,选择权在设备驱动程序。因此,对于支持随机访问的块设备,除了选择“none”算法外,驱动程序还有更直接的做法,就是注册自己的bio提交函数。这样bio生成后,就不会使用一般的提交函数提交给IO调度器,而是直接由driver处理。但是,如果设备很慢,bio提交可能会阻塞很长时间。所以这种方式一般被基于内存的“块设备”驱动使用(当然,这种块设备是驱动虚拟出来的)。