转自:http://blog.csdn.net/u011373710/article/details/70198080node
本文首先以“尽可能不涉及源代码”的方式讨论Linux虚拟文件系统
的存在的意义、实现方式;后续文章中以读文件
为例从面到点更有针对性的讨论其实现。在讨论的过程当中有一些地方可能说的不够全面,一是能力有限;另外一方面是但愿不要陷入过多的细节之中,将注意力集中在框架的设计上。但愿读者有一些编程基础、操做系统概念。本文讨论的Linux内核版本为2.6.24
。linux
经常使用的文件的读写的方式有同步读写
、异步读写
和内存映射
。前两种的主要区别是,同步读写
会在进程向操做系统发出读写请求后被阻塞,直至所请求的操做完成时读写的函数才会返回。也就是说,函数返回的时候数据已经从磁盘中读到内存中了,或者已经从内存中写入到磁盘上去了;异步读写
在发出读写请求后,函数会当即返回并不等待操做完成,这样的话通常有一个回调函数的存在,操做完成时操做系统会调用用户设置的回调函数;至于内存映射
使用的则是缺页机制。这篇文章中主要围绕同步读写
的方式,也是最经常使用的方式,讨论Linux的虚拟文件系统的实现原理。
要想说明白虚拟文件系统的做用就必需要了解文件系统的做用。对于一个特定的文件系统来讲,最主要的的设计方面就是数据在磁盘上的存储方式
,这个功能从应用程序猿的角度来看就是文件名到数据的映射
: 经过对 以文件名为主要的参数调用特定的函数(不一样的语言、平台函数不一样)完成文件的读写操做。而磁盘基本上能够看作一个以扇区
(一般为521Byte)为基本存储单位的超大数组。若是有以下的极其简单的文件系统,算法
扇区0存放着一整块磁盘的属性,如块大小、文件系统标记;1~6中存放着文件的属性和位置信息,每一个文件目录项占两个磁盘块,也就是说该文件系统的设计最多能够存放三个文件;一、2中存放着文件test.txt
的属性信息(日期、大小、归属)和位置信息;三、4存放着艳照门1.png
的属性信息和位置信息;7~15中存放着实际的数据,不一样的数据的颜色不一样。如图示的文件系统最多只能存放三个文件,很难有实际的用途,不过用来讲明文件系统的做用是很合适的,避免陷入过多的细节之中。
加入如今用户想要读取查看艳照门1.png
,那么文件系统要作的事情是这样的 :编程
艳照门1.png
。艳照门1.png
主要存储在12~15四个磁盘块上。png
文件的格式解析数据而后将图像呈如今用户面前。这里有几点点须要说明一下数组
- 真正的读写磁盘的操做并非文件系统来完成的,而是特定的磁盘的驱动程序来完成的。磁盘的驱动程序须要的参数是
逻辑磁盘块号
、对应的内存位置
和是读请求仍是写请求
。由磁盘驱动程序来完成读写的目的是,实现文件系统的设计和具体磁盘结构的分离。也就是说,咱们上面设计的极简的文件系统在驱动程序的帮助下不管是在机械硬盘上面,仍是在固态硬盘上面实现方式是相同的。磁盘驱动程序更加的贴近硬件设备,更加的“了解”硬件。譬如,若是使用的是固态硬盘那么驱动程序则不会对读写请求进行排序;若是使用的是机械硬盘,则会对读写请求进行排序。参考机械硬盘内部硬件结构和工做原理详解可知对于机械硬盘,按照顺序一、二、3
读取磁盘块的速度是快于一、三、2
速度的,因此磁盘驱动程序读写请求排序所作的事情简单讲就是将一、三、2
这样的读写请求排序成为一、二、3
这样的顺序,而后发送给磁盘控制器
(硬件)。而固态硬盘更贵的缘由在于其读写过程不包含物理属性(移动磁头),请求一、二、3
、一、三、2
和请求二、三、1
的速度都是同样的,因此不须要重排请求。- 1 中提到的请求排序的专业术语叫作
I/O调度
,这一工做应该是由驱动程序完成的,只不过内核提供了一些的经常使用算法的实现,方便了驱动程序的开发而已。- 一个对特定文件的读写请求可能会产生不少个对磁盘的读写请求。如上所说,要读文件首先要把文件的固有属性读到内存中加以分析,而后才能去读实际的文件。
上文以比较易懂的方式说明了文件系统一个最重要的工做文件名到磁盘数据的映射
,但是单从这一个方面彷佛看不出来虚拟文件系统存在的意义。那是由于,实际上文件系统要考虑不少的事情 :浏览器
磁盘扇区
同样大的,或者是多个连续的扇区存放在一块。以上面的最简文件系统为例,能够很轻松的设计一个这样的数据结构来记录艳照门1.png的第一块
也就是磁盘索引的12逻辑块
存放在内存的0xFFAA处、第二块到第四块
存放在0xAABB处。为了和内存管理模块更好的交互,Linux中的针对文件数据的缓存的单位为页
,一般大小为4KB,也正是内存管理模块使用的内存单位。这里须要读者有必定的操做系统的基础,可以大致上理解分页机制存在的缘由以及作了什么事情,因为篇幅缘由这里只须要知道在Linux中,内核使用struct page
结构体来描述内存中的每一页。找到一页对应的page结构体
就至关于知道了对应的物理页的物理地址、是否被占用等等很重要的属性。这些文字是想说明,文件系统须要和内存管理模块协调工做,毕竟分配
、回收
缓存的内存空间等操做都是须要内存管理模块支持的。艳照门1.png
以后,用户又想查看文件test.txt
,文件系统又要屡次读取1~6块磁盘来查找是否是有test.txt
这个文件。这样的话,引入一个针对文件名的缓存也是很重要的,这个缓存在Linux内核中叫作目录项缓存。和上面的缓存相似,只不过是针对目录项的。permission denied
的状况。若是没有管理员权限,操做系统(文件系统)是不会让你去读写一些受保护的文件的,window中可能感觉不是那么强烈。不过除了这中常规意义上的限制,权限检测还包括:文件是否越界、是否在读写一个目录项文件等操做。回顾这么多设计一个文件系统须要注意的地方能够发现,上面的几条对于每个文件系统都是须要的。也就是说,无论是上面提到的极简文件系统仍是在实际使用中的FAT、EXT文件系统,都须要考虑上面的这些因素。为了加快内核的开发、方便后续内核的扩展重构、使内核设计的更加优雅这才提出了虚拟文件系统的概念。虚拟文件系统作的事情就是实现了这样一个框架,在这个框架中上面说起的这些重要因素都有了默认的实现,须要特定的文件系统实现的为
文件相对块号到磁盘逻辑块号的映射关系
、目录项的解析方式
等。
文件相对块号就是指的相对于文件来讲是第几块,磁盘逻辑块号指的是相对于磁盘来讲是第几块。缓存也就是说,若是想让内核识别
极简文件系统
须要该文件系统的设计人员严格按照虚拟文件系统的架构编写须要的函数(都是函数指针的技术实现的),而后将文件系统注册到内核中去。数据结构
(在这以前文件系统和虚拟文件系统的概念界限是有点模糊的,从这里开始一直到文章的最后,文件系统只是表示磁盘上的数据存储的结构,其余的部分都算在虚拟文件系统里面的 )架构
虚拟文件系统之因此没有实现这两个方面是由于这些性质是特定于一个文件系统的。仍是上面读取
艳照门1.png
的例子,在极简文件系统中艳照门1.png第一块
是存放在磁盘逻辑12块
中的,也就是文件相对块号1->磁盘逻辑块号12
。而若是使用的是FAT文件系统那么就但是其余的映射关系。目录项的解析方式
须要特定的文件系统来实现就更好理解了,不一样的文件系统其目录项的字段设置、顺序、长度一般是不同的,因此须要让特定的文件系统来解析目录项。解析完以后返回一个统一的文件的表示,也就是大名鼎鼎的inode
。struct inode
的字段很是之多,对于一些没那么复杂的文件系统来讲多是一种浪费,由于它根本用不到那些复杂的字段。可是虚拟文件系统的设计理念是宁滥勿缺
: 毕竟要尽量地覆盖全部的文件系统,多的字段你能够不用,可是若是想用却没有那就麻烦大了。框架
这个时候看一下Linux虚拟文件系统的总体结构,再合适不过了。
虚拟文件系统(VFS,virtual file system)须要和各个实际的文件系统ext3
、… 、reiser
、proc
交互,大多数文件系统都须要虚拟文件系统提供的缓存机制(Buffer Cache),而proc
文件系统不须要缓存机制是由于其是基于内存的文件系统。那么按照上面的讨论其须要作的就是完成文件逻辑块号
到内存中数据块
之间的映射关系。再往下一层就是具体的设备驱动层,实际的读写操做都是须要设备驱动层去完成的,它下一层就是实际的物理设备了。
上面提到的特定于文件系统的操做是经过注册的方式让虚拟文件系统知道当前内核中支持哪些操做系统,注册的主要参数有文件系统的名字
、解析inode的函数(解析目录项)
、解析文件相对块号到磁盘逻辑块号的函数
,这都是上面讨论过的关键点。对于一些经常使用的文件系统不用注册也可以使用,这是由操做系统去注册的。注册使用的技术就是C语言中的函数指针
。注册完成以后,就能够经过挂载
的方式去使用一个具体的文件系统了。挂载须要的参数有被挂在的设备(本文讨论中限定为磁盘)
、该设备使用的文件系统(必须已经注册过)
、挂载点
。若是明白前文的讨论,那么挂载也是很好理解的。以上文的极简文件系统为例,艳照门1.png
存放在磁盘A
中,在磁盘A
没有被挂在以前操做系统(或者说虚拟文件系统)并不知道磁盘A
使用的什么文件系统,因此没有办法去读取它上面的数据并解析之。在用户执行了操做以极简文件系统挂载磁盘A到 /home/jingjinhao/片 下
以后,在访问/home/jiangjinhao/片/艳照门1.png
的时候虚拟文件系统就会调用极简系统注册的函数,去执行前文讨论过的寻找艳照门1.png
的过程。毫无疑问操做系统须要维护一个目录之间的层级关系以及不一样的文件系统之间的挂载关系,这正是struct dentry
和struct vfsmound
的做用。这之间的具体图示关系请参考mount过程分析之六——挂载关系(图解) 。感受本身没有能力写出来一篇比这还好的文章,推荐看一下这篇文章。
上文提到struct inode
、strcut dentry
和struct vfsmound
这三个数据结构都是虚拟文件系统很是重要的部分。虽然不大喜欢扣数据结构,不过为了下文更好的讨论这里仍是尽可能从原理上罗列一下核心数据结构。
struct address_space
这个数据结构是对上文讨论过的缓存
的抽象。该数据结构能够提供查找缓存、添加缓存的方法,也就是说对于一个文件找到了其对应的struct address_space
就可以增删改查
缓存的内容。暂时没必要关心起底层的实现是链表、数组仍是树(实际上是基数树),不管是什么其提供的功能老是不变的,只不过速度上可能会有差异。查找使用的参数是文件相对页号
,成功返回对应的物理页帧描述结构struct page
的指针(上文描述过),没有找到的话返回null
。这里的文件相对页号很好理解,举例来讲在页大小为4KB的状况下,0~4KB对也相对页号为0,4KB~8Kb对应的相对页号为1,以此类推。struct inode
是对一个文件的抽象,因此其中包含的主要字段有 : 文件的大小、日期、全部者等固有属性;指向缓存的指针struct address_space *
;指向块设备驱动程序的指针block_device*
,由于文件系统并不负责实际的读写,须要依靠驱动程序的帮助;一些锁。这几大类字段,在上文的讨论下都是比较好理解的,须要说明的一点就是inode
中是没有文件的名字这个字段,文件的名字包含在下面的dentry
中,因此取而代之的是指向对应文件的struct dentry
的指针。这并非说必定不能把文件名存储在inode
中,只不过当前虚拟文件系统的设计使然,再在inode
中存储的话就有点啰嗦了。struct dentry
首先实现了对目录层次结构的抽象,以下图struct dentry
的实例,须要强调的不只仅目录有对应的detry
实例,普通的文件也有对应的detry
,只不过普通文件的detry
实例没有子节点罢了。只有打开的文件或目录才有对应的节点,因此内存中树结构的完整度是≤磁盘上树结构的完整度的;没有在inode
中而是在detry
存储文件的名字的一大缘由struct detry
负责创建起前文讨论过的目录项缓存
(以Hash表的方式)。也就是说在打开一个文件的时候,虚拟文件系统会首先经过文件名
查找是否存在一个打开的detry
了,若是有的话就大可返回了;detry
最后一个重要的做用就是结合struct vfsmount
完成了挂载操做的数据结构的支持。上图中的示例在vfsmount
的视图中以下图示 detry
的支持的。struct file
结构体。该结构体是对一次文件操做的抽象,刚刚提到的几个方面外,file
中还包含了预读相关的一些字段。每一个进程控制块struct task
中都包含一个struct file *
的数组,进程打开的每一个文件对应其中的一项,这也解释了为何fopen
返回的是一个无符号整型了(数组的索引)。最多只能存三个文件
这相似的属性,这个结构体叫作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); }
其中比较重要的下篇文章可能用到的为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加载根文件系统? 多是这样太慢了。