Linux 虚拟文件系统概述

转自:http://blog.csdn.net/u011373710/article/details/70198080node

文章梗概

本文首先以“尽可能不涉及源代码”的方式讨论Linux虚拟文件系统的存在的意义、实现方式;后续文章中以读文件为例从面到点更有针对性的讨论其实现。在讨论的过程当中有一些地方可能说的不够全面,一是能力有限;另外一方面是但愿不要陷入过多的细节之中,将注意力集中在框架的设计上。但愿读者有一些编程基础、操做系统概念。本文讨论的Linux内核版本为2.6.24linux

正文

文件系统

经常使用的文件的读写的方式有同步读写异步读写内存映射。前两种的主要区别是,同步读写会在进程向操做系统发出读写请求后被阻塞,直至所请求的操做完成时读写的函数才会返回。也就是说,函数返回的时候数据已经从磁盘中读到内存中了,或者已经从内存中写入到磁盘上去了;异步读写在发出读写请求后,函数会当即返回并不等待操做完成,这样的话通常有一个回调函数的存在,操做完成时操做系统会调用用户设置的回调函数;至于内存映射使用的则是缺页机制。这篇文章中主要围绕同步读写的方式,也是最经常使用的方式,讨论Linux的虚拟文件系统的实现原理。 
要想说明白虚拟文件系统的做用就必需要了解文件系统的做用。对于一个特定的文件系统来讲,最主要的的设计方面就是数据在磁盘上的存储方式,这个功能从应用程序猿的角度来看就是文件名到数据的映射 : 经过对 以文件名为主要的参数调用特定的函数(不一样的语言、平台函数不一样)完成文件的读写操做。而磁盘基本上能够看作一个以扇区(一般为521Byte)为基本存储单位的超大数组。若是有以下的极其简单的文件系统,算法

极简文件系统

扇区0存放着一整块磁盘的属性,如块大小、文件系统标记;1~6中存放着文件的属性和位置信息,每一个文件目录项占两个磁盘块,也就是说该文件系统的设计最多能够存放三个文件;一、2中存放着文件test.txt的属性信息(日期、大小、归属)和位置信息;三、4存放着艳照门1.png的属性信息和位置信息;7~15中存放着实际的数据,不一样的数据的颜色不一样。如图示的文件系统最多只能存放三个文件,很难有实际的用途,不过用来讲明文件系统的做用是很合适的,避免陷入过多的细节之中。 
加入如今用户想要读取查看艳照门1.png,那么文件系统要作的事情是这样的 :编程

  1. 产生读入一、2块磁盘数据的请求,将请求发送给磁盘的驱动程序,等待数据读入到内存后,查看文件的名字,发现文件的名字并非艳照门1.png
  2. 产生读入三、4块磁盘数据的请求,将请求发送给磁盘的驱动程序,等待数据读入到内存后,查看文件的名字,发现名字匹配正确。而后分析目录项数据,得知艳照门1.png主要存储在12~15四个磁盘块上。
  3. 产生读入12~15四个磁盘块的请求,将请求发送给磁盘的驱动程序,等待数据读入到内存后便可按照png文件的格式解析数据而后将图像呈如今用户面前。

