文件读取流程

使用df -aT命令,发现根目录/对应的文件系统为ext3

Ext3文件系统结构与Ext2相似。用工具格式化磁盘或分区时, 会选择使用什么文件系统来格式化。若选用Ext2来格式化磁盘或分区, 则磁盘或分区的大部分空间被格式化成了许多个Inode和block两个数据结构。block有3种大小选项1K,2K,4K,可在格式化之前选择。Inode的大小固定为128bytes。每个文件都仅会占用一个Inode,Inode主要用来记录文件相关属性,比如权限、文件大小、修改时间等。block用来记录文件的数据,一个Inode可以关联多个block,且Inode记录一个block号码需要4byte。如果文件非常大的话,则Inode无法直接记录文件所包含的所有block号码。所以Ext2采用12个直接、1个间接、1个双间接、和1个三间接记录区,共需60bytes。

12个直接记录区直接指向带有文件数据的block。1个间接记录区指向一个无真实文件数据的block,此block充当Inode的扩展记录区,此block直接指向带有数据的block。若此扩展block为1K,那么它可记录256个block号码。双间接和三间接类似。



文件读取流程
1)通过挂载点信息找到/dev/sda2的inode号码为2,对应根目录/
2) 经过上个步骤,由于owner root有r,w,x权限,可从inode取得根目录/的block,然后再从block中取得etc/目录的inode为3303105。这里etc/相当于根目录/的数据。



经过上个步骤,由于owner root有r,w,x权限,可从inode 3303105的block中取得passwd的inode号码为3304908。这里passwd相当于父目录etc/的数据。
4)经过上个步骤,由于owner root有r,w权限,可从inode 3304908的block中取出数据。



一个操作系统可以支持多种底层不同的文件系统(比如NTFS, FAT, ext3, ext4),为了给内核和用户进程提供统一的文件系统视图,Linux在用户进程和底层文件系统之间加入了一个抽象层,即虚拟文件系统(Virtual File System, VFS),进程所有的文件操作都通过VFS,由VFS来适配各种底层不同的文件系统,完成实际的文件操作。



虚拟文件系统主要模块



1、超级块(super_block),用于保存一个文件系统的所有元数据,相当于这个文件系统的信息库,为其他的模块提供信息。因此一个超级块可代表一个文件系统。文件系统的任意元数据修改都要修改超级块。超级块对象是常驻内存并被缓存的。



2、目录项模块,管理路径的目录项。比如一个路径 /home/foo/hello.txt,那么目录项有home, foo, hello.txt。目录项的块,存储的是这个目录下的所有的文件的inode号和文件名等信息。其内部是树形结构,操作系统检索一个文件,都是从根目录开始,按层次解析路径中的所有目录,直到定位到文件。



3、inode模块,管理一个具体的文件,是文件的唯一标识,一个文件对应一个inode。通过inode可以方便的找到文件在磁盘扇区的位置。同时inode模块可链接到address_space模块,方便查找自身文件数据是否已经缓存。



4、打开文件列表模块,包含所有内核已经打开的文件。已经打开的文件对象由open系统调用在内核中创建,也叫文件句柄。打开文件列表模块中包含一个列表,每个列表表项是一个结构体struct file,结构体中的信息用来表示打开的一个文件的各种状态参数。



5、file_operations模块。这个模块中维护一个数据结构,是一系列函数指针的集合,其中包含所有可以使用的系统调用函数,例如open、read、write、mmap等。每个打开文件(打开文件列表模块的一个表项)都可以连接到file_operations模块,从而对任何已打开的文件,通过系统调用函数,实现各种操作。



6、address_space模块,它表示一个文件在页缓存中已经缓存了的物理页。它是页缓存和外部设备中文件系统的桥梁。如果将文件系统可以理解成数据源,那么address_space可以说关联了内存系统和文件系统。我们会在文章后面继续讨论。



模块间的相互作用和逻辑关系如下图所示:

由图可以看出:



