首页 > 代码库 > 文件读写的理论
文件读写的理论
为了提高效率,稍微复杂一些的操作系统对文件的读写都是带缓冲的,Linux当然也不例外。所谓缓冲,就是操作系统为最近刚读写的文件内容在内核保留一份副本,以便当再次需要已经缓冲存储在副本中的内容时就不必再临时从设备上读入,而需要写的时候则可以先写到副本中,待系统较为空闲的时候再从副本写入设备。在多进程的系统中,由于同一个文件可能为多个进程所共享,缓冲的作用就更为显著。
然而,怎么样实现缓冲,在哪一个层次上实现缓冲,确实一个值得考虑的问题。在文件层有三种主要的数据结构:file、dentry、inode。
先看file结构:前面讲过,一个file结构代表着目标文件的一个上下文,不但不同的进程可以在同一个文件上建立不同的上下文(每个进程都有自己的file结构体),就是同一个进程也可以通过打开同一个文件多次而建立起多个上下文。如果在file结构中设置一个缓冲区队列,那么缓冲区中的内容虽然贴近这个特定上下文的使用者,却不便于为多个进程共享,甚至不便于同一个进程打开的不同上下文共享,这显然是不合适的。
那么dentry结构怎么样呢?这个数据结构并不属于某一个上下文,也不属于一个进程,可以为所有进程和上下文共享。可是dentry结构与目标文件并不是一一对应的关系,通过文件链接,我们可以为已经存在的文件建立别名。一个dentry结构知识唯一的代表这文件系统中的一个节点,也就是一个路径名,但是多个节点可以同时代表同一个文件,所以还应该再抽象一次。
显然,在inode数据结构设置一个缓冲队列是最合适不过了,首先,inode结构与文件是一一对应的关系,即使一个文件有多个路径名,最后也归为同一个inode上。再说,一个文件中的内容是不能由其他文件共享的,在同一时间里,设备上的每一个记录都只能属于至多一个文件,将载有同一个文件内容的缓冲区都放在其所属文件的inode结构中是很自然的事。因此在inode数据结构中设置了一个指针i_mapping,它就指向一个address_space数据结构,缓冲区队列就在这个数据结构中。
不过,挂在缓冲区队列中的并不是记录块而是内存页面。也就说,文件的内容并不是以记录块为单位,而是以页面为单位进行缓冲的。为什么这个搞?这是为了将文件内容的缓冲与文件的内存映射结合在一起。进程可以通过系统调用mmap()将一个文件映射到它的用户空间。建立了这样的映射以后,就可以像访问内存一样访问这个文件。如果将文件的内容以页面为单位缓冲,放在附属于该文件的inode结构的缓冲队列中,那么只要相应的设置进程的内存映射表,就可以很自然地将这些缓冲页面映射到用户空间中。这样,在按常规方式文件操作访问一个文件时,就可以通过read()和write()系统调用访问目标文件的inode结构访问这些缓冲页面;而通过内存映射机制访问这个文件时,就可以经由页面映射直接访问这些缓冲着的页面。当目标页面不在内存中时,常规的文件操作通过系统调用read()、write()的底层将其从设备读入,而通过内存映射机制访问这个文件时,则由缺页异常的服务程序将目标页面从设备上读入。明白了这个背景,就明白上述的指针为什么叫i_mapping,它指向的数据结构为什么叫address_space就不会奇怪了。
可是,尽管以页面为单位的缓冲对于文件层确实是很好的选择,对于设备层则不那么合适了。对设备层而言,最自然的当然是以记录块为单位的缓冲,因为设备的读写都是以记录块为单位的。不过,从磁盘上读写主要的时间都花在准备工作上,一旦准备好了以后读一个记录块与接连读几个记录块相差并不大,而且每次只读写一个记录块反而是不经济的。所以每次读写若干连续的记录块、以页面为单位缓冲并不是问题。另一方面,如果以页面为单位缓冲,而一个页面相当于若干连续记录块,那么无论是对于缓冲页面还是对于记录块缓冲区,其控制信息显然应该游离于该页面之外,这些信息不应该映射到进程的用户空间。这个问题不难解决。在设备层中要保持一些buffer_head结构,让它们的b_data指针分别指向缓冲区页面中的相应位置就可以了。以一个缓冲页面为例,在文件层它通过一个page数据结构挂入所属inode结构的缓冲页面队列,并且同时又可以通过各个进程的页面映射表映射到这些进程的内存空间;而在设备层又通过若干buffer_head结构挂入其所在设备的缓冲区队列。
在这样一个结构框架中,一旦所欲访问的内容已经在缓冲页面队列中,读文件的效率就很高了,只要找到文件的inode结构就找到了缓冲页面的队列,从队列中找到相应的页面就可以读出了。