这里有几点点须要说明一下数组

  1. 真正的读写磁盘的操做并非文件系统来完成的,而是特定的磁盘的驱动程序来完成的。磁盘的驱动程序须要的参数是逻辑磁盘块号对应的内存位置是读请求仍是写请求。由磁盘驱动程序来完成读写的目的是,实现文件系统的设计和具体磁盘结构的分离。也就是说,咱们上面设计的极简的文件系统在驱动程序的帮助下不管是在机械硬盘上面,仍是在固态硬盘上面实现方式是相同的。磁盘驱动程序更加的贴近硬件设备,更加的“了解”硬件。譬如,若是使用的是固态硬盘那么驱动程序则不会对读写请求进行排序;若是使用的是机械硬盘,则会对读写请求进行排序。参考机械硬盘内部硬件结构和工做原理详解可知对于机械硬盘,按照顺序一、二、3读取磁盘块的速度是快于一、三、2速度的,因此磁盘驱动程序读写请求排序所作的事情简单讲就是将一、三、2这样的读写请求排序成为一、二、3这样的顺序,而后发送给磁盘控制器(硬件)。而固态硬盘更贵的缘由在于其读写过程不包含物理属性(移动磁头),请求一、二、3一、三、2和请求二、三、1的速度都是同样的,因此不须要重排请求。
  2. 1 中提到的请求排序的专业术语叫作I/O调度,这一工做应该是由驱动程序完成的,只不过内核提供了一些的经常使用算法的实现,方便了驱动程序的开发而已。
  3. 一个对特定文件的读写请求可能会产生不少个对磁盘的读写请求。如上所说,要读文件首先要把文件的固有属性读到内存中加以分析,而后才能去读实际的文件。

虚拟文件系统架构

上文以比较易懂的方式说明了文件系统一个最重要的工做文件名到磁盘数据的映射,但是单从这一个方面彷佛看不出来虚拟文件系统存在的意义。那是由于,实际上文件系统要考虑不少的事情 :浏览器

  1. 首先要考虑的一个重要的方面就是,磁盘和内存的速度差距。众所周知,磁盘的速度慢可是每单位空间价格更加便宜,而内存的速度基本上是磁盘的105,这之间的差距仍是很是大的。用户读写文件的时候常常存在着这样的行为,刚刚读写过的文件,颇有可能再次读写,因此引入缓存的概念是十分必要的。缓存所作的就是在读文件数据到内存以后,找个地方把数据存起来,内存不紧张的话,尽量长时间将数据保存在内存之中。若是再次读文件,文件系统就去查看是否是在缓存中已经有对应的数据了,若有的话就不须要再次去磁盘上读取数据;实在没有的话,才须要向驱动程序发出读请求。用户是意识不到缓存的存在,因此从用户的角度来看这将极大的加快了数据的读取速度。不过要说缺点的话,这样的设计可能会占用比较多的内存空间,使文件系统和内存管理变得更加复杂。不过现阶段的PC平台内存并非性能瓶颈,并且为了速度和用户体验这些都是值得的。
  2. 可想而知,缓存的单位指定不能是字节。若是是字节的话须要记录文件的每一个字节缓存在内存的哪一个位置,这样是不现实的。缓存的单位至少也是和磁盘扇区同样大的,或者是多个连续的扇区存放在一块。以上面的最简文件系统为例,能够很轻松的设计一个这样的数据结构来记录艳照门1.png的第一块也就是磁盘索引的12逻辑块存放在内存的0xFFAA处、第二块到第四块存放在0xAABB处。为了和内存管理模块更好的交互,Linux中的针对文件数据的缓存的单位为,一般大小为4KB,也正是内存管理模块使用的内存单位。这里须要读者有必定的操做系统的基础,可以大致上理解分页机制存在的缘由以及作了什么事情,因为篇幅缘由这里只须要知道在Linux中,内核使用struct page结构体来描述内存中的每一页。找到一页对应的page结构体就至关于知道了对应的物理页的物理地址、是否被占用等等很重要的属性。这些文字是想说明,文件系统须要和内存管理模块协调工做,毕竟分配回收缓存的内存空间等操做都是须要内存管理模块支持的。
  3. 第三个要考虑的部分和第一个相似,这个机制叫作“预读机制”。相似于,在浏览网页的时候有些浏览器可以提供预读,看完当前的网页点击下一页以后,下一页面可以很是快的呈如今面前。这里的预读机制也就是这个意思,尤为是对于一些比较大的文件。用户第一次想要的可能就是前1kb,这个时候文件系统向驱动程序发出的读请求是大于1kb的,多是4kb、多是1mb,多读出来的数据就存在上面提到的缓存。能够看出来预读机制缓存机制是完美结合的,协同工做的。
  4. 1中提到的这个缓存指的是对文件内容的缓存,还有一种缓存是针对目录项的缓存。回顾上文,若是读取艳照门1.png以后,用户又想查看文件test.txt,文件系统又要屡次读取1~6块磁盘来查找是否是有test.txt这个文件。这样的话,引入一个针对文件名的缓存也是很重要的,这个缓存在Linux内核中叫作目录项缓存。和上面的缓存相似,只不过是针对目录项的。
  5. 权限检测也是文件系统要作的很重要的一个工做,尤为是在Linux系统上面,常常会遇到读写文件时permission denied的状况。若是没有管理员权限,操做系统(文件系统)是不会让你去读写一些受保护的文件的,window中可能感觉不是那么强烈。不过除了这中常规意义上的限制,权限检测还包括:文件是否越界、是否在读写一个目录项文件等操做。
  6. 每一个进程都有都会以本身特定的方式(读、写)打开文件,且文件读取的当前位置也是特定于进程的,这些属性是须要记录的。并且对于一个文件不少时候可能会有多个进程同时去读写的状况,如何协调多个进程之间的读写关系,不至于出现错误的状况,也是文件系统设计时须要考虑的一个关键方面。
  7. 锁机制、命名空间等其余的一些部分。