1、每个模块都维护了一个X_op指针指向它所对应的操作对象X_operations。



2、超级块维护了一个s_files指针指向了“已打开文件列表模块”,即内核所有的打开文件的链表,这个链表信息是所有进程共享的。



3、目录操作模块和inode模块都维护了一个X_sb指针指向超级块,从而可以获得整个文件系统的元数据信息。



4、 目录项对象和inode对象各自维护了指向对方的指针,可以找到对方的数据。



5、已打开文件列表上每一个file结构体实例维护了一个f_dentry指针,指向了它对应的目录项,从而可以根据目录项找到它对应的inode信息。



6、已打开文件列表上每一个file结构体实例维护了一个f_op指针,指向可以对这个文件进行操作的所有函数集合file_operations。



7、inode中不仅有和其他模块关联的指针,重要的是它可以指向address_space模块,从而获得自身文件在内存中的缓存信息。



8、address_space内部维护了一个树结构来指向所有的物理页结构page,同时维护了一个host指针指向inode来获得文件的元数据



1、内核使用task_struct来表示单个进程的描述符,其中包含维护一个进程的所有信息。task_struct结构体中维护了一个 files的指针(和“已打开文件列表”上的表项是不同的指针)来指向结构体files_struct,files_struct中包含文件描述符表和打开的文件对象信息。



2、file_struct中的文件描述符表实际是一个file类型的指针列表(和“已打开文件列表”上的表项是相同的指针),可以支持动态扩展,每一个指针指向虚拟文件系统中文件列表模块的某一个已打开的文件。



3、file结构一方面可从f_dentry链接到目录项模块以及inode模块,获取所有和文件相关的信息,另一方面链接file_operations子模块,其中包含所有可以使用的系统调用函数,从而最终完成对文件的操作。这样,从进程到进程的文件描述符表,再关联到已打开文件列表上对应的文件结构,从而调用其可执行的系统调用函数,实现对文件的各种操作。



进程 vs 文件列表 vs Inode



1、多个进程可以同时指向一个打开文件对象(文件列表表项),例如父进程和子进程间共享文件对象;



2、一个进程可以多次打开一个文件,生成不同的文件描述符,每个文件描述符指向不同的文件列表表项。但是由于是同一个文件,inode唯一,所以这些文件列表表项都指向同一个inode。通过这样的方法实现文件共享(共享同一个磁盘文件);



I/O 缓冲区
概念



如高速缓存(cache)产生的原理类似,在I/O过程中,读取磁盘的速度相对内存读取速度要慢的多。因此为了能够加快处理数据的速度,需要将读取过的数据缓存在内存里。而这些缓存在内存里的数据就是高速缓冲区(buffer cache),下面简称为“buffer”。



具体来说,buffer(缓冲区)是一个用于存储速度不同步的设备或优先级不同的设备之间传输数据的区域。一方面,通过缓冲区,可以使进程之间的相互等待变少,从而使从速度慢的设备读入数据时,速度快的设备的操作进程不发生间断。另一方面,可以保护硬盘或减少网络传输的次数。



Buffer和Cache



buffer和cache是两个不同的概念:cache是高速缓存,用于CPU和内存之间的缓冲;buffer是I/O缓存,用于内存和硬盘的缓冲;简单的说,cache是加速“读”,而buffer是缓冲“写”,前者解决读的问题,保存从磁盘上读出的数据,后者是解决写的问题,保存即将要写入到磁盘上的数据。



Buffer Cache和 Page Cache



buffer cache和page cache都是为了处理设备和内存交互时高速访问的问题。buffer cache可称为块缓冲器,page cache可称为页缓冲器。在linux不支持虚拟内存机制之前,还没有页的概念,因此缓冲区以块为单位对设备进行。在linux采用虚拟内存的机制来管理内存后,页是虚拟内存管理的最小单位,开始采用页缓冲的机制来缓冲内存。Linux2.6之后内核将这两个缓存整合,页和块可以相互映射,同时,页缓存page cache面向的是虚拟内存,块I/O缓存Buffer cache是面向块设备。需要强调的是,页缓存和块缓存对进程来说就是一个存储系统,进程不需要关注底层的设备的读写。



