为什么需要,这个问题不妨反过来思考如果没有文件系统会怎样?以下为转载博客园某个博主精彩论述
http://www.cnblogs.com/shangye/p/6177993.html
如果没有文件系统
如果我们不在硬盘本身建立文件系统,我们直接面对硬盘的扇区。
如何读写文件
先看看对于操作普通文件来说,意味着什么。
我们要拿着一个小本本,上面记着,文件名,文件所在扇区以及文件大小。每次要读写文件,我们要人工查询这个账本,知道我们要的文件在哪里。如果文件A所在的扇区M已经写满了,随后的一个扇区M+1被文件B占用了,我们还想接着写文件A,怎么办呢?只能从其他地方找一个空闲扇区N,然后在账本上把N记录到文件A占用的扇区项中。
我们如何知道硬盘上还有哪些空间可以用呢?难道每次都从前往后把扇区使用情况计算一遍吗吗?可能还需要另起一个账本记录扇区使用情况,删除文件,我们把对应的扇区标记为空闲,如果创建文件,把对应的扇区标记为不能使用。
对于操作系统而言呢?我觉得,没有文件系统就不会有操作系统,这样的操作系统充其量就是一个硬盘驱动。为什么?可以设想一下创建文件的过程:
- 用户告诉这样的操作系统,说要创建一个文件A
- 计算机输出,请你自己记录好文件名,并告诉我要在哪个扇区创建。并且记录好这个文件你占用了哪些扇区
我不能忍受。。。
提炼上述过程中我们需要的信息
将变化的放在一起,将不变的放在一起。统一才有美感。
dir_entry
对于文件使用情况的账本而言,看起来要表述一个文件在硬盘上的信息,我们需要知道它占用了哪些扇区,它的名字,文件大小这样的信息。那么这些信息应该放在哪里呢?当然可以随机存放,但是存放完了,计算机如何在下次使用的时候找到这个文件呢?还是需要一份记录来索引这些信息,还不如把这些文件信息按照统一格式存放在一起,这就是目录结构(dir_entry)的由来。按照树状目录能得到任何文件信息。
sector_map
对于硬盘使用情况的账本而言,要记录好哪些扇区空闲,哪些已经被使用了。这就是sector_map的由来。
super_block
那么这些账本本身是存在于硬盘的某些地方,还需要一个总账本来记录这些管理块的信息,这个总账本就是super_block。
inode
那么inode的由来呢?为什么文件名和inode分开存放呢?
试想一下,如果文件名和文件的属性信息一起存放的话,一个文件目录项会占用很大的空间,一个扇区也许只能存几个文件的信息,而系统在查找文件的时候,可能要读很多次扇区才能找到需要的文件,这样大大影响系统的效率。毕竟我们在找文件的时候,不需要文件的信息,不需要知道文件大小、所在扇区等等信息全部与查找无关,为什么要这些信息来影响我们的速度呢?我们只要文件名来判断这是不是我们要找的文件。所以将文件的其余信息剥离出来概括为inode结构体。
inode_array
inode单独列出来了,存放在哪里呢?如何通过dir_entry找到inode呢?当然可以存放于任何扇区上,只不过dir_entry可能要加上inode所在扇区和在扇区中的偏移两个字段了,随之而来问题就是存放inode的扇区只能用来存放其他inode而不能用来存放文件数据了,因为我们给文件分配空间是按照扇区为单位的,难道一个扇区分给文件时候,还要记上一笔在偏移offset处是inode占用的,读写的时候请跳过,这样的逻辑恐怕没人会去用代码实现它吧。另外,由于“存放inode的扇区只能用来存放其他inode而不能用来存放文件数据”这样的原因,设计者就折中了一下,把indoe占用的扇区都提到一个单独空间,以后所有的inode都放到这个空间里,这个空间就是inode_array。
当然也会出现问题,可能inode_array满了,而硬盘空间还要很大剩余;或者硬盘空间嘛呢,inode_array还有很多剩余。这是很极端的情况,总要有不尽人意的地方,那就把这个不足最小化吧。
在存放inode的时候,怎么知道inode_array中哪个下标可以用呢?这就是又需要一份记录,来记录inode_array中哪些是空闲的,哪些是已经使用,这个记录就是inode_map。而inode在inode_array中的下标就是inode_num。dir_entry中记录了这个inode_num就可以在inode_array中找到对应的文件信息了。这个过程衔接的太美妙了。
文件系统的实现
文件系统需要的结构体大概都知道了,剩下的仅仅是需要规划处具体的结构体了。我们来看看。
需要在硬盘上保存的信息
超级块
Inode-map
Sector-map
Inode-array
上面几个结构体作者在书中都列举出来了,都是很好理解的。我不啰嗦再搬运过来了。
代码上实现的逻辑
设备号
正是在作者的讲解下,我算是真正的了解到设备号的意义。以前总是看书上说主设备号代表设备归属于哪个驱动,子设备号真正表明是哪个具体的设备。我虽然能顺着设备号找到驱动,能从驱动中看到子设备号对流程的分用作用,但是感觉总是欠缺点什么。我就好奇为什么linux 0.12中将0x300就能代表第一块硬盘,难道不能是0x400吗?为什么0代表整个硬盘,1代表第一个分区?分区编号要按照物理分区顺序吗?如果是0x400会产生什么影响呢?
跟着作者一起学着规划硬盘空间,才渐渐明白,这些编号可以随意编,跟硬盘上的分区顺序不存在某种必然的联系,只是最后落实到保存硬盘信息的结构体上的时候,不会出现偏差就可以了。
对于操作系统而言,每个分区都被当做一个独立的设备对待。看看书中所描述的硬盘信息结构体。
struct part_info { u32 base; /* # of start sector (NOT byte offset, but SECTOR) */ u32 size; /* how many sectors in this partition */ }; /* main drive struct, one entry per drive */ struct hd_info { int open_cnt; struct part_info primary[NR_PRIM_PER_DRIVE];//计算后NR_PRIM_PER_DRIVE = 5 struct part_info logical[NR_SUB_PER_DRIVE];// 计算后NR_SUB_PER_DRIVE = 64 };
由结构体可以看出来,硬盘上存在的每个分区都会被记录下来。
书中根设备编号是0x322,可以知道子设备号是0x22,一开始很困惑,这么大的子设备号,难道要分0x22个分区?或者说系统怎么就知道0x22表示的是根分区呢?
还得再看一段代码:
logidx = (p->DEVICE - MINOR_hd1a) % NR_SUB_PER_DRIVE; sect_nr += p->DEVICE < MAX_PRIM ? hd_info[drive].primary[p->DEVICE].base : hd_info[drive].logical[logidx].base;
先将设备号减去第一个逻辑设备的编号得到设备号在logical数组的下标。当然,可能这个设备号不是逻辑设备,而是主分区。没关系,下一步判断p->DEVICE 是不是小于MAX_PRIM,如果小于,说明是主分区,直接用p->DEVICE在primary数组中取值就可以了。
原来是这样,你想怎么样编号就怎么样编号,只要你自己能找到映射关系就可以了。
分区信息
硬盘的管理结构体已经设计好了,那么如何获取硬盘的分区信息呢?见硬盘驱动那篇总结。
文件描述符
内存中的文件如何和硬盘中的文件联系起来?当我们打开一个文件后,后续的操作,如何来标示我们操作的是一个文件而不是一段莫名其妙的内存呢?
首先,我们会想到将inode读到内存就好了,我们就知道文件的所有信息了。那文件名呢?好像文件名除了查找匹配能贡献一份力量,其它地方用不着啊,难道也一些读进来吗?仅仅是做个标识而已,用一个数不是更好、更简单吗?这就是文件描述符的作用。那文件描述符放在哪里呢?由于每个进程打开的文件不同,打开同一个文件的次序不同,那么文件描述符一般情况下也就不能作为进程共享的资源了(当然,域套接字是可以的,内核社区的人员一次又一次地刷新人们的理解力)。既然如此,文件描述符最好是进程私有的了,就只能放在进程表(也就是进程控制块)里面了。此外,机器资源有限,总不能让一个进程无限制的打开文件,最好大家都没内存了,只能歇菜了。所以,一个进程打开的文件数是有限制的,目前我们只给20个就好了。
file结构体
好像有了文件描述符就可以直接和inode关联起来了,没必要中间再加一层file结构体啊。我想是因为要以比较节约的方式共享文件吧,节约什么呢,除了内存还能有谁能让那些设计师精益求精呢?
- 我们当然可以在每个进程控制块里面分配20个存放文件信息的结构体,存放读写偏移指针、打开的权限、inode指针等等信息。但是能保证进程会长时间打开20个文件吗?如果不能保证,那不就浪费了。如果以后允许打开100个文件呢?难道进程控制块也要随之而增大吗?
- 关于共享文件,父子进程通过一个放在描述符数组里面的指针共享一个file结构体,而不用在单独维护一个file时候还要考虑同步。设想这样一个情形:父子进程都对一个文件进行写操作,父进程写了10个字符,按照需求该子进程接着写10个字符了,如果是父子进程单独维护file结构体,那么实际上只有子进程写了10个字符,父进程写的10个字符被覆盖了。如果共享呢?file中的pos每次操作对于两个进程而言都是同步的(当然这个例子不太严谨,它本身就存在同步问题,但是仅仅用来说明一点问题还是可以的)。
inode保存的信息
为什么不用inode本身当做系统或者进程操作文件的接口呢?这个问题比较好考虑。多个进程操作同一个文件,那读写指针的值肯定不一样,读写方式也不一样,其实这些不一样的地方提炼出来就是file结构体啦,file结构体的内容也不是随意产生的。
将变化的放在一起,将不变的放在一起。
如果有文件系统
再接下来考虑一下如果有文件系统能给我们带来什么好处呢?
读写接口
不过,首先还是要实现读写接口的,就套用linux惯用的读写接口就好了。
int read(int fd, void *buf, int count);
只不过linux是通过中断调用来和内核交互,咱们是通过给TASK_FS发送消息并同步等待来实现的。
读写流程
- 那么如果一个用户进程A请求读写一个文件X,那么A会向TASK_FS进程发送消息,告诉FS文件名和读写模式。
- 功能完备的文件系统还要考虑很多因素,诸如做下判断,看看文件路径是相对于当前目录还是根目录。我们比较简单,全部按照根目录实现,而且不支持多级目录,所有文件都放在根目录中。
- TASK_FS会给TASK_HD发消息,把目录区读给我。然后逐一比较有没有相同的文件名,假设有同名的,根据dir_entry中记录的inode_num算一下文件indoe所在的扇区是多少,然后再给TASK_HD发消息,把inode所在扇区读进来,文件具体的信息就有了。
- 后续操作这个文件,TASK_HD根据进程控制块中的信息来计算和决定该怎么操作文件数据。比如说根据文件描述找到file结构体,里面有读写指针知道下一步要操作的位置是哪里,通过file结构体找到inode,这样就知道文件数据在哪个扇区了。
通过上面简单的叙述,也可以窥见现代文件系统问什么加入了dentry这个成员,目录项在查找的时候也是经常用到的,还不如缓存在内存中,加快读写速度。
TASK_FS
TASK_FS在微内核的设计中,被设计为一个进程了,它不断地循环读取其它进程发给它的读写请求,但是一次只能处理一个请求,如果这个请求没有完成,那么其它进程只能挂接在TASK_FS的等待队列上等待了。不过没关系,过早的优化是万恶之源。