回顾这么多设计一个文件系统须要注意的地方能够发现,上面的几条对于每个文件系统都是须要的。也就是说,无论是上面提到的极简文件系统仍是在实际使用中的FAT、EXT文件系统,都须要考虑上面的这些因素。为了加快内核的开发、方便后续内核的扩展重构、使内核设计的更加优雅这才提出了虚拟文件系统的概念。虚拟文件系统作的事情就是实现了这样一个框架,在这个框架中上面说起的这些重要因素都有了默认的实现,须要特定的文件系统实现的为文件相对块号到磁盘逻辑块号的映射关系目录项的解析方式等。 
文件相对块号就是指的相对于文件来讲是第几块,磁盘逻辑块号指的是相对于磁盘来讲是第几块。缓存

也就是说,若是想让内核识别极简文件系统须要该文件系统的设计人员严格按照虚拟文件系统的架构编写须要的函数(都是函数指针的技术实现的),而后将文件系统注册到内核中去。数据结构

(在这以前文件系统和虚拟文件系统的概念界限是有点模糊的,从这里开始一直到文章的最后,文件系统只是表示磁盘上的数据存储的结构,其余的部分都算在虚拟文件系统里面的 )架构

虚拟文件系统之因此没有实现这两个方面是由于这些性质是特定于一个文件系统的。仍是上面读取艳照门1.png的例子,在极简文件系统中艳照门1.png第一块是存放在磁盘逻辑12块中的,也就是文件相对块号1->磁盘逻辑块号12。而若是使用的是FAT文件系统那么就但是其余的映射关系。目录项的解析方式须要特定的文件系统来实现就更好理解了,不一样的文件系统其目录项的字段设置、顺序、长度一般是不同的,因此须要让特定的文件系统来解析目录项。解析完以后返回一个统一的文件的表示,也就是大名鼎鼎的inodestruct inode的字段很是之多,对于一些没那么复杂的文件系统来讲多是一种浪费,由于它根本用不到那些复杂的字段。可是虚拟文件系统的设计理念是宁滥勿缺 : 毕竟要尽量地覆盖全部的文件系统,多的字段你能够不用,可是若是想用却没有那就麻烦大了。框架

这个时候看一下Linux虚拟文件系统的总体结构,再合适不过了。

此处输入图片的描述