buffer cache和page cache两者最大的区别是缓存的粒度。buffer cache面向的是文件系统的块。而内核的内存管理组件采用了比文件系统的块更高级别的抽象:页page,其处理的性能更高。因此和内存管理交互的缓存组件,都使用页缓存。



回到顶部
Page Cache
页缓存是面向文件,面向内存的。通俗来说,它位于内存和文件之间缓冲区,文件IO操作实际上只和page cache交互,不直接和内存交互。page cache可以用在所有以文件为单元的场景下,比如网络文件系统等等。page cache通过一系列的数据结构,比如inode, address_space, struct page,实现将一个文件映射到页的级别:



1、struct page结构标志一个物理内存页,通过page + offset就可以将此页帧定位到一个文件中的具体位置。同时struct page还有以下重要参数:



(1)标志位flags来记录该页是否是脏页,是否正在被写回等等;



(2)mapping指向了地址空间address_space,表示这个页是一个页缓存中页,和一个文件的地址空间对应;



(3)index记录这个页在文件中的页偏移量;



2、文件系统的inode实际维护了这个文件所有的块block的块号,通过对文件偏移量offset取模可以很快定位到这个偏移量所在的文件系统的块号,磁盘的扇区号。同样,通过对文件偏移量offset进行取模可以计算出偏移量所在的页的偏移量。



3、page cache缓存组件抽象了地址空间address_space这个概念来作为文件系统和页缓存的中间桥梁。地址空间address_space通过指针可以方便的获取文件inode和struct page的信息,所以可以很方便地定位到一个文件的offset在各个组件中的位置,即通过:文件字节偏移量 –> 页偏移量 –> 文件系统块号 block –> 磁盘扇区号



4、页缓存实际上就是采用了一个基数树结构将一个文件的内容组织起来存放在物理内存struct page中。一个文件inode对应一个地址空间address_space。而一个address_space对应一个页缓存基数树。它们之间的关系如下

address_space是Linux内核中的一个关键抽象,它被作为文件系统和页缓存的中间适配器,用来指示一个文件在页缓存中已经缓存了的物理页。因此,它是页缓存和外部设备中文件系统的桥梁。如果将文件系统可以理解成数据源,那么address_space可以说关联了内存系统和文件系统。
地址空间address_space链接到页缓存基数树和inode,因此address_space通过指针可以方便的获取文件inode和page的信息。



读文件



1、进程调用库函数向内核发起读文件请求;



2、内核通过检查进程的文件描述符定位到虚拟文件系统的已打开文件列表表项;



3、调用该文件可用的系统调用函数read()



3、read()函数通过文件表项链接到目录项模块,根据传入的文件路径,在目录项模块中检索,找到该文件的inode;



4、在inode中,通过文件内容偏移量计算出要读取的页;



5、通过inode找到文件对应的address_space;



6、在address_space中访问该文件的页缓存树,查找对应的页缓存结点:



(1)如果页缓存命中,那么直接返回文件内容;



(2)如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页;重新进行第6步查找页缓存;



7、文件内容读取成功。



写文件



前5步和读文件一致,在address_space中查询对应页的页缓存是否存在:



6、如果页缓存命中,直接把文件内容修改更新在页缓存的页中。写文件就结束了。这时候文件修改位于页缓存,并没有写回到磁盘文件中去。



7、如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页。此时缓存页命中,进行第6步。



8、一个页缓存中的页如果被修改,那么会被标记成脏页。脏页需要写回到磁盘中的文件块。有两种方式可以把脏页写回磁盘:



(1)手动调用sync()或者fsync()系统调用把脏页写回



(2)pdflush进程会定时把脏页写回到磁盘



