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