虚拟文件系统(VFS,virtual file system)须要和各个实际的文件系统ext3、… 、reiserproc交互,大多数文件系统都须要虚拟文件系统提供的缓存机制(Buffer Cache),而proc文件系统不须要缓存机制是由于其是基于内存的文件系统。那么按照上面的讨论其须要作的就是完成文件逻辑块号内存中数据块之间的映射关系。再往下一层就是具体的设备驱动层,实际的读写操做都是须要设备驱动层去完成的,它下一层就是实际的物理设备了。

虚拟文件系统如何知道可用的文件系统有哪些的

上面提到的特定于文件系统的操做是经过注册的方式让虚拟文件系统知道当前内核中支持哪些操做系统,注册的主要参数有文件系统的名字解析inode的函数(解析目录项)解析文件相对块号到磁盘逻辑块号的函数,这都是上面讨论过的关键点。对于一些经常使用的文件系统不用注册也可以使用,这是由操做系统去注册的。注册使用的技术就是C语言中的函数指针。注册完成以后,就能够经过挂载的方式去使用一个具体的文件系统了。挂载须要的参数有被挂在的设备(本文讨论中限定为磁盘)该设备使用的文件系统(必须已经注册过)挂载点。若是明白前文的讨论,那么挂载也是很好理解的。以上文的极简文件系统为例,艳照门1.png存放在磁盘A中,在磁盘A没有被挂在以前操做系统(或者说虚拟文件系统)并不知道磁盘A使用的什么文件系统,因此没有办法去读取它上面的数据并解析之。在用户执行了操做以极简文件系统挂载磁盘A到 /home/jingjinhao/片 下以后,在访问/home/jiangjinhao/片/艳照门1.png的时候虚拟文件系统就会调用极简系统注册的函数,去执行前文讨论过的寻找艳照门1.png的过程。毫无疑问操做系统须要维护一个目录之间的层级关系以及不一样的文件系统之间的挂载关系,这正是struct dentrystruct vfsmound的做用。这之间的具体图示关系请参考mount过程分析之六——挂载关系(图解) 。感受本身没有能力写出来一篇比这还好的文章,推荐看一下这篇文章。

不太喜欢的环节

上文提到struct inodestrcut dentrystruct vfsmound这三个数据结构都是虚拟文件系统很是重要的部分。虽然不大喜欢扣数据结构,不过为了下文更好的讨论这里仍是尽可能从原理上罗列一下核心数据结构。

  1. struct address_space这个数据结构是对上文讨论过的缓存的抽象。该数据结构能够提供查找缓存、添加缓存的方法,也就是说对于一个文件找到了其对应的struct address_space就可以增删改查缓存的内容。暂时没必要关心起底层的实现是链表、数组仍是树(实际上是基数树),不管是什么其提供的功能老是不变的,只不过速度上可能会有差异。查找使用的参数是文件相对页号,成功返回对应的物理页帧描述结构struct page的指针(上文描述过),没有找到的话返回null。这里的文件相对页号很好理解,举例来讲在页大小为4KB的状况下,0~4KB对也相对页号为0,4KB~8Kb对应的相对页号为1,以此类推。
  2. struct inode是对一个文件的抽象,因此其中包含的主要字段有 : 文件的大小、日期、全部者等固有属性;指向缓存的指针struct address_space *;指向块设备驱动程序的指针block_device*,由于文件系统并不负责实际的读写,须要依靠驱动程序的帮助;一些锁。这几大类字段,在上文的讨论下都是比较好理解的,须要说明的一点就是inode中是没有文件的名字这个字段,文件的名字包含在下面的dentry中,因此取而代之的是指向对应文件的struct dentry的指针。这并非说必定不能把文件名存储在inode中,只不过当前虚拟文件系统的设计使然,再在inode中存储的话就有点啰嗦了。
  3. struct dentry首先实现了对目录层次结构的抽象,以下图目录层次结构内存中每一个打开的的每一个节点都对应一个struct dentry的实例,须要强调的不只仅目录有对应的detry实例,普通的文件也有对应的detry,只不过普通文件的detry实例没有子节点罢了。只有打开的文件或目录才有对应的节点,因此内存中树结构的完整度是磁盘上树结构的完整度的;没有在inode中而是在detry存储文件的名字的一大缘由struct detry负责创建起前文讨论过的目录项缓存(以Hash表的方式)。也就是说在打开一个文件的时候,虚拟文件系统会首先经过文件名查找是否存在一个打开的detry了,若是有的话就大可返回了;detry最后一个重要的做用就是结合struct vfsmount完成了挂载操做的数据结构的支持。上图中的示例在vfsmount的视图中以下图示 vfsmount视图 
    此外硬连接的实现也是须要detry的支持的。
  4. 正如上面所说虚拟文件系统须要记录某个进程操做一个文件的方式、当前位置等属性,这些属性是特定于一个进程的,一个文件可能同时存在多种被打开的状态,由此引入了struct file结构体。该结构体是对一次文件操做的抽象,刚刚提到的几个方面外,file中还包含了预读相关的一些字段。每一个进程控制块struct task中都包含一个struct file *的数组,进程打开的每一个文件对应其中的一项,这也解释了为何fopen返回的是一个无符号整型了(数组的索引)。
  5. 除了上面讨论的四大方面的属性,还有一个方面的属性能够用一个单独的数据结构抽象出来,这就是特定于一个文件系统的属性,譬如极简文件系统的最多只能存三个文件这相似的属性,这个结构体叫作struct super_block。以极简文件系统为例其对应着磁盘逻辑地址0的块中的数据。