同时注意,脏页不能被置换出内存,如果脏页正在被写回,那么会被设置写回标记,这时候该页就被上锁,其他写请求被阻塞直到锁释放。



linux内核响应一个块设备文件读写的层次结构如图



Disk Caches,磁盘高速缓存。  将磁盘上的数据缓存在内存中,加速文件的读写。实际上,在一般情况下,read/write是只跟缓存打交道的。(当然,存在特殊情况。下面会说到。)  read就直接从缓存读数据。如果要读的数据还不在缓存中,则触发一次读盘操作,然后等待磁盘上的数据被更新到磁盘高速缓存中;write也是直接写到缓存里去,然后就不用管了。后续内核会负责将数据写回磁盘。  为了实现这样的缓存,每个文件的inode内嵌了一个address_space结构,通过inode->i_mapping来访问。address_space结构中维护了一棵radix树,用于磁盘高速缓存的内存页面就挂在这棵树上。而既然磁盘高速缓存是跟文件的inode关联上的,则打开这个文件的每个进程都共用同一份缓存。  radix树的具体实现细节这里可以不用关心,可以把它理解成一个数组。数组中的每个元素就是一个页面,文件的内容就顺序存放在这些页面中。  于是,通过要读写的文件pos,可以换算得到要读写的是第几页(pos是以字节为单位,只需要除以每个页的字节数即可)。  inode被载入内存的时候,对应的磁盘高速缓存是空的(radix树上没有页面)。随着文件的读写,磁盘上的数据被载入内存,相应的内存页被挂到radix树的相应位置上。  如果文件被写,则仅仅是对应inode的radix树上的对应页上的内容被更新,并不会直接写回磁盘。这样被写过,但还没有更新到磁盘的页称为脏页。  内核线程pdflush定期将每个inode上的脏页更新到磁盘,也会适时地将radix上的页面回收 当需要读写的文件内容尚未载入到对应的radix树时,read/write的执行过程会向底层的“通用块层”发起读请求,以便将数据读入。  而如果文件打开时指定了O_DIRECT选项,则表示绕开磁盘高速缓存,直接与“通用块层”打交道。  既然磁盘高速缓存提供了有利于提高读写效率的缓存机制,为什么又要使用O_DIRECT选项来绕开它呢?一般情况下,这样做的应用程序会自己在用户态维护一套更利于应用程序使用的专用的缓存机制,用以取代内核提供的磁盘高速缓存这种通用的缓存机制。(数据库程序通常就会这么干。)  既然使用O_DIRECT选项后,文件的缓存从内核提供的磁盘高速缓存变成了用户态的缓存,那么打开同一文件的不同进程将无法共享这些缓存(除非这些进程再创建一个共享内存什么的)。而如果对于同一个文件,某些进程使用了O_DIRECT选项,而某些又没有呢?没有使用O_DIRECT选项的进程读写这个文件时,会在磁盘高速缓存中留下相应的内容;而使用了O_DIRECT选项的进程读写这个文件时,需要先将磁盘高速缓存里面对应本次读写的脏数据写回磁盘,然后再对磁盘进行直接读写。 Generic Block Layer,通用块层。  linux内核为块设备抽象了统一的模型,把块设备看作是由若干个扇区组成的数组空间。扇区是磁盘设备读写的最小单位,通过扇区号可以指定要访问的磁盘扇区。  上层的读写请求在通用块层被构造成一个或多个bio结构,这个结构里面描述了一次请求--访问的起始扇区号?访问多少个扇区?是读还是写?相应的内存页有哪些、页偏移和数据长度是多少?等等……  这里面主要有两个问题:要访问的扇区号从哪里来?内存是怎么组织的?  前面说过,上层的读写请求通过文件pos可以定位到要访问的是相应的磁盘高速缓存的第几个页,而通过这个页index就可以知道要访问的是文件的第几个扇区,得到扇区的index。  但是,文件的第几个扇区并不等同于磁盘上的第几个扇区,得到的扇区index还需要由特定文件系统提供的函数来转换成磁盘的扇区号。文件系统会记载当前磁盘上的扇区使用情况,并且对于每一个inode,它依次使用了哪些扇区。


于是,通过文件系统提供的特定函数,上层请求的文件pos最终被对应到了磁盘上的扇区号。
可见,上层的一次请求可能跨多个扇区,可能形成多个非连续的扇区段。对应于每个扇区段,一个bio结构被构造出来。而由于块设备一般都支持一次性访问若干个连续的扇区,所以一个扇区段(不止一个扇区)可以包含在代表一次块设备IO请求的一个bio结构中。
接下来谈谈内存的组织。既然上层的一次读写请求可能跨多个扇区,它也可能跨越磁盘高速缓存上的多个页。于是,一个bio里面包含的扇区请求可能会对应一组内存页。而这些页是单独分配的,内存地址很可能不连续。
那么,既然bio描述的是一次块设备请求,块设备能够一次性访问一组连续的扇区,但是能够一次性对一组非连续的内存地址进行存取吗?
块设备一般是通过DMA,将块设备上一组连续的扇区上的数据拷贝到一组连续的内存页面上(或将一组连续的内存页面上的数据拷贝到块设备上一组连续的扇区),DMA本身一般是不支持一次性访问非连续的内存页面的。
但是某些体系结构包含了io-mmu。就像通过mmu可以将一组非连续的物理页面映射成连续的虚拟地址一样,对io-mmu进行编程,可以让DMA将一组非连续的物理内存看作连续的。所以,即使一个bio包含了非连续的多段内存,它也是有可能可以在一次DMA中完成的。当然,不是所有的体系结构都支持io-mmu,所以一个bio也可能在后面的设备驱动程序中被拆分成多个设备请求。
每个被构造的bio结构都会分别被提交,提交到底层的IO调度器中。
4、I/O SchedulerLayer,IO调度器。
我们知道,磁盘是通过磁头来读写数据的,磁头在定位扇区的过程中需要做机械的移动。相比于电和磁的传递,机械运动是非常慢速的,这也就是磁盘为什么那么慢的主要原因。
IO调度器要做的事情就是在完成现有请求的前提下,让磁头尽可能少移动,从而提高磁盘的读写效率。最有名的就是“电梯算法”。
在IO调度器中,上层提交的bio被构造成request结构,一个request结构包含了一组顺序的bio。而每个物理设备会对应一个request_queue,里面顺序存放着相关的request。
新的bio可能被合并到request_queue中已有的request结构中(甚至合并到已有的bio中),也可能生成新的request结构并插入到request_queue的适当位置上。具体怎么合并、怎么插入,取决于设备驱动程序选择的IO调度算法。大体上可以把IO调度算法就想象成“电梯算法”,尽管实际的IO调度算法有所改进。
除了类似“电梯算法”的IO调度算法,还有“none”算法,这实际上是没有算法,也可以说是“先来先服务算法”。因为现在很多块设备已经能够很好地支持随机访问了(比如固态磁盘、flash闪存),使用“电梯算法”对于它们没有什么意义。
IO调度器除了改变请求的顺序,还可能延迟触发对请求的处理。因为只有当请求队列有一定数目的请求时,“电梯算法”才能发挥其功效,否则极端情况下它将退化成“先来先服务算法”。
这是通过对request_queue的plug/unplug来实现的,plug相当于停用,unplug相当于恢复。请求少时将request_queue停用,当请求达到一定数目,或者request_queue里最“老”的请求已经等待很长一段时间了,这时候才将request_queue恢复。
在request_queue恢复的时候,驱动程序提供的回调函数将被调用,于是驱动程序开始处理request_queue。
一般来说,read/write系统调用到这里就返回了。返回之后可能等待(同步)或是继续干其他事(异步)。而返回之前会在任务队列里面添加一个任务,而处理该任务队列的内核线程将来会执行request_queue的unplug操作,以触发驱动程序处理请求。
5、Device Driver,设备驱动程序。
到了这里,设备驱动程序要做的事情就是从request_queue里面取出请求,然后操作硬件设备,逐个去执行这些请求。
除了处理请求,设备驱动程序还要选择IO调度算法,因为设备驱动程序最知道设备的属性,知道用什么样的IO调度算法最合适。甚至于,设备驱动程序可以将IO调度器屏蔽掉,而直接对上层的bio进行处理。(当然,设备驱动程序也可实现自己的IO调度算法。)
可以说,IO调度器是内核提供给设备驱动程序的一组方法。用与不用、使用怎样的方法,选择权在于设备驱动程序。
于是,对于支持随机访问的块设备,驱动程序除了选择“none”算法,还有一种更直接的做法,就是注册自己的bio提交函数。这样,bio生成后,并不会使用通用的提交函数,被提交到IO调度器,而是直接被驱动程序处理。
但是,如果设备比较慢的话,bio的提交可能会阻塞较长时间。所以这种做法一般被基于内存的“块设备”驱动使用(当然,这样的块设备是由驱动程序虚拟的)。
下面大致介绍一下read/write的执行流程:
sys_read。通过fd得到对应的file结构,然后调用vfs_read;
vfs_read。各种权限及文件锁的检查,然后调用file->f_op->read(若不存在则调用do_sync_read)。file->f_op是从对应的inode->i_fop而来,而inode->i_fop是由对应的文件系统类型在生成这个inode时赋予的。file->f_op->read很可能就等同于do_sync_read;
do_sync_read。f_op->read是完成一次同步读,而f_op->aio_read完成一次异步读。do_sync_read则是利用f_op->aio_read这个异步读操作来完成同步读,也就是在发起一次异步读之后,如果返回值是-EIOCBQUEUED,则进程睡眠,直到读完成即可。但实际上对于磁盘文件的读,f_op->aio_read一般不会返回-EIOCBQUEUED,除非是设置了O_DIRECT标志aio_read,或者是对于一些特殊的文件系统(如nfs这样的网络文件系统);
f_op->aio_read。这个函数通常是由generic_file_aio_read或者其封装来实现的;
generic_file_aio_read。一次异步读可能包含多个读操作(对应于readv系统调用),对于其中的每一个,调用do_generic_file_read;
do_generic_file_read。主要流程是在radix树里面查找是否存在对应的page,且该页可用。是则从page里面读出所需的数据,然后返回,否则通过file->f_mapping->a_ops->readpage去读这个页。(file->f_mapping->a_ops->readpage返回后,说明读请求已经提交了。但是磁盘上的数据还不一定就已经读上来了,需要等待数据读完。等待的方法就是lock_page:在调用file->f_mapping->a_ops->readpage之前会给page置PG_locked标记。而数据读完后,会将该标记清除,这个后面会看到。而这里的lock_page就是要等待PG_locked标记被清除。);
file->f_mapping是从对应inode->i_mapping而来,inode->i_mapping->a_ops是由对应的文件系统类型在生成这个inode时赋予的。而各个文件系统类型提供的a_ops->readpage函数一般是mpage_readpage函数的封装;
mpage_readpage。调用do_mpage_readpage构造一个bio,再调用mpage_bio_submit将其提交;
do_mpage_readpage。根据page->index确定需要读的磁盘扇区号,然后构造一组bio。其中需要使用文件系统类型提供的get_block函数来对应需要读取的磁盘扇区号;
mpage_bio_submit。设置bio的结束回调bio->bi_end_io为mpage_end_io_read,然后调用submit_bio提交这组bio;
submit_bio。调用generic_make_request将bio提交到磁盘驱动维护的请求队列中;
generic_make_request。一个包装函数,对于每一个bio,调用__generic_make_request;
__generic_make_request。获取bio对应的块设备文件对应的磁盘对象的请求队列bio->bi_bdev->bd_disk->queue,调用q->make_request_fn将bio添加到队列;
q->make_request_fn。设备驱动程序在其初始化时会初始化这个request_queue结构,并且设置q->make_request_fn和q->request_fn(这个下面就会用到)。前者用于将一个bio组装成request添加到request_queue,后者用于处理request_queue中的请求。一般情况下,设备驱动通过调用blk_init_queue来初始化request_queue,q->request_fn需要给定,而q->make_request_fn使用了默认的__make_request;
__make_request。会根据不同的调度算法来决定如何添加bio,生成对应的request结构加入request_queue结构中,并且决定是否调用q->request_fn,或是在kblockd_workqueue任务队列里面添加一个任务,等kblockd内核线程来调用q->request_fn;
q->request_fn。由驱动程序定义的函数,负责从request_queue里面取出request进行处理。从添加bio到request被取出,若干的请求已经被IO调度算法整理过了。驱动程序负责根据request结构里面的描述,将实际物理设备里面的数据读到内存中。当驱动程序完成一个request时,会调用end_request(或类似)函数,以结束这个request;
end_request。完成request的收尾工作,并且会调用对应的bio的的结束方法bio->bi_end_io,即前面设置的mpage_end_io_read;
mpage_end_io_read。如果page已更新则设置其up-to-date标记,并为page解锁,唤醒等待page解锁的进程。最后释放bio对象;
sys_write。跟sys_read一样,对应的vfs_write、do_sync_write、f_op->aio_write、generic_file_aio_write被顺序调用;
generic_file_aio_write。调用__generic_file_aio_write_nolock来进行写的处理,将数据写到磁盘高速缓存中。写完成之后,判断如果文件打开时使用了O_SYNC标记,则再调用sync_page_range将写入到磁盘高速缓存中的数据同步到磁盘(只同步文件头信息);
__generic_file_aio_write_nolock。进行一些检查之后,调用generic_file_buffered_write;
generic_file_buffered_write。调用generic_perform_write执行写,写完成之后,判断如果文件打开时使用了O_SYNC标记,则再调用generic_osync_inode将写入到磁盘高速缓存中的数据同步到磁盘(同步文件头信息和文件内容);
generic_perform_write。一次异步写可能包含多个写操作(对应于writev系统调用),对于其中牵涉的每一个page,调用file->f_mapping->a_ops->write_begin准备好需要写的磁盘高速缓存页面,然后将需要写的数据拷入其中,最后调用file->f_mapping->a_ops->write_end完成写;
file->f_mapping是从对应inode->i_mapping而来,inode->i_mapping->a_ops是由对应的文件系统类型在生成这个inode时赋予的。而各个文件系统类型提供的file->f_mapping->a_ops->write_begin函数一般是block_write_begin函数的封装、file->f_mapping->a_ops->write_end函数一般是generic_write_end函数的封装;
block_write_begin。调用grab_cache_page_write_begin在radix树里面查找要被写的page,如果不存在则创建一个。调用__block_prepare_write为这个page准备一组buffer_head结构,用于描述组成这个page的数据块(利用其中的信息,可以生成对应的bio结构);
generic_write_end。调用block_write_end提交写请求,然后设置page的dirty标记;
block_write_end。调用__block_commit_write为page中的每一个buffer_head结构设置dirty标记;
至此,write调用就要返回了。如果文件打开时使用了O_SYNC标记,sync_page_range或generic_osync_inode将被调用。否则write就结束了,等待pdflush内核线程发现radix树上的脏页,并最终调用到do_writepages写回这些脏页;
sync_page_range也是调用generic_osync_inode来实现的,而generic_osync_inode最终也会调用到do_writepages;
do_writepages。调用inode->i_mapping->a_ops->writepages,而后者一般是mpage_writepages函数的包装;
mpage_writepages。检查radix树中需要写回的page,对每一个page调用__mpage_writepage;
__mpage_writepage。这里也是构造bio,然后调用mpage_bio_submit来进行提交;
后面的流程跟read几乎就一样了……


Category linux