经过上面的讨论就能够经过下图来纵观虚拟文件系统的结构了,该图引自深刻Linux内核架构中文版418页,请暂时忽略各个*_operations,其他的不外乎刚刚讨论过的五个结构体,但愿读者可以认真看一下这个图片。 
虚拟文件系统总体结构

图中的几个*_operations都是一些函数指针的结构体,注册文件系统的精髓就是将本身实现的功能函数以函数指针的形式传递给虚拟文件系统。 
譬如对于ext2文件系统其对应的address_space_operations

const struct address_space_operations ext2_aops = { .readpage = ext2_readpage, .readpages = ext2_readpages, .writepage = ext2_writepage, .sync_page = block_sync_page, .write_begin = ext2_write_begin, .write_end = generic_write_end, .bmap = ext2_bmap, .direct_IO = ext2_direct_IO, .writepages = ext2_writepages, .migratepage = buffer_migrate_page, }; //重 static int ext2_readpage(struct file *file, struct page *page) { //@ page 要读的相对于文件的页号 //特定于ext2文件系统的 相对文件块号->磁盘逻辑块号 映射关系 函数 return mpage_readpage(page, ext2_get_block); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

其中比较重要的下篇文章可能用到的为ext2_readpage函数,该函数直接调用了mpage_readpage,这个函数是虚拟文件系统提供的,ext2_get_block是ext2文件系统提供的。下篇文章还有相关的讨论,这里再也不赘述。 
另外一个比较重要的函数为inode_operations->look_up = ext2_lookup这个函数就上文一再强调的解析目录项的函数。


引用

本文使用的引用基本上都在文中以超链的方式给出了,侵删。还须要声明一下,更多的是对现有博客和书籍的补充,具体的实现请参照Linux三本经典书籍

 

 

 

https://blog.csdn.net/bullbat/article/details/7245582

Linux虚拟文件系统(内核初始化)

 

linux 启动(boot)时,须要加载根文件系统,根文件系统中保存一些系统运行所必须的驱动,模块文件等。可是加载根文件系统自己就须要一个模块文件,因此须要先访问到这个模块文件,因此须要一个文件系统来进行访问,ramdisk就是用来作这个的。boot时内核先加载这个ramdisk,它是和内核同样,都是BootLoader经过低级命令加载的,其中储存着加载根文件系统所需的驱动,而后加载根文件系统。 为何不直接经过Bootloader加载根文件系统? 多是这样太慢了。