<?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;fid=11&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo中文社区 / 文件系统模块]]></title>
		<link>http://www.gentoo-zh.org/index.php</link>
		<description><![CDATA[Gentoo中文社区 最近发表的主题。]]></description>
		<lastBuildDate>Mon, 02 Jan 2023 01:22:11 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[文件系统的原理]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=648&amp;action=new</link>
			<description><![CDATA[<p>一、硬盘的物理结构：<br /><span class="postimg"><img src="https://www.iegum.com/d/file/p/2023/01-02/2ca3e602ce49a281c89efd2b64b414f3.gif" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; 硬盘存储数据是根据电、磁转换原理实现的。硬盘由一个或几个表面镀有磁性物质的金属或玻璃等物质盘片以及盘片两面所安装的磁头和相应的控制电路组成(图1)，其中盘片和磁头密封在无尘的金属壳中。<br />硬 盘工作时，盘片以设计转速高速旋转，设置在盘片表面的磁头则在电路控制下径向移动到指定位置然后将数据存储或读取出来。当系统向硬盘写入数据时，磁头中 “写数据”电流产生磁场使盘片表面磁性物质状态发生改变，并在写电流磁场消失后仍能保持，这样数据就存储下来了；当系统从硬盘中读数据时，磁头经过盘片指 定区域，盘片表面磁场使磁头产生感应电流或线圈阻抗产生变化，经相关电路处理后还原成数据。因此只要能将盘片表面处理得更平滑、磁头设计得更精密以及尽量 提高盘片旋转速度，就能造出容量更大、读写数据速度更快的硬盘。这是因为盘片表面处理越平、转速越快就能越使磁头离盘片表面越近，提高读、写灵敏度和速 度；磁头设计越小越精密就能使磁头在盘片上占用空间越小，使磁头在一张盘片上建立更多的磁道以存储更多的数据。</p><p>&#160; &#160; 二、硬盘的逻辑结构。</p><p>&#160; &#160; 硬盘由很多盘片(platter)组成，每个盘片的每个面都有一个读写磁头。如果有N个盘片。就有2N个面，对应2N个磁头(Heads)，从0、1、 2开始编号。每个盘片被划分成若干个同心圆磁道(逻辑上的，是不可见的。)每个盘片的划分规则通常是一样的。这样每个盘片的半径均为固定值R的同心圆再逻 辑上形成了一个以电机主轴为轴的柱面(Cylinders)，从外至里编号为0、1、2……每个盘片上的每个磁道又被划分为几十个扇区(Sector)， 通常的容量是512byte，并按照一定规则编号为1、2、3……形成Cylinders×Heads×Sector个扇区。这三个参数即是硬盘的物理参 数。我们下面的很多实践需要深刻理解这三个参数的意义。</p><p>&#160; &#160; 三、磁盘引导原理。</p><p>3.1 MBR(master boot record)扇区：<br />&#160; &#160; 计算机在按下power键以后，开始执行主板bios程序。进行完一系列检测和配置以后。开始按bios中设定的系统引导顺序引导系统。假定现在是硬 盘。Bios执行完自己的程序后如何把执行权交给硬盘呢。交给硬盘后又执行存储在哪里的程序呢。其实，称为mbr的一段代码起着举足轻重的作用。 MBR(master boot record),即主引导记录，有时也称主引导扇区。位于整个硬盘的0柱面0磁头1扇区(可以看作是硬盘的第一个扇区)，bios在执行自己固有的程序以 后就会jump到mbr中的第一条指令。将系统的控制权交由mbr来执行。在总共512byte的主引导记录中，MBR的引导程序占了其中的前446个字 节(偏移0H~偏移1BDH)，随后的64个字节(偏移1BEH~偏移1FDH)为DPT(Disk PartitionTable，硬盘分区表)，最后的两个字节“55 AA”(偏移1FEH~偏移1FFH)是分区有效结束标志。<br />&#160; &#160; MBR不随操作系统的不同而不同，意即不同的操作系统可能会存在相同的MBR，即使不同，MBR也不会夹带操作系统的性质。具有公共引导的特性。<br />我们来分析一段mbr。下面是用winhex查看的一块希捷120GB硬盘的mbr。</p><p><span class="postimg"><img src="https://www.iegum.com/d/file/p/2023/01-02/fa265f16aee4bb43868bfc04b7b547d5.gif" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160;你的硬盘的MBR引导代码可能并非这样。不过即使不同，所执行的功能大体是一样的。这是wowocock关于磁盘mbr的反编译，已加了详细的注释，感兴趣可以细细研究一下。<br />&#160; &#160; 我们看DPT部分。操作系统为了便于用户对磁盘的管理。加入了磁盘分区的概念。即将一块磁盘逻辑划分为几块。磁盘分区数目的多少只受限于C～Z的英文字母 的数目，在上图DPT共64个字节中如何表示多个分区的属性呢?microsoft通过链接的方法解决了这个问题。在DPT共64个字节中，以16个字节 为分区表项单位描述一个分区的属性。也就是说，第一个分区表项描述一个分区的属性，一般为基本分区。第二个分区表项描述除基本分区外的其余空间，一般而 言，就是我们所说的扩展分区。这部分的大体说明见表1。</p><p> <br />表1&#160; 图2分区表第一字段<br />字节位移 &#160; &#160; 字段长度 &#160; &#160; 值 &#160; &#160; 字段名和定义<br />0x01BE &#160; &#160; BYTE &#160; &#160; 0x80 &#160; &#160;&#160; &#160; 引导指示符(Boot Indicator)&#160; &#160;指明该分区是否是活动分区。<br />0x01BF &#160; &#160; BYTE &#160; &#160; 0x01 &#160; &#160; 开始磁头(Starting Head)<br />0x01C0 &#160; &#160; 6位 &#160; &#160; 0x01 &#160; &#160; 开始扇区(Starting Sector) 只用了0~5位。后面的两位(第6位和第7位)被开始柱面字段所使用<br />0x01C1 &#160; &#160; 10位 &#160; &#160; 0x00 &#160; &#160; 开始柱面(Starting Cylinder)&#160; &#160;除了开始扇区字段的最后两位外，还使用了1位来组成该柱面值。开始柱面是一个10位数，最大值为1023<br />0x01C2 &#160; &#160; BYTE &#160; &#160; 0x07 &#160; &#160; 系统ID(System ID) 定义了分区的类型，详细定义，请参阅图4<br />0x01C3 &#160; &#160; BYTE &#160; &#160; 0xFE &#160; &#160; 结束磁头(Ending Head)<br />0x01C4 &#160; &#160; 6位 &#160; &#160; 0xFF &#160; &#160; 结束扇区(Ending Sector)&#160; &#160; &#160;只使用了0~5位。最后两位(第6、7位)被结束柱面字段所使用<br />0x01C5 &#160; &#160; 10位 &#160; &#160; 0x7B &#160; &#160; 结束柱面(Ending Cylinder) 除了结束扇区字段最后的两位外，还使用了1位，以组成该柱面值。结束柱面是一个10位的数，最大值为1023<br />0x01C6 &#160; &#160; DWORD &#160; &#160; 0x0000003F &#160; &#160; 相对扇区数(Relative Sectors) 从该磁盘的开始到该分区的开始的位移量，以扇区来计算<br />0x01CA &#160; &#160; DWORD &#160; &#160; 0x00DAA83D &#160; &#160; 总扇区数(Total Sectors) 该分区中的扇区总数</p><br /><p>注：上表中的超过1字节的数据都以实际数据显示，就是按高位到地位的方式显示。存储时是按低位到高位存储的。两者表现不同，请仔细看清楚。以后出现的表，图均同。</p><p>也可以在winhex中看到这些参数的意义：<br /><span class="postimg"><img src="https://www.iegum.com/d/file/p/2023/01-02/deae59fda35517a3f439d05de5d38344.gif" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; 说明： 每个分区表项占用16个字节，假定偏移地址从0开始。如图3的分区表项3。分区表项4同分区表项3。<br />&#160; &#160; 1、0H偏移为活动分区是否标志，只能选00H和80H。80H为活动，00H为非活动。其余值对microsoft而言为非法值。 <br />&#160; &#160; 2、重新说明一下(这个非常重要)：大于1个字节的数被以低字节在前的存储格式格式(little endian format)或称反字节顺序保存下来。低字节在前的格式是一种保存数的方法，这样，最低位的字节最先出现在十六进制数符号中。例如，相对扇区数字段的值 0x3F000000的低字节在前表示为0x0000003F。这个低字节在前的格式数的十进制数为63。 <br />&#160; &#160; 3、系统在分区时，各分区都不允许跨柱面，即均以柱面为单位，这就是通常所说的分区粒度。有时候我们分区是输入分区的大小为7000M，分出来却是 6997M，就是这个原因。 偏移2H和偏移6H的扇区和柱面参数中,扇区占6位(bit)，柱面占10位(bit)，以偏移6H为例，其低6位用作扇区数的二进制表示。其高两位做柱 面数10位中的高两位，偏移7H组成的8位做柱面数10位中的低8位。由此可知，实际上用这种方式表示的分区容量是有限的，柱面和磁头从0开始编号,扇区 从1开始编号,所以最多只能表示1024个柱面×63个扇区×256个磁头×512byte=8455716864byte。即通常的8.4GB(实际上 应该是7.8GB左右)限制。实际上磁头数通常只用到255个(由汇编语言的寻址寄存器决定),即使把这3个字节按线性寻址，依然力不从心。 在后来的操作系统中，超过8.4GB的分区其实已经不通过C/H/S的方式寻址了。而是通过偏移CH～偏移FH共4个字节32位线性扇区地址来表示分区所 占用的扇区总数。可知通过4个字节可以表示2^32个扇区，即2TB=2048GB，目前对于大多数计算机而言，这已经是个天文数字了。在未超过 8.4GB的分区上，C/H/S的表示方法和线性扇区的表示方法所表示的分区大小是一致的。也就是说，两种表示方法是协调的。即使不协调，也以线性寻址为 准。(可能在某些系统中会提示出错)。超过8.4GB的分区结束C/H/S一般填充为FEH FFH FFH。即C/H/S所能表示的最大值。有时候也会用柱面对1024的模来填充。不过这几个字节是什么其实都无关紧要了。 <br />&#160; &#160; 虽然现在的系统均采用线性寻址的方式来处理分区的大小。但不可跨柱面的原则依然没变。本分区的扇区总数加上与前一分区之间的保留扇区数目依然必须是柱面容 量的整数倍。(保留扇区中的第一个扇区就是存放分区表的MBR或虚拟MBR的扇区，分区的扇区总数在线性表示方式上是不计入保留扇区的。如果是第一个分 区，保留扇区是本分区前的所有扇区。<br />&#160; &#160; 附：分区表类型标志如图4<br /><span class="postimg"><img src="https://www.iegum.com/d/file/p/2023/01-02/4b4395159d73973190e807b8a05a2a82.gif" alt="FluxBB bbcode 测试" /></span> </p><p>3.2 扩展分区：<br />&#160; &#160; 扩展分区中的每个逻辑驱动器都存在一个类似于MBR的扩展引导记录( Extended Boot Record, EBR)，也有人称之为虚拟mbr或扩展mbr，意思是一样的。扩展引导记录包括一个扩展分区表和该扇区的标签。扩展引导记录将记录只包含扩展分区中每个 逻辑驱动器的第一个柱面的第一面的信息。一个逻辑驱动器中的引导扇区一般位于相对扇区32或63。但是，如果磁盘上没有扩展分区，那么就不会有扩展引导记 录和逻辑驱动器。第一个逻辑驱动器的扩展分区表中的第一项指向它自身的引导扇区。第二项指向下一个逻辑驱动器的EBR。如果不存在进一步的逻辑驱动器，第 二项就不会使用，而且被记录成一系列零。如果有附加的逻辑驱动器，那么第二个逻辑驱动器的扩展分区表的第一项会指向它本身的引导扇区。第二个逻辑驱动器的 扩展分区表的第二项指向下一个逻辑驱动器的EBR。扩展分区表的第三项和第四项永远都不会被使用。<br />&#160; &#160; 通过一幅4分区的磁盘结构图可以看到磁盘的大致组织形式。如图5：<br /><span class="postimg"><img src="https://www.iegum.com/d/file/p/2023/01-02/0c959b9fd4fdc59dabc45fbe9091a9d2.gif" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; 关于扩展分区，如图6所示，扩展分区中逻辑驱动器的扩展引导记录是一个连接表。该图显示了一个扩展分区上的三个逻辑驱动器，说明了前面的逻辑驱动器和最后一个逻辑驱动器之间在扩展分区表中的差异。<br /><span class="postimg"><img src="https://www.iegum.com/d/file/p/2023/01-02/0f63acc281bd650e6fff927f752f7b70.gif" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; 除了扩展分区上最后一个逻辑驱动器外，表2中所描述的扩展分区表的格式在每个逻辑驱动器中都是重复的：第一个项标识了逻辑驱动器本身的引导扇区，第二个项 标识了下一个逻辑驱动器的EBR。最后一个逻辑驱动器的扩展分区表只会列出它本身的分区项。最后一个扩展分区表的第二个项到第四个项被使用。   </p><p>   表2&#160; 扩展分区表项的内容<br />扩展分区表项 &#160; &#160; 分区表项的内容<br />第一个项 &#160; &#160; 包括数据的开始地址在内的与扩展分区中当前逻辑驱动器有关的信息<br />第二个项 &#160; &#160; 有关扩展分区中的下一个逻辑驱动器的信息，包括包含下一个逻辑驱动器的EBR的扇区的地址。如果不存在进一步的逻辑驱动器的话，该字段不会被使用<br />第三个项 &#160; &#160; 未用<br />第四个项 &#160; &#160; 未用</p><p> <br />&#160; &#160; 扩展分区表项中的相对扇区数字段所显示的是从扩展分区开始到逻辑驱动器中第一个扇区的位移的字节数。总扇区数字段中的数是指组成该逻辑驱动器的扇区数目。总扇区数字段的值等于从扩展分区表项所定义的引导扇区到逻辑驱动器末尾的扇区数。</p><p>&#160; &#160; 有时候在磁盘的末尾会有剩余空间，剩余空间是什么呢？我们前面说到，分区是以1柱面的容量为分区粒度的，那么如果磁盘总空间不是整数个柱面的话，不够一个 柱面的剩下的空间就是剩余空间了，这部分空间并不参与分区，所以一般无法利用。照道理说，磁盘的物理模式决定了磁盘的总容量就应该是整数个柱面的容量，为 什么会有不够一个柱面的空间呢。在我的理解看来，本来现在的磁盘为了更大的利用空间，一般在物理上并不是按照外围的扇区大于里圈的扇区这种管理方式，只是 为了与操作系统兼容而抽象出来CHS。可能其实际空间容量不一定正好为整数个柱面的容量吧。 <br /> </p><p>&#160; &#160; 四、FAT分区原理。</p><p>先来一幅结构图：  <br /><span class="postimg"><img src="https://www.iegum.com/d/file/p/2023/01-02/381b27db971f5f1300e0ad693b0ff8e5.gif" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; 现在我们着重研究FAT格式分区内数据是如何存储的。FAT分区格式是MICROSOFT最早支持的分区格式，依据FAT表中每个簇链的所占位数(有关概念，后面会讲到)分为fat12、fat16、fat32三种格式&quot;变种&quot;，但其基本存储方式是相似的。<br />&#160; &#160; 仔细研究图7中的fat16和fat32分区的组成结构。下面依次解释DBR、FAT1、FAT2、根目录、数据区、剩余扇区的概念。提到的地址如无特别提示均为分区内部偏移。</p><p>4.1 关于DBR.</p><p>&#160; &#160; DBR区(DOS BOOT RECORD)即操作系统引导记录区的意思，通常占用分区的第0扇区共512个字节(特殊情况也要占用其它保留扇区，我们先说第0扇)。在这512个字节 中，其实又是由跳转指令，厂商标志和操作系统版本号，BPB(BIOS Parameter Block)，扩展BPB，os引导程序，结束标志几部分组成。 以用的最多的FAT32为例说明分区DBR各字节的含义。见图8。<br /><span class="postimg"><img src="https://www.iegum.com/d/file/p/2023/01-02/4b544bb543a9cbf8ece1861bd8108f01.gif" alt="FluxBB bbcode 测试" /></span> </p><p>图8的对应解释见表3    </p><p>单击此处查看PDF版全文<br />    表3&#160; &#160;FAT32分区上DBR中各部分的位置划分   <br />字节位移 &#160; &#160; 字段长度 &#160; &#160; 字段名 &#160; &#160; 对应图8颜色<br />0x00 &#160; &#160; 3个字节 &#160; &#160; 跳转指令 &#160; &#160;&#160; <br />0x03 &#160; &#160; 8个字节 &#160; &#160; 厂商标志和os版本号 &#160; &#160;&#160; <br />0x0B &#160; &#160; 53个字节 &#160; &#160; BPB &#160; &#160;&#160; <br />0x40 &#160; &#160; 26个字节 &#160; &#160; 扩展BPB &#160; &#160;&#160; <br />0x5A &#160; &#160; 420个字节 &#160; &#160; 引导程序代码 &#160; &#160;&#160; <br />0x01FE &#160; &#160; 2个字节 &#160; &#160; 有效结束标志 &#160; &#160;&#160; </p><p>图9给出了winhex对图8 DBR的相关参数解释：<br /><span class="postimg"><img src="https://www.iegum.com/d/file/p/2023/01-02/a00e8c0420897224769c613ce040dc85.gif" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; 根据上边图例，我们来讨论DBR各字节的参数意义。       <br />&#160; &#160; MBR将CPU执行转移给引导扇区，因此，引导扇区的前三个字节必须是合法的可执行的基于x86的CPU指令。这通常是一条跳转指令，该指令负责跳过接下来的几个不可执行的字节(BPB和扩展BPB)，跳到操作系统引导代码部分。<br />&#160; &#160; 跳转指令之后是8字节长的OEM ID，它是一个字符串， OEM ID标识了格式化该分区的操作系统的名称和版本号。为了保留与MS-DOS的兼容性，通常Windows 2000格式化该盘是在FAT16和FAT32磁盘上的该字段中记录了“MSDOS 5.0”，在NTFS磁盘上(关于ntfs，另述)，Windows 2000记录的是“NTFS”。通常在被Windows 95格式化的磁盘上OEM ID字段出现“MSWIN4.0”，在被Windows 95 OSR2和Windows 98格式化的磁盘上OEM ID字段出现“MSWIN4.1”。<br />&#160; &#160; 接下来的从偏移0x0B开始的是一段描述能够使可执行引导代码找到相关参数的信息。通常称之为BPB(BIOS Parameter Block)，BPB一般开始于相同的位移量，因此，标准的参数都处于一个已知的位置。磁盘容量和几何结构变量都被封在BPB之中。由于引导扇区的第一部 分是一个x86跳转指令。因此，将来通过在BPB末端附加新的信息，可以对BPB进行扩展。只需要对该跳转指令作一个小的调整就可以适应BPB的变化。图 9已经列出了项目的名称和取值，为了系统的研究，针对图8，将FAT32分区格式的BPB含义和扩展BPB含义释义为表格，见表4和表5。<br />表4&#160; FAT32分区的BPB字段     <br />字节位移 &#160; &#160; 字段长度(字节) &#160; &#160; 图8对应取值 &#160; &#160; 名称和定义<br />0x0B &#160; &#160; 2 &#160; &#160; 0x0200 &#160; &#160; 扇区字节数(Bytes Per Sector) 硬件扇区的大小。本字段合法的十进制值有512、1024、2048和4096。对大多数磁盘来说，本字段的值为512<br />0x0D &#160; &#160; 1 &#160; &#160; 0x08 &#160; &#160; 每簇扇区数 (Sectors Per Cluster),一簇中的扇区数。由于FAT32文件系统只能跟踪有限个簇(最多为4 294 967 296个)，因此，通过增加每簇扇区数，可以使FAT32文件系统支持最大分区数。一个分区缺省的簇大小取决于该分区的大小。本字段的合法十进制值有1、 2、4、8、16、32、64和128。Windows 2000的FAT32实现只能创建最大为32GB的分区。但是，Windows 2000能够访问由其他操作系统(Windows 95、OSR2及其以后的版本)所创建的更大的分区<br />0x0e &#160; &#160; 2 &#160; &#160; 0x0020 &#160; &#160; 保留扇区数(Reserved Sector) 第一个FAT开始之前的扇区数，包括引导扇区。本字段的十进制值一般为32<br />0x10 &#160; &#160; 1 &#160; &#160; 0x02 &#160; &#160; FAT数(Number of FAT) 该分区上FAT的副本数。本字段的值一般为2<br />0x11 &#160; &#160; 2 &#160; &#160; 0x0000 &#160; &#160; 根目录项数(Root Entries)只有FAT12/FAT16使用此字段。对FAT32分区而言,本字段必须设置为 0<br />0x13 &#160; &#160; 2 &#160; &#160; 0x0000 &#160; &#160; 小扇区数(Small Sector)(只有FAT12/FAT16使用此字段)对FAT32分区而言，本字段必须设置为0<br />0x15 &#160; &#160; 1 &#160; &#160; 0xF8 &#160; &#160; 媒体描述符( Media Descriptor)提供有关媒体被使用的信息。值0xF8表示硬盘，0xF0表示高密度的3.5寸软盘。媒体描述符要用于MS-DOS FAT16磁盘，在Windows 2000中未被使用<br />0x16 &#160; &#160; 2 &#160; &#160; 0x0000 &#160; &#160; 每FAT扇区数(Sectors Per FAT)只被FAT12/FAT16所使用,对FAT32分区而言，本字段必须设置为0<br />0x18 &#160; &#160; 2 &#160; &#160; 0x003F &#160; &#160; 每道扇区数(Sectors Per Track) 包含使用INT13h的磁盘的“每道扇区数”几何结构值。该分区被多个磁头的柱面分成了多个磁道<br />0x1A &#160; &#160; 2 &#160; &#160; 0x00FF &#160; &#160; 磁头数(Number of Head) 本字段包含使用INT 13h的磁盘的“磁头数”几何结构值。例如，在一张1.44MB 3.5英寸的软盘上，本字段的值为 2<br />0x1C &#160; &#160; 4 &#160; &#160; 0x0000003F &#160; &#160; 隐藏扇区数(Hidden Sector) 该分区上引导扇区之前的扇区数。在引导序列计算到根目录的数据区的绝对位移的过程中使用了该值。本字段一般只对那些在中断13h上可见的媒体有意义。在没有分区的媒体上它必须总是为0<br />0x20 &#160; &#160; 4 &#160; &#160; 0x007D043F &#160; &#160; 总扇区数(Large Sector) 本字段包含FAT32分区中总的扇区数<br />0x24 &#160; &#160; 4 &#160; &#160; 0x00001F32 &#160; &#160; 每FAT扇区数(Sectors Per FAT)(只被FAT32使用)该分区每个FAT所占的扇区数。计算机利用这个数和 FAT数以及隐藏扇区数(本表中所描述的)来决定根目录从哪里开始。该计算机还可以从目录中的项数决定该分区的用户数据区从哪里开始<br />0x28 &#160; &#160; 2 &#160; &#160; 0x00 &#160; &#160; </p><p>扩展标志(Extended Flag)(只被FAT32使用)该两个字节结构中各位的值为：<br />位0-3：活动 FAT数(从0开始计数，而不是1).<br />&#160; &#160; &#160; &#160;只有在不使用镜像时才有效<br />位4-6：保留<br />位7：0值意味着在运行时FAT被映射到所有的FAT<br />&#160; &#160; &#160;1值表示只有一个FAT是活动的<br />位8-15：保留<br />0x2A &#160; &#160; 2 &#160; &#160; 0x0000 &#160; &#160; 文件系统版本(File ystem Version)只供FAT32使用,高字节是主要的修订号，而低字节是次要的修订号。本字段支持将来对该FAT32媒体类型进行扩展。如果本字段非零，以前的Windows版本将不支持这样的分区<br />0x2C &#160; &#160; 4 &#160; &#160; 0x00000002 &#160; &#160; 根目录簇号(Root Cluster Number)(只供FAT32使用) 根目录第一簇的簇号。本字段的值一般为2，但不总是如此<br />0x30 &#160; &#160; 2 &#160; &#160; 0x0001 &#160; &#160; 文件系统信息扇区号 (File System Information SectorNumber)(只供FAT32使用) FAT32分区的保留区中的文件系统信息(File System Information, FSINFO)结构的扇区号。其值一般为1。在备份引导扇区(Backup Boot Sector)中保留了该FSINFO结构的一个副本，但是这个副本不保持更新<br />0x34 &#160; &#160; 2 &#160; &#160; 0x0006 &#160; &#160; 备份引导扇区(只供FAT32使用) 为一个非零值，这个非零值表示该分区保存引导扇区的副本的保留区中的扇区号。本字段的值一般为6，建议不要使用其他值<br />0x36 &#160; &#160; 12 &#160; &#160; 12个字节均为0x00 &#160; &#160; 保留(只供FAT32使用)供以后扩充使用的保留空间。本字段的值总为0</p><p>表5&#160; &#160;FAT32分区的扩展BPB字段           <br />字节位移 &#160; &#160; 字段长度(字节) &#160; &#160; 图8对应取值 &#160; &#160; 字段名称和定义<br />0x40 &#160; &#160; 1 &#160; &#160; 0x80 &#160; &#160; 物理驱动器号( Physical Drive Number) 与BIOS物理驱动器号有关。软盘驱动器被标识为0x00，物理硬盘被标识为0x80，而与物理磁盘驱动器无关。一般地，在发出一个INT13h BIOS调用之前设置该值，具体指定所访问的设备。只有当该设备是一个引导设备时，这个值才有意义<br />0x41 &#160; &#160; 1 &#160; &#160; 0x00 &#160; &#160; 保留(Reserved) FAT32分区总是将本字段的值设置为0<br />0x42 &#160; &#160; 1 &#160; &#160; 0x29 &#160; &#160; 扩展引导标签(Extended Boot Signature) 本字段必须要有能被Windows 2000所识别的值0x28或0x29<br />0x43 &#160; &#160; 4 &#160; &#160; 0x33391CFE &#160; &#160; 分区序号(Volume Serial Number) 在格式化磁盘时所产生的一个随机序号，它有助于区分磁盘<br />0x47 &#160; &#160; 11 &#160; &#160; &quot;NO NAME&quot; &#160; &#160; 卷标(Volume Label) 本字段只能使用一次，它被用来保存卷标号。现在，卷标被作为一个特殊文件保存在根目录中<br />0x52 &#160; &#160; 8 &#160; &#160; &quot;FAT32&quot; &#160; &#160; 系统ID(System ID) FAT32文件系统中一般取为&quot;FAT32&quot;</p><p>&#160; &#160; &#160;DBR的偏移0x5A开始的数据为操作系统引导代码。这是由偏移0x00开始的跳转指令所指向的。在图8所列出的偏移0x00~0x02的跳转指令&quot;EB 58 90&quot;清楚地指明了OS引导代码的偏移位置。jump 58H加上跳转指令所需的位移量，即开始于0x5A。此段指令在不同的操作系统上和不同的引导方式上，其内容也是不同的。大多数的资料上都说win98, 构建于fat基本分区上的win2000,winxp所使用的DBR只占用基本分区的第0扇区。他们提到，对于fat32，一般的32个基本分区保留扇区 只有第0扇区是有用的。实际上，以FAT32构建的操作系统如果是win98,系统会使用基本分区的第0扇区和第2扇区存储os引导代码；以FAT32构 建的操作系统如果是win2000或winxp,系统会使用基本分区的第0扇区和第0xC扇区(win2000或winxp,其第0xC的位置由第0扇区 的0xAB偏移指出)存储os引导代码。所以，在fat32分区格式上，如果DBR一扇区的内容正确而缺少第2扇区(win98系统)或第0xC扇区 (win2000或winxp系统)，系统也是无法启动的。如果自己手动设置NTLDR双系统，必须知道这一点。<br />&#160; &#160; &#160;DBR扇区的最后两个字节一般存储值为0x55AA的DBR有效标志，对于其他的取值，系统将不会执行DBR相关指令。上面提到的其他几个参与os引导的扇区也需以0x55AA为合法结束标志。</p><p>FAT16 DBR：<br />&#160; &#160; &#160;FAT32中DBR的含义大致如此，对于FAT12和FAT16其基本意义类似，只是相关偏移量和参数意义有小的差异，FAT格式的区别和来因，以后会说 到，此处不在多说FAT12与FAT16。我将FAT16的扇区参数意义列表。感兴趣的朋友自己研究一下，和FAT32大同小异的。</p><p>表6&#160; 一个FAT16分区上的引导扇区段<br />字节位移 &#160; &#160; 字段长度(字节) &#160; &#160; 字段名称<br />0x00 &#160; &#160; 3 &#160; &#160; 跳转指令(Jump Instruction)<br />0x03 &#160; &#160; 8 &#160; &#160; OEM ID<br />0x0B &#160; &#160; 25 &#160; &#160; BPB<br />0x24 &#160; &#160; 26 &#160; &#160; 扩展BPB<br />0x3E &#160; &#160; 448 &#160; &#160; 引导程序代码(Bootstrap Code)<br />0x01FE &#160; &#160; 4 &#160; &#160; 扇区结束标识符(0x55AA)</p><p>表7&#160; FAT16分区的BPB字段     <br />字节位移 &#160; &#160; 字段长度(字节) &#160; &#160; 例值 &#160; &#160; 名称和定义<br />0x0B &#160; &#160; 2 &#160; &#160; 0x0200 &#160; &#160; 扇区字节数(Bytes Per Sector) 硬件扇区的大小。本字段合法的十进制值有512、1024、2048和4096。对大多数磁盘来说，本字段的值为512<br />0x0D &#160; &#160; 1 &#160; &#160; 0x40 &#160; &#160; 每簇扇区数 (Sectors Per Cluster) 一个簇中的扇区数。由于FAT16文件系统只能跟踪有限个簇(最多为65536个)。因此，通过增加每簇的扇区数可以支持最大分区数。分区的缺省的簇的大 小取决于该 分区的大小。本字段合法的十进制值有 1、2、4、8、16、32、64和128。导致簇大于32KB(每扇区字节数*每簇扇区数)的值会引起磁盘错误和软件错误<br />0x0e &#160; &#160; 2 &#160; &#160; 0x0001 &#160; &#160; 保留扇区数(Reserved Sector) 第一个FAT开始之前的扇区数，包括引导扇区。本字段的十进制值一般为1<br />0x10 &#160; &#160; 1 &#160; &#160; 0x02 &#160; &#160; FAT数(Number of FAT)该分区上FAT的副本数。本字段的值一般为2<br />0x11 &#160; &#160; 2 &#160; &#160; 0x0200 &#160; &#160; 根目录项数 (Root Entries) 能够保存在该分区的根目录文件夹中的32个字节长的文件和文件夹名称项的总数。在一个典型的硬盘上，本字段的值为512。其中一个项常常被用作卷标号 (Volume Label)，长名称的文件和文件夹每个文件使用多个项。文件和文件夹项的最大数一般为511，但是如果使用的长文件名，往往都达不到这个数<br />0x13 &#160; &#160; 2 &#160; &#160; 0x0000 &#160; &#160; 小扇区数(Small Sector) 该分区上的扇区数，表示为16位(&lt;65536)。对大于65536个扇区的分区来说，本字段的值为0，而使用大扇区数来取代它<br />0x15 &#160; &#160; 1 &#160; &#160; 0xF8 &#160; &#160; 媒体描述符( Media Descriptor)提供有关媒体被使用的信息。值0xF8表示硬盘，0xF0表示高密度的3.5寸软盘。媒体描述符要用于MS-DOS FAT16磁盘，在Windows 2000中未被使用<br />0x16 &#160; &#160; 2 &#160; &#160; 0x00FC &#160; &#160; 每FAT扇区数(Sectors Per FAT) 该分区上每个FAT所占用的扇区数。计算机利用这个数和FAT数以及隐藏扇区数来决定根目录在哪里开始。计算机还可以根据根目录中的项数(512)决定该 分区的用户数据区从哪里开始<br />0x18 &#160; &#160; 2 &#160; &#160; 0x003F &#160; &#160; 每道扇区数(Sectors Per Trark)<br />0x1A &#160; &#160; 2 &#160; &#160; 0x0040 &#160; &#160; 磁头数(Number of head)<br />0x1C &#160; &#160; 4 &#160; &#160; 0x0000003F &#160; &#160; 隐藏扇区数(Hidden Sector) 该分区上引导扇区之前的扇区数。在引导序列计算到根目录和数据区的绝对位移的过程中使用了该值<br />0x20 &#160; &#160; 4 &#160; &#160; 0x003EF001 &#160; &#160; 大扇区数(Large Sector) 如果小扇区数字段的值为0，本字段就包含该FAT16分区中的总扇区数。如果小扇区数字段的值不为0，那么本字段的值为0</p><p>表8&#160; &#160;FAT16分区的扩展BPB字段           <br />字节位移 &#160; &#160; 字段长度(字节) &#160; &#160; 图8对应取值 &#160; &#160; 字段名称和定义<br />0x24 &#160; &#160; 1 &#160; &#160; 0x80 &#160; &#160; 物理驱动器号( Physical Drive Number) 与BIOS物理驱动器号有关。软盘驱动器被标识为0x00，物理硬盘被标识为0x80，而与物理磁盘驱动器无关。一般地，在发出一个INT13h BIOS调用之前设置该值，具体指定所访问的设备。只有当该设备是一个引导设备时，这个值才有意义<br />0x25 &#160; &#160; 1 &#160; &#160; 0x00 &#160; &#160; 保留(Reserved) FAT16分区一般将本字段的值设置为0<br />0x26 &#160; &#160; 1 &#160; &#160; 0x29 &#160; &#160; 扩展引导标签(Extended Boot Signature) 本字段必须要有能被Windows 2000所识别的值0x28或0x29<br />0x27 &#160; &#160; 2 &#160; &#160; 0x52368BA8 &#160; &#160; 卷序号(Volume Serial Number) 在格式化磁盘时所产生的一个随机序号，它有助于区分磁盘<br />0x2B &#160; &#160; 11 &#160; &#160; &quot;NO NAME&quot; &#160; &#160; 卷标(Volume Label) 本字段只能使用一次，它被用来保存卷标号。现在，卷标被作为一个特殊文件保存在根目录中<br />0x36 &#160; &#160; 8 &#160; &#160; &quot;FAT16&quot; &#160; &#160; 文件系统类型(File System Type) 根据该磁盘格式，该字段的值可以为FAT、FAT12或FAT16</p><p>4.2&#160; 关于保留扇区</p><p>&#160; &#160; &#160;在上述FAT文件系统DBR的偏移0x0E处，用2个字节存储保留扇区的数目。所谓保留扇区(有时候会叫系统扇区，隐藏扇区)，是指从分区DBR扇区开始 的仅为系统所有的扇区，包括DBR扇区。在FAT16文件系统中，保留扇区的数据通常设置为1，即仅仅DBR扇区。而在FAT32中，保留扇区的数据通常 取为32，有时候用Partition Magic分过的FAT32分区会设置36个保留扇区，有的工具可能会设置63个保留扇区。<br />&#160; &#160; &#160;FAT32中的保留扇区除了磁盘总第0扇区用作DBR，总第2扇区(win98系统)或总第0xC扇区(win2000,winxp)用作OS引导代码扩 展部分外，其余扇区都不参与操作系统管理与磁盘数据管理，通常情况下是没作用的。操作系统之所以在FAT32中设置保留扇区，是为了对DBR作备份或留待 以后升级时用。FAT32中，DBR偏移0x34占2字节的数据指明了DBR备份扇区所在，一般为0x06，即第6扇区。当FAT32分区DBR扇区被破 坏导致分区无法访问时。可以用第6扇区的原备份替换第0扇区来找回数据。</p><p>4.3&#160; FAT表和数据的存储原则。</p><p>&#160; &#160; &#160; &#160;FAT表(File Allocation Table 文件分配表)，是Microsoft在FAT文件系统中用于磁盘数据(文件)索引和定位引进的一种链式结构。假如把磁盘比作一本书，FAT表可以认为相当 于书中的目录，而文件就是各个章节的内容。但FAT表的表示方法却与目录有很大的不同。<br />&#160; &#160; &#160; 在FAT文件系统中，文件的存储依照FAT表制定的簇链式数据结构来进行。同时，FAT文件系统将组织数据时使用的目录也抽象为文件，以简化对数据的管理。</p><p> ★存储过程假想：<br />&#160; &#160; &#160; 我们模拟对一个分区存储数据的过程来说明FAT文件系统中数据的存储原则。<br />&#160; &#160; &#160; 假定现在有一个空的完全没有存放数据的磁盘，大小为100KB，我们将其想象为线形的空间地址。为了存储管理上的便利，我们人为的将这100KB的空间均 分成100份，每份1KB。我们来依次存储这样几个文件：A.TXT(大小10KB),B.TXT(大小53.6KB)，C.TXT(大小 20.5KB)。<br />&#160; &#160; &#160; 最起码能够想到，我们可以顺序的在这100KB空间中存放这3个文件。同时不要忘了，我们还要记下他们的大小和开始的位置，这样下次要用时才能找的到，这 就像是目录。为了便于查找，我们假定用第1K的空间来存储他们的特征(属性)。还有，我们设计的存储单位是1KB，所以，A.TXT我们需要10个存储单 位(为了说明方便，我们把存储单位叫做“簇”吧。也能少打点字，呵呵。)，B.TXT需要54个簇，C.TXT需要21个簇。可能有人会说B.TXT和 C.TXT不是各自浪费了不到1簇的空间吗？干嘛不让他们紧挨着，不是省地方吗？我的回答是，如果按照这样的方式存储，目录中原本只需要记下簇号，现在还 需要记下簇内的偏移，这样会增加目录的存储量，而且存取没有了规则，读取也不太方便，是得不偿失的。<br />&#160; &#160; 根据上面所说的思想，我们设计了这样的图4.3.1所示的存储方式。<br /><span class="postimg"><img src="https://www.iegum.com/d/file/p/2023/01-02/dc6790a70f904cdcc913b72c1265267b.gif" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; &#160; 我们再考虑如何来写这三个文件的目录。对于每个文件而言，一定要记录的有：文件名，开始簇，大小，创建日期、时间，修改日期、时间，文件的读写属性等。这 里大小能不能用结束簇来计算呢？一定不能，因为文件的大小不一定就是整数个簇的大小，否则的话像B.TXT的内容就是54KB的内容了，少了固然不行，可 多了也是不行的。那么我们怎么记录呢？可以想象一下。为了管理上的方便，我们用数据库的管理方式来管理我们的目录。于是我把1KB再分成10份，假定开始 簇号为0，定义每份100B的各个位置的代表含义如图4.3.2<br /><span class="postimg"><img src="https://www.iegum.com/d/file/p/2023/01-02/e1cfdaacf63eb87d458ab0ee46719213.gif" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; &#160; 这样设计的结构绝对可以对文件进行正确的读写了。接着让我们设计的文件系统工作吧。先改动个文件，比如A.TXT，增加点内容吧！咦？增加后往哪里放呀， 虽然存储块的后面有很多空间，但紧随其后B.TXT的数据还顶着呢？要是把A.TXT移到后边太浪费处理资源，而且也不一定解决问题。这个问题看来暂时解 决不了。<br />&#160; &#160; 那我们换个操作，把B.txt删了，b.txt的空间随之释放。这时候空间如图4.3.3，目录如图4.3.4<br /><span class="postimg"><img src="https://www.iegum.com/d/file/p/2023/01-02/d25216115e34fcfafa9f358105ec3b27.gif" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; &#160; 这个操作看来还可以，我们接着做，在存入一个文件D.txt(大小为60.3KB),总共100簇的空间只用了31簇，还有68簇剩余，按说能放下。可是？往那里放呢？没有61个连续的空间了，目录行没办法写了，看来无连续块存储暂时也不行。<br />&#160; &#160; 你一定能够想到我们可以在连续空间不够或增加文件长度的时候转移影响我们操作的其他文件，从而腾出空间来，但我要问你，那不是成天啥也不要干了，就是倒腾东西了吗？</p><p>&#160; &#160; 看来我们设计的文件系统有致命的漏洞，怎么解决呢？。。。。<br />。。。。。。</p><p>&#160; &#160; 其实可以这样解决：<br />&#160; &#160; 首先我们允许文件的不连续存储。目录中依然只记录开始簇和文件的大小。那么我们怎么记录文件占用那些簇呢，以文件映射簇不太方便，因为文件名是不固定的。 我们换个思想，可以用簇来映射文件，在整个存储空间的前部留下几簇来记录数据区中数据与簇号的关系。对于上例因为总空间也不大，所以用前部的1Kb的空间 来记录这种对应，假设3个文件都存储，空间分配如图4.3.5，同时修改一下目录，如图4.3.6<br /><span class="postimg"><img src="https://www.iegum.com/d/file/p/2023/01-02/d56823dca5da53bb105c5c31f5f12486.gif" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; &#160; 第一簇用来记录数据区中每一簇的被占用情况，暂时称其为文件分配表。结合文件分配表和文件目录就可以达到完全的文件读取了。我们想到，把文件分配表做成一个数据表，以图4.3.7的形式记录簇与数据的对应。<br />&#160; &#160; 用图4.3.7的组织方式是完全可以实现对文件占有簇的记录的。但还不够效率。比如文件名在文件分配表中记录太多，浪费空间，而实际上在目录中已经记录了文件的开始簇了。所以可以改良一下，用链的方式来存放占有簇的关系，变成图4.3.8的组织方式。<br /><span class="postimg"><img src="https://www.iegum.com/d/file/p/2023/01-02/b193889bc126c7f1ff3b6b0b9ebee56f.gif" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; &#160; 参照图4.3.8来理解一下文件分配表的意义。如文件a.txt我们根据目录项中指定的a.txt的首簇为2，然后找到文件分配表的第2簇记录，上面登记 的是3，我们就能确定下一簇是3。找到文件分配表的第3簇记录，上面登记的是4，我们就能确定下一簇是4......直到指到第11簇，发现下一个指向是 FF，就是结束。文件便丝毫无误读取完毕。</p><p>&#160; &#160; 我们再看上面提到的第三种情况，就是将b.txt删除以后，存入一个大小为60.3KB的d.txt。利用簇链可以很容易的实现。实现后的磁盘如图4.3.9&#160; 4.3.10&#160; 4.3.11<br /><span class="postimg"><img src="https://www.iegum.com/d/file/p/2023/01-02/497b8addddbecd651271ca4263bdd5d2.gif" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; &#160;上面是我们对文件存储的一种假设，也该揭开谜底的时候了。上面的思想其实就是fat文件系统的思想的精髓(但并不是，尤其像具体的参数的意义与我们所举的例子是完全不同的。请忘掉上边细节，努力记忆下边)。</p><p>★FAT16存储原理:   </p><p>&#160; &#160; 当把一部分磁盘空间格式化为fat文件系统时，fat文件系统就将这个分区当成整块可分配的区域进行规划，以便于数据的存储。一般来讲，其划分形式如图7所示。我们把FAT16部分提取出来，详细描述一下：<br />&#160; &#160; FAT16是Microsoft较早推出的文件系统，具有高度兼容性，目前仍然广泛应用于个人电脑尤其是移动存储设备中，FAT16简单来讲由图 4.3.11所示的6部分组成(主要是前5部分)。引导扇区(DBR)我们已经说过,FAT16在DBR之后没有留有任何保留扇区，其后紧随的便是FAT 表。FAT表是FAT16用来记录磁盘数据区簇链结构的。像前面我们说过的例子一样，FAT将磁盘空间按一定数目的扇区为单位进行划分，这样的单位称为 簇。通常情况下，每扇区512字节的原则是不变的。簇的大小一般是2n (n为整数)个扇区的大小，像 512B,1K,2K,4K,8K,16K,32K，64K。实际中通常不超过32K。 之所以簇为单位而不以扇区为单位进行磁盘的分配，是因为当分区容量较大时，采用大小为512b的扇区管理会增加fat表的项数，对大文件存取增加消耗，文 件系统效率不高。分区的大小和簇的取值是有关系的，见表9</p><p>图4.3.11 Fat16的组织形式<br />引导扇区 &#160; &#160; FAT1 &#160; &#160; FAT2(重复的) &#160; &#160; 根文件夹 &#160; &#160; 其他文件夹及所有文件 &#160; &#160; 剩余扇区<br />1扇区 &#160; &#160; 实际情况取大小 &#160; &#160; 同FAT1 &#160; &#160; 32个扇区 &#160; &#160; 开始簇编号(从2开始) &#160; &#160; 不足一簇</p><p>表9&#160; FAT16分区大小与对因簇大小<br />分区空间大小 &#160; &#160; 每个簇的扇区 &#160; &#160; 簇空间大小<br />0MB-32MB &#160; &#160; 1 &#160; &#160; 512个字节<br />33MB-64MB &#160; &#160; 2 &#160; &#160; 1k<br />65MB-128MB &#160; &#160; 4 &#160; &#160; 2k<br />129MB-225MB &#160; &#160; 8 &#160; &#160; 4k<br />256MB-511MB &#160; &#160; 16 &#160; &#160; 8k<br />512MB-1023MB &#160; &#160; 32 &#160; &#160; 16k<br />1024MB-2047MB &#160; &#160; 64 &#160; &#160; 32k<br />2048MB-4095MB &#160; &#160; 128 &#160; &#160; 64k</p><p>&#160; &#160; 注意：少于32680个扇区的分区中，簇空间大小可最多达到每个簇8个扇区。不管用户是使用磁盘管理器来格式化分区，还是使用命令提示行键入format 命令格式化，格式化程序都创建一个12位的FAT。少于16MB的分区，系统通常会将其格式化成12位的FAT，FAT12是FAT的初始实现形式，是针 对小型介质的。FAT12文件分配表要比FAT16和FAT32的文件分配表小，因为它对每个条目使用的空间较少。这就给数据留下较多的空间。所有用 FAT12格式化的5.25英寸软盘以及1.44MB的3.5英寸软盘都是由FAT12格式化的。除了FAT表中记录每簇链结的二进制位数与FAT16不 同外，其余原理与FAT16均相同，不再单独解释。。。</p><p>&#160; &#160; 格式化FAT16分区时，格式化程序根据分区的大小确定簇的大小，然后根据保留扇区的数目、根目录的扇区数目、数据区可分的簇数与FAT表本身所占空间 来确定FAT表所需的扇区数目，然后将计算后的结果写入DBR的相关位置。<br />&#160; &#160; FAT16 DBR参数的偏移0x11处记录了根目录所占扇区的数目。偏移0x16记录了FAT表所占扇区的数据。偏移0x10记录了FAT表的副本数目。系统在得到这几项参数以后，就可以确定数据区的开始扇区偏移了。<br />&#160; &#160; FAT16文件系统从根目录所占的32个扇区之后的第一个扇区开始以簇为单位进行数据的处理，这之前仍以扇区为单位。对于根目录之后的第一个簇，系统并不 编号为第0簇或第1簇 (可能是留作关键字的原因吧)，而是编号为第2簇，也就是说数据区顺序上的第1个簇也是编号上的第2簇。<br />&#160; &#160; FAT文件系统之所以有12，16，32不同的版本之分，其根本在于FAT表用来记录任意一簇链接的二进制位数。以FAT16为例，每一簇在FAT表中占 据2字节(二进制16位)。所以，FAT16最大可以表示的簇号为0xFFFF(十进制的65535)，以32K为簇的大小的话，FAT32可以管理的最 大磁盘空间为：32KB×65535=2048MB,这就是为什么FAT16不支持超过2GB分区的原因。<br />&#160; &#160; FAT表实际上是一个数据表，以2个字节为单位，我们暂将这个单位称为FAT记录项，通常情况其第1、2个记录项(前4个字节)用作介质描述。从第三个记录项开始记录除根目录外的其他文件及文件夹的簇链情况。根据簇的表现情况FAT用相应的取值来描述，见表10</p><p> <br />表10 FAT16记录项的取值含义(16进制)<br />FAT16记录项的取值 &#160; &#160; 对应簇的表现情况<br />0000 &#160; &#160; 未分配的簇<br />0002~FFEF &#160; &#160; 已分配的簇<br />FFF0~FFF6 &#160; &#160; 系统保留<br />FFF7 &#160; &#160; 坏簇<br />FFF8~FFFF &#160; &#160; 文件结束簇</p><p>&#160; &#160; &#160;看一幅在winhex所截FAT16的文件分配表，图10：<br /><span class="postimg"><img src="https://www.iegum.com/d/file/p/2023/01-02/d27a8644f789cd20bfb1b9ff9992be36.gif" alt="FluxBB bbcode 测试" /></span> <br />&#160; &#160;如图，FAT表以&quot;F8 FF FF FF&quot; 开头，此2字节为介质描述单元，并不参与FAT表簇链关系。小红字标出的是FAT扇区每2字节对应的簇号。<br />&#160; &#160;相对偏移0x4~0x5偏移为第2簇(顺序上第1簇)，此处为FF,表示存储在第2簇上的文件(目录)是个小文件，只占用1个簇便结束了。<br />&#160; &#160;第3簇中存放的数据是0x0005，这是一个文件或文件夹的首簇。其内容为第5簇，就是说接下来的簇位于第5簇——〉 FAT表指引我们到达FAT表的第5簇指向，上面写的数据是&quot;FF FF&quot;,意即此文件已至尾簇。<br />&#160; &#160;第4簇中存放的数据是0x0006，这又是一个文件或文件夹的首簇。其内容为第6簇，就是说接下来的簇位于第6簇——〉FAT表指引我们到达FAT表的第 6簇指向，上面写的数据是0x0007，就是说接下来的簇位于第7簇——〉FAT表指引我们到达FAT表的第7簇指向……直到根据FAT链读取到扇区相对 偏移0x1A~0x1B，也就是第13簇，上面写的数据是0x000E，也就是指向第14簇——〉14簇的内容为&quot;FF FF&quot;，意即此文件已至尾簇。<br />&#160; &#160; 后面的FAT表数据与上面的道理相同。不再分析。</p><p>&#160; &#160; FAT表记录了磁盘数据文件的存储链表，对于数据的读取而言是极其重要的，以至于Microsoft为其开发的FAT文件系统中的FAT表创建了一份备 份，就是我们看到的FAT2。FAT2与FAT1的内容通常是即时同步的，也就是说如果通过正常的系统读写对FAT1做了更改，那么FAT2也同样被更 新。如果从这个角度来看，系统的这个功能在数据恢复时是个天灾。</p><p>&#160; &#160; FAT文件系统的目录结构其实是一颗有向的从根到叶的树，这里提到的有向是指对于FAT分区内的任一文件(包括文件夹)，均需从根目录寻址来找到。可以这样认为：目录存储结构的入口就是根目录。<br />&#160; &#160; FAT文件系统根据根目录来寻址其他文件(包括文件夹)，故而根目录的位置必须在磁盘存取数据之前得以确定。FAT文件系统就是根据分区的相关DBR参数 与DBR中存放的已经计算好的FAT表(2份)的大小来确定的。格式化以后，跟目录的大小和位置其实都已经确定下来了：位置紧随FAT2之后，大小通常为 32个扇区。根目录之后便是数据区第2簇。<br />&#160; &#160; FAT文件系统的一个重要思想是把目录(文件夹)当作一个特殊的文件来处理，FAT32甚至将根目录当作文件处理(旁：NTFS将分区参数、安全权限等好 多东西抽象为文件更是这个思想的升华)，在FAT16中，虽然根目录地位并不等同于普通的文件或者说是目录，但其组织形式和普通的目录(文件夹)并没有不 同。FAT分区中所有的文件夹(目录)文件，实际上可以看作是一个存放其他文件(文件夹)入口参数的数据表。所以目录的占用空间的大小并不等同于其下所有 数据的大小，但也不等同于0。通常是占很小的空间的，可以看作目录文件是一个简单的二维表文件。其具体存储原理是：<br />&#160; &#160; 不管目录文件所占空间为多少簇，一簇为多少字节。系统都会以32个字节为单位进行目录文件所占簇的分配。这32个字节以确定的偏移来定义本目录下的一个文件(或文件夹)的属性，实际上是一个简单的二维表。<br />&#160; &#160; 这32个字节的各字节偏移定义如表11：</p><p> <br />表11&#160; &#160;FAT16目录项32个字节的表示定义<br />字节偏移(16进制) &#160; &#160; 字节数 &#160; &#160; 定义<br />0x0~0x7 &#160; &#160; 8 &#160; &#160; 文件名<br />0x8~0xA &#160; &#160; 3 &#160; &#160; 扩展名<br />0xB &#160; &#160; 1 &#160; &#160; 属性字节 &#160; &#160; 00000000(读写)<br />00000001(只读)<br />00000010(隐藏)<br />00000100(系统)<br />00001000(卷标)<br />&#160; 00010000(子目录)<br />00100000(归档)<br />0xC~0x15 &#160; &#160; 10 &#160; &#160; 系统保留<br />0x16~0x17 &#160; &#160; 2 &#160; &#160; 文件的最近修改时间<br />0x18~0x19 &#160; &#160; 2 &#160; &#160; 文件的最近修改日期<br />0x1A~0x1B &#160; &#160; 2 &#160; &#160; 表示文件的首簇号<br />0x1C~0x1F &#160; &#160; 4 &#160; &#160; 表示文件的长度</p><p>    对图10中的一些取值进行说明：<br />    (1)、对于短文件名，系统将文件名分成两部分进行存储，即主文件名+扩展名。 0x0~0x7字节记录文件的主文件名，0x8~0xA记录文件的扩展名，取文件名中的ASCII码值。不记录主文件名与扩展名之间的&quot;.&quot;&#160; 主文件名不足8个字符以空白符(20H)填充，扩展名不足3个字符同样以空白符(20H)填充。0x0偏移处的取值若为00H，表明目录项为空；若为 E5H，表明目录项曾被使用，但对应的文件或文件夹已被删除。(这也是误删除后恢复的理论依据)。文件名中的第一个字符若为“.”或“..”表示这个簇记 录的是一个子目录的目录项。“.”代表当前目录；“..”代表上级目录(和我们在dos或windows中的使用意思是一样的，如果磁盘数据被破坏，就可 以通过这两个目录项的具体参数推算磁盘的数据区的起始位置，猜测簇的大小等等，故而是比较重要的)<br />    (2)、0xB的属性字段：可以看作系统将0xB的一个字节分成8位，用其中的一位代表某种属性的有或无。这样，一个字节中的8位每位取不同的值就能反映各个属性的不同取值了。如00000101就表示这是个文件，属性是只读、系统。<br />    (3)、0xC~0x15在原FAT16的定义中是保留未用的。在高版本的WINDOWS系统中有时也用它来记录修改时间和最近访问时间。那样其字段的意义和FAT32的定义是相同的，见后边FAT32。<br />    (4)、0x16~0x17中的时间=小时*2048+分钟*32+秒/2。得出的结果换算成16进制填入即可。也就是：0x16字节的0~4位是以2秒为单位的量值；0x16字节的5~7位和0x17字节的0~2位是分钟；0x17字节的3~7位是小时。<br />&#160; &#160; (5)、0x18~0x19中的日期=(年份-1980)*512+月份*32+日。得出的结果换算成16进制填入即可。也就是：0x18字节0~4位是日期数；0x18字节5~7位和0x19字节0位是月份；0x19字节的1~7位为年号，原定义中0~119分别代表1980~2099，目前高版本的Windows允许取0~127，即年号最大可以到2107年。<br />&#160; &#160; (6)、0x1A~0x1B存放文件或目录的表示文件的首簇号，系统根据掌握的首簇号在FAT表中找到入口，然后再跟踪簇链直至簇尾，同时用0x1C~0x1F处字节判定有效性。就可以完全无误的读取文件(目录)了。<br />&#160; &#160; (7)、普通子目录的寻址过程也是通过其父目录中的目录项来指定的，与数据文件(指非目录文件)不同的是目录项偏移0xB的第4位置1，而数据文件为0。</p><p>    对于整个FAT分区而言，簇的分配并不完全总是分配干净的。如一个数据区为99个扇区的FAT系 统，如果簇的大小设定为2扇区，就会有1个扇区无法分配给任何一个簇。这就是分区的剩余扇区，位于分区的末尾。有的系统用最后一个剩余扇区备份本分区的 DBR，这也是一种好的备份方法。<br />    早的FAT16系统并没有长文件名一说，Windows操作系统已经完全支持在FAT16上的长文件名了。FAT16的长文件名与FAT32长文件名的定义是相同的，关于长文件名，在FAT32部分再详细作解释。</p><p>★FAT32存储原理：<br />&#160; &#160; FAT32是个非常有功劳的文件系统，Microsoft成功地设计并运用了它，直到今天NTFS铺天盖地袭来的时候，FAT32依然占据着 Microsoft Windows文件系统中重要的地位。FAT32最早是出于FAT16不支持大分区、单位簇容量大以致空间急剧浪费等缺点设计的。实际应用中，FAT32 还是成功的。<br />    FAT32与FAT16的原理基本上是相同的，图4.3.12标出了FAT32分区的基本构成。</p><p>图4.3.12 Fat32的组织形式<br />引导扇区 &#160; &#160; 其余保留扇区 &#160; &#160; FAT1 &#160; &#160; FAT2(重复的) &#160; &#160; 根文件夹首簇 &#160; &#160; 其他文件夹及所有文件 &#160; &#160; 剩余扇区<br />1扇区 &#160; &#160; 31个扇区 &#160; &#160; 实际情况取大小 &#160; &#160; 同FAT1 &#160; &#160; 第2簇 &#160; &#160;&#160; &#160;&#160; &#160; 不足一簇<br />保留扇区 &#160; &#160;&#160; &#160;&#160; &#160;&#160; &#160;&#160; &#160; ┗━━━━━━━━数据区━━━━━━━━┛</p><p>&#160; &#160; FAT32在格式化的过程中就根据分区的特点构建好了它的DBR，其中BPB参数是很重要的，可以回过头来看一下表4和表5。首先FAT32保留扇区的数 目默认为32个，而不是FAT16的仅仅一个。这样的好处是有助于磁盘DBR指令的长度扩展，而且可以为DBR扇区留有备份空间。上面我们已经提到，构建 在FAT32上的win98或win2000、winXP，其操作系统引导代码并非只占一个扇区了。留有多余的保留扇区就可以很好的拓展OS引导代码。在 BPB中也记录了DBR扇区的备份扇区编号。备份扇区可以让我们在磁盘遭到意外破坏时恢复DBR。<br />&#160; &#160; FAT32的文件分配表的数据结构依然和FAT16相同，所不同的是，FAT32将记录簇链的二进制位数扩展到了32位，故而这种文件系统称为 FAT32。32位二进制位的簇链决定了FAT表最大可以寻址2T个簇。这样即使簇的大小为1扇区，理论上仍然能够寻址1TB范围内的分区。但实际中 FAT32是不能寻址这样大的空间的，随着分区空间大小的增加，FAT表的记录数会变得臃肿不堪，严重影响系统的性能。所以在实际中通常不格式化超过 32GB的FAT32分区。WIN2000及之上的OS已经不直接支持对超过32GB的分区格式化成FAT32，但WIN98依然可以格式化大到 127GB的FAT32分区，但这样没必要也不推荐。同时FAT32也有小的限制，FAT32卷必须至少有65527个簇，所以对于小的分区，仍然需要使 用FAT16或FAT12。<br />&#160; &#160; 分区变大时，如果簇很小，文件分配表也随之变大。仍然会有上面的效率问题存在。既要有效地读写大文件，又要最大可能的减少空间的浪费。FAT32同样规定了相应的分区空间对应的簇的大小，见表12：</p><p>表12&#160; FAT32分区大小与对因簇大小<br />分区空间大小 &#160; &#160; 每个簇的扇区 &#160; &#160; 簇空间大小<br />&lt;8GB &#160; &#160; 8 &#160; &#160; 4k<br />&gt;=8GB且&lt;16GB &#160; &#160; 16 &#160; &#160; 8k<br />&gt;=16GB且&lt;32GB &#160; &#160; 32 &#160; &#160; 16k<br />&gt;=32GB &#160; &#160; 64 &#160; &#160; 32k</p><p>&#160; &#160; 簇的取值意义和FAT16类似，不过是位数长了点罢了，比较见表13：</p><p>表13 FAT各系统记录项的取值含义(16进制)<br />FAT12记录项的取值 &#160; &#160; FAT16记录项的取值 &#160; &#160; FAT32记录项的取值 &#160; &#160; 对应簇的表现情况<br />000 &#160; &#160; 0000 &#160; &#160; 00000000 &#160; &#160; 未分配的簇<br />002~FFF &#160; &#160; 0002~FFEF &#160; &#160; 00000002~FFFFFFEF &#160; &#160; 已分配的簇<br />FF0~FF6 &#160; &#160; FFF0~FFF6 &#160; &#160; FFFFFFF0~FFFFFFF6 &#160; &#160; 系统保留<br />FF7 &#160; &#160; FFF7 &#160; &#160; FFFFFFF7 &#160; &#160; 坏簇<br />FF8~FFF &#160; &#160; FFF8~FFFF &#160; &#160; FFFFFFF8~FFFFFFFF &#160; &#160; 文件结束簇</p><p>&#160; &#160; FAT32的另一项重大改革是根目录的文件化，即将根目录等同于普通的文件。这样根目录便没有了FAT16中512个目录项的限制，不够用的时候增加簇 链，分配空簇即可。而且，根目录的位置也不再硬性地固定了，可以存储在分区内可寻址的任意簇内，不过通常根目录是最早建立的(格式化就生成了)目录表。所 以，我们看到的情况基本上都是根目录首簇占簇区顺序上的第1个簇。在图4.3.12中也是按这种情况制作的画的。<br />&#160; &#160; FAT32对簇的编号依然同FAT16。顺序上第1个簇仍然编号为第2簇，通常为根目录所用(这和FAT16是不同的，FAT16的根目录并不占簇区空间，32个扇区的根目录以后才是簇区第1个簇)  <br />&#160; &#160; FAT32的文件寻址方法与FAT16相同，但目录项的各字节参数意义却与FAT16有所不同，一方面它启用了FAT16中的目录项保留字段，同时又完全支持长文件名了。<br />&#160; &#160; 对于短文件格式的目录项。其参数意义见表14：</p><p>表14&#160; &#160;FAT32短文件目录项32个字节的表示定义<br />字节偏移(16进制) &#160; &#160; 字节数 &#160; &#160; 定义<br />0x0~0x7 &#160; &#160; 8 &#160; &#160; 文件名<br />0x8~0xA &#160; &#160; 3 &#160; &#160; 扩展名<br />0xB* &#160; &#160; 1 &#160; &#160; 属性字节 &#160; &#160; 00000000(读写)<br />00000001(只读)<br />00000010(隐藏)<br />00000100(系统)<br />00001000(卷标)<br />&#160; 00010000(子目录)<br />00100000(归档)<br />0xC &#160; &#160; 1 &#160; &#160; 系统保留<br />0xD &#160; &#160; 1 &#160; &#160; 创建时间的10毫秒位<br />0xE~0xF &#160; &#160; 2 &#160; &#160; 文件创建时间<br />0x10~0x11 &#160; &#160; 2 &#160; &#160; 文件创建日期<br />0x12~0x13 &#160; &#160; 2 &#160; &#160; 文件最后访问日期<br />0x14~0x15 &#160; &#160; 2 &#160; &#160; 文件起始簇号的高16位<br />0x16~0x17 &#160; &#160; 2 &#160; &#160; 文件的最近修改时间<br />0x18~0x19 &#160; &#160; 2 &#160; &#160; 文件的最近修改日期<br />0x1A~0x1B &#160; &#160; 2 &#160; &#160; 文件起始簇号的低16位<br />0x1C~0x1F &#160; &#160; 4 &#160; &#160; 表示文件的长度</p><p>&#160; &#160; &#160; * 此字段在短文件目录项中不可取值0FH,如果设值为0FH，目录段为长文件名目录段</p><p>说明：<br />&#160; &#160; (1)、这是FAT32短文件格式目录项的意义。其中文件名、扩展名、时间、日期的算法和FAT16时相同的。<br />&#160; &#160; (2)、由于FAT32可寻址的簇号到了32位二进制数。所以系统在记录文件(文件夹)开始簇地址的时候也需要32位来记录，FAT32启用目录项偏移0x12~0x13来表示起始簇号的高16位。<br />&#160; &#160; (3)、文件长度依然用4个字节表示，这说明FAT32依然只支持小于4GB的文件(目录)，超过4GB的文件(目录),系统会截断处理。</p><p>&#160; &#160; FAT32的一个重要的特点是完全支持长文件名。长文件名依然是记录在目录项中的。为了低版本的OS或程序能正确读取长文件名文件，系统自动为所有长文件 名文件创建了一个对应的短文件名，使对应数据既可以用长文件名寻址，也可以用短文件名寻址。不支持长文件名的OS或程序会忽略它认为不合法的长文件名字 段，而支持长文件名的OS或程序则会以长文件名为显式项来记录和编辑，并隐藏起短文件名。<br />&#160; &#160; 当创建一个长文件名文件时，系统会自动加上对应的短文件名，其一般有的原则：<br />&#160; &#160; (1)、取长文件名的前6个字符加上&quot;~1&quot;形成短文件名，扩展名不变。<br />&#160; &#160; (2)、如果已存在这个文件名，则符号&quot;~&quot;后的数字递增，直到5。<br />&#160; &#160; (3)、如果文件名中&quot;~&quot;后面的数字达到5，则短文件名只使用长文件名的前两个字母。通过数学操纵长文件名的剩余字母生成短文件名的后四个字母，然后加后缀&quot;~1&quot;直到最后(如果有必要，或是其他数字以避免重复的文件名)。<br />&#160; &#160; (4)、如果存在老OS或程序无法读取的字符，换以&quot;_&quot;</p><p>&#160; &#160; 长文件名的实现有赖于目录项偏移为0xB的属性字节，当此字节的属性为：只读、隐藏、系统、卷标，即其值为0FH时，DOS和WIN32会认为其不合法而 忽略其存在。这正是长文件名存在的依据。将目录项的0xB置为0F，其他就任由系统定义了，Windows9x或Windows 2000、XP通常支持不超过255个字符的长文件名。系统将长文件名以13个字符为单位进行切割，每一组占据一个目录项。所以可能一个文件需要多个目录 项，这时长文件名的各个目录项按倒序排列在目录表中，以防与其他文件名混淆。<br />&#160; &#160; 长文件名中的字符采用unicode形式编码(一个巨大的进步哦)，每个字符占据2字节的空间。其目录项定义如表15。</p><p> <br />表15&#160; &#160;FAT32长文件目录项32个字节的表示定义<br />字节偏移<br />(16进制) &#160; &#160; 字节数 &#160; &#160; 定义<br />0x0 &#160; &#160; 1 &#160; &#160; 属性字节位意义 &#160; &#160; 7 &#160; &#160; 保留未用<br />6 &#160; &#160; 1表示长文件最后一个目录项<br />5 &#160; &#160; 保留未用<br />4 &#160; &#160; 顺序号数值<br />3<br />2<br />1<br />0<br />0x1~0xA &#160; &#160; 10 &#160; &#160; 长文件名unicode码①<br />0xB &#160; &#160; 1 &#160; &#160; 长文件名目录项标志，取值0FH<br />0xC &#160; &#160; 1 &#160; &#160; 系统保留<br />0xD &#160; &#160; 1 &#160; &#160; 校验值(根据短文件名计算得出)<br />0xE~0x19 &#160; &#160; 12 &#160; &#160; 长文件名unicode码②<br />0x1A~0x1B &#160; &#160; 2 &#160; &#160; 文件起始簇号(目前常置0)<br />0x1C~0x1F &#160; &#160; 4 &#160; &#160; 长文件名unicode码③</p><p>    系统在存储长文件名时，总是先按倒序填充长文件名目录 项，然后紧跟其对应的短文件名。从表15可以看出，长文件名中并不存储对应文件的文件开始簇、文件大小、各种时间和日期属性。文件的这些属性还是存放在短 文件名目录项中，一个长文件名总是和其相应的短文件名一一对应，短文件名没有了长文件名还可以读，但长文件名如果没有对应的短文件名，不管什么系统都将忽 略其存在。所以短文件名是至关重要的。在不支持长文件名的环境中对短文件名中的文件名和扩展名字段作更改(包括删除，因为删除是对首字符改写E5H)，都 会使长文件名形同虚设。长文件名和短文件名之间的联系光靠他们之间的位置关系维系显然远远不够。其实，长文件名的0xD字节的校验和起很重要的作用，此校 验和是用短文件名的11个字符通过一种运算方式来得到的。系统根据相应的算法来确定相应的长文件名和短文件名是否匹配。这个算法不太容易用公式说明，我们 用一段c程序来加以说明。<br />&#160; &#160; 假设文件名11个字符组成字符串shortname[],校验和用chknum表示。得到过程如下：</p><p>&#160; &#160; int i，j,chknum=0;<br />&#160; &#160; for (i=11; i&gt;0; i--)<br />&#160; &#160; &#160; &#160; chksum = ((chksum &amp; 1) ? 0x80 : 0) + (chksum &gt;&gt; 1) + shortname[j++];</p><p>&#160; &#160; 如果通过短文件名计算出来的校验和与长文件名中的0xD偏移处数据不相等。系统无论如何都不会将它们配对的。<br />&#160; &#160; 依据长文件名和短文件名对目录项的定义，加上对簇的编号和链接，FAT32上数据的读取便游刃有余了<br />Sharing is the most beautiful and effective especially for the peaple like you and me. Please contact me if it is needed. aidon1428@hotmail.com(this address is always avalible)</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Mon, 02 Jan 2023 01:22:11 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=648&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[OS之存储管理---文件系统的基本内容]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=645&amp;action=new</link>
			<description><![CDATA[<p>什么是文件</p><p>操作系统对存储设备的物理属性加以抽象，从而定义逻辑存储单位，即文件。文件由操作系统映射到物理设备上，这些存储设备是非易失的。文件是记录在外存上的相关信息的命名组合。从用户角度来看，文件是逻辑外存的最小分配单元，也就是说，数据只有通过文件才能存储到外存。文件的类型有很多，比如文本文件为按行组着的字符序列、源文件为函数序列，每个函数包括声明和可执行语句、可执行文件为一系列代码段，以供加载程序调入内存并执行。<br />文件属性</p><p>为了标示文件的各种特定信息，需要文件属性来存储文件的特定信息。文件属性因操作系统而异。但是通常包括：</p><p>&#160; &#160; 名称：符号文件名是以人类可读形式来保存的唯一信息<br />&#160; &#160; 标识符：这种唯一标记标示文件系统的文件，是文件的非人类可读名称<br />&#160; &#160; 类型：支持不同类型文件的系统需要这种信息<br />&#160; &#160; 位置：该信息为指向设备与设备上文件位置的指针<br />&#160; &#160; 尺寸：该属性包括文件的当前大小（以字节、字、块为单位）以及可能允许的最大尺寸<br />&#160; &#160; 保护：访问控制信息确定谁能进行读取、写入、执行等<br />&#160; &#160; 时间、日期和用户标识：文件创建、最后修改、最后使用的相关信息可以保存。这些数据用户保护、安全和使用监控。</p><p>有些文件系统还支持扩展文件属性，包括文件的字符编码和安全功能，如文件的校检和。所有的文件的信息保存在目录结构中，该目录结构保存在外存中。<br />文件操作</p><p>为了正确的定义文件，操作系统可以提供系统调用，来对文件进行：创建、写入、读取、重新定位、删除、截断文件等操作。</p><p>&#160; &#160; 创建文件：创建文件需要两个步骤：<br />&#160; &#160; １、必须在文件系统中为文件找到空闲空间。<br />&#160; &#160; ２、必须在目录中创建新文件的条目<br />&#160; &#160; 写文件：使用一个系统调用指定文件名称和要写入的信息。系统搜索目录查找文件的位置。系统应保留写指针用于指向需要进行下次写操作的文件位置。<br />&#160; &#160; 读文件：指明文件名称和需要文件的下一块该放在哪里（在内存中）。系统需要保留一个读指针指向要进行下一次读取操作的文件位置。当前操作位置可以作为进程的当前文件位置指针。读和写操作都是用相同的指针，可节省空间并降低系统复杂度。<br />&#160; &#160; 重新定位文件：搜索目录以寻找适当的条目，并且将当前文件位置指针重新定位到给定值<br />&#160; &#160; 删除文件：释放文件的所有文件空间，并删除指定的目录条目<br />&#160; &#160; 截断文件：让文件重置为零，并释放它的文件空间，但是允许所有属性保持不变（除了文件长度）</p><p>在首次使用文件之前进行系统调用open()，操作系统有一个打开文件表用来维护所有打开文件的信息，open()根据文件名搜索目录，将目录条目复制到打开文件表。当文件不在使用的时候，进程关闭该文件，操作系统从打开文件表中删除它的条目。<br />当多个进程可以同时打开文件的环境，这可能会发生操作系统采用两级的内部表：每个进程表和整个系统表。每个进程表跟它打开的所有文件，该表存储的是进程对文件的使用信息。<br />单个进程表的每个条目相应的指向整个系统的打开文件表。系统表包含与进程无关的信息。系统打开文件表为每个文件关联一个打开计数，用来表示多少进程打开了这个文件。当打开计数为0时，可以从系统打开文件表中删除这个文件条目。<br />每个打开文爱你具有如下的关联信息：</p><p>&#160; &#160; 文件指针：该指针对操作系统的每个进程是唯一的，因此必须和磁盘文件属性分开保存<br />&#160; &#160; 文件打开计数<br />&#160; &#160; 文件的磁盘位置<br />&#160; &#160; 访问权限：该信息保存在进程的打开文件表中</p><p>有的操作系统提供文件锁，用于一个继承锁定文件，防止其他进程访问它。<br />文件锁一般有联众类型：</p><p>&#160; &#160; 共享锁：类似读者锁，便于多个进程可以并发获取它<br />&#160; &#160; 独占锁：类似写者锁，一次只有一个进程可以获得这样的锁</p><p>有的操作系统还提供强制和建议文件锁定机制。如果锁定方案是强制性的，那么操作系统确保锁定完整性；如果锁是建议的，软件开发人员应该确保适当的获取和释放锁，比如Windows采用强制锁定，而UNIX采用建议锁定。<br />文件类型</p><p>设计文件系统时，需要考虑系统是否应该识别和支持文件类型。如果系统识别文件的类型，则它就能按合理的方式来操作文件。一般文件名分为两部分，即名称和扩展。<br />UNIX系统采用位于某些文件开始部分的幻数，大致表明文件类型，不是所有问阿金都有幻数，所以系统特征不能仅仅基于这种信息。UNIX也不记录创建程序的名称。UNIX允许文件名扩展提示，但是操作系统不强制也不依赖这些扩展名；这些扩展名主要帮助用户确定吧文件内容的类型。<br />文件结构</p><p>文件类型也可用于指示文件的内部结构。但是操作系统支持多个文件结构会带来一个缺点：操作系统会变得太复杂。有写操作系统强加（并支持）最小数量的文件结构。UNIX认为每个文件为8位字节序列，而操作系统不必对这些位做出解释。<br />内部文件结构</p><p>磁盘系统通常具有明确定义的块大小，这是由扇区大小决定的。所有的磁盘I/O按块（物理记录）为单位执行，所有的块的大小相同。物理记录大小不太可能刚好匹配期望的逻辑记录的长度，逻辑记录的长度甚至可能不同。解决方法是，将多个逻辑记录包装到一个物理块中。<br />访问方法</p><p>&#160; &#160; 顺序访问<br />&#160; &#160; 文件的信息按顺序（即一个记录接着一个记录的）加以处理，这种访问模式是目前最常见的，比如编辑器和编译器通常以这种方式进行访问文件。<br />&#160; &#160; 顺序访问基于文件的磁带模型，不但适用于顺序访问设备，也适用于随机访问设备。<br />&#160; &#160; 直接访问<br />&#160; &#160; 直接访问也称为相对访问，文件是由固定长度的逻辑记录组成的，以允许程序按任意顺序进行快速读取和写入记录。直接访问方法基于文件的磁盘模型，因为磁盘允许对任何文件块的随机访问。<br />&#160; &#160; 对于直接访问方法，必须修改文件操作以便包括块号作为参数，read(n)，其中的n就是块号。<br />&#160; &#160; 用户提供给操作系统的块号，通常为相对块号，相对块号是相对于文件开头的索引。</p><p>目录和磁盘的结构</p><p>一个存储设备可以按整体来用于文件系统，也可以进行细分来提供更细粒度的控制。比如，一个磁盘可以划分为四个分区，每个分区可以有单独的文件系统。存储设备还可以组成RAID集，一起提供保护以免受到单个磁盘故障。包含文件系统的分区通常称为卷。卷可以是设备的一部分，或整个设备，或由多个设备组成的RAID集。包含文件西荣的每个卷也应包含有关系统内的文件信息，这些信息保存在设备目录或卷目录表中<br />存储结构</p><p>通用计算机系统有多个存储设备，这些存储设备可以分成保存文件系统的卷，计算机系统可能没有文件系统，也可能有多个文件系统，而且文件系统的类型可以不同。<br />在这里插入图片描述<br />一些常见的文件系统类型：</p><p>&#160; &#160; tmpfs：“临时”文件系统，是在易失性内存中创建的，当系统重启或崩溃的时候，它的内容会被擦除<br />&#160; &#160; objfs：“虚拟”文件系统。本质上这是一个内核接口，但是看起来像一个文件系统；它让调试器访问内核符号<br />&#160; &#160; ctfs：维护“合同”信息的虚拟文件系统，以管理哪些进程在系统引导时启动并且运行时必须继续运行。<br />&#160; &#160; lofs：“环回”文件系统，允许一个文件系统代替另一个来被访问<br />&#160; &#160; procfs：虚拟文件系统，将所有进程信息作为文件系统来呈现<br />&#160; &#160; ufs, zfs：通用文件系统</p><p>目录概述</p><p>目录可视为符号表，可将文件名称转成目录条目。常见考虑特定的目录结构的时候，需要对目录执行的操作如下：</p><p>&#160; &#160; 搜索文件<br />&#160; &#160; 创建文件<br />&#160; &#160; 删除文件<br />&#160; &#160; 遍历目录<br />&#160; &#160; 重命名文件<br />&#160; &#160; 遍历文件系统</p><p>单级目录</p><p>最容易实现的就是单级目录，所有文件都包含在同一目录中。但是当文件数量增加或系统有多个用户的时候，单级目录有重要的限制。<br />两级目录</p><p>为每个用户创建一个单独的目录(UFD)。同时系统有一个主文件目录(MFD)，通过用户名或账户可以索引MFD，每个条目指向该用户的UFD。当用户引用特定文件时，只搜索自己的UFD。这种数据结构将一个用户和其他用户相隔离，但是当用户需要在某个任务上进行合作并且访问彼此文件时，隔离确是个缺点。<br />树形目录</p><p>树形目录是最常见的目录结构，有一个根目录，系统内每个文件都有唯一的路径名。目录包括一组文件和子目录，目录只不过是一个文件，但是他是按照特殊方式处理的，每个目录条目都有一位来将条目定义为文件或子目录。<br />无环图目录</p><p>树结构截止共享文件或目录。无环图(就是没有循环的图)允许目录共享子目录和文件。同一文件或子目录可出现在两个不同的目录中。无环图是树形目录方案的自然扩展。<br />在这里插入图片描述<br />实现共享文件和目录的方法有多个：</p><p>&#160; &#160; 使用链接技术<br />&#160; &#160; 在两个共享目录中复制有关他们的所有信息<br />&#160; &#160; 两个条目因为复制其实是相同且相等，但是有一个问题即使在修改文件呢时要维护一致性。</p><p>实现共享目录和文件时会涉及到删除，即共享文件的分配空间何时可以被释放和重用：</p><p>&#160; &#160; 当用户删除时就删除，但是这种操作可能留下悬挂指针，以指向不存在的文件<br />&#160; &#160; 另一种删除方法是保留文件，直到它的所有引用都被删除，这时需要一种机制来确定文件的最后一个引用被删除。</p><p>在UNIX和Linux中的硬链接采用的就是第二种方法，而软链接采用的是第一种方法。<br />通用图目录</p><p>无环图目录保证了结构中不存在环，如果允许目录中有环，则无论从正确性和性能角度来说，同样需要避免多次搜索同一部分。如果存在环时，即使不再坑你引用一个目录或文件时，引用计数也可能不为0，这种情况下通常需要垃圾收集方案，确定何时最后引用已被删除并重新分配磁盘空间</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sun, 01 Jan 2023 15:35:37 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=645&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[文件系统imap：inode节点位图(inodemap)管理空闲inode]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=601&amp;action=new</link>
			<description><![CDATA[<p>摘取自骏马金龙的第4章ext文件系统机制原理剖析</p><p>在写文件(Linux中一切皆文件)时需要为其分配一个inode号。</p><p>其实，在格式化创建文件系统后，所有的inode号都已计算好（创建文件系统时会为每个块组计算好该块组拥有哪些inode号），因此产生了问题：要为文件分配哪一个inode号呢？又如何知道某一个inode号是否已经被分配了呢？</p><p>既然是&quot;是否被占用&quot;的问题，使用位图是最佳方案，像bmap记录block的占用情况一样。标识inode号是否被分配的位图称为inodemap简称为imap。这时要为一个文件分配inode号只需扫描imap即可知道哪一个inode号是空闲的。</p><p>这样理解更容易些，类似bmap块位图一样，inode号是预先规划好的。inode号分配后，文件删除也会释放inode号。分配和释放的inode号，像是在一个地图上挖掉一块，用完再补回来一样。</p><p>imap存在着和bmap和inode table一样需要解决的问题：如果文件系统比较大，imap本身就会很大，每次存储文件都要进行扫描，会导致效率不够高。同样，优化的方式是将文件系统占用的block划分成块组，每个块组有自己的imap范围。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Mon, 05 Dec 2022 03:36:56 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=601&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[对于硬盘驱动的理解]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=600&amp;action=new</link>
			<description><![CDATA[<p>目录</p><p>如何读写硬盘</p><p>&#160; 读写操作</p><p>&#160; 硬盘控制器端口及作用</p><p>&#160; 硬盘中断</p><p>&#160; 硬盘分区信息的获取</p><p>&#160; 如何读写文件</p><p>&#160; TASK_HD<br />如何读写硬盘<br />读写操作</p><p>&#160; 第一次看到linux 0.12关于读写硬盘几行代码时候，感觉很费解。<br />复制代码</p><p>do_hd = intr_addr;<br />outb_p(hd_info[drive].ctl,HD_CMD);<br />port=HD_DATA;<br />outb_p(nsect,++port);<br />outb_p(sect,++port);<br />outb_p(cyl,++port);<br />outb_p(cyl&gt;&gt;8,++port);<br />outb_p(0xA0|(drive&lt;&lt;4)|head,++port);<br />outb(cmd,++port)</p><p>复制代码</p><p>&#160; 我还是不明白怎么这样就可以读写硬盘了。但是代码到此就结束了。</p><p>&#160; 一直好奇程序是如何控制硬件的，这些指令就是一个个电信号在cpu中流动，怎么就能把硬盘中的数据拿到内存中呢？</p><p>&#160; 正好在同一个学期开设了《计算机组成原理》和《微机原理与接口技术》这两门课程，那个时候才了解到端口的意思，了解到cpu寻址、数据传输的流程。</p><p>&#160; 往端口写了数据和指令，剩下的我们只能相信硬件制造商的设计和生产能力了，然后默默等待硬件的回应。我记得当时自己疑惑了一段时间，苦于没有人来提醒这一点，可能会的人感觉这根本不是问题吧。<br />硬盘控制器端口及作用</p><p>&#160; Linux 0.12当时操作的硬盘是CHS寻址模式，起始扇区编号是1。对于《实现》来说，用bochs自带的工具bximage命令生成的虚拟硬盘是LBA寻址模式的，起始扇区编号是0。CHS模式和LBA模式的端口号和操作方式都一样，只是有一些端口代表的意义不一样了,来看一下LBA寻址模式的端口作用，借用书中的表9.1。</p><p>表1&#160; &#160; &#160; &#160; &#160; &#160;LBA寻址模式的硬盘端口及其作用 <br />I/O端口 &#160; &#160; 读时 &#160; &#160; 写时&#160; <br />primary &#160; &#160; secondary<br />1F0H &#160; &#160; 170H &#160; &#160;  Data <br />1F1H&#160; &#160; &#160; 171H&#160; &#160; &#160;  Error &#160; &#160;  Features<br />1F2H&#160; &#160; &#160; 172H&#160; &#160; &#160;  Sector count <br />1F3H &#160; &#160; 173H&#160; &#160; &#160;  LBA low <br />1F4H&#160; &#160; &#160; 174H&#160; &#160; &#160;  LBA mid <br />1F5H&#160; &#160; &#160; 175H&#160; &#160; &#160;  LBA high <br />1F6H&#160; &#160; &#160; 176H&#160; &#160; &#160;  Device <br />1F7H&#160; &#160; &#160; 177H&#160; &#160; &#160;  Status &#160; &#160;  Command<br />3F6H &#160; &#160; 376H&#160; &#160; &#160;  Alternate status &#160; &#160;  Device control</p><p> </p><p>&#160; 其中Device寄存器比较特殊，它用来指明寻址模式。来看一下格式。 </p><p>表2&#160; &#160; &#160; &#160; &#160; &#160;Device寄存器各个bit为的意义<br />Bit位 &#160; &#160; 值 &#160; &#160; 意义<br />7 &#160; &#160; 1 &#160; &#160;  <br />6 &#160; &#160; L &#160; &#160; 0表示CHS模式，1表示LBA模式<br />5 &#160; &#160; 1 &#160; &#160;  <br />4 &#160; &#160; DRV &#160; &#160; 0表示主盘，1表示从盘<br />3 &#160; &#160; HS3 &#160; &#160; </p><p>如果是L=0，CHS模式，那么这四位的值表示磁头号</p><p> </p><p>如果L=1，LBA模式，那么这四位的值表示LBA的24到27位<br />2 &#160; &#160; HS2<br />1 &#160; &#160; HS1<br />0 &#160; &#160; HS0</p><p> </p><p>&#160; 从上面的代码可以很清楚的看到如何读写硬盘，往相应的端口写上我们要读多少个扇区，读哪个扇区，哪个柱面，哪个磁头，哪个硬盘，然后告诉硬盘我们的需求cmd，读或者写。 </p><p>&#160; 另外，CHS模式下，硬盘扇区编号从1开始编号。LBA模式下，从0开始编号。<br />硬盘中断</p><p>&#160; 我们怎么知道硬盘的工作做完了没有呢？只能等待硬盘产生中断信号，通过8259A告诉cpu，这个中断信号是哪个硬件产生的。</p><p>&#160; 在书中，用的是微内核，所有的进程都给TASK_HD(硬盘驱动)发送读写硬盘的命令，而不是自己调用硬盘驱动中的读写函数。所以中断产生后，仅仅需要通知TASK_HD这个进程，TASK_HD会把硬盘准备好的数据读到发出读请求进程指定的内存位置。<br />硬盘分区信息的获取</p><p>&#160; 前面说了如何向硬盘发送命令，让它读写哪些扇区，但是这些参数都是我们提前计算好的。如何计算这些参数？我们又是如何知道该读写那个扇区呢？</p><p>&#160; 之所以把分区信息的介绍放到读写文件这一小节中，是因为我觉得分区信息和文件关联很大。我们要读写文件，才需要知道分区信息，如果我们不需要按照文件形式来读写硬盘，那么知不知道分区信息就无所谓啦，凭我们的大脑记住要读取的数据在第几个分区，到时候直接汇编操作寄存器就好啦。</p><p>&#160; 那为什么要分区呢？似乎不分区把所有的数据都杂糅在一起，电脑也可以正常运行啊。我百度了一下，大概是由于为了把操作系统和数据分开吧。试想，如果所有的东西和操作系统共处一个空间，那么操作系统崩溃了，这个空间的所有数据的记录索引在重新安装操作系统后都会失效，尽管数据本身依然很正常，但是由于记录索引丢失，我们却没法找到他们。如果分区了，那么最多操作系统的所在分区的数据拿不到了，其他分区数据的记录索引还在。</p><p>&#160; 如何获取分区信息?</p><p>&#160; 在硬盘的0号扇区(MBR扇区)偏移0x1BE处保存的有一张硬盘主分区表。只有四个表项，也就是说一个硬盘只能记录四个主分区，据说是因为当初IBM认为一个PC上装4个操作系统(只有主分区上能安装操作系统)就够用了。如果想要更多的分区，那么需要在格式化的时候指明一个(只能有一个主分区记录能用于扩展分区)表项用作扩展分区，扩展分区并不能直接使用，在这个扩展分区里面我们还要划分出逻辑分区，每一个逻辑分区的起始扇区记录的分区表只能使用两个表项。</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; 书中根设备编号是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; 原来是这样，你想怎么样编号就怎么样编号，只要你自己能找到映射关系就可以了。</p><p>&#160; 获取信息的步骤：</p><p>&#160; &#160; device = 0，style = P_PRIMARY<br />&#160; &#160; 调用获取分区信息函数<br />&#160; &#160; 如果style == P_ EXTENDED执行第10步<br />&#160; &#160; 读取设备device的起始扇区，提取0x1BE处的4个表项到part_tbl<br />&#160; &#160; 令i=0<br />&#160; &#160; 判断第i个分区表项part_tbl[ i ]<br />&#160; &#160; 如果是主分区，记录起始扇区sect_start和扇区数目setcs到相应的primary[i+1]。<br />&#160; &#160; 如果是扩展分区，记录起始扇区sect_start和扇区数目setcs到相应的primary[i+1]，令device += i+1，style = P_ EXTENDED跳到第2步<br />&#160; &#160; 如果i&gt;=4，结束；否则i++，执行第6步<br />&#160; &#160; 扩展分区的起始扇区ext_start_sect = primary[device].base(这个值在第8步中已经计算出来了)，求出该扩展分区的第一个逻辑分区的编号，nr_1st_sub = (device-1) * NR_SUB_PER_PART，计算该扩展分区第0个逻辑分区的起始地址s= ext_start_sect<br />&#160; &#160; 令i=0(由于是递归调用，此处i的值并不影响第5步的i)<br />&#160; &#160; 读取第nr_1st_sub+i个逻辑分区的起始扇区s，提取0x1BE处的2个表项(逻辑分区只使用分区表的两个表项)到part_tbl<br />&#160; &#160; 记录逻辑分区的信息到logical[nr_1st_sub+i]<br />&#160; &#160; s = ext_start_sect + part_tbl[1].start_sect<br />&#160; &#160; 如果i&gt;=16，本次递归结束，返回到第8步；否则i++，执行第12步</p><p>&#160; 感觉文字叙述理解起来可能比较模糊，但是比代码实现起来还是省事一些，像读分区起始扇区，一句话带过，知道怎么做就可以了，如果用代码描述，可能还要牵扯到其他知识点。<br />如何读写文件</p><p>&#160; 其实对于硬盘驱动而言，没有文件这个概念，只有扇区。硬盘驱动能接受的参数就是要读写的起始扇区，读写扇区个数。文件这个概念由上层的文件系统来处理。</p><p>&#160; 这个时候，我们会想起来inode结构体中有两个记录是i_dev和i_start_sect，这两个元素把上层文件系统和硬盘关联起来了。当我们要读某某个文件的时候，文件系统告诉硬盘驱动读目录区，把文件的inode号找到，再读indoe到内存中，这个时候就有了文件在哪个分区i_dev，数据存放在第i_start_sect号扇区，及之后一共的x800个扇区中，这个i_start_sect的值是相对于分区i_dev为起始偏移的。</p><p>&#160; 知道i_dev和i_start_sect之后，硬盘驱动可以做什么呢？首先将以i_dev分区为起始偏移的i_start_sect转化为相对于整个硬盘。怎么转化呢？上面获取分区信息的时候，每个分区的起始扇区都被记录，我们找到i_dev的起始扇区，加上i_start_sect就是相对于整个硬盘的了。</p><p>&#160; 这样就把文件读进来了。至于读文件哪一段的内容，其实还是上层的文件系统来记录处理的，还记得file结构体中有一个元素是pos，这个值就是用来标明要读写的内容在文件中的偏移。将pos/SECT_SIZE再加上上面计算的文件相对于整个硬盘的偏移，就是要读写的某一段数据了。</p><p>&#160;  所谓块设备的名称也许就是这样由来的吧，一次最少处理的数据是一个扇区(当然，不同的硬盘给出的接口肯定不一样)。<br />TASK_HD</p><p>&#160; 这样一来，TASK_HD的任务就是很简单了啊，接收TASK_FS发送的读写请求，将针对于i_dev设备的i_start_sect转化为相对于整个硬盘的扇区号，再加上pos/SECT_SIZE，然后读写这个扇区交给TASK_FS就什么都不管了。进入下一个循环。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sat, 03 Dec 2022 08:21:50 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=600&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[对文件系统的理解]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=599&amp;action=new</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?id=599&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[Linux文件系统解析]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=485&amp;action=new</link>
			<description><![CDATA[<p>在LINUX系统中有一个重要的概念：一切都是文件，既Linux以文件的形式对计算机中的数据和硬件资源进行管理。 在LINUX系统中，把一切资源都看作是文件，包括硬件设备。每个硬件都被看成是一个文件，通常称为设备文件，这样用户就可以用读写文件的方式实现对硬件的访问。<br />一、Linux文件体系</p><p>&#160; &#160; &#160; &#160; &#160;Linux文件的类型有很多种（下面会详细说），这些文件被LINUX使用目录树进行管理，所谓的目录树就是以根目录（/）为主，向下呈现分支状的一种文件结构。不同于纯粹的ext2之类的文件系统，我把它称为文件体系，一切皆文件和文件目录树的资源管理方式一起构成了Linux的文件体系，让Linux操作系统可以方便使用系统资源。</p><p>&#160; &#160; &#160; &#160; 文件系统比文件体系涵盖的内容少很多，Linux文件体系主要在于把操作系统相关的东西用文件这个载体实现：文件系统挂载在操作系统上，操作系统整个系统又放在文件系统里。</p><p> <br />1.1、Linux中的文件类型</p><p>那就先简单说说Linux中的文件类型，主要关注普通文件、目录文件和符号连接文件。</p><p>&#160; &#160; 普通文件（-）<br />&#160; &#160; &#160; &#160; 从Linux的角度来说，类似mp4、pdf、html这样应用层面上的文件类型都属于普通文件<br />&#160; &#160; &#160; &#160; Linux用户可以根据访问权限对普通文件进行查看、更改和删除<br />&#160; &#160; 目录文件（d，directory file）<br />&#160; &#160; &#160; &#160; 目录文件对于用惯Windows的用户来说不太容易理解，目录也是文件的一种<br />&#160; &#160; &#160; &#160; 目录文件包含了各自目录下的文件名和指向这些文件的指针，打开目录事实上就是打开目录文件，只要有访问权限，你就可以随意访问这些目录下的文件（普通文件的执行权限就是目录文件的访问权限），但是只有内核的进程能够修改它们<br />&#160; &#160; &#160; &#160; 虽然不能修改，但是我们能够通过vim去查看目录文件的内容<br />&#160; &#160; 符号链接（l，symbolic link）<br />&#160; &#160; &#160; &#160; 这种类型的文件类似Windows中的快捷方式，是指向另一个文件的间接指针，也就是我们常说的软链接<br />&#160; &#160; 块设备文件（b，block）和字符设备文件（c，char）<br />&#160; &#160; &#160; &#160; 这些文件一般隐藏在/dev目录下，在进行设备读取和外设交互时会被使用到<br />&#160; &#160; &#160; &#160; 比如磁盘光驱就是块设备文件，串口设备则属于字符设备文件<br />&#160; &#160; &#160; &#160; 系统中的所有设备要么是块设备文件，要么是字符设备文件，无一例外<br />&#160; &#160; FIFO（p，pipe）<br />&#160; &#160; &#160; &#160; 管道文件主要用于进程间通讯。比如使用mkfifo命令可以创建一个FIFO文件，启用一个进程A从FIFO文件里读数据，启动进程B往FIFO里写数据，先进先出，随写随读。<br />&#160; &#160; 套接字（s，socket）<br />&#160; &#160; &#160; &#160; 用于进程间的网络通信，也可以用于本机之间的非网络通信<br />&#160; &#160; &#160; &#160; 这些文件一般隐藏在/var/run目录下，证明着相关进程的存在</p><p>Linux 的文件是没有所谓的扩展名的，一个 Linux文件能不能被执行与它是否可执行的属性有关，只要你的权限中有 x ，比如[ -rwx-r-xr-x ] 就代表这个文件可以被执行，与文件名没有关系。跟在 Windows下能被执行的文件扩展名通常是 .com .exe .bat 等不同。<br />不过，可以被执行跟可以执行成功不一样。比如在 root 主目彔下的 install.log 是一个文本文件，修改权限成为 -rwxrwxrwx 后这个文件能够真的执行成功吗？ 当然不行，因为它的内容根本就没有可以执行的数据。所以说，这个 x 代表这个文件具有可执行的能力， 但是能不能执行成功，当然就得要看该文件的内容了。<br />虽然如此，不过我们仍然希望能从扩展名来了解该文件是什么东西，所以一般我们还是会以适当的扩展名来表示该文件是什么种类的。<br />所以Linux 系统上的文件名真的只是让你了解该文件可能的用途而已， 真正的执行与否仍然需要权限的规范才行。比如常见的/bin/ls 这个显示文件属性的指令要是权限被修改为无法执行，那么ls 就变成不能执行了。这种问题最常发生在文件传送的过程中。例如你在网络上下载一个可执行文件，但是偏偏在你的 Linux 系统中就是无法执行，那就可能是档案的属性被改变了。而且从网络上传送到你 的 Linux 系统中，文件的属性权限确实是会被改变的<br />1.2、Linux目录树</p><p>对Linux系统和用户来说，所有可操作的计算机资源都存在于目录树这个逻辑结构中，对计算机资源的访问都可以认为是目录树的访问。就硬盘来说，所有对硬盘的访问都变成了对目录树中某个节点也就是文件夹的访问，访问时不需要知道它是硬盘还是硬盘中的文件夹。<br />目录树的逻辑结构也非常简单，就是从根目录（/）开始，不断向下展开各级子目录。<br />目录</p><p>从上到下，你所看到的目录如下<br />/bin</p><p>/bin 目录是包含一些二进制文件的目录，即可以运行的一些应用程序。 你会在这个目录中找到上面提到的 ls 程序，以及用于新建和删除文件和目录、移动它们基本工具。还有其它一些程序，等等。文件系统树的其他部分有更多的 bin 目录，但我们将在一会儿讨论这些目录。<br />/boot</p><p>/boot 目录包含启动系统所需的文件，包括系统核心文件。我必须要说吗？ 好吧，我会说：不要动它！ 如果你在这里弄乱了其中一个文件，你可能无法运行你的 Linux，修复被破坏的系统是非常痛苦的一件事。 另一方面，不要太担心无意中破坏系统：你必须拥有超级用户权限才能执行此操作。<br />/dev</p><p>/dev 目录包含设备文件，，如打印机，硬盘等外围设备等。 其中许多是在启动时或甚至在运行时生成的。 例如，如果你将新的网络摄像头或 USB 随身碟连接到你的机器中，则会自动弹出一个新的设备条目。<br />/etc</p><p>/etc 的目录很重要 ，是“ 要配置的所有内容(Everything To Configure)”，因为它包含大部分（如果不是全部的话）的系统配置文件以及管理相关软件。 例如，包含系统名称、用户及其密码、网络上计算机名称以及硬盘上分区的安装位置和时间的文件都在这里。 再说一遍，如果你是 Linux 的新手，最好是不要在这里接触太多，直到你对系统的工作有更好的理解。<br />/home</p><p>/home 是存放用户专属目录。在我的情况下，/home 下有两个目录：/home/paul，其中包含我所有的东西；另外一个目录是 /home/guest 目录，以防有客人需要使用我的电脑。<br />/lib</p><p>/lib 存放一些共享的函数库。库是包含应用程序可以使用的代码文件。它们包含应用程序用于在桌面上绘制窗口、控制外围设备或将文件发送到硬盘的代码片段。</p><p>在文件系统周围散布着更多的 lib 目录，但是这个直接挂载在 / 的 /lib 目录是特殊的，除此之外，它包含了所有重要的内核模块。 内核模块是使你的显卡、声卡、WiFi、打印机等工作的驱动程序。<br />/media</p><p>在 /media 目录中，当你插入外部存储器试图访问它时，将自动挂载它。与此列表中的大多数其他项目不同，/media 并不追溯到 1970 年代，主要是因为当计算机正在运行而动态地插入和检测存储（U 盘、USB 硬盘、SD 卡、外部 SSD 等)，这是近些年才发生的事。<br />/mnt</p><p>然而，/mnt 目录是一些过去的残余。这是你手动挂载存储设备或分区的地方。现在不常用了。<br />/opt</p><p>/opt 目录通常是你编译软件（即，你从源代码构建，并不是从你的系统的软件库中安装软件）的地方。应用程序最终会出现在 /opt/bin 目录，库会在 /opt/lib 目录中出现。</p><p>稍微的题外话：应用程序和库的另一个地方是 /usr/local，在这里安装软件时，也会有 /usr/local/bin和 /usr/local/lib 目录。开发人员如何配置文件来控制编译和安装过程，这就决定了软件安装到哪个地方。<br />/proc</p><p>/proc目录中存放系统核心和执行程序之间的信息就像 /dev 是一个虚拟目录。它包含有关你的计算机的信息，例如关于你的 CPU 和你的 Linux 系统正在运行的内核的信息。与 /dev 一样，文件和目录是在计算机启动或运行时生成的，因为你的系统正在运行且会发生变化。<br />/root</p><p>/root 是系统的超级用户（也称为“管理员”）的主目录。 它与其他用户的主目录是分开的，因为你不应该动它。 所以把自己的东西放在你自己的目录中，伙计们。<br />/run</p><p>/run 是另一个新出现的目录。系统进程出于自己不可告人的原因使用它来存储临时数据。这是另一个不要动它的文件夹。<br />/sbin</p><p>/sbin 与 /bin 类似，但它包含的应用程序只有超级用户（即首字母的 s ）才需要。你可以使用 sudo命令使用这些应用程序，该命令暂时允许你在许多 Linux 发行版上拥有超级用户权限。/sbin 目录通常包含可以安装、删除和格式化各种东西的工具。你可以想象，如果你使用不当，这些指令中有一些是致命的，所以要小心处理。<br />/usr</p><p>/usr 目录是在 UNIX 早期用户的主目录所处的地方。然而，正如我们上面看到的，现在 /home 是用户保存他们的东西的地方。如今，/usr 包含了大量目录，而这些目录又包含了应用程序、库、文档、壁纸、图标和许多其他需要应用程序和服务共享的内容。</p><p>你还可以在 /usr 目录下找到 bin，sbin，lib 目录，它们与挂载到根目录下的那些有什么区别呢？现在的区别不是很大。在早期，/bin 目录（挂载在根目录下的）只会包含一些基本的命令，例如 ls、mv 和 rm ；这是一些在安装系统的时候就会预装的一些命令，用于维护系统的一个基本的命令。 而 /usr/bin 目录则包含了用户自己安装和用于工作的软件，例如文字处理器，浏览器和一些其他的软件。</p><p>但是许多现代的 Linux 发行版只是把所有的东西都放到 /usr/bin 中，并让 /bin 指向 /usr/bin，以防彻底删除它会破坏某些东西。因此，Debian、Ubuntu 和 Mint 仍然保持 /bin 和 /usr/bin （和 /sbin 和 /usr/sbin ）分离；其他的，比如 Arch 和它衍生版，只是有一个“真实”存储二进制程序的目录，/usr/bin，其余的任何 bin 目录是指向 /usr/bin` 的“假”目录。<br />/srv</p><p>/srv 目录包含服务器的数据。如果你正在 Linux 机器上运行 Web 服务器，你网站的 HTML文件将放到 /srv/http（或 /srv/www）。 如果你正在运行 FTP 服务器，则你的文件将放到 /srv/ftp。<br />/sys</p><p>/sys 是另一个类似 /proc 和 /dev 的虚拟目录，它还包含连接到计算机的设备的信息。</p><p>在某些情况下，你还可以操纵这些设备。 例如，我可以通过修改存储在 /sys/devices/pci0000:00/0000:00:02.0/drm/card1/card1-eDP-1/intel_backlight/brightness 中的值来更改笔记本电脑屏幕的亮度（在你的机器上你可能会有不同的文件）。但要做到这一点，你必须成为超级用户。原因是，与许多其它虚拟目录一样，在 /sys 中打乱内容和文件可能是危险的，你可能会破坏系统。直到你确信你知道你在做什么。否则不要动它。<br />/tmp</p><p>/tmp 包含临时文件，通常由正在运行的应用程序放置。文件和目录通常（并非总是）包含应用程序现在不需要但以后可能需要的数据。</p><p>你还可以使用 /tmp 来存储你自己的临时文件 —— /tmp 是少数挂载到根目录下而你可以在不成为超级用户的情况下与它进行实际交互的目录之一。<br />/var</p><p>/var 目录中 存放经常变动的文件，如日志文件，临时文件，电子邮箱。最初被如此命名是因为它的内容被认为是 可变的(variable)，因为它经常变化。今天，它有点用词不当，因为还有许多其他目录也包含频繁更改的数据，特别是我们上面看到的虚拟目录。</p><p>不管怎样，/var 目录包含了放在 /var/log 子目录的日志文件之类。日志是记录系统中发生的事件的文件。如果内核中出现了什么问题，它将被记录到 /var/log 下的文件中；如果有人试图从外部侵入你的计算机，你的防火墙也将记录尝试。它还包含用于任务的假脱机程序。这些“任务”可以是你发送给共享打印机必须等待执行的任务，因为另一个用户正在打印一个长文档，或者是等待递交给系统上的用户的邮件。<br />————————————————<br />版权声明：本文为CSDN博主「轩阁楼主」的原创文章，遵循CC 4.0 BY-SA版权协议，转载请附上原文出处链接及本声明。<br />原文链接：https://blog.csdn.net/xuangelouzhu/article/details/118082541</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sun, 30 Oct 2022 14:48:52 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=485&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[linux源码解读（二十五）：mmap原理和实现方式]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=468&amp;action=new</link>
			<description><![CDATA[<p>众所周知，linux的理念是万物皆文件，自然少不了对文件的各种操作，常见的诸如open、read、write等，都是大家耳熟能详的操作。除了这些常规操作外，还有一个不常规的操作：mmap，其在file_operations结构体中的定义如下： 这个函数的作用是什么了？<br /><span class="postimg"><img src="https://img2022.cnblogs.com/blog/2052730/202202/2052730-20220223094418782-211451644.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; &#160; 1、对于读写文件，传统经典的api都是这样的：先open文件，拿到文件的fd；再调用read或write读写文件。由于文件存放在磁盘，3环的app是没有权限直接操作磁盘的，所以需要通过系统调用进入操作系统的内核，再通过事先安装好的驱动读写磁盘数据。这样一来，磁盘的数据会分别存放在内核空间和用户空间，也就是同一份数据会在内存内部放在两个不同的地方，而且也需要拷贝2次，整个过程是“又费柴油又费马达”；流程示例如下：<br /><span class="postimg"><img src="https://img2022.cnblogs.com/blog/2052730/202202/2052730-20220223095923841-776621370.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; 这样做既然浪费内存空间，也浪费拷贝的时间，该怎么优化了？</p><p>&#160; &#160;2、上述做法的结症在于同一份数据拷贝2次，那么能不能只拷贝1次了？答案是可以的，mmap就是这么干的！</p><p>&#160; （1）先看看mmap的用例，直观了解一下是怎么使用的，如下：</p><div class="codebox"><pre><code>#include&lt;stdlib.h&gt;
#include&lt;sys/mman.h&gt;
#include&lt;fcntl.h&gt;

int main(void)
{
    int *p;
    int fd = open(&quot;hello&quot;, O_RDWR);
    if(fd &lt; 0)
    {
        perror(&quot;open hello&quot;);
        exit(1);
    }
    p = mmap(NULL,6, PROT_WRITE, MAP_SHARED, fd, 0);
    if(p == MAP_FAILED)
    {
       perror(&quot;mmap&quot;); //程序进里面了，证明mmap失败
       exit(1);
    }
    close(fd);
    p[0] = 0x30313233;
    munmap(p, 6);
    return 0;

}</code></pre></div><p>&#160; 用例是不是很简单了？还是先调用open函数得到文件的fd，再调用mmap建立文件在内存的映射，这时得到了文件在内存映射的地址p，最后通过p指针读写文件数据！整个逻辑非常简单，是个码农都能看懂！这么简单方便、效率还高（只复制一次）的mmap又是怎么实现的了？</p><p>&#160; （2）先说一下mmap的原理：mmap只复制1次的原理也简单，就是在进程的虚拟内存开辟一块空间，映射到内核的物理内存，并建立和文件的关联，再把文件内容读到这块内存；后续3环的app读写文件都不走磁盘了，而是直接读写这块建立好映射的内存！等到进程退出或出意外奔溃，操作系统把映射内存的数据重新写回磁盘的文件！<br /><span class="postimg"><img src="https://img2022.cnblogs.com/blog/2052730/202202/2052730-20220223103718987-1378429682.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; &#160; &#160;mmap的原理也不复杂，具体是到代码层面是怎么做的了？</p><p>&#160; &#160; （3）从上面的demo可以看出，3环应用层直接调用的是mmap函数，但很明显这个功能因为涉及到磁盘读写，肯定是需要操作系统支持得，所以mmap肯定需要通过系统调用进入内核执行代码。操作系统提供的系统调用函数是do_mmap，在mm\mmap.c文件中，代码如下：</p><div class="codebox"><pre class="vscroll"><code>/*
 * The caller must hold down_write(&amp;current-&gt;mm-&gt;mmap_sem).
 根据用户传入的参数做了一系列的检查，然后根据参数初始化vm_area_struct的标志vm_flags、
 vma-&gt;vm_file = get_file(file)建立文件与vma的映射
 */
unsigned long do_mmap(struct file *file, unsigned long addr,
            unsigned long len, unsigned long prot,
            unsigned long flags, vm_flags_t vm_flags,
            unsigned long pgoff, unsigned long *populate)
{
    struct mm_struct *mm = current-&gt;mm;//当前进程的虚拟内存描述符
    int pkey = 0;

    *populate = 0;

    if (!len)
        return -EINVAL;

    /*
     * Does the application expect PROT_READ to imply PROT_EXEC?
     *
     * (the exception is when the underlying filesystem is noexec
     *  mounted, in which case we dont add PROT_EXEC.)
     */
    if ((prot &amp; PROT_READ) &amp;&amp; (current-&gt;personality &amp; READ_IMPLIES_EXEC))
        if (!(file &amp;&amp; path_noexec(&amp;file-&gt;f_path)))
            prot |= PROT_EXEC;
    /* 假如没有设置MAP_FIXED标志，且addr小于mmap_min_addr, 因为可以修改addr,
    所以就需要将addr设为mmap_min_addr的页对齐后的地址 */
    if (!(flags &amp; MAP_FIXED))
        addr = round_hint_to_min(addr);

    /* Careful about overflows.. 检查长度，防止溢出??*/
    /* 进行Page大小的对齐,因为内存映射大小必须页对齐 */
    len = PAGE_ALIGN(len);
    if (!len)
        return -ENOMEM;

    /* offset overflow? */
    if ((pgoff + (len &gt;&gt; PAGE_SHIFT)) &lt; pgoff)
        return -EOVERFLOW;

    /* Too many mappings? */
    /* 判断该进程的地址空间的虚拟区间数量是否超过了限制 */
    if (mm-&gt;map_count &gt; sysctl_max_map_count)
        return -ENOMEM;

    /* Obtain the address to map to. we verify (or select) it and ensure
     * that it represents a valid section of the address space.
     从当前进程的用户空间获取一个未被映射区间的起始地址:这里就涉及到红黑树了 
     */
    addr = get_unmapped_area(file, addr, len, pgoff, flags);
    if (offset_in_page(addr))/* 检查addr是否有效 */
        return addr;

    if (prot == PROT_EXEC) {
        pkey = execute_only_pkey(mm);
        if (pkey &lt; 0)
            pkey = 0;
    }

    /* Do simple checking here so the lower-level routines won&#039;t have
     * to. we assume access permissions have been handled by the open
     * of the memory object, so we don&#039;t do any here.
     */
    vm_flags |= calc_vm_prot_bits(prot, pkey) | calc_vm_flag_bits(flags) |
            mm-&gt;def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
    /* 假如flags设置MAP_LOCKED，即类似于mlock()将申请的地址空间锁定在内存中, 
    检查是否可以进行lock*/
    if (flags &amp; MAP_LOCKED)
        if (!can_do_mlock())
            return -EPERM;

    if (mlock_future_check(mm, vm_flags, len))
        return -EAGAIN;

    if (file) {
        struct inode *inode = file_inode(file);
        /*根据标志指定的map种类，把为文件设置的访问权考虑进去。
        如果所请求的内存映射是共享可写的，就要检查要映射的文件是为写入而打开的，而不
        是以追加模式打开的，还要检查文件上没有上强制锁。
        对于任何种类的内存映射，都要检查文件是否为读操作而打开的。
        */

        switch (flags &amp; MAP_TYPE) {
        case MAP_SHARED:
            if ((prot&amp;PROT_WRITE) &amp;&amp; !(file-&gt;f_mode&amp;FMODE_WRITE))
                return -EACCES;

            /*
             * Make sure we don&#039;t allow writing to an append-only
             * file..
             */
            if (IS_APPEND(inode) &amp;&amp; (file-&gt;f_mode &amp; FMODE_WRITE))
                return -EACCES;

            /*
             * Make sure there are no mandatory locks on the file.
             */
            if (locks_verify_locked(file))
                return -EAGAIN;

            vm_flags |= VM_SHARED | VM_MAYSHARE;
            if (!(file-&gt;f_mode &amp; FMODE_WRITE))
                vm_flags &amp;= ~(VM_MAYWRITE | VM_SHARED);

            /* fall through */
        case MAP_PRIVATE:
            if (!(file-&gt;f_mode &amp; FMODE_READ))
                return -EACCES;
            if (path_noexec(&amp;file-&gt;f_path)) {
                if (vm_flags &amp; VM_EXEC)
                    return -EPERM;
                vm_flags &amp;= ~VM_MAYEXEC;
            }

            if (!file-&gt;f_op-&gt;mmap)
                return -ENODEV;
            if (vm_flags &amp; (VM_GROWSDOWN|VM_GROWSUP))
                return -EINVAL;
            break;

        default:
            return -EINVAL;
        }
    } else {
        switch (flags &amp; MAP_TYPE) {
        case MAP_SHARED:
            if (vm_flags &amp; (VM_GROWSDOWN|VM_GROWSUP))
                return -EINVAL;
            /*
             * Ignore pgoff.
             */
            pgoff = 0;
            vm_flags |= VM_SHARED | VM_MAYSHARE;
            break;
        case MAP_PRIVATE:
            /*
             * Set pgoff according to addr for anon_vma.
             */
            pgoff = addr &gt;&gt; PAGE_SHIFT;
            break;
        default:
            return -EINVAL;
        }
    }

    /*
     * Set &#039;VM_NORESERVE&#039; if we should not account for the
     * memory use of this mapping.
     */
    if (flags &amp; MAP_NORESERVE) {
        /* We honor MAP_NORESERVE if allowed to overcommit */
        if (sysctl_overcommit_memory != OVERCOMMIT_NEVER)
            vm_flags |= VM_NORESERVE;

        /* hugetlb applies strict overcommit unless MAP_NORESERVE */
        if (file &amp;&amp; is_file_hugepages(file))
            vm_flags |= VM_NORESERVE;
    }
    /*创建和初始化虚拟内存区域，并加入红黑树管理*/
    addr = mmap_region(file, addr, len, vm_flags, pgoff);
    if (!IS_ERR_VALUE(addr) &amp;&amp;
        ((vm_flags &amp; VM_LOCKED) ||
        /*
        假如没有设置MAP_POPULATE标志位内核并不在调用mmap()时就为进程分配物理内存空间，
        而是直到下次真正访问地址空间时发现数据不存在于物理内存空间时才触发Page Fault
        ，将缺失的 Page 换入内存空间
        */
         (flags &amp; (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE))
        *populate = len;
    return addr;
}</code></pre></div><p>&#160; 代码有很多，但是核心功能其实并不复杂：找到空闲的虚拟内存地址，并根据不同的文件打开方式设置不同的vm标志位flag！在函数末尾处调用了mmap_region函数，核心功能是创建和初始化虚拟内存区域，并加入红黑树节点进行管理，代码如下：</p><div class="codebox"><pre class="vscroll"><code>/*创建和初始化虚拟内存区域，并加入红黑树节点进行管理*/
unsigned long mmap_region(struct file *file, unsigned long addr,
        unsigned long len, vm_flags_t vm_flags, unsigned long pgoff)
{
    struct mm_struct *mm = current-&gt;mm;
    struct vm_area_struct *vma, *prev;
    int error;
    struct rb_node **rb_link, *rb_parent;
    unsigned long charged = 0;

    /* Check against address space limit.
       申请的虚拟内存空间是否超过了限制 */
    if (!may_expand_vm(mm, vm_flags, len &gt;&gt; PAGE_SHIFT)) {
        unsigned long nr_pages;

        /*
         * MAP_FIXED may remove pages of mappings that intersects with
         * requested mapping. Account for the pages it would unmap.
         */
        nr_pages = count_vma_pages_range(mm, addr, addr + len);

        if (!may_expand_vm(mm, vm_flags,
                    (len &gt;&gt; PAGE_SHIFT) - nr_pages))
            return -ENOMEM;
    }

    /* Clear old maps 
    检查[addr, addr+len)的区间是否存在映射空间，假如存在重合的映射空间需要munmap*/
    while (find_vma_links(mm, addr, addr + len, &amp;prev, &amp;rb_link,
                  &amp;rb_parent)) {
        if (do_munmap(mm, addr, len))
            return -ENOMEM;
    }

    /*
     * Private writable mapping: check memory availability
     */
    if (accountable_mapping(file, vm_flags)) {
        charged = len &gt;&gt; PAGE_SHIFT;
        if (security_vm_enough_memory_mm(mm, charged))
            return -ENOMEM;
        vm_flags |= VM_ACCOUNT;
    }

    /*
     * Can we just expand an old mapping?
     检查是否可以合并[addr, addr+len)区间内的虚拟地址空间vma
     */
    vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
            NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX);
    if (vma)/* 假如合并成功，即使用合并后的vma, 并跳转至out */
        goto out;

    /*
     * Determine the object being mapped and call the appropriate
     * specific mapper. the address has already been validated, but
     * not unmapped, but the maps are removed from the list.
     如果不能和已有的虚拟内存区域合并，通过 Memory Descriptor 来申请一个 vma
     */
    vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
    if (!vma) {
        error = -ENOMEM;
        goto unacct_error;
    }

    vma-&gt;vm_mm = mm;
    vma-&gt;vm_start = addr;
    vma-&gt;vm_end = addr + len;
    vma-&gt;vm_flags = vm_flags;
    vma-&gt;vm_page_prot = vm_get_page_prot(vm_flags);
    vma-&gt;vm_pgoff = pgoff;
    INIT_LIST_HEAD(&amp;vma-&gt;anon_vma_chain);//vma通过链表连接，这里初始化链表头
    
    /* 假如指定了文件映射 */
    if (file) {
         /* 映射的文件不允许写入，调用 deny_write_accsess(file) 排斥常规的文件操作 */
        if (vm_flags &amp; VM_DENYWRITE) {
            error = deny_write_access(file);
            if (error)
                goto free_vma;
        }
        if (vm_flags &amp; VM_SHARED) {/* 映射的文件允许其他进程可见, 标记文件为可写 */
            error = mapping_map_writable(file-&gt;f_mapping);
            if (error)
                goto allow_write_and_free_vma;
        }

        /* -&gt;mmap() can change vma-&gt;vm_file, but must guarantee that
         * vma_link() below can deny write-access if VM_DENYWRITE is set
         * and map writably if VM_SHARED is set. This usually means the
         * new file must not have been exposed to user-space, yet.
         */
        vma-&gt;vm_file = get_file(file);/* 递增 File 的引用次数，返回 File 赋给 vma */
        error = file-&gt;f_op-&gt;mmap(file, vma); /* 调用文件系统指定的 mmap 函数*/
        if (error)
            goto unmap_and_free_vma;

        /* Can addr have changed??
         *
         * Answer: Yes, several device drivers can do it in their
         *         f_op-&gt;mmap method. -DaveM
         * Bug: If addr is changed, prev, rb_link, rb_parent should
         *      be updated for vma_link()
         */
        WARN_ON_ONCE(addr != vma-&gt;vm_start);

        addr = vma-&gt;vm_start;
        vm_flags = vma-&gt;vm_flags;
        /* 假如标志为 VM_SHARED，但没有指定映射文件，需要调用 shmem_zero_setup()
           shmem_zero_setup() 实际映射的文件是 dev/zero
        */
    } else if (vm_flags &amp; VM_SHARED) {
        error = shmem_zero_setup(vma);
        if (error)
            goto free_vma;
    }
    /*新分配的vma加入红黑树*/
    vma_link(mm, vma, prev, rb_link, rb_parent);
    /* Once vma denies write, undo our temporary denial count */
    if (file) {
        if (vm_flags &amp; VM_SHARED)
            mapping_unmap_writable(file-&gt;f_mapping);
        if (vm_flags &amp; VM_DENYWRITE)
            allow_write_access(file);
    }
    file = vma-&gt;vm_file;
out:
    perf_event_mmap(vma);
    /* 更新进程的虚拟地址空间 mm */
    vm_stat_account(mm, vm_flags, len &gt;&gt; PAGE_SHIFT);
    if (vm_flags &amp; VM_LOCKED) {
        if (!((vm_flags &amp; VM_SPECIAL) || is_vm_hugetlb_page(vma) ||
                    vma == get_gate_vma(current-&gt;mm)))
            mm-&gt;locked_vm += (len &gt;&gt; PAGE_SHIFT);
        else
            vma-&gt;vm_flags &amp;= VM_LOCKED_CLEAR_MASK;
    }

    if (file)
        uprobe_mmap(vma);

    /*
     * New (or expanded) vma always get soft dirty status.
     * Otherwise user-space soft-dirty page tracker won&#039;t
     * be able to distinguish situation when vma area unmapped,
     * then new mapped in-place (which must be aimed as
     * a completely new data area).
     */
    vma-&gt;vm_flags |= VM_SOFTDIRTY;

    vma_set_page_prot(vma);

    return addr;

unmap_and_free_vma:
    vma-&gt;vm_file = NULL;
    fput(file);

    /* Undo any partial mapping done by a device driver. */
    unmap_region(mm, vma, prev, vma-&gt;vm_start, vma-&gt;vm_end);
    charged = 0;
    if (vm_flags &amp; VM_SHARED)
        mapping_unmap_writable(file-&gt;f_mapping);
allow_write_and_free_vma:
    if (vm_flags &amp; VM_DENYWRITE)
        allow_write_access(file);
free_vma:
    kmem_cache_free(vm_area_cachep, vma);
unacct_error:
    if (charged)
        vm_unacct_memory(charged);
    return error;
}</code></pre></div><p>&#160; 以上两个函数的核心功能是查找、分配、初始化空闲的vma，并加入链表和红黑树管理，同时设置vma的各种flags属性，便于后续管理！那么问题来了：数据最终都是要存放在物理内存的，截至目前所有的操作都是虚拟内存，这些vma都是在哪和物理内存建立映射的了？关键的函数是remap_pfn_range，在mm/memory.c文件中；</p><div class="codebox"><pre class="vscroll"><code>/**
 * remap_pfn_range - remap kernel memory to userspace
   将内核空间的内存映射到用户空间，或者说是
   将用户空间的一个vma虚拟内存区映射到以page开始的一段连续物理页面上
 * @vma: user vma to map to：需要映射(或者说挂载关联)物理地址的vma
 * @addr: target user address to start at：用户空间地址的起始位置
 * @pfn: physical address of kernel memory:内核的物理地址空间
 * @size: size of map area
 * @prot: page protection flags for this mapping：内存页面的属性
 *
 *  Note: this is only safe if the mm semaphore is held when called.
 */
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
            unsigned long pfn, unsigned long size, pgprot_t prot)
{
    pgd_t *pgd;
    unsigned long next;
    /*需要映射的虚拟地址尾部：注意要页对齐，因为cpu硬件是以页为单位管理内存的*/
    unsigned long end = addr + PAGE_ALIGN(size);
    struct mm_struct *mm = vma-&gt;vm_mm;
    unsigned long remap_pfn = pfn;
    int err;

    /*
     * Physically remapped pages are special. Tell the
     * rest of the world about it:
     *   VM_IO tells people not to look at these pages
     *    (accesses can have side effects).
     *   VM_PFNMAP tells the core MM that the base pages are just
     *    raw PFN mappings, and do not have a &quot;struct page&quot; associated
     *    with them.
     *   VM_DONTEXPAND
     *      Disable vma merging and expanding with mremap().
     *   VM_DONTDUMP
     *      Omit vma from core dump, even when VM_IO turned off.
     *
     * There&#039;s a horrible special case to handle copy-on-write
     * behaviour that some programs depend on. We mark the &quot;original&quot;
     * un-COW&#039;ed pages by matching them up with &quot;vma-&gt;vm_pgoff&quot;.
     * See vm_normal_page() for details.
     */
    if (is_cow_mapping(vma-&gt;vm_flags)) {
        if (addr != vma-&gt;vm_start || end != vma-&gt;vm_end)
            return -EINVAL;
        vma-&gt;vm_pgoff = pfn;
    }

    err = track_pfn_remap(vma, &amp;prot, remap_pfn, addr, PAGE_ALIGN(size));
    if (err)
        return -EINVAL;
    /*改变虚拟地址的标志*/
    vma-&gt;vm_flags |= VM_IO | VM_PFNMAP | VM_DONTEXPAND | VM_DONTDUMP;

    BUG_ON(addr &gt;= end);
    pfn -= addr &gt;&gt; PAGE_SHIFT;
    /*
    /* To find an entry in a generic PGD。宏定义展开后如下：
    #define pgd_index(address) (((address) &gt;&gt; PGDIR_SHIFT) &amp; (PTRS_PER_PGD-1))
    #define pgd_offset(mm, address) ((mm)-&gt;pgd+pgd_index(address))
    查找addr第1级页目录项中对应的页表项的地址
    */
    pgd = pgd_offset(mm, addr);
    /*刷新TLB缓存；这个缓存和CPU的L1、L2、L3的缓存思想一致，
    既然进行地址转换需要的内存IO次数多，且耗时，
    那么干脆就在CPU里把页表尽可能地cache起来不就行了么，
    所以就有了TLB(Translation Lookaside Buffer)，
    专门用于改进虚拟地址到物理地址转换速度的缓存。
    其访问速度非常快，和寄存器相当，比L1访问还快。*/
    flush_cache_range(vma, addr, end);
    do {
        /*
        计算下一个将要被映射的虚拟地址，如果addr到end可以被一个pgd映射的话，那么返回end的值
        */
        next = pgd_addr_end(addr, end);
        /*完成虚拟内存和物理内存映射，本质就是填写完CR3指向的页表；
        过程就是逐级完成：1级是pgd，上面已经完成；2级是pud，3级是pmd，4级是pte
        */
        err = remap_pud_range(mm, pgd, addr, next,
                pfn + (addr &gt;&gt; PAGE_SHIFT), prot);
        if (err)
            break;
    } while (pgd++, addr = next, addr != end);

    if (err)
        untrack_pfn(vma, remap_pfn, PAGE_ALIGN(size));

    return err;
}</code></pre></div><p>&#160; 最核心的就是remap_pud_range方法了，从这个方法开始，逐级构造页表的各个映射转换！阅读代码前，可以先熟悉一下4级页表转换原理如下：<br /><span class="postimg"><img src="https://img2022.cnblogs.com/blog/2052730/202202/2052730-20220223163732202-1704601936.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160; &#160; 代码如下：3个方法的结构类似，层层深入，直到最后一级pte！pte内部调用set_pte_at方法最终完成物理地址和虚拟地址的映射！</p><div class="codebox"><pre class="vscroll"><code>/*
 * maps a range of physical memory into the requested pages. the old
 * mappings are removed. any references to nonexistent pages results
 * in null mappings (currently treated as &quot;copy-on-access&quot;)
 */
static int remap_pte_range(struct mm_struct *mm, pmd_t *pmd,
            unsigned long addr, unsigned long end,
            unsigned long pfn, pgprot_t prot)
{
    pte_t *pte;
    spinlock_t *ptl;

    pte = pte_alloc_map_lock(mm, pmd, addr, &amp;ptl);
    if (!pte)
        return -ENOMEM;
    arch_enter_lazy_mmu_mode();
    do {
        BUG_ON(!pte_none(*pte));
        /*这是映射的最后一级：把物理地址的值填写到pte表项*/
        set_pte_at(mm, addr, pte, pte_mkspecial(pfn_pte(pfn, prot)));
        pfn++;
    } while (pte++, addr += PAGE_SIZE, addr != end);
    arch_leave_lazy_mmu_mode();
    pte_unmap_unlock(pte - 1, ptl);
    return 0;
}

static inline int remap_pmd_range(struct mm_struct *mm, pud_t *pud,
            unsigned long addr, unsigned long end,
            unsigned long pfn, pgprot_t prot)
{
    pmd_t *pmd;
    unsigned long next;

    pfn -= addr &gt;&gt; PAGE_SHIFT;
    pmd = pmd_alloc(mm, pud, addr);
    if (!pmd)
        return -ENOMEM;
    VM_BUG_ON(pmd_trans_huge(*pmd));
    do {
        next = pmd_addr_end(addr, end);
        if (remap_pte_range(mm, pmd, addr, next,
                pfn + (addr &gt;&gt; PAGE_SHIFT), prot))
            return -ENOMEM;
    } while (pmd++, addr = next, addr != end);
    return 0;
}

static inline int remap_pud_range(struct mm_struct *mm, pgd_t *pgd,
            unsigned long addr, unsigned long end,
            unsigned long pfn, pgprot_t prot)
{
    pud_t *pud;
    unsigned long next;

    pfn -= addr &gt;&gt; PAGE_SHIFT;
    /*返回pgd值*/
    pud = pud_alloc(mm, pgd, addr);
    if (!pud)
        return -ENOMEM;
    do {
        next = pud_addr_end(addr, end);
        if (remap_pmd_range(mm, pud, addr, next,
                pfn + (addr &gt;&gt; PAGE_SHIFT), prot))
            return -ENOMEM;
    } while (pud++, addr = next, addr != end);
    return 0;
}</code></pre></div><p>注意事项&amp;总结事项：</p><p>1、脱壳的时候如果遇到mmap就要注意了：有可能是要加载壳文件了！</p><p>2、页对齐的代码：也可以借鉴用来做其他数字的对齐，把PAGE_SIZE改成其他数字就好</p><p>#define PAGE_MASK (~(PAGE_SIZE-1))<br />#define PAGE_ALIGN(x) ((x + PAGE_SIZE - 1) &amp; PAGE_MASK)</p><p>3、核心原理：只分配1块物理内存，把进程的虚拟地址映射到这块物理内存，达到读写一次到位的目的！ </p><p> </p><p>参考：</p><p>1、https://mp.weixin.qq.com/s/y4LT5rtLZXXSvk66w3tVcQ&#160; 三种实现mmap的方式</p><p>2、https://www.bilibili.com/video/BV1XK411A7q2&#160; linux mmap机制</p><p>3、https://www.bilibili.com/video/BV1mk4y1C76p&#160; mmap机制</p><p>4、https://www.leviathan.vip/2019/01/13/mmap%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/&#160; mmap源码分析</p><p>5、https://www.cnblogs.com/pengdonglin137/p/8150981.html&#160; remap_pfn_range源码分析</p><p>6、https://zhuanlan.zhihu.com/p/79607142&#160; TLB缓存</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Tue, 11 Oct 2022 06:52:52 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=468&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[linux源码解读（七）：文件系统——可执行文件的加载和执行]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=448&amp;action=new</link>
			<description><![CDATA[<p>1、windows中可执行文件是PE格式的，以exe作为后缀结尾（当然驱动sys和动态链接dll也是PE格式的，但普通用户用不上）；用户使用也很方便，直接双击exe文件就能开始运行了；linux也类似，可执行文件是ELF格式的，用户双击也能运行；这么方便的功能在底层是怎么实现的了？先阐述一下大概的流程：</p><p>&#160; 可执行文件是放磁盘的，既然要执行，用户在双击后肯定要先加载到内存的高速缓存区<br />&#160; &#160; &#160; &#160;0号和1号进程都是操作系统的内核，其他用户进程都是这两个进程fork出来的，新进程也不例外，这里先调用fork创建新进程<br />&#160; &#160; &#160; &#160;从高速缓存区读取文件头，里面存了代码段、数据段的起始和size信息，由此设置进程的ldt；<br />&#160; &#160; &#160; &#160;设置新进程的tss，保存运行时的context；<br />&#160; &#160; &#160; &#160;重定位操作系统提供的api地址、设置sp；<br />&#160; &#160; &#160; &#160;分配内存空间存放程序的参数(用户传递的)和环境变量(操作系统内核传递的)；<br />&#160; 目前windows下部分杀毒软件能监控用户有没有点击运行危险的程序，大概率是hook了鼠标双击的中断handler（也就是上述的第一步）；一旦发现用户尝试打开危险程序就弹窗告警！</p><p>&#160; 2、（1）正式解读代码前，先做一些铺垫：早期ds和fs两个寄存器都在使用，区别如下：如果代码在用户态运行，ds和fs都指向用户程序的数据段，没任何区别；但是如果代码在内核态运行，ds指向内核的数据段，而fs仍然指向用户态的数据段！不同的态取数据时需要设置不同的fs值！这点很重要，下面的copy_string()函数会频繁设置fs的值！</p><p><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211209204451993-1012756205.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160;（2）用户输入参数后，总要找个地方存嘛，既然是进程的参数，就放进程的内存空间呗！linux使用了32个页面，一共128KB的内存空间存放用户参数和环境变量，拷贝代码如下（作用和三环下的memcpy是一样的，没本质区别）：</p><div class="codebox"><pre class="vscroll"><code>/*
 * &#039;copy_string()&#039; copies argument/envelope strings from user
 * memory to free pages in kernel mem. These are in a format ready
 * to be put directly into the top of new user memory.
 *
 * Modified by TYT, 11/24/91 to add the from_kmem argument, which specifies
 * whether the string and the string array are from user or kernel segments:
 * 
 * from_kmem     argv *(参数指针，也就是参数地址)           argv **(真正的用户输入参数)
 *    0          user space                                   user space
 *    1          kernel space                                 user space
 *    2          kernel space                                 kernel space
 * 
 * We do this by playing games with the fs segment register.  Since it
 * it is expensive to load a segment register, we try to avoid calling
 * set_fs() unless we absolutely have to.
 */
//// 复制指定个数的参数字符串到参数和环境空间中。
// 参数：argc - 欲添加参数个数; argv - 参数指针数组；page - 参数和环境空间页面
// 指针数组。p - 参数表空间中偏移指针，始终指向已复制串的头部；from_kmem - 字符
// 串来源标志。在 do_execve()函数中，p初始化为指向参数表(128kb)空间的最后一个长
// 字处，参数字符串是以堆栈操作方式逆向往其中复制存放的。因此p指针会随着复制信
// 息的增加而逐渐减小，并始终指向参数字符串的头部。字符串来源标志from_kmem应该
// 是TYT为了给execve()增添执行脚本文件的功能而新加的参数。当没有运行脚本文件的
// 功能时，所有参数字符串都在用户数据空间中。
// 返回：参数和环境空间当前头部指针。若出错则返回0.
/*
char *str=&quot;hello&quot;;
copy_strings(1,&amp;str,page,p,1);
*/
static unsigned long copy_strings(int argc,char ** argv,unsigned long *page,
        unsigned long p, int from_kmem)
{
    char *tmp, *pag=NULL;
    int len, offset = 0;
    unsigned long old_fs, new_fs;

    // 首先取当前段寄存器ds（指向内核数据段）和fs值，分别保存到变量new_fs和
    // old_fs中。如果字符串和字符串数组(指针)来自内核空间，则设置fs段寄存器指向
    // 内核数据段。
    if (!p)
        return 0;    /* bullet-proofing */
    new_fs = get_ds();
    old_fs = get_fs();
    if (from_kmem==2)/*字符串和字符串数组(指针)来自内核空间*/
        set_fs(new_fs);/*参数指针和参数本身都在内核，让fs和ds指向的位置相同，表示当前代码运行的所有数据都用内核态的数据段取*/
    while (argc-- &gt; 0) {/*遍历每个参数*/
        // 首先取需要复制的当前字符串指针。如果字符串在用户空间而字符串数组（字
        // 符串指针）在内核空间，则设置fs段寄存器指向内核数据段(ds).并在内核数
        // 据空间中取了字符串指针tmp之后就立刻回复fs段寄存器原值(fs再指回用户空
        // 间)。否则不用修改fs值而直接从用户空间取字符串指针到tmp.
        if (from_kmem == 1)/*参数指针在内核态，但是参数本身在用户态*/
            set_fs(new_fs);/*因为参数指针在内核态，而下面要用fs访问内核态，所以需要把fs设置程ds*/
        if (!(tmp = (char *)get_fs_long(((unsigned long *)argv)+argc)))/*tmp还是参数指针，并不是参数本身；指向最后一个参数，在argsc--的带动下在遍历每个参数*/
            panic(&quot;argc is wrong&quot;);
        if (from_kmem == 1)
            set_fs(old_fs);/*因为最终的参数在用户态，而下面要用fs去访问，所以要用之前保存的fs还原*/
        // 然后从用户空间取该字符串，并计算该参数字符串长度len.此后tmp指向该字
        // 符串末端。如果该字段字符串长度超过此时参数和环境空间中还剩余的空闲长
        // 度，则空间不够了。于是回复fs段寄存器值(如果被改变的话)并返回0.不过因
        // 为参数和环境空间留有128KB，所以通常不可能发生这种情况。
        len=0;        /* remember zero-padding */
        do {
            len++; /*求单个参数的字符长度*/
        } while (get_fs_byte(tmp++));
        if (p-len &lt; 0) {    /* this shouldn&#039;t happen - 128kB ：P：剩余可存参数和环境变量的字符个数*/
            set_fs(old_fs);
            return 0;
        }
        // 接着我们逆向逐个字符地把字符串复制到参数和环境空间末端处。在循环复制
        // 字符串的字符过程中，我们首先要判断参数和环境空间相应位置是否已经有内
        // 存页面。如果还没有就先为其申请1页内存页面。偏移量offset被用作为在一
        // 个页面中的当前指针偏移量。因为刚开始执行本函数时，偏移量offset被初始
        // 化为0，所以(offset-1&lt;0)肯定成立而使得offset重新被设置为当前p指针在页
        // 面返回内的偏移值。
        while (len) {/*遍历参数的每个字符*/
            --p; /*这里偏移P也在变化，P的初始值PAGE_SIZE*MAX_ARG_PAGES-4，在do_execve中定义的*/
            --tmp;--len; 
            if (--offset &lt; 0) {
                offset = p % PAGE_SIZE;
                // 如果字符串和字符串数组都在内核空间中，那么为了从内核数据空间
                // 复制字符串内容，下面会把fs设置为指向内核数据段。
                if (from_kmem==2)
                    set_fs(old_fs);
                // 如果当前偏移量p所在的串空间页面指针数组项page[p/PAGE_SIZE]==
                // 0,表示此时p指针所处的空间内存页面还不存在，则需申请一空闲内
                // 存页，并将该页面指针填入指针数组，同时也使页面指针pag 指向该
                // 新页面，若申请不到空闲页面则返回0.
                if (!(pag = (char *) page[p/PAGE_SIZE]) &amp;&amp;
                    !(pag = (char *) page[p/PAGE_SIZE] =
                      (unsigned long *) get_free_page())) 
                    return 0;
                // 如果字符串在内核空间，则设置fs段寄存器指向内核数据段(ds)。
                if (from_kmem==2)
                    set_fs(new_fs);

            }
            // 然后从fs段中复制字符串的1字节到参数和环境空间内存页面pag的offset出。
            *(pag + offset) = get_fs_byte(tmp);
        }
    }
    // 如果字符串和字符串数组在内核空间，则恢复fs段寄存器原值。最后，返回参数和
    // 环境空间已复制参数的头部偏移值。
    if (from_kmem==2)
        set_fs(old_fs);
    return p;
}</code></pre></div><p>&#160; 上面的代码稍微有点繁琐，这里画个图直观展示<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211210220627768-1942457394.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160;代码中好些地方涉及到set_fs，为啥要频繁设置fs了？原因如下：argv本身也是个变量，本身也要内存来存放；argv*和argv同理，这两个并不存用户输入的参数，仅仅是指针；argv**指向的地址才是最终存用户输入参数的地方，那么现在问题来了：argv*和argv**可能分别存在内核和用户态，但代码要get_fs函数来读取这两个变量，怎么办了？只能不停的改变fs来反复读取argv*和argv**了！比如from_kmem参数是1，argv*和argv**分别被存放在内核和用户态，此时如果要用get_fs_long读取argv*，需要把fs设置成ds，所以要调用set_fs(new_fs)；读取完argv*后如果要继续读取用户态的argv**，就要把已经改成ds的fs还原成以前的fs了，所以要调用set_fs(old_fs);<br /> * from_kmem&#160; &#160; &#160;argv *(参数指针)&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;argv **(真正的用户输入参数)<br /> *&#160; &#160; 0&#160; &#160; &#160; &#160; &#160; user space&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;user space<br /> *&#160; &#160; 1&#160; &#160; &#160; &#160; &#160; kernel space&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;user space<br /> *&#160; &#160; 2&#160; &#160; &#160; &#160; &#160; kernel space&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;kernel space<br />&#160; &#160;（3）参数或环境变量要求用空格隔开，个数是这样计算的：因为argv是二级指针，所以tmp每隔4个byte查找1次。如果指针是空，说明没指向任何参数；</p><div class="codebox"><pre><code>/*
 * count() counts the number of arguments/envelopes
 */
//// 计算参数个数
// 参数：argv - 参数指针数组，最后一个指针项是NULL
// 统计参数指针数组中指针的个数。关于函数参数传递指针的指针的作用，在sched.c中。
static int count(char ** argv)
{
    int i=0;
    char ** tmp;

    if ((tmp = argv))
        while (get_fs_long((unsigned long *) (tmp++)))
            i++;

    return i;
}</code></pre></div><p>&#160; &#160;（4）总所周知，文件都是有头部信息的，不同平台对不同文件都规定了不同的头部信息，操作系统就是根据文件的头部信息区分文件类型；早期linux 0.11版本的文件头信息如下: 只有8个字段，非常简单；</p><div class="codebox"><pre><code>struct exec {
  unsigned long a_magic;    /* Use macros N_MAGIC, etc for access；可执行文件的类型 */
  unsigned a_text;        /* length of text, in bytes */
  unsigned a_data;        /* length of data, in bytes */
  unsigned a_bss;        /* length of uninitialized data area for file, in bytes */
  unsigned a_syms;        /* length of symbol table data in file, in bytes */
  unsigned a_entry;        /* start address */
  unsigned a_trsize;        /* length of relocation info for text, in bytes */
  unsigned a_drsize;        /* length of relocation info for data, in bytes */
};</code></pre></div><p>&#160; &#160; a_magic字段的取值：</p><div class="codebox"><pre><code>#ifndef N_MAGIC
#define N_MAGIC(exec) ((exec).a_magic)
#endif

#ifndef OMAGIC
/* Code indicating object file or impure executable.  */
#define OMAGIC 0407
/* Code indicating pure executable.  */
#define NMAGIC 0410
/* Code indicating demand-paged executable.  */
#define ZMAGIC 0413
#endif /* not OMAGIC */</code></pre></div><p>&#160; &#160; （5）shell脚本中，上个可执行文件运行完后，如果要接着运行下一个脚本，需要更改ldt，linux 的代码如下；注意：ldt中代码段基址和数据段基址是一样的！</p><div class="codebox"><pre class="vscroll"><code>//// 修改任务局部描述符表的内容
// 修改局部描述符表LDT中描述符的段基址和段限长，并将参数和环境空间页面放置在数
// 据段末端。
// 参数：text_size - 执行文件头部中a_text字段给出的代码长度值；
// page - 参数和环境空间页面指针数组。
// 返回：数据段限长值(64MB)
static unsigned long change_ldt(unsigned long text_size,unsigned long * page)
{
    unsigned long code_limit,data_limit,code_base,data_base;
    int i;

    // 首先根据执行文件头部代码长度字段a_text值，计算以页面长度为边界的代码段限
    // 长。并设置数据段查高难度为64 MB.然后取当前进程局部描述符表代码段描述符中
    // 代码段基址，代码段基址与数据段基址相同。并使用这些新值重新设置局部表中代
    // 码段和数据段描述符中的基址和段限长。这里请注意，由于被加载的新程序的代码
    // 和数据段基址与原程序相同，因此没有必要再重复去设置他们。
    code_limit = text_size+PAGE_SIZE -1;/*PAGE_SIZE是文件头长度，这里加上代码段长度*/
    code_limit &amp;= 0xFFFFF000;/*代码段低12bit清零，和页对齐*/
    data_limit = 0x4000000;/*数据段长度*/
    code_base = get_base(current-&gt;ldt[1]);
    data_base = code_base;/*代码段和数据段的基址都一样*/
    set_base(current-&gt;ldt[1],code_base);
    set_limit(current-&gt;ldt[1],code_limit);
    set_base(current-&gt;ldt[2],data_base);
    set_limit(current-&gt;ldt[2],data_limit);
/* make sure fs points to the NEW data segment */
    // fs段寄存器中放入局部表数据段描述符的选择符(0x17)。即默认情况下fs都指向任
    // 务数据段。__asm__(&quot;pushl $0x17\n\tpop %%fs&quot;::)；
    // 然后将参数和环境空间已存放数据的页面（最多有MAX_ARG_PAGES页，128kb）放到
    // 数据段末端。方法是从进程空间末端逆向一页一页地放。函数put_page()用于吧物
    // 理页面映射到进程逻辑空间中。
    __asm__(&quot;pushl $0x17\n\tpop %%fs&quot;::);
    data_base += data_limit;
    for (i=MAX_ARG_PAGES-1 ; i&gt;=0 ; i--) {
        data_base -= PAGE_SIZE;
        if (page[i])
            put_page(page[i],data_base);
    }
    return data_limit;
}</code></pre></div><p> （6）前面做了大量铺垫，本文最重要的函数do_execve终于闪亮登场。整个流程原理上并不复杂：</p><p>&#160; &#160; 先把文件从磁盘读到缓存区，然后检查文件的各种权限<br />&#160; &#160; &#160; 然后检查文件头a_magic字段，看看是shell还是elf（当然这个时候的文件头和现在的elf格式差异还挺大的，但原理都是一样的）；<br />&#160; &#160; &#160; 如果是shell，拷贝环境变量和参数，并逐行读取shell命令执行<br />&#160; &#160; &#160; 如果是可执行文件，同样拷贝环境变量和参数，设置ldt；<br />&#160; &#160; &#160; 根据文件头的a_entry数据设置eip的值<br />&#160; 前面4步都是准备工作，第5步相当于扣动扳机了！</p><div class="codebox"><pre class="vscroll"><code>/*
 * &#039;do_execve()&#039; executes a new program.
 */
//// execve()系统中断调用函数。加载并执行子进程
// 该函数是系统中断调用（int 0x80）功能号__NR_execve调用的函数。函数的参数是进
// 入系统调用处理过程后直接到调用本系统嗲用处理过程和调用本函数之前逐步压入栈中
// 的值。
// eip - 调用系统中断的程序代码指针。
// tmp - 系统中断中在调用_sys_execve时的返回地址，无用；
// filename - 被执行程序文件名指针；
// argv - 命令行参数指针数组的指针；
// envp - 环境变量指针数组的指针。
// 返回：如果调用成功，则不返回；否则设置出错号，并返回-1.
/*
    ./add  1  2  3  
    ./run.sh  helloworld
*/
int do_execve(unsigned long * eip,long tmp,char * filename,
    char ** argv, char ** envp)
{
    struct m_inode * inode;
    struct buffer_head * bh;
    struct exec ex;
    unsigned long page[MAX_ARG_PAGES];/*每个进程都有参数指针数组；注意：每个元素都是参数指针，并不直接存放参数*/
    int i,argc,envc;
    int e_uid, e_gid;
    int retval;
    int sh_bang = 0;                            // 控制是否需要执行的脚本程序
    /*p指向参数和环境空间的最后部；注意：P只是个偏移，不是绝对地址；
    每次调用copy_string后P都会减少，以此确保copy到连续的内存空间
    */ 
    unsigned long p=PAGE_SIZE*MAX_ARG_PAGES-4;  //p=128KB-4

    // 在正式设置执行文件的运行环境之前，让我们先干这些杂事。内核准备了128kb(32
    // 个页面)空间来存放化执行文件的命令行参数和环境字符串。上杭把p初始设置成位
    // 于128KB空间中的当前位置。
    // 另外，参数eip[1]是调用本次系统调用的原用户程序代码段寄存器CS值，其中的段
    // 选择符当然必须是当前任务的代码段选择符0x000f.若不是该值，那么CS只能会是
    // 内核代码段的选择符0x0008.但这是绝对不允许的，因为内核代码是常驻内存而不
    // 能被替换掉的。因此下面根据eip[1]的值确认是否符合正常情况。然后再初始化
    // 128KB的参数和环境串空间，把所有字节清零，并取出执行文件的i节点。再根据函
    // 数参数分别计算出命令行参数和环境字符串的个数argc和envc。另外，执行文件必
    // 须是常规文件。
    if ((0xffff &amp; eip[1]) != 0x000f)/*先判断一下权限够不够*/
        panic(&quot;execve called from supervisor mode&quot;);
    for (i=0 ; i&lt;MAX_ARG_PAGES ; i++)    /* clear page-table */
        page[i]=0;/*参数指针清零*/
    if (!(inode=namei(filename)))        /* get executables inode：查看文件的元信息 */
        return -ENOENT;
    argc = count(argv);
    envc = count(envp);
    
restart_interp:
    if (!S_ISREG(inode-&gt;i_mode)) {    /* must be regular file */
        retval = -EACCES;
        goto exec_error2;
    }
    // 下面检查当前进程是否有权运行指定的执行文件。即根据执行文件i节点中的属性，
    // 看看本进程是否有权执行它。在把执行文件i节点的属性字段值取到i中后，我们首
    // 先查看属性中是否设置了&quot;设置-用户-ID&quot;(set-user_id)标志和“设置-组-ID”(set_group-id)
    // 标志。这两个标志主要是让一般用户能够执行特权用户(如超级用户root)的程序，
    // 例如改变密码的程序passwd等。如果set-user-id标志置位，则后面执行进程的有
    // 效用户ID(euid)就设置成执行文件的用户ID，否则设置成当前进程的euid。如果执
    // 行文件set-group-id被置位的话，则执行进程的有效组ID（egid）就被设置为执行
    // 文件的组ID。否则设置成当前进程的egid。这里暂时把这两个判断出来的值保存在
    // 变量e_uid和e_gid中。
    i = inode-&gt;i_mode;                      // 取文件属性字段
    e_uid = (i &amp; S_ISUID) ? inode-&gt;i_uid : current-&gt;euid;
    e_gid = (i &amp; S_ISGID) ? inode-&gt;i_gid : current-&gt;egid;
    // 现在根据进程的euid和egid和执行文件的访问属性进行比较。如果执行文件属于运
    // 行进程的用户，则把文件属性值i右移6位，此时最低3位是文件宿主的访问权限标
    // 志。否则的话如果执行文件与当前进程的用户属性同租，则使属性值最低3位是执
    // 行文件组用户的访问权限标志。否则此时属性值最低3位就是其他用户访问该执行
    // 文件的权限。
    // 然后我们根据属性字i的最低3bit值来判断当前进程是否有权限运行这个执行文件。
    // 如果选出的相应用户没有运行该文件的权利(位0是执行权限)，并且其他用户也没
    // 有任何权限或者当前进程用户不是超级用户，则表明当前进程没有权利运行这个执
    // 行文件。于是置不可执行出错码，并跳转到exec_error2处去做退出处理。
    if (current-&gt;euid == inode-&gt;i_uid)
        i &gt;&gt;= 6;
    else if (current-&gt;egid == inode-&gt;i_gid)
        i &gt;&gt;= 3;
    if (!(i &amp; 1) &amp;&amp;/*是否有执行权限*/
        !((inode-&gt;i_mode &amp; 0111) &amp;&amp; suser())) {
        retval = -ENOEXEC;
        goto exec_error2;
    }
    // 程序执行到这里，说明当前进程有运行指定执行文件的权限。因此从这里开始我们
    // 需要取出执行文件头部数据并根据其中的信息来分析设置运行环境，或者运行另一
    // 个shell程序来执行脚本程序。首先读取执行文件第1块数据到高速缓冲块中。并复
    // 制缓冲块数据到ex中。如果执行文件开始的两个字节是字符&#039;#!&#039;，则说明执行文件
    // 是一个脚本文件。如果想运行脚本文件，我们就需要执行脚本文件的解释程序(例
    // 如shell程序)。 他指明了运行脚本
    // 文件需要的解释程序。运行方法从脚本文件第一行中取出其中的解释程序名及后面
    // 的参数(若有的话)，然后将这些参数和脚本文件名放进执行文件（此时是解释程序）
    // 的命令行参数空间中。在这之前我们当然需要先把函数指定的原有命令行参数和环
    // 境字符串放到128KB空间中，而这里建立起来的命令行参数则放到它们前面位置处(
    // 因为是逆向放置)。最后让内核执行脚本文件的解释程序。下面就是在设置好解释
    // 程序的脚本文件名等参数后，取出解释程序的i节点并跳转去执行解释程序。由于
    // 我们需要跳转去执行，因此在下面确认处并处理了脚本文件之后需要设置一个禁止
    // 再次执行下面的脚本处理代码标志sh_bang。在后面的代码中该标志也用来表示我
    // 们已经设置好执行的命令行参数，不用重复设置。
    if (!(bh = bread(inode-&gt;i_dev,inode-&gt;i_zone[0]))) {/*当前执行程序或shell脚本文件头结构，就是ELF的雏形*/
        retval = -EACCES;
        goto exec_error2;
    }
    ex = *((struct exec *) bh-&gt;b_data);    /* read exec-header：读取的文件头数据用结构体“格式化” */
    if ((bh-&gt;b_data[0] == &#039;#&#039;) &amp;&amp; (bh-&gt;b_data[1] == &#039;!&#039;) &amp;&amp; (!sh_bang)) {/*根据a_magic判断文件类型，这里是shell脚本*/
        /*
         * This section does the #! interpretation.
         * Sorta complicated, but hopefully it will work.  -TYT
         */

        char buf[1023], *cp, *interp, *i_name, *i_arg;
        unsigned long old_fs;

        // 从这里开始，我们从脚本文件中提取解释程序名以及其参数，并把解释程序名、
        // 解释程序的参数和脚本文件名组合放入环境参数块中。首先复制脚本文件头1
        // 行字符&#039;#!&#039;后面的字符串到buf中，其中含有脚本解释程序名，也可能包含解
        // 释程序的几个参数。然后对buf中的内容进行处理。删除开始空格、制表符。
        strncpy(buf, bh-&gt;b_data+2, 1022);/*跳过#!两个char字符，把后续所有的数据拷到buf*/
        brelse(bh);
        iput(inode);
        buf[1022] = &#039;\0&#039;;/*一个block是1024byte，除去文件头的#!，只剩1022byte了；然后以0结尾*/
        if ((cp = strchr(buf, &#039;\n&#039;))) {
            *cp = &#039;\0&#039;;
            for (cp = buf; (*cp == &#039; &#039;) || (*cp == &#039;\t&#039;); cp++);
        }
        if (!cp || *cp == &#039;\0&#039;) {
            retval = -ENOEXEC; /* No interpreter name found */
            goto exec_error1;
        }
        // 此时我们得到了开头是脚本解释程序名的一行内容(字符串)。下面分析改行。
        // 首先取第一个字符串，它应该是解释程序名，此时i_name指向该名称。若解释
        // 程序名后还有字符，则它们应该是解释程序的参数串，于是令i_arg指向该串。
        interp = i_name = cp;
        i_arg = 0;
        for ( ; *cp &amp;&amp; (*cp != &#039; &#039;) &amp;&amp; (*cp != &#039;\t&#039;); cp++) {
             if (*cp == &#039;/&#039;)
                i_name = cp+1;
        }
        if (*cp) {
            *cp++ = &#039;\0&#039;;
            i_arg = cp;
        }
        /*
         * OK, we&#039;ve parsed out the interpreter name and
         * (optional) argument.
         */
        // 现在我们要把上面解析出来的解释程序名i_name及其参数i_arg和脚本文件名作
        // 即使程序的参数放进环境和参数块中。不过首先我们需要把函数提供的原来一
        // 些参数和环境字符串先放进去，然后再放这里解析出来的。例如对于命令行参
        // 数来说，如果原来的参数是&quot;-arg1-arg2&quot;、解释程序名是bash、其参数是&quot;-iarg1
        //  -iarg2&quot;、脚本文件名(即原来的执行文件名)是&quot;example.sh&quot;，那么放入这里
        //  的参数之后，新的命令行类似于这样：
        //  &quot;bash -iarg1 -iarg2 example.sh -arg1 -arg2&quot;
        //  这里我们把sh_bang标志置上，然后把函数参数提供的原有参数和环境字符串
        //  放入到空间中。环境字符串和参数个数分别是envc和argc-1个。少复制的一
        //  个原有参数是原来的执行文件名，即这里的脚本文件名。[[??? 这里可以看
        //  出，实际上我们需要去另行处理脚本文件名，即这里完全可以复制argc个参
        //  数，包括原来执行文件名(即现在的脚本文件名)。因为它位于同一个位置上]]
        //  注意！这里指针p随着复制信息增加而逐渐向小地址方向移动，因此这两个复
        //  制串函数执行完后，环境参数串信息块位于程序命令行参数串信息块的上方，
        //  并且p指向程序的第一个参数串。copy_strings()最后一个参数(0)指明参数
        //  字符串在用户空间。
        if (sh_bang++ == 0) {
            /*每拷贝1次，P就减少拷贝的字节数*/
            p = copy_strings(envc, envp, page, p, 0);
            /*下面传入的P是上面的返回值；由于P是偏移，所以下面接着上面往下继续copy*/
            p = copy_strings(--argc, argv+1, page, p, 0);
        }
        /*
         * Splice in (1) the interpreter&#039;s name for argv[0]
         *           (2) (optional) argument to interpreter
         *           (3) filename of shell script
         *
         * This is done in reverse order, because of how the
         * user environment and arguments are stored.
         */
        // 接着我们逆向复制脚本文件名、解释程序的参数和解释程序文件名到参数和环
        // 境空间中。若出错，则置出错码，跳转到exec_error1。另外，由于本函数参
        // 数提供的脚本文件名filename在用户空间，而这里赋予copy_string()的脚本
        // 文件名指针在内核空间，因此这个复制字符串函数的最后一个参数(字符串来
        // 源标志)需要被设置成1.若字符串在内核空间，则copy_strings()的最后一个
        // 参数要设置成2。
        p = copy_strings(1, &amp;filename, page, p, 1);
        argc++;
        if (i_arg) {
            p = copy_strings(1, &amp;i_arg, page, p, 2);
            argc++;
        }
        p = copy_strings(1, &amp;i_name, page, p, 2);
        argc++;
        if (!p) {
            retval = -ENOMEM;
            goto exec_error1;
        }
        /*
         * OK, now restart the process with the interpreter&#039;s inode.
         */
        // 最后我们取得解释程序的i节点指针，然后跳转到上面去执行解释程序。为了
        // 获得解释程序的i节点，我们需要使用namei()函数，但是该函数所使用的参数
        // (文件名)是从用户数据空间得到的，即从段寄存器fs指向空间中取得。因此调
        // 用namei()函数之前我们需要先临时让fs指向内核数据空间，以让函数能从内
        // 核空间得到解释程序名，并在namei()返回后恢复fs的默认设置。因此这里我
        // 们先临时保存原fs段寄存器（原指向用户数据段）的值，将其设置成指向内核
        // 数据段，然后取解释程序的i节点。之后再恢复fs的原值。并跳转到restart_interp
        // 出重新处理新的执行文件——脚本文件解释程序。
        old_fs = get_fs();
        set_fs(get_ds());
        if (!(inode=namei(interp))) { /* get executables inode */
            set_fs(old_fs);
            retval = -ENOENT;
            goto exec_error1;
        }
        set_fs(old_fs);
        goto restart_interp;
    }
    // 此时缓冲块中的执行文件头结构数据已经复制到了ex中。于是先释放该缓冲块，并
    // 开始对ex中的执行头信息进行判断处理。对于Linux0.11内核来说，它仅支持ZMAGIC
    // 执行文件格式，并且执行文件代码都从逻辑地址0开始执行，因此不支持含有代码
    // 或数据重定位信息的执行文件。当然，如果执行文件实在太大或者执行文件残缺不
    // 全，那么我们也不能运行它。因此对于下列情况将不执行程序：如果执行文件不是
    // 需求页可执行文件（ZMAGIC）、或者代码和数据重定位部分不等于0，或者（代码段
    // + 数据段+堆）长度超过50MB、或者执行文件长度小于（代码段+数据段+符号表长度
    // +执行头部分）长度的总和。
    brelse(bh);
    if (N_MAGIC(ex) != ZMAGIC || ex.a_trsize || ex.a_drsize ||
        ex.a_text+ex.a_data+ex.a_bss&gt;0x3000000 ||/*程序加上栈堆才4M，这么大的量肯定是错的*/
        inode-&gt;i_size &lt; ex.a_text+ex.a_data+ex.a_syms+N_TXTOFF(ex)) {
        retval = -ENOEXEC;
        goto exec_error2;
    }
    // 另外，如果执行文件中代码开始处没有位于1个页面(1024字节)边界处，则也不能
    // 执行。因为需求页(Demand paging)技术要求加载执行文件内容时以页面为单位，
    // 因此要求执行文件映象中代码和数据都从页面边界处开始。
    if (N_TXTOFF(ex) != BLOCK_SIZE) {
        printk(&quot;%s: N_TXTOFF != BLOCK_SIZE. See a.out.h.&quot;, filename);
        retval = -ENOEXEC;
        goto exec_error2;
    }
    // 如果sh_bang标志没有设置，则复制指定个数的命令行参数和环境字符串到参数和
    // 环境空间中。若sh_bang标志已经设置，则表明是将运行脚本解释程序，此时环境
    // 变量页面已经复制，无须再复制。同样，若sh_bang没有置位而需要复制的话，那
    // 么此时指针p随着复制信息增加而逐渐向小地址方向移动，因此这两个复制串函数
    // 执行完后，环境参数串信息块位于程序参数串信息块上方，并且p指向程序的第1个
    // 参数串。事实上，p是128KB参数和环境空间中的偏移值。因此如果p=0，则表示环
    // 境变量与参数空间页面已经被占满，容纳不下了。
    if (!sh_bang) {
        p = copy_strings(envc,envp,page,p,0);
        p = copy_strings(argc,argv,page,p,0);
        if (!p) {
            retval = -ENOMEM;
            goto exec_error2;
        }
    }
/* OK, This is the point of no return */
    // 前面我们针对函数参数提供的信息对需要运行执行文件的命令行参数和环境空间进
    // 行了设置，但还没有为执行文件做过什么实质性的工作，即还没有做过为执行文件
    // 初始化进程任务结构信息、建立页表等工作。现在我们就来做这些工作。由于执行
    // 文件直接使用当前进程的“躯壳”，即当钱进程将被改造成执行文件的进程，因此我
    // 们需要首先释放当前进程占用的某些系统资源，包括关闭指定的已打开文件、占用
    // 的页表和内存页面等。然后根据执行文件头结构信息修改当前进程使用的局部描述
    // 符表LDT中描述符的内容，重新设置代码段和数据段描述符的限长，再利用前面处
    // 理得到的e_uid和e_gid等信息来设置进程任务结构中相关的字段。最后把执行本次
    // 系统调用程序的返回地址eip[]指向执行文件中代码的其实位置处。这样当本系统
    // 调用退出返回后就会去运行新执行文件的代码了。注意，虽然此时新执行文件代码
    // 和数据还没有从文件中加载到内存中，但其参数和环境块已经在copy_strings()中
    // 使用get_free_page()分配了物理内存页来保存数据，并在change_ldt()函数中使
    // 用put_page()放到了进程逻辑空间的末端处。另外，在create_tables()中也会由
    // 于在用户栈上存放参数和环境指针表而引起缺页异常，从而内存管理程序也会就此
    // 为用户栈空间映射物理内存页。
    //
    // 这里我们首先放回进程原执行程序的i节点，并且让进程executable字段指向新执行
    // 文件的i节点。然后复位原进程的所有信号处理句柄。再根据设定的执行时关闭文件
    // 句柄（close_on_exec）位图标志，关闭指定的打开文件，并复位该标志。
    /*
    一个shell脚本可能有N个文件顺序执行；执行完前面的文件后，需要把当前线程的executable换成下一个文件的
    */
    if (current-&gt;executable)
        iput(current-&gt;executable);
    current-&gt;executable = inode;/*指向当前需要执行文件的inode节点*/
    for (i=0 ; i&lt;32 ; i++)
        current-&gt;sigaction[i].sa_handler = NULL;/*清空当前进程的信号处理函数*/
    for (i=0 ; i&lt;NR_OPEN ; i++)
        if ((current-&gt;close_on_exec&gt;&gt;i)&amp;1)/*原线程执行后该关闭的文件都先关闭了，并清空位图*/
            sys_close(i);
    current-&gt;close_on_exec = 0;
    // 然后根据当前进程指定的基地址和限长，释放原来程序的代码段和数据段所对应的
    // 内存页表指定的物理内存页面及页表本身。此时新执行文件并没有占用主内存区任
    // 何页面，因此在处理器真正运行新执行文件代码时就会引起缺页异常中断，此时内
    // 存管理程序执行缺页处理而为新执行文件申请内存页面和设置相关表项，并且把相
    // 关执行文件页面读入内存中。如果“上次任务使用了协处理器”指向的是当前进程，
    // 则将其置空，并复位使用了协处理器的标志。
    free_page_tables(get_base(current-&gt;ldt[1]),get_limit(0x0f));
    free_page_tables(get_base(current-&gt;ldt[2]),get_limit(0x17));
    if (last_task_used_math == current)
        last_task_used_math = NULL;
    current-&gt;used_math = 0;
    // 然后我们根据新执行文件头结构中的代码长度字段a_text的值修改局部表中描述符
    // 基地址和段限长，并将128KB的参数和环境空间页面放置在数据段末端。执行下面
    // 语句之后，p此时更改成以数据段起始处为原点的偏移值，但仍指向参数和环境空
    // 间数据开始处，即已转换成为栈指针值。然后调用内部函数create_tables()在栈中
    // 穿件环境和参数变量指针表，供程序的main()作为参数使用，并返回该栈指针。
    p += change_ldt(ex.a_text,page)-MAX_ARG_PAGES*PAGE_SIZE;/*上一个文件已经执行完，当然要换成下一个文件的ldt*/
    p = (unsigned long) create_tables((char *)p,argc,envc);
    // 接着再修改各字段值为新执行文件的信息。即令进程任务结构代码尾字段end_code
    // 等于执行文件的代码长度a_text；数据尾字段end_data等于执行文件的代码段长度
    // 加数据段长度(a_data+a_text)；并令进程堆结尾字段brk=a_text+a_data+a_bss.
    // brk用于指明进程当前数据段（包括未初始化数据部分）末端位置。然后设置进程
    // 栈开始字段为栈指针所在页面，并重新设置进程的有效用户id和有效组id。
    current-&gt;brk = ex.a_bss +
        (current-&gt;end_data = ex.a_data +
        (current-&gt;end_code = ex.a_text));
    current-&gt;start_stack = p &amp; 0xfffff000;
    current-&gt;euid = e_uid;
    current-&gt;egid = e_gid;
    // 如果执行文件代码加数据长度的末端不再页面边界上，则把最后不到1页长度的内
    // 存过空间初始化为零。
    i = ex.a_text+ex.a_data;
    while (i&amp;0xfff)
        put_fs_byte(0,(char *) (i++));
    // 最后将原调用系统中断的程序在堆栈上的代码指针替换为指向新执行程序的入口点，
    // 并将栈指针替换为执行文件的栈指针。此后返回指令将这些栈数据并使得CPU去执
    // 行新执行文件，因此不会返回到原调用系统中断的程序中去了。
    eip[0] = ex.a_entry;        /* eip, magic happens :-)：前面做了大量的准备工作，终于在这里改eip了 */
    eip[3] = p;            /* stack pointer */
    return 0;
exec_error2:
    iput(inode);
exec_error1:
    for (i=0 ; i&lt;MAX_ARG_PAGES ; i++)
        free_page(page[i]);
    return(retval);
}</code></pre></div><p>&#160; &#160;上述代码很多，为方便理解，这里继续画个内存分布图：大部分代码都在往进程的空间拷贝各种数据！<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211212212403421-1440366016.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; 总的来说，进程内存从上到下的分布：参数列表、环境变量、栈、数据段、代码段；</p><p>&#160; 某大厂有句厂训名言：指哪打哪！意思就是领导要求基层员工干啥，基层员工就干啥，100%服从命令，不能有任何质疑！个人感觉cpu是典型的指哪打哪：</p><p>&#160; ds指向的内存地址就是数据段的开始，mov等指令就从ds指定的数据段取数据；至于业务逻辑上是不是对的，cpu硬件是没法判断的，需要软件程序员来确保！<br />&#160; &#160; &#160; &#160;ss指向的内存地址就是栈段的开始，push、pop等指令就在ss指定的段读写数据；<br />&#160; &#160; &#160; &#160;cs段指向的内存地址就是代码段的开始，eip就把当前指向内存地址的二进制码读出来当成代码执行；至于取出来的二进制码是不是代码（比如错误把数据当成了代码），执行的逻辑是不是对的，cpu硬件也是没法判断的，同样需要软件程序员来保障！<br />&#160; 理清了上面的逻辑后再去看代码，发现代码虽然多，但是并不难：主要就干了下面几件事：</p><p>&#160; &#160; &#160;权限检查、其他struct的属性字段（文件inode、进程task struct）<br />&#160; &#160; &#160;数据来回复制倒腾（我感觉80%都是这类代码）<br />&#160; &#160; &#160;设置eip，跳转到目标地址</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sat, 08 Oct 2022 04:57:22 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=448&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[linux源码解读（六）：文件系统——虚拟文件系统VFS]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=447&amp;action=new</link>
			<description><![CDATA[<p>linux的设计理念：万物皆文件！换句话说：所有的设备，包括但不限于磁盘、串口、网卡、pipe管道、打印机等统一看成是文件。对于用户来说，所有操作都是通过open、read、write、ioctl、close等接口操作的，确实很方便；但是对于linux，底层明明是不同的硬件设备，这些设备怎么才能统一被上述接口识别和适配了？识别和适配这层接口的功能就是虚拟文件系统，简称VFS，整体架构图如下：</p><p><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211207204354477-1600486215.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160;1、file_dev.c定义了文件的读写函数；</p><p>&#160; &#160; &#160; &#160; &#160; （1）为了更好地管理文件，linux同样也定义了file结构体：</p><div class="codebox"><pre><code>struct file {
    unsigned short f_mode;/*FMODE_READ或FMODE_WRITE，标识标识文件是否可读或可写*/
    unsigned short f_flags;/*O_RDONLY/O_NONBLOCK/O_SYNC：O_NONBLOCK 打开文件是否阻塞*/
    unsigned short f_count;/*文件被多少进程引用？*/
    struct m_inode * f_inode;/*文件对应的inode节点，里面存了很多文件的元信息；文件存放的描述：磁盘block-&gt;inode-&gt;struct file-&gt;file_table-&gt;file descriptor*/
    off_t f_pos;/*当前文件的读写位置偏移，lseek修改的*/
};</code></pre></div><p>&#160; （2）这个file_read函数已经很接近用户使用的read函数了，仅仅是多了第一个inode参数：</p><div class="codebox"><pre class="vscroll"><code>//// 文件读函数 - 根据i节点和文件结构，读取文件中数据。
// 由i节点我们可以知道设备号，由filp结构可以知道文件中当前读写指针位置。buf指定
// 用户空间中缓冲区位置，count是需要读取字节数。返回值是实际读取的字节数，或出错号(小于0).
int file_read(struct m_inode * inode, struct file * filp, char * buf, int count)
{
    int left,chars,nr;
    struct buffer_head * bh;

    // 首先判断参数的有效性。若需要读取的字节数count小于等于0，则返回0.若还需要读
    // 取的字节数不等于0，就循环执行下面操作，直到数据全部读出或遇到问题。在读循环
    // 操作过程中，我们根据i节点和文件表结构信息，并利用bmap()得到包含文件当前读写
    // 位置的数据块在设备上对应的逻辑块号nr。若nr不为0，则从i节点指定的设备上读取该
    // 逻辑块。如果读操作是吧则退出循环。若nr为0，表示指定的数据块不存在，置缓冲块
    // 指针为NULL。(filp-&gt;f_pos)/BLOCK_SIZE用于计算出文件当前指针所在的数据块号。
    if ((left=count)&lt;=0)
        return 0;
    while (left) {
        if ((nr = bmap(inode,(filp-&gt;f_pos)/BLOCK_SIZE))) {/*把f_pos位置之前数据所在磁盘的block号返回*/
            if (!(bh=bread(inode-&gt;i_dev,nr)))/*从对应的磁盘block号读取数据，也就是从磁盘读取(filp-&gt;f_pos)/BLOCK_SIZE块的数据到内存的缓存区*/
                break;
        } else
            bh = NULL;
        // 接着我们计算文件读写指针在数据块中的偏移值nr，则在该数据块中我们希望读取的
        // 字节数为(BLOCK_SIZE-nr)。然后和现在还需读取的字节数left做比较。其中小值
        // 即为本次操作需读取的字节数chars。如果(BLOCK_SIZE-nr) &gt; left，则说明该块
        // 是需要读取的最后一块数据。反之还需要读取下一块数据。之后调整读写文件指针。
        // 指针前移此次将读取的字节数chars，剩余字节计数left相应减去chars。
        nr = filp-&gt;f_pos % BLOCK_SIZE;/*f_pos在block内的offset*/
        chars = MIN( BLOCK_SIZE-nr , left );/*BLOCK_SIZE-nr：当前块的剩余区域；left：文件要读取的剩余size; 注意：要读取的count不一定等于文件当前大小f_pos*/
        filp-&gt;f_pos += chars;
        left -= chars;
        // 若上面从设备上读到了数据，则将p指向缓冲块中开始读取数据的位置，并且复制chars
        // 字节到用户缓冲区buf中。否则往用户缓冲区中填入chars个0值字节。
        if (bh) {
            char * p = nr + bh-&gt;b_data;
            while (chars--&gt;0)
                put_fs_byte(*(p++),buf++);/*从内核缓存区copy到用户指定的buf*/
            brelse(bh);
        } else {
            while (chars--&gt;0)
                put_fs_byte(0,buf++);
        }
    }
    // 修改该i节点的访问时间为当前时间。返回读取的字节数，若读取字节数为0，则返回
    // 出错号。CURRENT_TIME是定义在include/linux/sched.h中的宏，用于计算UNIX时间。
    // 即从1970年1月1日0时0分0秒开始，到当前的时间，单位是秒。
    inode-&gt;i_atime = CURRENT_TIME;
    return (count-left)?(count-left):-ERROR;
}</code></pre></div><p>&#160; &#160;与上面对应的是file_write，把用户指定的数据写入缓存区（注意：此时并未调用sys_sync函数将数据从缓存区写入磁盘）：</p><div class="codebox"><pre class="vscroll"><code>//// 文件写函数 - 根据i节点和文件结构信息，将用户数据写入文件中。
// 由i节点我们可以知道设备号，而由file结构可以知道文件中当前读写指针位置。buf指定
// 用户态缓冲区的位置，count为需要写入的字节数。返回值是实际写入的字节数，或出错号。
int file_write(struct m_inode * inode, struct file * filp, char * buf, int count)
{
    off_t pos;
    int block,c;
    struct buffer_head * bh;
    char * p;
    int i=0;

/*
 * ok, append may not work when many processes are writing at the same time
 * but so what. That way leads to madness anyway.
 */
    // 首先确定数据写入文件的位置。如果是要向文件后添加数据，则将文件读写指针移到
    // 文件尾部。否则就将在文件当前读写指针处写入。
    if (filp-&gt;f_flags &amp; O_APPEND)
        pos = inode-&gt;i_size;
    else
        pos = filp-&gt;f_pos;
    // 然后在已写入字节数i(刚开始为0)小于指定写入字节数count时，循环执行以下操作。
    // 在循环操作过程中，我们先取文件数据块号(pos/BLOCK_SIZE)在设备上对应的逻辑
    // 块号block。如果对应的逻辑块不存在就创建一块。如果得到的逻辑块号=0，则表示
    // 创建失败，于是退出循环。否则我们根据该逻辑块号读取设备上的相应逻辑块，若出
    // 错也退出循环。
    while (i&lt;count) {
        if (!(block = create_block(inode,pos/BLOCK_SIZE)))
            break;
        if (!(bh=bread(inode-&gt;i_dev,block)))
            break;
        // 此时缓冲块指针bh正指向刚读入的文件数据库。现在再求出文件当前读写指针在该
        // 数据块中的偏移值c，并将指针p指向缓冲块中开始写入数据的位置，并置该缓冲块已
        // 修改标志。对于块中当前指针，从开始读写位置到块末共可写入c=(BLOCK_SIZE - c)
        // 个字节。若c大于剩余还需写入的字节数(count - i)，则此次只需再写入c = (count - i)
        // 个字节即可。
        c = pos % BLOCK_SIZE;
        p = c + bh-&gt;b_data;
        bh-&gt;b_dirt = 1;
        c = BLOCK_SIZE-c;
        if (c &gt; count-i) c = count-i;
        // 在写入数据之前，我们先预先设置好下一次循环操作要读写文件中的位置。因此我们
        // 把pos指针前移此次需写入的字节数。如果此时pos位置值超过了文件当前长度，则
        // 修改i节点中文件长度字段，并置i节点已修改标志。然后把此次要写入的字节数c累加到
        // 已写入字节计数值i中，供循环判断使用。接着从用户缓冲区buf中复制c个字节到告诉缓
        // 冲块中p指向的开始位置处。复制完后就释放该缓冲块。
        pos += c;
        if (pos &gt; inode-&gt;i_size) {
            inode-&gt;i_size = pos;
            inode-&gt;i_dirt = 1;
        }
        i += c;
        while (c--&gt;0)
            *(p++) = get_fs_byte(buf++);/*注意：此时数据只是写入了缓存区，并未立即写入磁盘*/
        brelse(bh);
    }
    // 当数据已全部写入文件或者在写操作工程中发生问题时就会退出循环。此时我们更改文件修改
    // 时间为当前时间，并调整文件读写指针。如果此次操作不是在文件尾部添加数据，则把文件
    // 读写指针调整到当前读写位置pos处，并更改文件i节点的修改时间为当前时间。最后返回写入
    // 的字节数，若写入字节数为0，则返回出错号-1.
    inode-&gt;i_mtime = CURRENT_TIME;
    if (!(filp-&gt;f_flags &amp; O_APPEND)) {
        filp-&gt;f_pos = pos;
        inode-&gt;i_ctime = CURRENT_TIME;
    }
    return (i?i:-1);
}</code></pre></div><p>&#160; &#160;2、和读写文件类似，linux 0.11版本也提供了读写块设备的api，在block_dev.c文件中；代码结构和读写文件没本质区别：</p><p>&#160; 注意：这里求block号、block内部偏移的代码：int block = *pos &gt;&gt; BLOCK_SIZE_BITS;&#160; int offset = *pos &amp; (BLOCK_SIZE-1);&#160; 搞逆向时遇到这类代码，需要第一时间知道代码背后的逻辑意义！</p><div class="codebox"><pre class="vscroll"><code>//// 数据块写函数 - 向指定设备从给定偏移出写入制定长度数据。
// 参数：dev - 设备号； pos - 设备文件中偏移量指针；buf - 用户空间中缓冲区地址；
// count - 要传送的字节数
// 返回已写入字节数。若没有写入任何字节或出错，则返回出错号。
// 对于内核来说，写操作是向高速缓冲区中写入数据。什么时候数据最终写入设备是由高
// 速缓冲管理程序决定并处理的。另外，因为块设备是以块为单位进行读写，因此对于写
// 开始位置不处于块起始处时，需要先将开始字节所在的整个块读出，然后将需要写的数
// 据从写开始处填写满该块，再将完整的一块数据写盘（即交由高速缓冲程序去处理）。
int block_write(int dev, long * pos, char * buf, int count)
{
    // 首先由文件中位置pos换算成开始读写盘快的块序号block，并求出需写第1字节在该
    // 块中的偏移位置offset.
    int block = *pos &gt;&gt; BLOCK_SIZE_BITS;        // pos所在文件数据块号，相当于除以1024
    int offset = *pos &amp; (BLOCK_SIZE-1);         // pos在数据块中偏移值，相当于模1024
    int chars;
    int written = 0;
    struct buffer_head * bh;
    register char * p;                          // 局部寄存器变量，被存放在寄存器中

    // 然后针对要写入的字节数count，循环执行以下操作，知道数据全部写入。在循环执行
    // 过程中，先计算在当前处理的数据块中可写入的字节数。如果写入的字节数填不满一块，
    // 那么就只需写count字节。如果正要写1块数据内容，则直接申请1块高速缓冲块，并把
    // 用户数据放入即可。否则就需要读入将被写入部分数据的数据块，并预读下两块数据。
    // 然后将块号递增1，为下次操作做好准备。如果缓冲块操作失败，则返回已写字节数，
    // 如果没有写入任何字节，则返回出错号(负数).
    while (count&gt;0) {
        chars = BLOCK_SIZE - offset;
        if (chars &gt; count)
            chars=count;
        if (chars == BLOCK_SIZE)
            bh = getblk(dev,block);
        else
            bh = breada(dev,block,block+1,block+2,-1);
        block++;
        if (!bh)
            return written?written:-EIO;
        // 接着先把指针p指向读出数据的缓冲块中开始写入数据的位置处。若最后一次循环写入
        // 的数据不足一块，则需从块开始处填写（修改）所需的字节，因此这里需预先设置offset
        // 为零。此后将文件中偏移指针pos前移此次将要写的字节数chars，并累加这些要写的
        // 字节数到统计值written中，再把还需要写的计数值count减去此次要写的字节数chars.
        // 然后我们从用户缓冲区复制chars个字节到p指向的高速缓冲中开始写入的位置处。复制
        // 完后就设置该缓冲区块已修改标志，并释放该缓冲区(也即该缓冲区引用计数递减1)。
        p = offset + bh-&gt;b_data;
        offset = 0;
        *pos += chars;
        written += chars;           // 累计写入字节数
        count -= chars;
        while (chars--&gt;0)
            *(p++) = get_fs_byte(buf++);
        bh-&gt;b_dirt = 1;
        brelse(bh);
    }
    return written;
}

//// 数据块读函数 - 从指定设备和位置处读入指定长度数据到用户缓冲区中。
// 参数：dev - 设备号；pos - 设备文件中偏移量指针；buf - 用户空间缓冲区地址；
// count - 要传送的字节数。
// 返回已读入字节数。若没有读入任何字节或出错，则返回出错号。
int block_read(int dev, unsigned long * pos, char * buf, int count)
{
    // 首先由文件中位置pos换算成开始读写盘块的块序号block，并求出需读第1个字节在块中
    // 的偏移位置offset.
    int block = *pos &gt;&gt; BLOCK_SIZE_BITS;
    int offset = *pos &amp; (BLOCK_SIZE-1);
    int chars;
    int read = 0;
    struct buffer_head * bh;
    register char * p;

    // 然后针对要读入的字节数count，循环执行以下操作，直到数据全部读入。在循环执行
    // 过程中，先计算在当前处理的数据块中需读入的字节数。如果需要读入的字节数还不满
    // 一块，那么就只需要读count字节。然后调用读块函数breada()读如需要的数据块，并
    // 预读下两块数据，如果读操作出错，则返回已读字节数，如果没有读入任何字节，则
    // 返回出错号。然后将块号递增1.为下次操作做好准备。如果缓冲块操作失败，则返回已
    // 写字节数，如果没有读入任何字节，则返回出错号（负数）。
    while (count&gt;0) {
        chars = BLOCK_SIZE-offset;
        if (chars &gt; count)
            chars = count;
        if (!(bh = breada(dev,block,block+1,block+2,-1)))
            return read?read:-EIO;
        block++;
        // 接着先把指针p指向读出盘块的缓冲中开始读入数据的位置处。若最后一次循环读
        // 操作的数据不足一块，则需从块起始处读取所需字节，因此这里需预先设置offset
        // 为零。此后将文件中偏移指针pos前移此次将要读的字节数chars,并且累加这些要读
        // 的字节数到统计值read中。再把还需要读的计数值count减去此次要读的字节数chars。
        // 然后我们从高速缓冲块中p指向的开始读的位置处复制chars个字节到用户缓冲区中，
        // 同时把用户缓冲区指针前移。本次复制完后就释放该缓冲块。
        p = offset + bh-&gt;b_data;
        offset = 0;
        *pos += chars;
        read += chars;                      // 读入累计字节数
        count -= chars;
        while (chars--&gt;0)
            put_fs_byte(*(p++),buf++);
        brelse(bh);
    }
    return read;
}</code></pre></div><p>&#160; &#160; 3、除了常见的磁盘等块设备，还有串口这类的字符型设备，读写接口定义在了char_dev.c文件中了：</p><div class="codebox"><pre class="vscroll"><code>extern int tty_read(unsigned minor,char * buf,int count);
extern int tty_write(unsigned minor,char * buf,int count);

// 定义字符设备读写函数指针类型
typedef int (*crw_ptr)(int rw,unsigned minor,char * buf,int count,off_t * pos);

//// 串口终端读写操作函数。
// 参数：rw - 读写命令；minor - 终端子设备号；buf - 缓冲区；count - 读写字节数
// pos - 读写操作当前指针，对于中断操作，该指针无用
// 返回：实际读写的字节数。若失败则返回出错码。
static int rw_ttyx(int rw,unsigned minor,char * buf,int count,off_t * pos)
{
    return ((rw==READ)?tty_read(minor,buf,count):
        tty_write(minor,buf,count));
}

//// 终端读写操作函数。
// 同rw_ttyx，只是增加了对进程是否有控制终端的检测。
static int rw_tty(int rw,unsigned minor,char * buf,int count, off_t * pos)
{
    // 若进程没有控制终端，则返回出错号。否则调用终端读写函数rw_ttyx()，
    // 并返回实际读写字节数。
    if (current-&gt;tty&lt;0)
        return -EPERM;
    return rw_ttyx(rw,current-&gt;tty,buf,count,pos);
}

// 内存数据读写，早期版本暂时没实现
static int rw_ram(int rw,char * buf, int count, off_t *pos)
{
    return -EIO;
}

// 物理内存数据读写，早期版本暂时没实现
static int rw_mem(int rw,char * buf, int count, off_t * pos)
{
    return -EIO;
}

// 内核虚拟内存数据读写，早期版本暂时没实现
static int rw_kmem(int rw,char * buf, int count, off_t * pos)
{
    return -EIO;
}

//// 端口读写操作函数
// 参数：rw - 读写命令； buf - 缓冲区; cout - 读写字节数; pos - 端口地址。
// 返回：实际读写的字节数。
static int rw_port(int rw,char * buf, int count, off_t * pos)
{
    int i=*pos;

    // 对于所要求读写的字节数，并且端口地址小于64K时，循环执行单个字节的
    // 读写操作。若是读命令，则从端口i中读取一个字节内容并放到用户缓冲区中。
    // 若是写命令，则从用户数据缓冲区中取一字节输出到端口i。
    while (count--&gt;0 &amp;&amp; i&lt;65536) {
        if (rw==READ)
            put_fs_byte(inb(i),buf++);
        else
            outb(get_fs_byte(buf++),i);
        i++;
    }
    // 然后计算读/写字节数，调整相应读写指针，并返回读/写的字节数。
    i -= *pos;
    *pos += i;
    return i;
}

//// 内存读写操作函数
static int rw_memory(int rw, unsigned minor, char * buf, int count, off_t * pos)
{
    // 根据内存设备子设备号，分别调用不同的内存读写函数。
    switch(minor) {
        case 0:
            return rw_ram(rw,buf,count,pos);
        case 1:
            return rw_mem(rw,buf,count,pos);
        case 2:
            return rw_kmem(rw,buf,count,pos);
        case 3:
            return (rw==READ)?0:count;    /* rw_null */
        case 4:
            return rw_port(rw,buf,count,pos);
        default:
            return -EIO;
    }
}</code></pre></div><p>&#160; 这里的编码方式非常巧妙：先定义一个数组，数组的每个元素都是函数入口；然后在rw_char函数中，根据major(dev)找到对应所需的函数入口，然后通过函数指针的方式调用：</p><div class="codebox"><pre><code>// 字符设备读写函数指针表 file_operations
static crw_ptr crw_table[]={
    NULL,        /* nodev */
    rw_memory,    /* /dev/mem etc */
    NULL,        /* /dev/fd */
    NULL,        /* /dev/hd */
    rw_ttyx,    /* /dev/ttyx */
    rw_tty,        /* /dev/tty */
    NULL,        /* /dev/lp */
    NULL};        /* unnamed pipes */

// 字符设备读写操作函数
// 参数：rw - 读写命令；dev - 设备号；buf - 缓冲区; count - 读写字节数；pos - 读写指针。
// 返回：实际读/写字节数
int rw_char(int rw,int dev, char * buf, int count, off_t * pos)
{
    crw_ptr call_addr;

    // 如果设备号超出系统设备数，则返回出错码。如果该设备没有对应的读/写函数，也
    // 返回出错码。否则调用对应设备的读写操作函数，并返回实际读/写的字节数。
    if (MAJOR(dev)&gt;=NRDEVS)
        return -ENODEV;
    if (!(call_addr=crw_table[MAJOR(dev)]))
        return -ENODEV;
    return call_addr(rw,MINOR(dev),buf,count,pos);
}</code></pre></div><p>&#160; 4、前面说了，不同硬件设备有不同的读写接口，但是在VFS这一层确统一起来了，linux 0.11版本是怎么做的了？在rea_write.c函数中做了封装：</p><p>&#160; &#160; （1）这里先是导入不同类型的读写函数：</p><div class="codebox"><pre><code>// 字符设备读写函数。
extern int rw_char(int rw,int dev, char * buf, int count, off_t * pos);
// 读管道操作函数。
extern int read_pipe(struct m_inode * inode, char * buf, int count);
// 写管道操作函数
extern int write_pipe(struct m_inode * inode, char * buf, int count);
// 块设备读操作函数
extern int block_read(int dev, off_t * pos, char * buf, int count);
// 块设备写操作函数
extern int block_write(int dev, off_t * pos, char * buf, int count);
// 读文件操作函数
extern int file_read(struct m_inode * inode, struct file * filp,
        char * buf, int count);
// 写文件操作函数
extern int file_write(struct m_inode * inode, struct file * filp,
        char * buf, int count);</code></pre></div><p>&#160; （2）通过sys_read和sys_write彻底封装了上述导入的不同类型设备的读写函数：这两个函数内部都会通过S_ISCHR(inode-&gt;i_mode)、S_ISBLK(inode-&gt;i_mode)、S_ISDIR(inode-&gt;i_mode) 、S_ISREG(inode-&gt;i_mode)等方式判断，根据不同的设备类型调用不同的读写函数！</p><div class="codebox"><pre class="vscroll"><code>//// 读文件系统调用
// 参数fd是文件句柄，buf是缓冲区，count是预读字节数
int sys_read(unsigned int fd,char * buf,int count)
{
    struct file * file;
    struct m_inode * inode;

    // 函数首先对参数有效性进行判断。如果文件句柄值大于程序最多打开文件数NR_OPEN，
    // 或者需要读取的字节计数值小于0，或者该句柄的文件结构指针为空，则返回出错码并
    // 退出。若需读取的字节数count等于0，则返回0退出。
    if (fd&gt;=NR_OPEN || count&lt;0 || !(file=current-&gt;filp[fd]))
        return -EINVAL;
    if (!count)
        return 0;
    // 然后验证存放数据的缓冲区内存限制。并取文件的i节点。用于根据该i节点的属性，分
    // 别调用相应的读操作函数。若是管道文件，并且是读管道文件模式，则进行读管道操作，
    // 若成功则返回读取的字节数，否则返回出错码，退出。如果是字符型文件，则进行读
    // 字符设备操作，并返回读取的字符数。如果是块设备文件，则执行块设备读操作，并
    // 返回读取的字节数。
    verify_area(buf,count);
    inode = file-&gt;f_inode;
    if (inode-&gt;i_pipe)
        return (file-&gt;f_mode&amp;1)?read_pipe(inode,buf,count):-EIO;
    if (S_ISCHR(inode-&gt;i_mode))
        return rw_char(READ,inode-&gt;i_zone[0],buf,count,&amp;file-&gt;f_pos);
    if (S_ISBLK(inode-&gt;i_mode))
        return block_read(inode-&gt;i_zone[0],&amp;file-&gt;f_pos,buf,count);
    // 如果是目录文件或者是常规文件，则首先验证读取字节数count的有效性并进行调整(若
    // 读去字节数加上文件当前读写指针值大于文件长度，则重新设置读取字节数为文件长度
    // -当前读写指针值，若读取数等于0，则返回0退出)，然后执行文件读操作，返回读取的
    // 字节数并退出。
    if (S_ISDIR(inode-&gt;i_mode) || S_ISREG(inode-&gt;i_mode)) {
        if (count+file-&gt;f_pos &gt; inode-&gt;i_size)
            count = inode-&gt;i_size - file-&gt;f_pos;
        if (count&lt;=0)
            return 0;
        return file_read(inode,file,buf,count);
    }
    // 执行到这里，说明我们无法判断文件的属性。则打印节点文件属性，并返回出错码退出。
    printk(&quot;(Read)inode-&gt;i_mode=%06o\n\r&quot;,inode-&gt;i_mode);
    return -EINVAL;
}

//// 写文件系统调用
// 参数fd是文件句柄，buf是用户缓冲区，count是欲写字节数。
int sys_write(unsigned int fd,char * buf,int count)
{
    struct file * file;
    struct m_inode * inode;

    // 同样地，我们首先判断函数参数的有效性。若果进程文件句柄值大于程序最多打开文件数
    // NR_OPEN，或者需要写入的字节数小于0，或者该句柄的文件结构指针为空，则返回出错码
    // 并退出。如果需读取字节数count等于0，则返回0退出。
    if (fd&gt;=NR_OPEN || count &lt;0 || !(file=current-&gt;filp[fd]))
        return -EINVAL;
    if (!count)
        return 0;
    // 然后验证存放数据的缓冲区内存限制。并取文件的i节点。用于根据该i节点属性，分别调
    // 用相应的读操作函数。若是管道文件，并且是写管道文件模式，则进行写管道操作，若成
    // 功则返回写入的字节数，否则返回出错码退出。如果是字符设备文件，则进行写字符设备
    // 操作，返回写入的字符数退出。如果是块设备文件，则进行块设备写操作，并返回写入的
    // 字节数退出。若是常规文件，则执行文件写操作，并返回写入的字节数，退出。
    inode=file-&gt;f_inode;
    if (inode-&gt;i_pipe)
        return (file-&gt;f_mode&amp;2)?write_pipe(inode,buf,count):-EIO;
    if (S_ISCHR(inode-&gt;i_mode))
        return rw_char(WRITE,inode-&gt;i_zone[0],buf,count,&amp;file-&gt;f_pos);
    if (S_ISBLK(inode-&gt;i_mode))
        return block_write(inode-&gt;i_zone[0],&amp;file-&gt;f_pos,buf,count);
    if (S_ISREG(inode-&gt;i_mode))
        return file_write(inode,file,buf,count);
    // 执行到这里，说明我们无法判断文件的属性。则打印节点文件属性，并返回出错码退出。
    printk(&quot;(Write)inode-&gt;i_mode=%06o\n\r&quot;,inode-&gt;i_mode);
    return -EINVAL;
}</code></pre></div><p>&#160; &#160;（3）open.c的sys_open函数，返回文件句柄（本质就是file结构体的指针）：</p><div class="codebox"><pre class="vscroll"><code>//// 打开（或创建）文件系统调用。
// 参数filename是文件名，flag是打开文件标志，它可取值：O_RDONLY（只读）、O_WRONLY
// （只写）或O_RDWR(读写)，以及O_EXCL（被创建文件必须不存在）、O_APPEND（在文件
// 尾添加数据）等其他一些标志的组合。如果本调用创建了一个新文件，则mode就用于指
// 定文件的许可属性。这些属性有S_IRWXU（文件宿主具有读、写和执行权限）、S_IRUSR
// （用户具有读文件权限）、S_IRWXG（组成员具有读、写和执行权限）等等。对于新创
// 建的文件，这些属性只应用与将来对文件的访问，创建了只读文件的打开调用也将返回
// 一个可读写的文件句柄。如果调用操作成功，则返回文件句柄(文件描述符)，否则返回出错码。
int sys_open(const char * filename,int flag,int mode)
{
    struct m_inode * inode;
    struct file * f;
    int i,fd;

    // 首先对参数进行处理。将用户设置的文件模式和屏蔽码相与，产生许可的文件模式。
    // 为了为打开文件建立一个文件句柄，需要搜索进程结构中文件结构指针数组，以查
    // 找一个空闲项。空闲项的索引号fd即是文件句柄值。若已经没有空闲项，则返回出错码。
    mode &amp;= 0777 &amp; ~current-&gt;umask;
    for(fd=0 ; fd&lt;NR_OPEN ; fd++)
        if (!current-&gt;filp[fd])
            break;
    if (fd&gt;=NR_OPEN)
        return -EINVAL;
    // 然后我们设置当前进程的执行时关闭文件句柄(close_on_exec)位图，复位对应的
    // bit位。close_on_exec是一个进程所有文件句柄的bit标志。每个bit位代表一个打
    // 开着的文件描述符，用于确定在调用系统调用execve()时需要关闭的文件句柄。当
    // 程序使用fork()函数创建了一个子进程时，通常会在该子进程中调用execve()函数
    // 加载执行另一个新程序。此时子进程中开始执行新程序。若一个文件句柄在close_on_exec
    // 中的对应bit位被置位，那么在执行execve()时应对应文件句柄将被关闭，否则该
    // 文件句柄将始终处于打开状态。当打开一个文件时，默认情况下文件句柄在子进程
    // 中也处于打开状态。因此这里要复位对应bit位。
    current-&gt;close_on_exec &amp;= ~(1&lt;&lt;fd);
    // 然后为打开文件在文件表中寻找一个空闲结构项。我们令f指向文件表数组开始处。
    // 搜索空闲文件结构项(引用计数为0的项)，若已经没有空闲文件表结构项，则返回
    // 出错码。
    f=0+file_table;
    for (i=0 ; i&lt;NR_FILE ; i++,f++)
        if (!f-&gt;f_count) break;
    if (i&gt;=NR_FILE)
        return -EINVAL;
    // 此时我们让进程对应文件句柄fd的文件结构指针指向搜索到的文件结构，并令文件
    // 引用计数递增1。然后调用函数open_namei()执行打开操作，若返回值小于0，则说
    // 明出错，于是释放刚申请到的文件结构，返回出错码i。若文件打开操作成功，则
    // inode是已打开文件的i节点指针。
    (current-&gt;filp[fd]=f)-&gt;f_count++;
    if ((i=open_namei(filename,flag,mode,&amp;inode))&lt;0) {
        current-&gt;filp[fd]=NULL;
        f-&gt;f_count=0;
        return i;
    }
    // 根据已打开文件的i节点的属性字段，我们可以知道文件的具体类型。对于不同类
    // 型的文件，我们需要操作一些特别的处理。如果打开的是字符设备文件，那么对于
    // 主设备号是4的字符文件(例如/dev/tty0)，如果当前进程是组首领并且当前进程的
    // tty字段小于0(没有终端)，则设置当前进程的tty号为该i节点的子设备号，并设置
    // 当前进程tty对应的tty表项的父进程组号等于当前进程的进程组号。表示为该进程
    // 组（会话期）分配控制终端。对于主设备号是5的字符文件(/dev/tty)，若当前进
    // 程没有tty，则说明出错，于是放回i节点和申请到的文件结构，返回出错码(无许可)。
/* ttys are somewhat special (ttyxx major==4, tty major==5) */
    if (S_ISCHR(inode-&gt;i_mode)) {
        if (MAJOR(inode-&gt;i_zone[0])==4) {
            if (current-&gt;leader &amp;&amp; current-&gt;tty&lt;0) {
                current-&gt;tty = MINOR(inode-&gt;i_zone[0]);
                tty_table[current-&gt;tty].pgrp = current-&gt;pgrp;
            }
        } else if (MAJOR(inode-&gt;i_zone[0])==5)
            if (current-&gt;tty&lt;0) {
                iput(inode);
                current-&gt;filp[fd]=NULL;
                f-&gt;f_count=0;
                return -EPERM;
            }
    }
/* Likewise with block-devices: check for floppy_change */
    // 如果打开的是块设备文件，则检查盘片是否更换过。若更换过则需要让高速缓冲区
    // 中该设备的所有缓冲块失败。
    if (S_ISBLK(inode-&gt;i_mode))
        check_disk_change(inode-&gt;i_zone[0]);
    // 现在我们初始化打开文件的文件结构。设置文件结构属性和标志，置句柄引用计数
    // 为1，并设置i节点字段为打开文件的i节点，初始化文件读写指针为0.最后返回文
    // 件句柄号。
    f-&gt;f_mode = inode-&gt;i_mode;
    f-&gt;f_flags = flag;
    f-&gt;f_count = 1;
    f-&gt;f_inode = inode;
    f-&gt;f_pos = 0;
    return (fd);
}</code></pre></div><p>&#160; 为了方便直观理解，图示如下：<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211209100805574-657158312.png" alt="FluxBB bbcode 测试" /></span></p><p> 为什么linux系统的理念是“万物皆文件”了？我们现在使用的这套软硬件系统最原始的名称叫information technology，核心目的是存储和读取数据！IT高速发展到现在，最核心的目的还是存储和读取数据，这一点几十年都没变！为了方便上层app读写数据，linux抽象出了VFS：上层app所有的操作都统一了接口名称，不同的设备实现这些接口就行了！对于app来说，只认接口就足够了！</p><p> </p><p>&#160; &#160;</p><p> </p><p>参考：</p><p>1、https://zhuanlan.zhihu.com/p/66597013&#160; &#160;详解 Linux 中的虚拟文件系统</p><p>2、https://www.bilibili.com/video/BV1tQ4y1d7mo?p=27 linux内核精讲</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sat, 08 Oct 2022 04:51:45 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=447&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[linux源码解读（五）：文件系统——文件和目录的操作]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=446&amp;action=new</link>
			<description><![CDATA[<p>对于普通用户，平时使用操作系统是肯定涉及到创建、更改、删除文件(比如mkdir、rmdir、rm、chmod、ln等)；有些文件是高权限用户建的，低权限用户甚至都打不开，也删不掉；为了方便管理不同业务类型的文件，还需要在不同的逻辑分区建文件夹，分门别类各种文件；linux下用ls -l命令还可以查看文件的详细属性，这一系列的功能构师怎么实现的了？功能都在fs/namei.c文件中</p><p>&#160; 1、（1）权限检查，核心就是依靠inode结构体中的i_mode成员变量了！这个变量是unsigned short类型，一共2byte=16bit长；linux用低9位表示当前用户权限、用户组权限、其他用户权限，用户平时用ls -l查到的权限就是靠这个字段得到的！举个例子：rwx------表示当前用户有读写执行权限，用户组没有任何权限，其他用户也没有任何权限，所有权限表示刚好使用9bit；</p><p>&#160; i_mode节点右移3位，与上0007后得到用户组权限<br />&#160; &#160; &#160; &#160;i_mode节点右移6位，与上0007后得到当前用户权限<br />&#160; &#160; &#160; &#160;chmod改的就是i_mode这个字段</p><div class="codebox"><pre class="vscroll"><code>/*
 *    permission()
 *
 * is used to check for read/write/execute permissions on a file.
 * I don&#039;t know if we should look at just the euid or both euid and
 * uid, but that should be easily changed.
 */
//// 检测文件访问权限
// 参数：inode - 文件的i节点指针；mask - 访问属性屏蔽码。
// 返回：访问许可返回1，否则返回0.
static int permission(struct m_inode * inode,int mask)
{
    int mode = inode-&gt;i_mode;

/* special case: not even root can read/write a deleted file */
    // 如果i节点有对应的设备，但该i节点的连接计数值等于0，表示该文件
    // 已被删除，则返回。否则，如果进程的有效用户ID(euid)与i节点的
    // 用户id相同，则取文件宿主的访问权限。否则如果与组id相同，
    // 则取组用户的访问权限。
    if (inode-&gt;i_dev &amp;&amp; !inode-&gt;i_nlinks)
        return 0;
    else if (current-&gt;euid==inode-&gt;i_uid)
        mode &gt;&gt;= 6;
    else if (current-&gt;egid==inode-&gt;i_gid)
        mode &gt;&gt;= 3;
    /* &amp;0007：取最后3位
       &amp;mask：取传入参数的位
    */
    if (((mode &amp; mask &amp; 0007) == mask) 
            || suser())/*要么是管理员，是超级用户*/
        return 1;
    return 0;
}</code></pre></div><p>&#160; &#160;（2）因为是涉及到设备名、文件名、目录路径的比对，自然少不了字符串相关的操作。平时在3环做应用开发，码农都习惯于使用操作系统提供的库函数，比如strcmp、strcat等，但是现在还在内核，哪来的库函数直接调用了，只能自己动手重新写字符串的比较函数，如下：</p><div class="codebox"><pre class="vscroll"><code>*
 * ok, we cannot use strncmp, as the name is not in our data space.
 * Thus we&#039;ll have to use match. No big problem. Match also makes
 * some sanity tests.
 *
 * NOTE! unlike strncmp, match returns 1 for success, 0 for failure.
 */
//// 指定长度字符串比较函数
// 参数：len - 比较的字符串长度；name - 文件名指针；de - 目录项结构
// 返回：相同返回1，不同返回0. 
// 下面函数中的寄存器变了same被保存在eax寄存器中，以便高效访问。
static int match(int len,const char * name,struct dir_entry * de)
{
    register int same ;

    // 首先判断函数参数的有效性。如果目录项指针空，或者目录项i节点等于0，或者
    // 要比较的字符串长度超过文件名长度，则返回0.如果要比较的长度len小于NAME_LEN，
    // 但是目录项中文件名长度超过len，也返回0.
    if (!de || !de-&gt;inode || len &gt; NAME_LEN)
        return 0;
    if (len &lt; NAME_LEN &amp;&amp; de-&gt;name[len])
        return 0;
    // 然后使用嵌套汇编语句进行快速比较操作。他会在用户数据空间(fs段)执行字符串的比较
    // 操作。%0 - eax（比较结果same）；%1 - eax (eax初值0)；%2 - esi(名字指针)；
    // %3 - edi(目录项名指针)；%4 - ecx(比较的字节长度值len).
    __asm__(&quot;cld\n\t&quot;
        &quot;fs ; repe ; cmpsb\n\t&quot;
        &quot;setz %%al&quot;
        :&quot;=a&quot; (same)
        :&quot;0&quot; (0),&quot;S&quot; ((long) name),&quot;D&quot; ((long) de-&gt;name),&quot;c&quot; (len)
        );
    return same;
}</code></pre></div><p>&#160; （3）还有在某个目录下查找名为xxx的文件，比如：&quot;find /home -name test&quot;命令，就是在home目录下查找名为test的文件，实现如下：</p><p>&#160; 注意：函数的参数有两个双重指针，第二个双重指针明显是用来保存返回值的！</p><div class="codebox"><pre class="vscroll"><code>/*
 *    find_entry()
 *
 * finds an entry in the specified directory with the wanted name. It
 * returns the cache buffer in which the entry was found, and the entry
 * itself (as a parameter - res_dir). It does NOT read the inode of the
 * entry - you&#039;ll have to do that yourself if you want to.
 *
 * This also takes care of the few special cases due to &#039;..&#039;-traversal
 * over a pseudo-root and a mount point.
 */
//// 查找指定目录和文件名的目录项。 find -name &quot;xxx&quot; /xxx/xxx
// 参数：*dir - 指定目录i节点的指针；name - 文件名；namelen - 文件名长度；
// 该函数在指定目录的数据（文件）中搜索指定文件名的目录项。并对指定文件名
// 是&#039;..&#039;的情况根据当前进行的相关设置进行特殊处理。关于函数参数传递指针的指针
// 作用，请参见seched.c中的注释。
// 返回：成功则函数高速缓冲区指针，并在*res_dir处返回的目录项结构指针。失败则
// 返回空指针NULL。
static struct buffer_head * find_entry(struct m_inode ** dir,
    const char * name, int namelen, struct dir_entry ** res_dir)
{
    int entries;
    int block,i;
    struct buffer_head * bh;
    struct dir_entry * de;
    struct super_block * sb;

    // 同样，本函数一上来也需要对函数参数的有效性进行判断和验证。如果我们在前面
    // 定义了符号常数NO_TRUNCATE,那么如果文件名长度超过最大长度NAME_LEN，则不予
    // 处理。如果没有定义过NO_TRUNCATE，那么在文件名长度超过最大长度NAME_LEN时截短之。
#ifdef NO_TRUNCATE
    if (namelen &gt; NAME_LEN)
        return NULL;
#else
    if (namelen &gt; NAME_LEN)
        namelen = NAME_LEN;
#endif
    // 首先计算本目录中目录项项数entries(也即是当前目录中能存放的最大目录个数)。目录i节点i_size字段中含有本目录包含的数据
    // 长度，因此其除以一个目录项的长度（16字节）即课得到该目录中目录项数。然后置空 
    // 返回目录项结构指针。如果长度等于0，则返回NULL，退出。
    entries = (*dir)-&gt;i_size / (sizeof (struct dir_entry));
    *res_dir = NULL;
    if (!namelen)
        return NULL;
    // 接下来我们对目录项文件名是&#039;..&#039;的情况进行特殊处理。如果当前进程指定的根i节点就是
    // 函数参数指定的目录，则说明对于本进程来说，这个目录就是它伪根目录，即进程只能访问
    // 该目录中的项而不能后退到其父目录中去。也即对于该进程本目录就如同是文件系统的根目录，
    // 因此我们需要将文件名修改为‘.’。
    // 否则，如果该目录的i节点号等于ROOT_INO（1号）的话，说明确实是文件系统的根i节点。
    // 则取文件系统的超级块。如果被安装到的i节点存在，则先放回原i节点，然后对被安装到
    // 的i节点进行处理。于是我们让*dir指向该被安装到的i节点；并且该i节点的引用数加1.
    // 即针对这种情况，我们悄悄的进行了“偷梁换柱”工程。:-)
/* check for &#039;..&#039;, as we might have to do some &quot;magic&quot; for it */
    if (namelen==2 &amp;&amp; get_fs_byte(name)==&#039;.&#039; &amp;&amp; get_fs_byte(name+1)==&#039;.&#039;) {
/* &#039;..&#039; in a pseudo-root results in a faked &#039;.&#039; (just change namelen) */
        if ((*dir) == current-&gt;root)
            namelen=1;
        else if ((*dir)-&gt;i_num == ROOT_INO) {
/* &#039;..&#039; over a mount-point results in &#039;dir&#039; being exchanged for the mounted
   directory-inode. NOTE! We set mounted, so that we can iput the new dir */
            sb=get_super((*dir)-&gt;i_dev);
            if (sb-&gt;s_imount) {
                iput(*dir);
                (*dir)=sb-&gt;s_imount;
                (*dir)-&gt;i_count++;
            }
        }
    }
    // 现在我们开始正常操作，查找指定文件名的目录项在什么地方。因此我们需要读取目录的
    // 数据，即取出目录i节点对应块设备数据区中的数据块（逻辑块）信息。这些逻辑块的块号
    // 保存在i节点结构的i_zone[9]数组中。我们先取其中第一个块号。如果目录i节点指向的
    // 第一个直接磁盘块好为0，则说明该目录竟然不含数据，这不正常。于是返回NULL退出，
    // 否则我们就从节点所在设备读取指定的目录项数据块。当然，如果不成功，则也返回NULL 退出。
    if (!(block = (*dir)-&gt;i_zone[0]))
        return NULL;
    if (!(bh = bread((*dir)-&gt;i_dev,block)))
        return NULL;
    // 此时我们就在这个读取的目录i节点数据块中搜索匹配指定文件名的目录项。首先让de指向
    // 缓冲块中的数据块部分。并在不超过目录中目录项数的条件下，循环执行搜索。其中i是目录中
    // 的目录项索引号。在循环开始时初始化为0.
    i = 0;
    de = (struct dir_entry *) bh-&gt;b_data;
    while (i &lt; entries) {
        // 如果当前目录项数据块已经搜索完，还没有找到匹配的目录项，则释放当前目录项数据块。
        // 再读入目录的下一个逻辑块。若这块为空。则只要还没有搜索完目录中的所有目录项，就
        // 跳过该块，继续读目录的下一逻辑块。若该块不空，就让de指向该数据块，然后在其中继续
        // 搜索。其中DIR_ENTRIES_PER_BLOCK可得到当前搜索的目录项所在目录文件中的块号，而bmap()
        // 函数则课计算出在设备上对应的逻辑块号.
        if ((char *)de &gt;= BLOCK_SIZE+bh-&gt;b_data) {
            brelse(bh);
            bh = NULL;
            if (!(block = bmap(*dir,i/DIR_ENTRIES_PER_BLOCK)) ||
                !(bh = bread((*dir)-&gt;i_dev,block))) {
                i += DIR_ENTRIES_PER_BLOCK;
                continue;
            }
            de = (struct dir_entry *) bh-&gt;b_data;
        }
        // 如果找到匹配的目录项的话，则返回该目录项结构指针de和该目录项i节点指针*dir以及该目录项
        // 数据块指针bh，并退出函数。否则继续在目录项数据块中比较下一个目录项。
        if (match(namelen,name,de)) {
            *res_dir = de;
            return bh;
        }
        de++;
        i++;
    }
    // 如果指定目录中的所有目录项都搜索完后，还没有找到相应的目录项，则释放目录的数据块，
    // 最后返回NULL（失败）。
    brelse(bh);
    return NULL;
}</code></pre></div><p>&#160; &#160;这个函数的开头就出现了一个新的结构体dir_entry，是这样定义的：结构体很简单，只有2个字段，分别是当前文件或目录中包含的inode个数，以及自己的名字；最后一个参数也是用这个结构体保存找到的文件名称和inode节点号数，通过inode节点号数从inode位图查看该inode是否被使用，也可以查找到该文件的inode节点在磁盘的block位置，进而找到文件元信息；</p><div class="codebox"><pre><code>struct dir_entry {
    unsigned short inode;
    char name[NAME_LEN];
};</code></pre></div><p>&#160; （4）既然能够查找文件，也就能新建文件或目录，linux的实现方式如下：</p><div class="codebox"><pre class="vscroll"><code>/*
 *    add_entry()
 *
 * adds a file entry to the specified directory, using the same
 * semantics as find_entry(). It returns NULL if it failed.
 *
 * NOTE!! The inode part of &#039;de&#039; is left at 0 - which means you
 * may not sleep between calling this and putting something into
 * the entry, as someone else might have used it while you slept.
 */
//// 根据指定的目录和文件名添加目录项
// 参数：dir - 指定目录的i节点；name - 文件名；namelen - 文件名长度；
// 返回：高速缓冲区指针；res_dir - 返回的目录项结构指针。
static struct buffer_head * add_entry(struct m_inode * dir,
    const char * name, int namelen, struct dir_entry ** res_dir)
{
    int block,i;
    struct buffer_head * bh;
    struct dir_entry * de;

    // 同样，本函数一上来也需要对函数参数的有效性进行判断和验证。
    // 如果我们在前面定义了符号常数NO_TRUNCATE，那么如果文件名长
    // 度超过最大长度NAME_LEN，则不予处理。如果没有定义过NO_TRUNCATE，
    // 那么在文件名长度超过最大长度NAME_LEN时截短之。
    *res_dir = NULL;
#ifdef NO_TRUNCATE
    if (namelen &gt; NAME_LEN)
        return NULL;
#else
    if (namelen &gt; NAME_LEN)
        namelen = NAME_LEN;
#endif
    // 现在我们开始操作，向指定目录中添加一个指定文件名的目录项。因此
    // 我们需要先读取目录的数据，即取出目录i节点对应块数据区中的数据块
    // 信息。这些逻辑块的块号保存在i节点结构i_zone[9]数组中。我们先取
    // 其中第1个块号，如果目录i节点指向的第一个直接磁盘块号为0，则说明
    // 该目录竟然不含数据，这不正常。于是返回NULL退出。否则我们就从节点
    // 所在设备读取指定目录项数据块。当然，如果不成功，则也返回NULL退出。
    // 如果参数提供的文件名长度等于0，则也返回NULL退出。
    if (!namelen)
        return NULL;
    if (!(block = dir-&gt;i_zone[0]))/*目录文件存储的第一个磁盘逻辑块号，肯定不会是0（0是引导块）*/
        return NULL;
    //目录数据必须存磁盘，不能只存内存，否则关机后就全丢了
    if (!(bh = bread(dir-&gt;i_dev,block)))/*读取目录文件第一个逻辑块的数据到缓存区，里面存放的都是dir_entry，所以下面把b_data强转成dir_entry*/
        return NULL;
    // 此时我们就在这个目录i节点数据块中循环查找最后未使用的空目录项。
    // 首先让目录项结构指针de指向缓冲块中的数据块部分，即第一个目录项处。
    // 其中i是目录中的目录项索引号，在循环开始时初始化为0.
    i = 0;
    de = (struct dir_entry *) bh-&gt;b_data;
    while (1) {
        // 如果当前目录项数据块已经搜索完毕，但还没有找到需要的空目录项，
        // 则释放当前目录项数据块，再读入目录的下一个逻辑块。如果对应的逻辑块。
        // 如果对应的逻辑块不存在就创建一块。如果读取或创建操作失败则返回空。
        // 如果此次读取的磁盘逻辑块数据返回的缓冲块数据为空，说明这块逻辑块
        // 可能是因为不存在而新创建的空块，则把目录项索引值加上一块逻辑块所
        // 能容纳的目录项数DIR_ENTRIES_PER_BLOCK，用以跳过该块并继续搜索。
        // 否则说明新读入的块上有目录项数据，于是让目录项结构指针de指向该块
        // 的缓冲块数据部分，然后在其中继续搜索。其中i/DIR_ENTRIES_PER_BLOCK可
        // 计算得到当前搜索的目录项i所在目录文件中的块号，而create_block函数则可
        // 读取或创建出在设备上对应的逻辑块。
        if ((char *)de &gt;= BLOCK_SIZE+bh-&gt;b_data) {
            brelse(bh);
            bh = NULL;
            block = create_block(dir,i/DIR_ENTRIES_PER_BLOCK);
            if (!block)
                return NULL;
            if (!(bh = bread(dir-&gt;i_dev,block))) {
                i += DIR_ENTRIES_PER_BLOCK;
                continue;
            }
            de = (struct dir_entry *) bh-&gt;b_data;
        }
        // 如果当前所操作的目录项序号i乘上目录结构大小所在长度值已经超过了该目录
        // i节点信息所指出的目录数据长度值i_size，则说明整个目录文件数据中没有
        // 由于删除文件留下的空目录项，因此我们只能把需要添加的新目录项附加到
        // 目录文件数据的末端处。于是对该处目录项进行设置（置该目录项的i节点指针
        // 为空），并更新该目录文件的长度值（加上一个目录项的长度），然后设置目录
        // 的i节点已修改标志，再更新该目录的改变时间为当前时间。
        if (i*sizeof(struct dir_entry) &gt;= dir-&gt;i_size) {
            de-&gt;inode=0;
            dir-&gt;i_size = (i+1)*sizeof(struct dir_entry);
            dir-&gt;i_dirt = 1;
            dir-&gt;i_ctime = CURRENT_TIME;
        }
        // 若当前搜索的目录项de的i节点为空，则表示找到一个还未使用的空闲目录项
        // 或是添加的新目录项。于是更新目录的修改时间为当前时间，并从用户数据区
        // 复制文件名到该目录项的文件名字段，置含有本目录项的相应高速缓冲块已修改
        // 标志。返回该目录项的指针以及该高速缓冲块的指针，退出。
        if (!de-&gt;inode) {
            dir-&gt;i_mtime = CURRENT_TIME;
            for (i=0; i &lt; NAME_LEN ; i++)
                de-&gt;name[i]=(i&lt;namelen)?get_fs_byte(name+i):0;
            bh-&gt;b_dirt = 1;
            *res_dir = de;
            return bh;
        }
        de++;
        i++;
    }
    // 本函数执行不到这里。这也许是Linus在写这段代码时，先复制了上面的find_entry()
    // 函数的代码，而后修改成本函数的。:-)
    brelse(bh);
    return NULL;
}</code></pre></div><p>&#160; （5）截至目前，linux文件系统涉及到好多的结构体、缓存区、磁盘(主要是inode、buffer_head、dir_entry、block等)，不熟悉的初学者看到这里估计都开始晕菜了，这里有个现成的图示（参考1），展示了各个结构体的关系：<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211206105144928-638193118.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160;整个磁盘文件数据读取流程如下：</p><p>&#160; 先根据文件路径找到文件对应的inode节点。假设是个绝对路径，文件路径是/a/b/c.txt；系统初始化的时候我们已经拿到了根目录对应的inode（磁盘上第一个inode节点就是根目录所在的节点，从这里也可以看出，文件目录也必须保存在磁盘，而不仅仅是保存在内存，避免断电后丢失），把根目录文件的block内容读进来，是一系列的dir_entry结构体。然后逐个遍历，比较文件名是不是等于a，最后得到一个目录a对应的dir_entry；<br />&#160; &#160; &#160; &#160;dir_entry结构体不仅保存了文件名，还保存了对应的inode号；根据inode号把a目录文件的内容也读取进来；以此类推，得到c对应的dir_entry<br />&#160; &#160; &#160; &#160;再根据c对应的dir_entry的inode号，从磁盘把inode的内容读进来，发现就是个普通文件；至此，找到了这个文件对应的inode节点，完成fd-&gt;file结构体-&gt;inode结构体的赋值<br />&#160; &#160; &#160; &#160;最后根据fd找到对应的inode节点，根据file结构体的pos字段；根据数据在文件中的偏移，可以算出应该取i_zone[9]字段的哪个索引，文件的前7块对应索引0-6，前7到7+512对应索引7等。得到索引后，读取i_zone数组在该索引的值，即我们要读取的数据在硬盘的数据块。然后把这个数据块从硬盘读取进来。返回给用户<br />&#160; &#160; &#160; &#160; 整个流程总结：磁盘inode首节点-&gt;dir_entry-&gt;根据pathname查找目录或文件inode编号-&gt;从磁盘读取inode内容-&gt;分析i_zone得到文件内容的block编号-&gt;从磁盘读数据；整个思路流程和内存管理的CR3分页检索没有任何本质区别；<br />&#160; &#160; &#160; （6）linux常用的命令还有“cat&#160; /home/test.c”、“cd /home/jdk/java” 等目录相关的操作。通过前面的分析得知，操作文件或目录，本质就是读写其元信息，这些都存放在inode里面，所以想想办法得到inode，代码如下：</p><div class="codebox"><pre class="vscroll"><code>/*
 *    get_dir()
 *
 * Getdir traverses the pathname until it hits the topmost directory.
 * It returns NULL on failure.
 */
//// 搜寻指定路径的目录（或文件名）的i节点。
// 参数：pathname - 路径名
// 返回：目录或文件的i节点指针。
static struct m_inode * get_dir(const char * pathname)
{
    char c;
    const char * thisname;
    struct m_inode * inode;
    struct buffer_head * bh;
    int namelen,inr,idev;
    struct dir_entry * de;

    // 搜索操作会从当前任务结构中设置的根（或伪根）i节点或当前工作目录i节点
    // 开始，因此首先需要判断进程的根i节点指针和当前工作目录i节点指针是否有效。
    // 如果当前进程没有设定根i节点，或者该进程根i节点指向是一个空闲i节点（引用为0），
    // 则系统出错停机。如果进程的当前工作目录i节点指针为空，或者该当前工作目录
    // 指向的i节点是一个空闲i节点，这也是系统有问题，停机。
    if (!current-&gt;root || !current-&gt;root-&gt;i_count)
        panic(&quot;No root inode&quot;);
    if (!current-&gt;pwd || !current-&gt;pwd-&gt;i_count)
        panic(&quot;No cwd inode&quot;);
    // 如果用户指定的路径名的第1个字符是&#039;/&#039;，则说明路径名是绝对路径名。则从
    // 根i节点开始操作，否则第一个字符是其他字符，则表示给定的相对路径名。
    // 应从进程的当前工作目录开始操作。则取进程当前工作目录的i节点。如果路径
    // 名为空，则出错返回NULL退出。此时变量inode指向了正确的i节点 -- 进程的
    // 根i节点或当前工作目录i节点之一。
    if ((c=get_fs_byte(pathname))==&#039;/&#039;) {
        inode = current-&gt;root;
        pathname++;
    } else if (c)
        inode = current-&gt;pwd;
    else
        return NULL;    /* empty name is bad */
    // 然后针对路径名中的各个目录名部分和文件名进行循环出路，首先把得到的i节点
    // 引用计数增1，表示我们正在使用。在循环处理过程中，我们先要对当前正在处理
    // 的目录名部分（或文件名）的i节点进行有效性判断，并且把变量thisname指向
    // 当前正在处理的目录名部分（或文件名）。如果该i节点不是目录类型的i节点，
    // 或者没有可进入该目录的访问许可，则放回该i节点，并返回NULL退出。当然，刚
    // 进入循环时，当前的i节点就是进程根i节点或者是当前工作目录的i节点。
    inode-&gt;i_count++;
    while (1) {
        thisname = pathname;
        if (!S_ISDIR(inode-&gt;i_mode) || !permission(inode,MAY_EXEC)) {
            iput(inode);
            return NULL;
        }
        // 每次循环我们处理路径名中一个目录名（或文件名）部分。因此在每次循环中
        // 我们都要从路径名字符串中分离出一个目录名（或文件名）。方法是从当前路径名
        // 指针pathname开始处搜索检测字符，知道字符是一个结尾符（NULL）或者是一
        // 个&#039;/&#039;字符。此时变量namelen正好是当前处理目录名部分的长度，而变量thisname
        // 正指向该目录名部分的开始处。此时如果字符是结尾符NULL，则表明以及你敢搜索
        // 到路径名末尾，并已到达最后指定目录名或文件名，则返回该i节点指针退出。
        // 注意！如果路径名中最后一个名称也是一个目录名，但其后面没有加上&#039;/&#039;字符，
        // 则函数不会返回该最后目录的i节点！例如：对于路径名/usr/src/linux，该函数
        // 将只返回src/目录名的i节点。
        for(namelen=0;(c=get_fs_byte(pathname++))&amp;&amp;(c!=&#039;/&#039;);namelen++)
            /* nothing */ ;
        if (!c)
            return inode;
        // 在得到当前目录名部分（或文件名）后，我们调用查找目录项函数find_entry()在
        // 当前处理的目录中寻找指定名称的目录项。如果没有找到，则返回该i节点，并返回
        // NULL退出。然后在找到的目录项中取出其i节点号inr和设备号idev，释放包含该目录
        // 项的高速缓冲块并放回该i节点。然后去节点号inr的i节点inode，并以该目录项为
        // 当前目录继续循环处理路径名中的下一目录名部分（或文件名）。
        if (!(bh = find_entry(&amp;inode,thisname,namelen,&amp;de))) {
            iput(inode);
            return NULL;
        }
        inr = de-&gt;inode;                        // 当前目录名部分的i节点号
        idev = inode-&gt;i_dev;
        brelse(bh);
        iput(inode);
        if (!(inode = iget(idev,inr)))          // 取i节点内容。
            return NULL;
    }
}</code></pre></div><p>&#160; &#160;（7）查找目录的最高路径，比如：</p><p> cd&#160; /home/test的最高路径是test（最后一个反斜杠后面是test）：basename是test，namelen=4；<br /> cd&#160; /home/test/的最高路径是空的（最后一个反斜杠后面是空的）：basename是null，namelen=0；</p><div class="codebox"><pre class="vscroll"><code>/*
 *    dir_namei()
 *
 * dir_namei() returns the inode of the directory of the
 * specified name, and the name within that directory.
 */
// 参数：pathname - 目录路径名；namelen - 路径名长度；name - 返回的最顶层目录名。
// 返回：指定目录名最顶层目录的i节点指针和最顶层目录名称及长度。出错时返回NULL。
// 注意！！这里&quot;最顶层目录&quot;是指路径名中最靠近末端的目录。
static struct m_inode * dir_namei(const char * pathname,
    int * namelen, const char ** name)
{
    char c;
    const char * basename;
    struct m_inode * dir;

    // 首先取得指定路径名最顶层目录的i节点。然后对路径名Pathname 进行搜索检测，查出
    // 最后一个&#039;/&#039;字符后面的名字字符串，计算其长度，并且返回最顶层目录的i节点指针。
    // 注意！如果路径名最后一个字符是斜杠字符&#039;/&#039;，那么返回的目录名为空，并且长度为0.
    // 但返回的i节点指针仍然指向最后一个&#039;/&#039;字符钱目录名的i节点。
    if (!(dir = get_dir(pathname)))
        return NULL;
    basename = pathname;
    while ((c=get_fs_byte(pathname++)))
        if (c==&#039;/&#039;)
            basename=pathname;
    *namelen = pathname-basename-1;
    *name = basename;
    return dir;
}</code></pre></div><p>&#160; &#160;（8）这个可能是最有用的函数之一了：namei函数，用户传入路径，返回对应的inode节点</p><div class="codebox"><pre class="vscroll"><code>/*
 *    namei()
 *
 * is used by most simple commands to get the inode of a specified name.
 * Open, link etc use their own routines, but this is enough for things
 * like &#039;chmod&#039; etc.
 */
//// 取指定路径名的i节点。
// 参数：pathname - 路径名。
// 返回：对应的i节点。
struct m_inode * namei(const char * pathname)
{
    const char * basename;
    int inr,dev,namelen;
    struct m_inode * dir;
    struct buffer_head * bh;
    struct dir_entry * de;

    // 首先查找指定路径的最顶层目录的目录名并得到其i节点，若不存在，则返回NULL退出。
    // 如果返回的最顶层名字长度是0，则表示该路径名以一个目录名为最后一项。因此我们
    // 已经找到对应目录的i节点，可以直接返回该i节点退出。
    if (!(dir = dir_namei(pathname,&amp;namelen,&amp;basename)))
        return NULL;
    if (!namelen)            /* special case: &#039;/usr/&#039; etc */
        return dir;
    // 然后在返回的顶层目录中寻找指定文件名目录项的i节点。注意！因为如果最后也是一个
    // 目录名，但其后没有加&#039;/&#039;,则不会返回该目录的i节点！例如：/usr/src/linux,将只返回
    // src/目录名的i节点。因为函数dir_namei()把不以&#039;/&#039;结束的最后一个名字当作一个文件名
    // 来看待，所以这里需要单独对这种情况使用寻找目录项i节点函数find_entry()进行处理。
    // 此时de中含有寻找到的目录项指针，而dir是包含该目录项的目录的i节点指针。
    bh = find_entry(&amp;dir,basename,namelen,&amp;de);
    if (!bh) {
        iput(dir);
        return NULL;
    }
    // 接着取该目录项的i节点号和设备号，并释放包含该目录项的高速缓冲块并返回目录i节点。
    // 然后取对应节点号的i节点，修改其被访问时间为当前时间，并置已修改标志。最后返回
    // 该i节点指针。
    inr = de-&gt;inode;
    dev = dir-&gt;i_dev;/*子目录设备号要和父目录一致*/
    brelse(bh);
    iput(dir);
    dir=iget(dev,inr);
    if (dir) {
        dir-&gt;i_atime=CURRENT_TIME;
        dir-&gt;i_dirt=1;
    }
    return dir;
}</code></pre></div><p>&#160; &#160;namei没有做任何权限的判断，也只是查找现成的dir_entry，如果没有就返回null了，所以只能用在find -name这种命令；但实际用户使用时，还涉及到文件的权限校验，文件打开方式判断（只读？可读可写？）等，情况比find -name这种命令复杂很多，需要单独重新写个接口来实现这些需求，如下：相比namei，</p><p>&#160; 检查了权限和打开模式；<br />&#160; &#160; &#160; &#160;如果没找到对饮的inode就新建inode，而不是直接返回null；“宏观”层面感受：用户调用open函数想打开一个文件，如果该文件不存在，就新建文件，并返回文件的handler！</p><div class="codebox"><pre class="vscroll"><code>/*
 *    open_namei()
 *
 * namei for open - this is in fact almost the whole open-routine.
 */
//// 文件打开namei函数。
// 参数filename是文件名，flag是打开文件标志，他可取值：O_RDONLY(只读)、O_WRONLY(只写)
// 或O_RDWR(读写)，以及O_CREAT(创建)、O_EXCL(被创建文件必须不存在)、O_APPEND(在文件尾
// 添加数据)等其他一些标志的组合。如果本调用创建了一个新文件，则mode就用于指定文件的
// 许可属性。这些属性有S_IRWXU(文件宿主具有读、写和执行权限)、S_IRUSR(用户具有读文件
// 权限)、S_IRWXG(组成员具有读、写和执行权限)等等。对于新创建的文件，这些属性只应用于
// 将来对文件的访问，创建了只读文件的打开调用也将返回一个可读写的文件句柄。
// 返回：成功返回0，否则返回出错码；res_inode - 返回对应文件路径名的i节点指针。
int open_namei(const char * pathname, int flag, int mode,
    struct m_inode ** res_inode)
{
    const char * basename;
    int inr,dev,namelen;
    struct m_inode * dir, *inode;
    struct buffer_head * bh;
    struct dir_entry * de;

    // 首先对函数参数进行合理的处理。如果文件访问模式标志是只读(0)，但是文件截零标志
    // O_TRUNC却置位了，则在文件打开标志中添加只写O_WRONLY。这样做的原因是由于截零标志
    // O_TRUNC必须在文件可写情况下才有效。然后使用当前进程的文件访问许可屏蔽码，屏蔽掉
    // 给定模式中的相应位，并添上对普通文件标志I_REGULAR。该标志将用于打开的文件不存在
    // 而需要创建文件时，作为新文件的默认属性。
    if ((flag &amp; O_TRUNC) &amp;&amp; !(flag &amp; O_ACCMODE))
        flag |= O_WRONLY;
    mode &amp;= 0777 &amp; ~current-&gt;umask;
    mode |= I_REGULAR;
    // 然后根据指定的路径名寻找对应的i节点，以及最顶端目录名及其长度。此时如果最顶端目录
    // 名长度为0（例如&#039;/usr/&#039;这种路径名的情况），那么若操作不是读写、创建和文件长度截0，
    // 则表示是在打开一个目录名文件操作。于是直接返回该目录的i节点并返回0退出。否则说明
    // 进程操作非法，于是放回该i节点，返回出错码。
    if (!(dir = dir_namei(pathname,&amp;namelen,&amp;basename)))
        return -ENOENT;
    if (!namelen) {            /* special case: &#039;/usr/&#039; etc */
        if (!(flag &amp; (O_ACCMODE|O_CREAT|O_TRUNC))) {
            *res_inode=dir;
            return 0;
        }
        iput(dir);
        return -EISDIR;
    }
    // 接着根据上面得到的最顶层目录名的i节点dir，在其中查找取得路径名字符串中最后的文件名
    // 对应的目录项结构de，并同时得到该目录项所在的高速缓冲区指针。如果该高速缓冲指针为NULL，
    // 则表示没有找到对应文件名的目录项，因此只可能是创建文件操作。此时如果不是创建文件，则
    // 放回该目录的i节点，返回出错号退出。如果用户在该目录没有写的权力，则放回该目录的i节点，
    // 返回出错号退出。
    bh = find_entry(&amp;dir,basename,namelen,&amp;de);
    if (!bh) {
        if (!(flag &amp; O_CREAT)) {
            iput(dir);
            return -ENOENT;
        }
        if (!permission(dir,MAY_WRITE)) {
            iput(dir);
            return -EACCES;
        }
        // 现在我们确定了是创建操作并且有写操作许可。因此我们就在目录i节点对设备上申请一个
        // 新的i节点给路径名上指定的文件使用。若失败则放回目录的i节点，并返回没有空间出错码。
        // 否则使用该新i节点，对其进行初始设置：置节点的用户id；对应节点访问模式；置已修改
        // 标志。然后并在指定目录dir中添加一个新目录项。
        inode = new_inode(dir-&gt;i_dev);
        if (!inode) {
            iput(dir);
            return -ENOSPC;
        }
        inode-&gt;i_uid = current-&gt;euid;
        inode-&gt;i_mode = mode;
        inode-&gt;i_dirt = 1;
        bh = add_entry(dir,basename,namelen,&amp;de);
        // 如果返回的应该含有新目录项的高速缓冲区指针为NULL，则表示添加目录项操作失败。于是
        // 将该新i节点的引用计数减1，放回该i节点与目录的i节点并返回出错码退出。否则说明添加
        // 目录项操作成功。于是我们来设置该新目录的一些初始值：置i节点号为新申请的i节点的号
        // 码；并置高速缓冲区已修改标志。然后释放该高速缓冲区，放回目录的i节点。返回新目录
        // 项的i节点指针，并成功退出。
        if (!bh) {
            inode-&gt;i_nlinks--;
            iput(inode);
            iput(dir);
            return -ENOSPC;
        }
        de-&gt;inode = inode-&gt;i_num;
        bh-&gt;b_dirt = 1;
        brelse(bh);
        iput(dir);
        *res_inode = inode;
        return 0;
    }
    // 若上面在目录中取文件名对应目录项结构的操作成功（即bh不为NULL），则说明指定打开的文件已
    // 经存在。于是取出该目录项的i节点号和其所在设备号，并释放该高速缓冲区以及放回目录的i节点
    // 如果此时堵在操作标志O_EXCL置位，但现在文件已经存在，则返回文件已存在出错码退出。
    inr = de-&gt;inode;
    dev = dir-&gt;i_dev;
    brelse(bh);
    iput(dir);
    if (flag &amp; O_EXCL)
        return -EEXIST;
    // 然后我们读取该目录项的i节点内容。若该i节点是一个目录i节点并且访问模式是只写或读写，或者
    // 没有访问的许可权限，则放回该i节点，返回访问权限出错码退出。
    if (!(inode=iget(dev,inr)))
        return -EACCES;
    if ((S_ISDIR(inode-&gt;i_mode) &amp;&amp; (flag &amp; O_ACCMODE)) ||
        !permission(inode,ACC_MODE(flag))) {
        iput(inode);
        return -EPERM;
    }
    // 接着我们更新该i节点的访问时间字段值为当前时间。如果设立了截0标志，则将该i节点的文件长度
    // 截0.最后返回该目录项i节点的指针，并返回0（成功）。
    inode-&gt;i_atime = CURRENT_TIME;
    if (flag &amp; O_TRUNC)
        truncate(inode);
    *res_inode = inode;
    return 0;
}</code></pre></div><p>&#160; &#160;至此，不知道各读者有没有发现文件和目录相关操作的共性：全都围绕inode和dir_entry两个结构体各种操作！</p><p>&#160; dir_entry有字符串数组，存放了目录或文件的字符，可以先根据字符串找到目标dir_entry<br />&#160; &#160; &#160; &#160;取出目标dir_entry的inode字段，这个字段标时了inode节点的偏移位置（或者说在磁盘上block的位置）<br />&#160; &#160; &#160; &#160;利用inode偏移从磁盘读取inode节点，这里面包含了文件的元信息，尤其时i_zone字段，根据这个进一步从磁盘读取文件数据<br />&#160; &#160; &#160; &#160;磁盘中的inode通过inode位图标记是否使用；dir_entry在inode根节点；内存中inode存放在inode_table数组！不论是在磁盘，还是在内存，本质上都是把inode或dir_entry结构体的实例集合起来统一管理（检索查找）！<br />&#160; 两个结构体本质上都是用来做索引，dir_entry字段少，相当于简版的索引！inode字段多，相当于完整的索引！</p><p> （9）依次类推，mknod也是类似的操作（这居然还是个系统调用，级别相当的高）：</p><div class="codebox"><pre class="vscroll"><code>//// 创建一个设备特殊文件或普通文件节点(node)
// 该函数创建名称为filename,由mode和dev指定的文件系统节点（普通文件、设备特殊文件或命名管道）
// 参数：filename - 路径名；mode - 指定使用许可以及所创建节点的类型；dev - 设备号。
// 返回：成功则返回0，否则返回出错码。
int sys_mknod(const char * filename, int mode, int dev)
{
    const char * basename;
    int namelen;
    struct m_inode * dir, * inode;
    struct buffer_head * bh;
    struct dir_entry * de;

    // 首先检查操作许可和参数的有效性并取路径名中顶层目录的i节点。如果不是超级用户，则返回
    // 访问许可出错码。如果找不到对应路径名中顶层目录的i节点，则返回出错码。如果最顶端的
    // 文件名长度为0，则说明给出的路径名最后没有指定文件名，放回该目录i节点，返回出错码退出。
    // 如果在该目录中没有写的权限，则放回该目录的i节点，返回访问许可出错码退出。如果不是超级
    // 用户，则返回访问许可出错码。
    if (!suser())
        return -EPERM;
    if (!(dir = dir_namei(filename,&amp;namelen,&amp;basename)))
        return -ENOENT;
    if (!namelen) {
        iput(dir);
        return -ENOENT;
    }
    if (!permission(dir,MAY_WRITE)) {
        iput(dir);
        return -EPERM;
    }
    // 然后我们搜索一下路径名指定的文件是否已经存在。若已经存在则不能创建同名文件节点。
    // 如果对应路径名上最后的文件名的目录项已经存在，则释放包含该目录项的缓冲区块并放回
    // 目录的i节点，放回文件已存在的出错码退出。
    bh = find_entry(&amp;dir,basename,namelen,&amp;de);
    if (bh) {
        brelse(bh);
        iput(dir);
        return -EEXIST;
    }
    // 否则我们就申请一个新的i节点，并设置该i节点的属性模式。如果要创建的是块设备文件或者是
    // 字符设备文件，则令i节点的直接逻辑块指针0等于设备号。即对于设备文件来说，其i节点的
    // i_zone[0]中存放的是该设备文件所定义设备的设备号。然后设置该i节点的修改时间、访问
    // 时间为当前时间，并设置i节点已修改标志。
    inode = new_inode(dir-&gt;i_dev);
    if (!inode) {
        iput(dir);
        return -ENOSPC;
    }
    inode-&gt;i_mode = mode;
    if (S_ISBLK(mode) || S_ISCHR(mode))
        inode-&gt;i_zone[0] = dev;
    inode-&gt;i_mtime = inode-&gt;i_atime = CURRENT_TIME;
    inode-&gt;i_dirt = 1;
    // 接着为这个新的i节点在目录中新添加一个目录项。如果失败（包含该目录项的高速缓冲块指针为
    // NULL），则放回目录的i节点，吧所申请的i节点引用连接计数复位，并放回该i节点，返回出错码退出。
    bh = add_entry(dir,basename,namelen,&amp;de);
    if (!bh) {
        iput(dir);
        inode-&gt;i_nlinks=0;
        iput(inode);
        return -ENOSPC;
    }
    // 现在添加目录项操作也成功了，于是我们来设置这个目录项内容。令该目录项的i节点字段于新i节点
    // 号，并置高速缓冲区已修改标志，放回目录和新的i节点，释放高速缓冲区，最后返回0（成功）。
    de-&gt;inode = inode-&gt;i_num;/*刚创建的inode和dir_entry做映射*/
    bh-&gt;b_dirt = 1;
    iput(dir);
    iput(inode);
    brelse(bh);
    return 0;
}</code></pre></div><p>&#160; &#160;早期连创建目录都是系统调用，只能系统管理员创建的：</p><div class="codebox"><pre class="vscroll"><code>//// 创建一个目录
// 参数：pathname - 路径名；mode - 目录使用的权限属性。
// 返回：成功则返回0，否则返回出错码。
int sys_mkdir(const char * pathname, int mode)
{
    const char * basename;
    int namelen;
    struct m_inode * dir, * inode;
    struct buffer_head * bh, *dir_block;
    struct dir_entry * de;

    // 首先检查操作许可和参数的有效性并取路径名中顶层目录的i节点。如果不是超级用户，则
    // 放回访问许可出错码。如果找不到对应路径名中顶层目录的i节点，则返回出错码。如果最
    // 顶端的文件名长度为0，则说明给出的路径名最后没有指定文件名，放回该目录i节点，返回
    // 出错码退出。如果在该目录中没有写权限，则放回该目录的i节点，返回访问许可出错码退出。
    // 如果不是超级用户，则返回访问许可出错码。
    if (!suser())
        return -EPERM;
    if (!(dir = dir_namei(pathname,&amp;namelen,&amp;basename)))
        return -ENOENT;
    if (!namelen) {
        iput(dir);
        return -ENOENT;
    }
    if (!permission(dir,MAY_WRITE)) {
        iput(dir);
        return -EPERM;
    }
    // 然后我们搜索一下路径名指定的目录名是否已经存在。若已经存在则不能创建同名目录节点。
    // 如果对应路径名上最后的目录名的目录项已经存在，则释放包含该目录项的缓冲区块并放回
    // 目录的i节点，返回文件已经存在的出错码退出。否则我们就申请一个新的i节点，并设置该i
    // 节点的属性模式：置该新i节点对应的文件长度为32字节（2个目录项的大小），置节点已修改
    // 标志，以及节点的修改时间和访问时间，2个目录项分别用于‘.’和&#039;..&#039;目录。
    bh = find_entry(&amp;dir,basename,namelen,&amp;de);
    if (bh) {
        brelse(bh);
        iput(dir);
        return -EEXIST;
    }
    inode = new_inode(dir-&gt;i_dev);
    if (!inode) {
        iput(dir);
        return -ENOSPC;
    }
    inode-&gt;i_size = 32;/*目录的inode节点size是32，这个是固定的*/
    inode-&gt;i_dirt = 1;
    inode-&gt;i_mtime = inode-&gt;i_atime = CURRENT_TIME;
    // 接着为该新i节点申请一用于保存目录项数据的磁盘块，用于保存目录项结构信息。并令i节
    // 点的第一个直接块指针等于该块号。如果申请失败则放回对应目录的i节点；复位新申请的i
    // 节点连接计数；放回该新的i节点，返回没有空间出错码退出。否则置该新的i节点已修改标志。
    if (!(inode-&gt;i_zone[0]=new_block(inode-&gt;i_dev))) {
        iput(dir);
        inode-&gt;i_nlinks--;
        iput(inode);
        return -ENOSPC;
    }
    inode-&gt;i_dirt = 1;
    // 从设备上读取新申请的磁盘块（目的是吧对应块放到高速缓冲区中）。若出错，则放回对应
    // 目录的i节点；释放申请的磁盘块；复位新申请的i节点连接计数；放回该新的i节点，返回没有
    // 空间出错码退出。
    if (!(dir_block=bread(inode-&gt;i_dev,inode-&gt;i_zone[0]))) {
        iput(dir);
        free_block(inode-&gt;i_dev,inode-&gt;i_zone[0]);
        inode-&gt;i_nlinks--;
        iput(inode);
        return -ERROR;
    }
    // 然后我们在缓冲块中建立起所创建目录文件中的2个默认的新目录项(&#039;.&#039;和&#039;..&#039;)结构数据。
    // 首先令de指向存放目录项的数据块，然后置该目录项的i节点号字段等于新申请的i节点号，
    // 名字字段等于&#039;.&#039;。然后de指向下一个目录项结构，并在该结构中存放上级目录的i节点号
    // 和名字&#039;..&#039;。然后设置该高速缓冲块 已修改标志，并释放该缓冲块。再初始化设置新i节点
    // 的模式字段，并置该i节点已修改标志。
    de = (struct dir_entry *) dir_block-&gt;b_data;
    de-&gt;inode=inode-&gt;i_num;
    strcpy(de-&gt;name,&quot;.&quot;);/*新创建的目录，用ls -al查询会发现有.和..这两个目录*/
    de++;
    de-&gt;inode = dir-&gt;i_num;
    strcpy(de-&gt;name,&quot;..&quot;);
    inode-&gt;i_nlinks = 2;
    dir_block-&gt;b_dirt = 1;
    brelse(dir_block);
    inode-&gt;i_mode = I_DIRECTORY | (mode &amp; 0777 &amp; ~current-&gt;umask);
    inode-&gt;i_dirt = 1;
    // 现在我们在指定目录中新添加一个目录项，用于存放新建目录的i节点号和目录名。如果
    // 失败(包含该目录项的高速缓冲区指针为NULL)，则放回目录的i节点；所申请的i节点引用
    // 连接计数复位，并放回该i节点。返回出错码退出。
    bh = add_entry(dir,basename,namelen,&amp;de);
    if (!bh) {
        iput(dir);
        free_block(inode-&gt;i_dev,inode-&gt;i_zone[0]);
        inode-&gt;i_nlinks=0;
        iput(inode);
        return -ENOSPC;
    }
    // 最后令该新目录项的i节点字段等于新i节点号，并置高速缓冲块已修改标志，放回目录和
    // 新的i节点，是否高速缓冲块，最后返回0(成功).
    de-&gt;inode = inode-&gt;i_num;
    bh-&gt;b_dirt = 1;
    dir-&gt;i_nlinks++;
    dir-&gt;i_dirt = 1;
    iput(dir);
    iput(inode);
    brelse(bh);
    return 0;
}</code></pre></div><p>&#160; &#160;（10）创建硬链接：本质就是新建该路径的dir_entry，然后和文件原inode映射绑定！</p><p>找到指定文件的inode<br />再指定文件的路径中创建新的dir_entry<br />新创建的dir_entry映射到原inode：de-&gt;inode = oldinode-&gt;i_num</p><div class="codebox"><pre class="vscroll"><code>//// 为文件建立一个文件名目录项
// 为一个已存在的文件创建一个新链接(也称为硬链接 - hard link)
// 参数：oldname - 原路径名；newname - 新的路径名
// 返回：若成功则返回0，否则返回出错号。
int sys_link(const char * oldname, const char * newname)
{
    struct dir_entry * de;
    struct m_inode * oldinode, * dir;
    struct buffer_head * bh;
    const char * basename;
    int namelen;

    // 首先对原文件名进行有效性验证，它应该存在并且不是一个目录名。所以我们先取得原文件
    // 路径名对应的i节点oldname.若果为0，则表示出错，返回出错号。若果原路径名对应的是
    // 一个目录名，则放回该i节点，也返回出错号。
    oldinode=namei(oldname);
    if (!oldinode)
        return -ENOENT;
    if (S_ISDIR(oldinode-&gt;i_mode)) {
        iput(oldinode);
        return -EPERM;
    }
    // 然后查找新路径名的最顶层目录的i节点dir，并返回最后的文件名及其长度。如果目录的
    // i节点没有找到，则放回原路径名的i节点，返回出错号。如果新路径名中不包括文件名，
    // 则放回原路径名i节点和新路径名目录的i节点，返回出错号。
    dir = dir_namei(newname,&amp;namelen,&amp;basename);
    if (!dir) {
        iput(oldinode);
        return -EACCES;
    }
    if (!namelen) {//以反斜杠结尾，后面啥都没了，导致namelen=0；
        iput(oldinode);
        iput(dir);
        return -EPERM;
    }
    // 我们不能跨设备建立硬链接。因此如果新路径名顶层目录的设备号与原路径名的设备号不
    // 一样，则放回新路径名目录的i节点和原路径名的i节点，返回出错号。另外，如果用户没
    // 有在新目录中写的权限，则也不能建立连接，于是放回新路径名目录的i节点和原路径名
    // 的i节点，返回出错号。
    if (dir-&gt;i_dev != oldinode-&gt;i_dev) {
        iput(dir);
        iput(oldinode);
        return -EXDEV;
    }
    if (!permission(dir,MAY_WRITE)) {
        iput(dir);
        iput(oldinode);
        return -EACCES;
    }
    // 现在查询该新路径名是否已经存在，如果存在则也不能建立链接。于是释放包含该已存在
    // 目录项的高速缓冲块，放回新路径名目录的i节点和原路径名的i节点，返回出错号。
    bh = find_entry(&amp;dir,basename,namelen,&amp;de);
    if (bh) {
        brelse(bh);
        iput(dir);
        iput(oldinode);
        return -EEXIST;
    }
    // 现在所有条件都满足了，于是我们在新目录中添加一个目录项。若失败则放回该目录的
    // i节点和原路径名的i节点，返回出错号。否则初始设置该目录项的i节点号等于原路径名的
    // i节点号，并置包含该新添加目录项的缓冲块已修改标志，释放该缓冲块，放回目录的i节点。
    bh = add_entry(dir,basename,namelen,&amp;de);
    if (!bh) {
        iput(dir);
        iput(oldinode);
        return -ENOSPC;
    }
    de-&gt;inode = oldinode-&gt;i_num;/*老的inode对一个的block号给新建的dir_entry，借此建立映射*/
    bh-&gt;b_dirt = 1;
    brelse(bh);
    iput(dir);
    // 再将原节点的硬链接计数加1，修改其改变时间为当前时间，并设置i节点已修改标志。最后
    // 放回原路径名的i节点，并返回0（成功）。
    oldinode-&gt;i_nlinks++;
    oldinode-&gt;i_ctime = CURRENT_TIME;
    oldinode-&gt;i_dirt = 1;
    iput(oldinode);
    return 0;
}</code></pre></div><p>&#160; 总结：</p><p>&#160; 目录本质上就是一系列dir_entry的集合！创建/修改目录就是创建/修改dir_entry，创建/修改文件就是创建/修改inode；&#160; &#160;<br />&#160; &#160; &#160; &#160;逆向或破解时掌握dir_entry和inode，就相当于掌握了所有的目录和文件；</p><br /><br /><br /><p>参考：</p><p>1、https://zhuanlan.zhihu.com/p/76595175&#160; &#160; 深入浅出文件系统原理之文件读取（基于linux0.11）</p><p>2、https://www.bilibili.com/video/BV1tQ4y1d7mo?p=27&#160; linux内核精讲</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sat, 08 Oct 2022 04:42:01 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=446&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[linux源码解读（四）：文件系统——挂载和卸载]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=445&amp;action=new</link>
			<description><![CDATA[<p>对于普通用户而言，日常用的都是windows操作系统。windows把整个物理硬盘分成C、D、E、F.....等逻辑分区，用户可以随意在各个逻辑分区存放数据文件；逻辑分区之间是独立互不影响的，格式化某个逻辑分区，不会影响其他逻辑分区的数据，所以C、D、E、F.....等逻辑分区就是磁盘的根目录；如果要调整逻辑分区的大小，需要用专业的工具操作，比如windows自带的diskmgmt.msc工具。扩容时，需要把空闲的磁盘空间“加入”到目标逻辑分区，这个过程就是挂载，挂载后用户就能正常使用；而在linux下，所有的文件、目录、设备都有一个路径，这个路径永远以/开头，用/分隔。如果有设备加入（比如增加磁盘、U盘等），同样需要挂载到某个路径下、让linux的目录和新设备的目录合二为一后才能正常使用（谁让linux是万物皆文件了？设备都被当成是文件，自然要放在某个目录下了）；上面的说法可能有点抽象，这里举个栗子：公司跳槽来了一位新同事，这位新同事先要和公司签订劳动合同，然后去HR那里注册身份信息、提交银行卡号等，这些一切手续办理完毕后才会去业务部门报道开始干活；linux下面也类似：新增设备后，先把设备相关信息加入到某些结构体（这里用的是超级块），然后通过这些结构体去读写（检索）该设备，否则内核是没法管理新增设备的！ 前面介绍过，linux采用了super_block保存inode位图和数据块位图信息，所以super_block是整个文件系统“最上层”的结构体，所有mount、unmount等操作也都是从super_block开始的！所有相关的操作都在super.c文件里，接下来我们一一解析！</p><p>&#160; 1、这里再说明一下各种概念之间的关系，如下图：</p><p>&#160; linux操作系统内核或app不会直接读写磁盘，都是直接读写高速缓存区的（通过memcpy拷贝内存数据）<br />&#160; &#160; &#160; &#160;高速缓存区调用ll_rw_block函数读写磁盘<br />&#160; &#160; &#160; &#160;super_block、inode、数据块等结构体即存磁盘，也可以存内存，通过ll_rw_block函数在内存和磁盘之间同步<br />&#160; &#160; &#160; &#160;buffer_head的block和磁盘中的block是有一定对应关系的</p><p><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211203183132677-2000773010.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; 这个图看起来有点复杂，其实原理很简单：</p><p>&#160; 内存和磁盘本质功能都一样：都是用来存储数据的，唯一的区别就是掉电后数据丢不丢<br />&#160; &#160; &#160; &#160;既然存了数据，自然也要读取了，就涉及到寻址；内存寻址很简单：内存最小的存储单位是byte，每个byte都有自己的地址编号，可以根据地址编号直接读数据，比如 int *p=0x12345678，意味着从地址0x12345678处开始依次读取4byte的数据，数据结尾地址就是0x12345678+4=0x1234567b；<br />&#160; &#160; &#160; &#160;相比内存，磁盘读数据稍微要“麻烦”一点：磁盘被人为划分成了块，每块的大小是512byte*8=4KB；为了方便读取块的数据，也需要给每个块编号，这个编号和内存的地址编号本质都是一样的；为了标记块是否被使用，或者被哪些文件使用，诞生了inode结构体，具体使用了i_zone字段记录！进一步：为了记录哪些inode结构体被使用，又诞生了inode位图（存放inode结构体的block是有限的，导致inode结构体的数量也受限，需要省着点用！）<br />&#160; &#160; &#160; &#160;内存、磁盘这类块存储设备，使用结构体管理时，结构体的字段一般都有：是否锁定lock、是否更新update、是否有数据dirty、被使用/引用次数count、当前被那个进程在使用task、后面有哪些进程在排队等待wait.......<br />&#160; &#160;2、（1）既然super_block是“根索引”，意味着抓住了super_block，就等于抓住了整个文件系统（类似进程的内存用页目录表、页表项管理和索引类似，抓住了CR3，就得到了进程的所有内存）。为了管理所有的super_block，Linux早期的0.11版本采用了最原始的数组：</p><p>&#160; &#160;// 超级块结构表数组（NR_SUPER = 8）<br />&#160; &#160;struct super_block super_block[NR_SUPER];<br />&#160; 这里要吐槽了：数组只有8个元素，意味着只能同时管理8个文件系统；不过后期的linux貌似改了，用链表串接了所有的super_block，只要内存够，可以存好多的super_block！</p><p>&#160; &#160; &#160; （2）超级块本质上也是内存的一块，所以和其他快一样，都涉及到锁、等待、释放等操作，代码如下：可以看到和inode、block等思路完全一样，没任何区别！</p><div class="codebox"><pre class="vscroll"><code>// 以下3个函数（lock_super()、free_super()和wait_on_super()）的作用与inode.c文件中头
// 3个函数的作用雷同，只是这里操作的对象换成了超级块。
//// 锁定超级块
// 如果超级块已被锁定，则将当前任务置为不可中断的等待状态，并添加到该超级块等待队列
// s_wait中。直到该超级块解锁并明确地唤醒本地任务。然后对其上锁。
static void lock_super(struct super_block * sb)
{
    cli();                          // 关中断
    while (sb-&gt;s_lock)              // 如果该超级块已经上锁，则睡眠等待。
        sleep_on(&amp;(sb-&gt;s_wait));
    sb-&gt;s_lock = 1;                 // 会给超级块加锁（置锁定标志）
    sti();                          // 开中断
}

//// 对指定超级块解锁
// 复位超级块的锁定标志，并明确地唤醒等待在此超级块等待队列s_wait上的所有进程。
// 如果使用ulock_super这个名称则可能更妥贴。
static void free_super(struct super_block * sb)
{
    cli();
    sb-&gt;s_lock = 0;             // 复位锁定标志
    wake_up(&amp;(sb-&gt;s_wait));     // 唤醒等待该超级块的进程。
    sti();
}

//// 睡眠等待超级解锁
// 如果超级块已被锁定，则将当前任务置为不可中断的等待状态，并添加到该超级块的等待
// 队列s_wait中。知道该超级块解锁并明确的唤醒本地任务.
static void wait_on_super(struct super_block * sb)
{
    cli();
    while (sb-&gt;s_lock)
        sleep_on(&amp;(sb-&gt;s_wait));
    sti();
}1</code></pre></div><p>&#160; &#160;（3）设备是被super_block总管的，所以设备肯定要和super_block映射的，这里通过设备号在super_block数组挨个查找映射好的super_block!</p><div class="codebox"><pre><code>//// 取指定设备的超级块
// 在超级块表（数组）中搜索指定设备dev的超级块结构信息。若找到刚返回超级块的指针，
// 否则返回空指针
struct super_block * get_super(int dev)
{
    struct super_block * s;

    // 首先判断参数给出设备的有效性。若设备号为0则返回NULL，然后让s指向超级块数组
    // 起始处，开始搜索整个超级块数组，以寻找指定设备dev的超级块。
    if (!dev)
        return NULL;
    s = 0+super_block;
    while (s &lt; NR_SUPER+super_block)
        // 如果当前搜索项是指定设备的超级块，即该超级块的设备号字段值与函数参数指定的
        // 相同，则先等待该超级块解锁。在等待期间，该超级块项有可能被其他设备使用，因此
        // 等待返回之后需要再判断一次是否是指定设备的超级块，如果是则返回该超级块的指针。
        // 否则就重新对超级块数组再搜索一遍，因此此时s需重又指向超级块数组开始处。
        if (s-&gt;s_dev == dev) {
            wait_on_super(s);
            if (s-&gt;s_dev == dev)
                return s;
            s = 0+super_block;
        // 如果当前搜索项不是，则检查下一项，如果没有找到指定的超级块，则返回空指针。
        } else
            s++;
    return NULL;
}</code></pre></div><p>&#160; &#160; &#160; super_block用完后可以释放，这里把super_block结构体的dev清零，表示不和任何设备映射（或则说部管理任何设备）！同时释放位图块和逻辑块，最后释放该块上的锁，然后唤醒等待的进程；</p><div class="codebox"><pre class="vscroll"><code>//// 释放指定设备的超级块
// 释放设备所使用的超级块数组项，并释放该设备i节点的位图和逻辑块位图所占用的高速缓冲块。
// 如果超级块对应的文件系统是根文件系统，或者其某个i节点上已经安装有其他的文件系统，
// 则不能释放该超级块。
void put_super(int dev)
{
    struct super_block * sb;
    /* struct m_inode * inode;*/
    int i;

    // 首先判断参数的有效性和合法性。如果指定设备是根文件系统设备，则显示警告信息“根
    // 系统盘改变了，准备生死决战吧”，并返回。然后在超级块表现中寻找指定设备号的文件系统
    // 超级块。如果找不到指定设备的超级块，则返回。另外，如果该超级块指明该文件系统所安装
    // 到的i节点还没有被处理过，则显示警告信息并返回。在文件系统卸载操作中，s_imount会先被
    // 置成NULL以后才会调用本函数。
    if (dev == ROOT_DEV) {
        printk(&quot;root diskette changed: prepare for armageddon\n\r&quot;);
        return;
    }
    if (!(sb = get_super(dev)))
        return;
    if (sb-&gt;s_imount) {
        printk(&quot;Mounted disk changed - tssk, tssk\n\r&quot;);
        return;
    }
    // 然后在找到指定设备的超级块之后，我们先锁定该超级块，再置该超级块对应的设备号字段
    // s_dev为0，也即释放该设备上的文件系统超级块。然后释放该超级块占用的其他内核资源，
    // 即释放该设备上文件系统i节点位图和逻辑块位图在缓冲区中所占用的缓冲块。下面常数符号
    // I_MAP_SLOTS和Z_MAP_LOTS均等于8，用于分别指明i节点位图和逻辑块位图占用的磁盘逻辑块
    // 数。注意，若这些缓冲块内荣被修改过，则需要作同步操作才能把缓冲块中的数据写入设备
    // 中,函数最后对该超级块解锁，并返回。
    lock_super(sb);
    sb-&gt;s_dev = 0;
    for(i=0;i&lt;I_MAP_SLOTS;i++)
        brelse(sb-&gt;s_imap[i]);
    for(i=0;i&lt;Z_MAP_SLOTS;i++)
        brelse(sb-&gt;s_zmap[i]);
    free_super(sb);
    return;
}</code></pre></div><p>&#160; 读取指定设备上的super_block:</p><p>&#160; 传入设备号，通过设备号找到内存的super_block结构体，并初始化部分属性<br />&#160; &#160; &#160; &#160;调用bread读取指定设备的第1号block（0号block是磁盘的引导块），也就是超级块；这里不理解的小伙伴建议看看上面的图：block在磁盘内部也是顺着编号的<br />&#160; &#160; &#160; &#160;从设备读出来的super_block结构体目前还防缓存区，这里需要复制到从super_block数组中找到的super_block中<br />&#160; &#160; &#160; &#160;继续调用bread读inode位图块和数据块位图，然后存放在super_block的s_imap、s_zmap数组中<br />&#160; &#160; &#160; &#160;解锁super_block结构体，唤醒其他进程使用</p><div class="codebox"><pre class="vscroll"><code>//// 读取指定设备的超级块
// 如果指定设备dev上的文件系统超级块已经在超级块表中，则直接返回该超级块项的指针。否则
// 就从设备dev上读取超级块到缓冲块中，并复制到超级块中。并返回超级块指针。
static struct super_block * read_super(int dev)
{
    struct super_block * s;
    struct buffer_head * bh;
    int i,block;

    // 首先判断参数的有效性。如果没有指明设备，则返回空指针。然后检查该设备是否可更换过
    // 盘片（也即是否软盘设备）。如果更换盘片，则高速缓冲区有关设备的所有缓冲块均失效，
    // 需要进行失效处理，即释放原来加载的文件系统。
    if (!dev)
        return NULL;
    check_disk_change(dev);
    // 如果该设备的超级块已经在超级块表中，则直接返回该超级块的指针。否则，首先在超级块
    // 数组中找出一个空项（也即字段s_dev=0的项）。如果数组已经占满则返回空指针。
    if ((s = get_super(dev)))
        return s;
    for (s = 0+super_block ;; s++) {
        if (s &gt;= NR_SUPER+super_block)
            return NULL;
        if (!s-&gt;s_dev)
            break;
    }
    // 在超级块数组中找到空项之后，就将该超级块项用于指定设备dev上的文件系统。于是对该
    // 超级块结构中的内存字段进行部分初始化处理。这些配置项只在内存出现
    s-&gt;s_dev = dev;
    s-&gt;s_isup = NULL;
    s-&gt;s_imount = NULL;
    s-&gt;s_time = 0;
    s-&gt;s_rd_only = 0;
    s-&gt;s_dirt = 0;
    // 然后锁定该超级块，并从设备上读取超级块信息到bh指向的缓冲块中。超级块位于设备的第
    // 2个逻辑块（1号块）中，（第1个是引导盘块）。如果读超级块操作失败，则释放上面选定
    // 的超级块数组中的项（即置s_dev=0），并解锁该项，返回空指针退出。否则就将设备上读取
    // 的超级块信息从缓冲块数据区复制到超级块数组相应项结构中。并释放存放读取信息的高速
    // 缓冲块。
    lock_super(s);
    if (!(bh = bread(dev,1))) {
        s-&gt;s_dev=0;
        free_super(s);
        return NULL;
    }
    *((struct d_super_block *) s) =
        *((struct d_super_block *) bh-&gt;b_data);
    brelse(bh);
    // 现在我们从设备dev上得到了文件系统的超级块，于是开始检查这个超级块的有效性并从设备
    // 上读取i节点位图和逻辑块位图等信息。如果所读取的超级块的文件系统魔数字段不对，说明
    // 设备上不是正确的文件系统，因此同上面一样，释放上面选定的超级块数组中的项，并解锁该
    // 项，返回空指针退出。对于该版Linux内核，只支持MINIX文件系统1.0版本，其魔数是0x1371。
    if (s-&gt;s_magic != SUPER_MAGIC) {
        s-&gt;s_dev = 0;
        free_super(s);
        return NULL;
    }
    // 下面开始读取设备上i节点的位图和逻辑块位图数据。首先初始化内存超级块结构中位图空间。
    // 然后从设备上读取i节点位图和逻辑块位图信息，并存放在超级块对应字段中。i节点位图保存
    // 在设备上2号块开始的逻辑块中，共占用s_imap_blocks个块，逻辑块位图在i节点位图所在块
    // 的后续块中，共占用s_zmap_blocks个块。
    for (i=0;i&lt;I_MAP_SLOTS;i++)
        s-&gt;s_imap[i] = NULL;
    for (i=0;i&lt;Z_MAP_SLOTS;i++)
        s-&gt;s_zmap[i] = NULL;
    block=2;
    for (i=0 ; i &lt; s-&gt;s_imap_blocks ; i++)
        if ((s-&gt;s_imap[i]=bread(dev,block)))
            block++;/*block块号后移*/
        else
            break;
    for (i=0 ; i &lt; s-&gt;s_zmap_blocks ; i++)
        if ((s-&gt;s_zmap[i]=bread(dev,block)))
            block++;/*block块号后移*/
        else
            break;
    // 如果读出的位图块数不等于位图应该占有的逻辑块数，说明文件系统位图信息有问题，
    // 因此只能释放前面申请并占用的所有资源，即释放i节点位图和逻辑块位图占用
    // 的高速缓冲块、释放上面选定的超级块数组项、解锁该超级块项，并返回空指针退出。
    if (block != 2+s-&gt;s_imap_blocks+s-&gt;s_zmap_blocks) {
        for(i=0;i&lt;I_MAP_SLOTS;i++)
            brelse(s-&gt;s_imap[i]);
        for(i=0;i&lt;Z_MAP_SLOTS;i++)
            brelse(s-&gt;s_zmap[i]);
        s-&gt;s_dev=0;
        free_super(s);
        return NULL;
    }
    // 否则一切成功，另外，由于对申请空闲i节点的函数来讲，如果设备上所有的i节点已经全被使用
    // 则查找函数会返回0值。因此0号i节点是不能用的，所以这里将位图中第1块的最低bit位设置为1，
    // 以防止文件系统分配0号i节点。同样的道理，也将逻辑块位图的最低位设置为1.最后函数解锁该
    // 超级块，并放回超级块指针。
    s-&gt;s_imap[0]-&gt;b_data[0] |= 1;
    s-&gt;s_zmap[0]-&gt;b_data[0] |= 1;
    free_super(s);
    return s;
}</code></pre></div><p>&#160; 3、本文最重要的两个函数：sys_umount和sys_mount；从函数名就能看出来，这两个都是系统调用！linux的mount和unmount命令底层调用就是这两个函数。先看sys_unmount：</p><p>传入的参数是设备名称，比如这里要卸载u盘，传入u盘名称，根据名称找到inode节点<br />根据inode节点找到设备号<br />如果这个inode节点有进程正在使用（比如正在从u盘复制数据），返回BUSY<br />一切就绪后重置super_block的关键字段，同时释放super_block，同步缓存区和设备的数据；<br />&#160; 从代码看，umount做了很多判断，也释放和同步了数据，所以为什么U盘用完后建议先umount（windows下是卸载），完毕后再拔U盘，而不是复制完后直接简单粗暴地拔U盘！</p><div class="codebox"><pre class="vscroll"><code>//// 卸载文件系统（系统调用）
// 参数dev_name是文件系统所在设备的设备文件名
// 该函数首先根据参数给出的设备文件名获得设备号，然后复位文件系统超级块中的相应字段，释放超级
// 块和位图占用的缓冲块，最后对该设备执行高速缓冲与设备上数据的同步操作。若卸载操作成功则返回
// 0，否则返回出错码。
int sys_umount(char * dev_name)
{
    struct m_inode * inode;
    struct super_block * sb;
    int dev;

    // 首先根据设备文件名找到对应的i节点，并取其中的设备号。设备文件所定义设备的设备号是保存在其
    // i节点的i_zone[0]中的，参见sys_mknod()的代码。另外，由于文件系统需要存放在设备上，因此如果
    // 不是设备文件，则返回刚申请的i节点dev_i,返回出错码。
    if (!(inode=namei(dev_name)))
        return -ENOENT;
    dev = inode-&gt;i_zone[0];
    if (!S_ISBLK(inode-&gt;i_mode)) {
        iput(inode);
        return -ENOTBLK;
    }
    // OK,现在上面为了得到设备号而取得的i节点已完成了它的使命，因此这里放回该设备文件的i节点。接着
    // 我们来检查一下卸载该文件系统的条件是否满足。如果设备上是根文件系统，则不能被卸载，返回。
    iput(inode);
    if (dev==ROOT_DEV)
        return -EBUSY;
    // 如果在超级块表中没有找到该设备上文件系统的超级块，或者已找到但是该设备上文件系统
    // 没有安装过，则返回出错码。如果超级块所指明的被安装到的i节点并没有置位其安装标志
    // i_mount，则显示警告信息。然后查找一下i节点表，看看是否有进程在使用该设备上的文件（if (inode-&gt;i_dev==dev &amp;&amp; inode-&gt;i_count)），
    // 如果有则返回出错码；比如挂载的U盘正在复制数据，这时的设备是被某个进程占用的，此刻umount肯定时不行的
    if (!(sb=get_super(dev)) || !(sb-&gt;s_imount))
        return -ENOENT;
    if (!sb-&gt;s_imount-&gt;i_mount)
        printk(&quot;Mounted inode has i_mount=0\n&quot;);
    for (inode=inode_table+0 ; inode&lt;inode_table+NR_INODE ; inode++)
        if (inode-&gt;i_dev==dev &amp;&amp; inode-&gt;i_count)
                return -EBUSY;
    // 现在该设备上文件系统的卸载条件均得到满足，因此我们可以开始实施真正的卸载操作了。
    // 首先复位被安装到的i节点的安装标志，释放该i节点。然后置超级块中被安装i节点字段为
    // 空，并放回设备文件系统的根i节点。接着置超级块中被安装系统根i节点指针为空。
    sb-&gt;s_imount-&gt;i_mount=0;
    iput(sb-&gt;s_imount);
    sb-&gt;s_imount = NULL;
    iput(sb-&gt;s_isup);
    sb-&gt;s_isup = NULL;
    // 最后我们释放该设备上的超级块以及位图占用的高速缓冲块，并对该设备执行高速缓冲与
    // 设备上数据的同步操作，然后返回0，表示卸载成功。
    put_super(dev);
    sync_dev(dev);
    return 0;
}</code></pre></div><p>&#160; sys_mount的操作和sys_umount基本是相反的：由于需要把新设备挂载到某个目录下，所以这里涉及到目录的操作了，但是不复杂，目录的结构体也是inode！</p><div class="codebox"><pre class="vscroll"><code>//// 安装文件系统（系统调用）
// 参数dev_name是设备文件名，dir_name是安装到的目录名，rw_flag被安装文件系统的可
// 读写标志。将被加载的地方必须是一个目录名，并并且对应的i节点没有被其他程序占用。
// 若操作成功则返回0，否则返回出错号。
int sys_mount(char * dev_name, char * dir_name, int rw_flag)
{
    struct m_inode * dev_i, * dir_i;
    struct super_block * sb;
    int dev;

    // 首先根据设备文件名找到对应的i节点，以取得其中的设备号。对于块特殊设备文件，
    // 设备号在其i节点的i_zone[0]中。另外，由于文件凶必须在块设备中，因此如果不是
    // 块设备文件，则放回刚取得的i节点dev_i，返回出错码。
    if (!(dev_i=namei(dev_name)))
        return -ENOENT;
    dev = dev_i-&gt;i_zone[0];
    if (!S_ISBLK(dev_i-&gt;i_mode)) {
        iput(dev_i);
        return -EPERM;
    }
    // OK,现在上面为了得到设备号而取得i节点dev_i已完成了它的使命，因此这里放回该
    // 设备文件的i节点。接着我们来检查一下文件系统安装到的目录名是否有效。于是根据
    // 给定的目录文件名找到对应的i节点dir_i。如果该i节点的引用计数不为1（仅在这里引用），
    // 或者该i节点的节点号是根文件系统的节点号1，则放回该i节点的返回出错码。另外，如果
    // 该节点不是一个目录文件节点，则也放回该i节点，返回出错码。因为文件系统只能安装
    // 在一个目录名上。
    iput(dev_i);
    if (!(dir_i=namei(dir_name)))
        return -ENOENT;
    if (dir_i-&gt;i_count != 1 || dir_i-&gt;i_num == ROOT_INO) {
        iput(dir_i);
        return -EBUSY;
    }
    if (!S_ISDIR(dir_i-&gt;i_mode)) {
        iput(dir_i);
        return -EPERM;
    }
    // 现在安装点也检查完毕，我们开始读取要安装文件系统的超级块信息。如果读取超级块操
    // 作失败，则返回该安装点i节点的dir_i并返回出错码。一个文件系统的超级块首先从超级
    // 块表中进行搜索，如果不在超级块表中就从设备上读取。
    if (!(sb=read_super(dev))) {
        iput(dir_i);
        return -EBUSY;
    }
    // 在得到了文件系统超级块之后，我们对它先进行检测一番。如果将要被安装的文件系统已经
    // 安装在其他地方，则放回该i节点，返回出错码。如果将要安装到的i节点已经安装了文件系
    // 统(安装标志已经置位)，则放回该i节点，也返回出错码。
    if (sb-&gt;s_imount) {
        iput(dir_i);
        return -EBUSY;
    }
    if (dir_i-&gt;i_mount) {
        iput(dir_i);
        return -EPERM;
    }
    // 最后设置被安装文件系统超级块的“被安装到i节点”字段指向安装到的目录名的i节点。并设置
    // 安装位置i节点的安装标志和节点已修改标志。然后返回（安装成功）。
    sb-&gt;s_imount=dir_i;
    dir_i-&gt;i_mount=1;
    dir_i-&gt;i_dirt=1;        /* NOTE! we don&#039;t iput(dir_i) */
    return 0;            /* we do that in umount */
}</code></pre></div><p>&#160; &#160;4、最后一个重要的函数：挂载根文件</p><p>既然是根文件，说明文件内部还未存放任何数据，先把file_table的引用计数清零<br />接着把super_block的dev、lock、wait属性清零<br />读取设备的super_block初始化，后续会用super_block结构体管理根节点和设备<br />设置super_block的inode位图和逻辑块位图</p><div class="codebox"><pre class="vscroll"><code>//// 安装根文件系统
// 该函数属于系统初始化操作的一部分。函数首先初始化文件表数组file_table[]和超级块表（数组）
// 然后读取根文件系统超级块，并取得文件系统根i节点。最后统计并显示出根文件系统上的可用资源
// （空闲块数和空闲i节点数）。该函数会在系统开机进行初始化设置时被调用。
void mount_root(void)
{
    int i,free;
    struct super_block * p;
    struct m_inode * mi;

    // 若磁盘i节点结构不是32字节，则出错停机。该判断用于防止修改代码时出现不一致情况。
    if (32 != sizeof (struct d_inode))
        panic(&quot;bad i-node size&quot;);
    // 首先初始化文件表数组（共64项，即系统同时只能打开64个文件）和超级块表。这里将所有文件
    // 结构中的引用计数设置为0（表示空闲），并发超级块表中各项结构的设备字段初始化为0（也
    // 表示空闲）。如果根文件系统所在设备是软盘的话，就提示“插入根文件系统盘，并按回车键”，
    // 并等待按键。
    for(i=0;i&lt;NR_FILE;i++)
        file_table[i].f_count=0;                        // 初始化文件表，文件统一用这个数组表示
    if (MAJOR(ROOT_DEV) == 2) {
        printk(&quot;Insert root floppy and press ENTER&quot;);   // 提示插入根文件系统盘
        wait_for_keypress();
    }
    for(p = &amp;super_block[0] ; p &lt; &amp;super_block[NR_SUPER] ; p++) {
        p-&gt;s_dev = 0;
        p-&gt;s_lock = 0;
        p-&gt;s_wait = NULL;
    }
    // 做好以上“份外”的初始化工作之后，我们开始安装根文件系统。于是从根设备上读取文件系统
    // 超级块，并取得文件系统的根i节点（1号节点）在内存i节点表中的指针。如果读根设备上超级
    // 块是吧或取根节点失败，则都显示信息并停机。
    if (!(p=read_super(ROOT_DEV)))
        panic(&quot;Unable to mount root&quot;);
    if (!(mi=iget(ROOT_DEV,ROOT_INO)))
        panic(&quot;Unable to read root i-node&quot;);
    // 现在我们对超级块和根i节点进行设置。把根i节点引用次数递增3次。因此后面也引用了该i节点。
    // 另外，iget()函数中i节点引用计数已被设置为1。然后置该超级块的被安装文件系统i节点和被
    // 安装到i节点。再设置当前进程的当前工作目录和根目录i节点。此时当前进程是1号进程（init进程）。
    mi-&gt;i_count += 3 ;    /* NOTE! it is logically used 4 times, not 1 */
    p-&gt;s_isup = p-&gt;s_imount = mi;
    current-&gt;pwd = mi;
    current-&gt;root = mi;
    // 然后我们对根文件系统的资源作统计工作。统计该设备上空闲块数和空闲i节点数。首先令i等于
    // 超级块中表明的设备逻辑块总数。然后根据逻辑块相应bit位的占用情况统计出空闲块数。这里
    // 宏函数set_bit()只是在测试bit位，而非设置bit位。“i&amp;8191”用于取得i节点号在当前位图块中对应
    // 的bit位偏移值。&quot;i&gt;&gt;13&quot;是将i除以8192，也即除一个磁盘块包含的bit位数。
    free=0;
    i=p-&gt;s_nzones;
    while (-- i &gt;= 0)
        if (!set_bit(i&amp;8191,p-&gt;s_zmap[i&gt;&gt;13]-&gt;b_data))/*逻辑块位图*/
            free++;
    // 在显示过设备上空闲逻辑块数/逻辑块总数之后。我们再统计设备上空闲i节点数。首先令i等于超级块
    // 中表明的设备上i节点总数+1.加1是将0节点也统计进去，然后根据i节点位图相应bit位的占用情况计算
    // 出空闲i节点数。最后再显示设备上可用空闲i节点数和i节点总数
    printk(&quot;%d/%d free blocks\n\r&quot;,free,p-&gt;s_nzones);
    free=0;
    i=p-&gt;s_ninodes+1;
    while (-- i &gt;= 0)
        if (!set_bit(i&amp;8191,p-&gt;s_imap[i&gt;&gt;13]-&gt;b_data))
            free++;
    printk(&quot;%d/%d free inodes\n\r&quot;,free,p-&gt;s_ninodes);
}</code></pre></div><p>&#160; &#160;5、前几天把办公的笔记本加装了一块SSD盘，一共232GB大小，未存储任何文件，结果windows光是建system volume就耗费1.2GB的内存，大概率是用在了类似super_block、inode类似的管理结构体<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211216100137429-1091102339.png" alt="FluxBB bbcode 测试" /></span> </p><p>参考:</p><p>1、https://www.disktool.cn/content-center/resize-partition/how-to-change-partition-size-windows-10.html&#160; 如果在windows扩展或缩减分区大小</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sat, 08 Oct 2022 04:33:22 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=445&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[linux源码解读（三）：文件系统——inode]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=444&amp;action=new</link>
			<description><![CDATA[<p>众所周知，计算机系统在掉电后也能存储数据的就是磁盘了，所以大量数据大部分时间是存放在磁盘的；现在新买的PC，磁盘从数百G到1TB不等；服务器的磁盘从数十TB到上百TB，这么大的存储空间，该怎么高效地管理和使用了？站在硬件角度，cpu的分页机制把虚拟内存切割成大量4KB大小的块，所以4KB也成了硬件层面最小的内存分配单元；对比内存，磁盘的管理方式也类似，只不过磁盘最小的存储或读写单元是512byte，称之为扇区（用户哪怕只想读1格byte，驱动每次也要读512byte的数据）；不过现在的文件一般都远超512byte，所以存储单个文件肯定需要超过1个扇区的空间，这就导致了磁盘的磁头要挨个读不同的扇区，花费大量时间在磁盘上寻址，导致IO效率低下，形成了瓶颈！为了提升读取效率，磁盘一般都是一次性连续读取多个扇区，即一次性读取一个&quot;块&quot;（block）。这种由多个扇区组成的&quot;块&quot;，是文件存取的最小单位。&quot;块&quot;的大小，最常见的是4KB（和内存页的大小保持一致，便于从磁盘读写数据？？？），即连续八个 sector组成一个 block；</p><p>&#160; 1、上一篇文章介绍了高速缓存区，为了方便管理这么一大块缓存区，linux采用了buffer_head结构体来描述缓存区的各种属性；同理：磁盘上也是被人为划分成了很多“块”，为了方便管理这些块，也需要相应的结构体，linux采用结构体叫m_inode（或则这样理解：文件数据都存放在block中，那么很显然，我们还必须找到一个地方储存文件的元信息，比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode，中文译名为“索引节点”；每一个文件都有对应的inode，里面包含了与该文件有关的一些信息），如下：</p><p>&#160; 注意：</p><p>&#160; 一个文件只需要一个inode节点来存储文件的元信息就够了，所以文件和inode节点是一一对应的（注意这里是文件，不是文件名）；<br />&#160; &#160; &#160; &#160; 如果说文件很大，占用了很多的磁盘block，怎么才能找全文件的占用的所有磁盘block了？此刻就要用到inode结构体的i_zone[9]字段了，文件中的数据存放在哪个硬盘上的逻辑块上就是由这个数组来映射的：前面7个是直接存储文件数据块，第8个是间接块，第9个是二级间接块！所有直接+间接+二级间接块加起来，一共64M，这个在0.11版本所在的1991年已经非常大了！</p><div class="codebox"><pre class="vscroll"><code>struct m_inode {
    unsigned short i_mode;/*文件类型和属性，ls查看的结果，比如drwx------*/
    unsigned short i_uid;/*文件宿主id*/
    unsigned long i_size;
    unsigned long i_mtime;/*文件内容上一次变动的时间*/
    unsigned char i_gid;/*groupid：宿主所在的组id*/
    unsigned char i_nlinks; /*链接数：有多少个其他的文件夹链接到这里*/
    unsigned short i_zone[9];/*文件映射的逻辑块号*/
/* these are in memory also */
    struct task_struct * i_wait;/*等待该inode节点的进程队列*/
    unsigned long i_atime;/*文件上一次打开的时间*/
    unsigned long i_ctime;/*文件的inode上一次变动的时间*/
    unsigned short i_dev;/*设备号*/
    unsigned short i_num;
    /* 多少个进程在使用这个inode*/
    unsigned short i_count;
    unsigned char i_lock;/*互斥锁*/
    unsigned char i_dirt;
    unsigned char i_pipe;
    unsigned char i_mount;
    unsigned char i_seek;
    /*
    数据是否是最新的，或者说有效的，
    update代表数据的有效性，dirt代表文件是否需要回写,
    比如写入文件的时候，a进程写入的时候，dirt是1，因为需要回写到硬盘，
    但是数据是最新的，update是1，这时候b进程读取这个文件的时候，可以从
    缓存里直接读取。
      */
    unsigned char i_update;
};</code></pre></div><p>&#160; 为了把内存文件块的数据映射到磁盘的block，linux专门写了_bmap函数：<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211205224841644-926053460.png" alt="FluxBB bbcode 测试" /></span> </p><p>i_zone映射关系图示：</p><div class="codebox"><pre class="vscroll"><code>//// 文件数据块映射到盘块的处理操作。（block位图处理函数，bmap - block map）
// 参数：inode - 文件的i节点指针；block - 文件中的数据块号；create - 创建块标志。
// 该函数把指定的文件数据块block对应到设备上逻辑块上，并返回逻辑块号。如果创建标志
// 置位，则在设备上对应逻辑块不存在时就申请新磁盘块，返回文件数据块block对应在设备
// 上的逻辑块号（盘块号）。
static int _bmap(struct m_inode * inode,int block,int create)
{
    struct buffer_head * bh;
    int i;

    // 首先判断参数文件数据块号block的有效性。如果块号小于0，则停机。如果块号大于
    // 直接块数7+间接块数(相当于二级指针)512+二次间接块数(相当于三级指针)512*512，超出文件系统表示范围，则停机。
    // 这种间接块、二次间接块类似内存分页的机制
    if (block&lt;0)
        panic(&quot;_bmap: block&lt;0&quot;);
    if (block &gt;= 7+512+512*512)
        panic(&quot;_bmap: block&gt;big&quot;);
    // 然后根据文件块号的大小值和是否设置了创建标志分别进行处理。如果该块号小于7，
    // 则使用直接块表示。如果创建标志置位，并且i节点中对应块的逻辑块(区段)字段为0，
    // 则相应设备申请一磁盘块（逻辑块），并且将磁盘上逻辑块号（盘块号）填入逻辑块
    // 字段中。然后设置i节点改变时间，置i节点已修改标志。然后返回逻辑块号。
    if (block&lt;7) {
        if (create &amp;&amp; !inode-&gt;i_zone[block])
            if ((inode-&gt;i_zone[block]=new_block(inode-&gt;i_dev))) {
                inode-&gt;i_ctime=CURRENT_TIME;
                inode-&gt;i_dirt=1;
            }
        return inode-&gt;i_zone[block];
    }
    // 如果该块号&gt;=7,且小于7+512，则说明使用的是一次间接块。下面对一次间接块进行处理。
    // 如果是创建，并且该i节点中对应间接块字段i_zone[7]是0，表明文件是首次使用间接块，
    // 则需申请一磁盘块用于存放间接块信息，并将此实际磁盘块号填入间接块字段中。然后
    // 设置i节点修改标志和修改时间。如果创建时申请磁盘块失败，则此时i节点间接块字段
    // i_zone[7] = 0,则返回0.或者不创建，但i_zone[7]原来就为0，表明i节点中没有间接块，
    // 于是映射磁盘是吧，则返回0退出。
    block -= 7;
    if (block&lt;512) {
        if (create &amp;&amp; !inode-&gt;i_zone[7])
            if ((inode-&gt;i_zone[7]=new_block(inode-&gt;i_dev))) {
                inode-&gt;i_dirt=1;
                inode-&gt;i_ctime=CURRENT_TIME;
            }
        if (!inode-&gt;i_zone[7])
            return 0;
        // 现在读取设备上该i节点的一次间接块。并取该间接块上第block项中的逻辑块号（盘块
        // 号）i。每一项占2个字节。如果是创建并且间接块的第block项中的逻辑块号为0的话，
        // 则申请一磁盘块，并让间接块中的第block项等于该新逻辑块块号。然后置位间接块的
        // 已修改标志。如果不是创建，则i就是需要映射（寻找）的逻辑块号。
        if (!(bh = bread(inode-&gt;i_dev,inode-&gt;i_zone[7])))
            return 0;
        i = ((unsigned short *) (bh-&gt;b_data))[block];
        if (create &amp;&amp; !i)
            if ((i=new_block(inode-&gt;i_dev))) {
                ((unsigned short *) (bh-&gt;b_data))[block]=i;
                bh-&gt;b_dirt=1;
            }
        // 最后释放该间接块占用的缓冲块，并返回磁盘上新申请或原有的对应block的逻辑块号。
        brelse(bh);
        return i;
    }
    // 若程序运行到此，则表明数据块属于二次间接块。其处理过程与一次间接块类似。下面是对
    // 二次间接块的处理。首先将block再减去间接块所容纳的块数(512)，然后根据是否设置了
    // 创建标志进行创建或寻找处理。如果是新创建并且i节点的二次间接块字段为0，则序申请一
    // 磁盘块用于存放二次间接块的一级信息，并将此实际磁盘块号填入二次间接块字段中。之后，
    // 置i节点已修改标志和修改时间。同样地，如果创建时申请磁盘块失败，则此时i节点二次
    // 间接块字段i_zone[8]为0，则返回0.或者不是创建，但i_zone[8]原来为0，表明i节点中没有
    // 间接块，于是映射磁盘块失败，返回0退出。
    block -= 512;
    if (create &amp;&amp; !inode-&gt;i_zone[8])
        if ((inode-&gt;i_zone[8]=new_block(inode-&gt;i_dev))) {
            inode-&gt;i_dirt=1;
            inode-&gt;i_ctime=CURRENT_TIME;
        }
    if (!inode-&gt;i_zone[8])
        return 0;
    // 现在读取设备上该i节点的二次间接块。并取该二次间接块的一级块上第 block/512 项中
    // 的逻辑块号i。如果是创建并且二次间接块的一级块上第 block/512 项中的逻辑块号为0的
    // 话，则需申请一磁盘块(逻辑块)作为二次间接块的二级快i，并让二次间接块的一级块中
    // 第block/512 项等于二级块的块号i。然后置位二次间接块的一级块已修改标志。并释放
    // 二次间接块的一级块。如果不是创建，则i就是需要映射的逻辑块号。
    if (!(bh=bread(inode-&gt;i_dev,inode-&gt;i_zone[8])))
        return 0;
    i = ((unsigned short *)bh-&gt;b_data)[block&gt;&gt;9];
    if (create &amp;&amp; !i)
        if ((i=new_block(inode-&gt;i_dev))) {
            ((unsigned short *) (bh-&gt;b_data))[block&gt;&gt;9]=i;
            bh-&gt;b_dirt=1;
        }
    brelse(bh);
    // 如果二次间接块的二级块块号为0，表示申请磁盘块失败或者原来对应块号就为0，则返回
    // 0退出。否则就从设备上读取二次间接块的二级块，并取该二级块上第block项中的逻辑块号。
    if (!i)
        return 0;
    if (!(bh=bread(inode-&gt;i_dev,i)))
        return 0;
    i = ((unsigned short *)bh-&gt;b_data)[block&amp;511];
    // 如果是创建并且二级块的第block项中逻辑块号为0的话，则申请一磁盘块（逻辑块），作为
    // 最终存放数据信息的块。并让二级块中的第block项等于该新逻辑块块号(i)。然后置位二级块
    // 的已修改标志。
    if (create &amp;&amp; !i)
        if ((i=new_block(inode-&gt;i_dev))) {
            ((unsigned short *) (bh-&gt;b_data))[block&amp;511]=i;
            bh-&gt;b_dirt=1;
        }
    // 最后释放该二次间接块的二级块，返回磁盘上新申请的或原有的对应block的逻辑块号。
    brelse(bh);
    return i;
}</code></pre></div><p>&#160; 通过上述的结构体，inode是管理起来了，但还是不够，还缺了一些属性，比如inode又多少了？那些被使用了？哪些还空着？块被锁定了么等等，为了继续管理这些属性，linux又创建了一个叫做super_block的结构体：</p><div class="codebox"><pre><code>struct super_block {
    unsigned short s_ninodes;/*i节点数量*/
    unsigned short s_nzones;/*文件系统总长度：block &lt; sb-&gt;s_firstdatazone || block &gt;= sb-&gt;s_nzones*/
    unsigned short s_imap_blocks;/*i节点位图数量*/
    unsigned short s_zmap_blocks;/*数据块位图数量*/
    unsigned short s_firstdatazone;/*第一个块的位置：block &lt; sb-&gt;s_firstdatazone || block &gt;= sb-&gt;s_nzones*/
    unsigned short s_log_zone_size;
    unsigned long s_max_size;
    unsigned short s_magic;
    /* These are only in memory */
    struct buffer_head * s_imap[8];/*i node位图在高速缓存区的指针数组*/
    struct buffer_head * s_zmap[8];/*逻辑块位图在高速缓存区的指针数组*/
    unsigned short s_dev;/*设备号，可以通过该号找到超级块*/
    struct m_inode * s_isup;/*根目录的i node*/
    struct m_inode * s_imount; /*文件系统filesystem安装的i node*/
    unsigned long s_time;/*修改时间*/
    struct task_struct * s_wait;/*等待该块的进程*/
    unsigned char s_lock;/*是否被锁定*/
    unsigned char s_rd_only;/*是否只读*/
    unsigned char s_dirt;/*是否被修改*/
};</code></pre></div><p>&#160; Linux文件系统格式化时候，格式化上面三个区域：supper block， inode 与 block 的区块，假设某一个数据的属性与权限数据是放置到 inode 5 号，而这个 inode 记录了档案数据的实际放置点为 3,4,10 这四个 block 号码，此时我们的操作系统就能够据此来寻找数据了，称为索引式文件系统；上述的文字描述看起来可能有点抽象，这些属性之间的关系如下图所示：通过超级块检索数据块位图和inode块位图；再通过数据块位图检索数据块，inode块位图检索inode节点块！所以说抓住了超级块，就等于检索了整个文件系统！</p><p>&#160; 注意：<br />&#160; 下面图示中每个块的大小统一都是1024byte=1KB，所以一个数据块位图能表示1024*8=8192个数据块！每个数据块是1KB，单个超级块一共能管理8MB的磁盘空间！0.11这个版本一共用了8个超级块，能管理8*8MB=64MB的磁盘空间！<br />&#160; &#160; &#160; &#160; block号是线性增加的，所以block号的计算方法(inode.c/read_node方法)：block = 2 + sb-&gt;s_imap_blocks + sb-&gt;s_zmap_blocks + (inode-&gt;i_num-1)/INODES_PER_BLOCK;<br />&#160; inode也是存放在快里面的，每块能存放inode节点数量计算公式：#define INODES_PER_BLOCK&#160; ((BLOCK_SIZE)/(sizeof (struct d_inode)))</p><p><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211201120344747-1244463072.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; 和task数组类似，linux仍然采用数组的形式统一集中管理所有超级块，这个版本一共设置了8个超级块：</p><p>// 超级块结构表数组（NR_SUPER = 8）<br />struct super_block super_block[NR_SUPER];<br />&#160; 通过遍历超级块数组、比对设备号找到超级块结构体；这里注意：linux常见的mount命令，本质就是把super_block的dev字段设置成对应的设备，让super_block关联上设备；然后把super_block读到高速缓存区，后续操作系统或应用程序直接读写该缓存区；最后把super_block的实例加入超级块数组，便于统一管理！</p><div class="codebox"><pre><code>//// 取指定设备的超级块
// 在超级块表（数组）中搜索指定设备dev的超级块结构信息。若找到刚返回超级块的指针，
// 否则返回空指针
struct super_block * get_super(int dev)
{
    struct super_block * s;
    // 首先判断参数给出设备的有效性。若设备号为0则返回NULL，然后让s指向超级块数组
    // 起始处，开始搜索整个超级块数组，以寻找指定设备dev的超级块。
    if (!dev)
        return NULL;
    s = 0+super_block;
    while (s &lt; NR_SUPER+super_block)
        // 如果当前搜索项是指定设备的超级块，即该超级块的设备号字段值与函数参数指定的
        // 相同，则先等待该超级块解锁。在等待期间，该超级块项有可能被其他设备使用，因此
        // 等待返回之后需要再判断一次是否是指定设备的超级块，如果是则返回该超级块的指针。
        // 否则就重新对超级块数组再搜索一遍，因此此时s需重又指向超级块数组开始处。
        if (s-&gt;s_dev == dev) {
            wait_on_super(s);
            if (s-&gt;s_dev == dev)
                return s;
            s = 0+super_block;
        // 如果当前搜索项不是，则检查下一项，如果没有找到指定的超级块，则返回空指针。
        } else
            s++;
    return NULL;
}</code></pre></div><p>&#160; &#160;2、上面的各种框架搭建好后，在正式填充和使用这些结构体之前，还需要完善位图工具，毕竟数据块和inode都涉及到位图块的使用了嘛！linux有个bitmap.c文件提供了大量的位图操作，比如：</p><p>&#160; （1）clear_block:清空1024byte的内存，作用了memset完全一样！</p><div class="codebox"><pre><code>//// 将指定地址（addr）处的一块1024字节内存清零
// 输入：eax = 0; ecx = 以字节为单位的数据块长度（BLOCK_SIZE/4）；edi ＝ 指定
// 起始地址addr。
#define clear_block(addr) \
__asm__ __volatile__ (&quot;cld\n\t&quot; \       // 清方向位
    &quot;rep\n\t&quot; \                         // 重复执行存储数据(0).
    &quot;stosl&quot; \
    ::&quot;a&quot; (0),&quot;c&quot; (BLOCK_SIZE/4),&quot;D&quot; ((long) (addr)))</code></pre></div><p>&#160; &#160;(2) 指定bit位置1，并返回原bit值；</p><div class="codebox"><pre><code>//// 把指定地址开始的第nr个位偏移处的bit位置位(nr可大于321).返回原bit位值。
// 输入：%0-eax(返回值)：%1 -eax(0)；%2-nr，位偏移值；%3-(addr)，addr的内容。
// res是一个局部寄存器变量。该变量将被保存在指定的eax寄存器中，以便于高效
// 访问和操作。这种定义变量的方法主要用于内嵌汇编程序中。详细说明可以参考
// gcc手册”在指定寄存器中的变量“。整个宏是一个语句表达式(即圆括号括住的组合句)，
// 其值是组合语句中最后一条表达式语句res的值。
// btsl指令用于测试并设置bit位。把基地址(%3)和bit位偏移值(%2)所指定的bit位值
// 先保存到进位标志CF中，然后设置该bit位为1.指令setb用于根据进位标志CF设置
// 操作数(%al)。如果CF=1则%al = 1,否则%al = 0。
#define set_bit(nr,addr) ({\
register int res ; \
__asm__ __volatile__(&quot;btsl %2,%3\n\tsetb %%al&quot;: \
&quot;=a&quot; (res):&quot;0&quot; (0),&quot;r&quot; (nr),&quot;m&quot; (*(addr))); \
res;})</code></pre></div><p>&#160; &#160;相应的，也有对指定bit清0的方法：</p><div class="codebox"><pre><code>//// 复位指定地址开始的第nr位偏移处的bit位。返回原bit位值的反码。
// 输入：%0-eax(返回值)；%1-eax(0)；%2-nr,位偏移值；%3-(addr)，addr的内容。
// btrl指令用于测试并复位bit位。其作用与上面的btsl类似，但是复位指定bit位。
// 指令setnb用于根据进位标志CF设置操作数(%al).如果CF=1则%al=0,否则%al=1.
#define clear_bit(nr,addr) ({\
register int res ; \
__asm__ __volatile__(&quot;btrl %2,%3\n\tsetnb %%al&quot;: \
&quot;=a&quot; (res):&quot;0&quot; (0),&quot;r&quot; (nr),&quot;m&quot; (*(addr))); \
res;})</code></pre></div><p>&#160; （3）从指定地址开始寻找第一个bit为0的位，目的就是找第一个没被用的块；</p><div class="codebox"><pre><code>//// 从addr开始寻找第1个0值bit位。
// 输入：%0-ecx(返回值)；%1-ecx(0); %2-esi(addr).
// 在addr指定地址开始的位图中寻找第1个是0的bit位，并将其距离addr的bit位偏移
// 值返回。addr是缓冲块数据区的地址，扫描寻找的范围是1024字节（8192bit位）。
#define find_first_zero(addr) ({ \
int __res; \
__asm__ __volatile__ (&quot;cld\n&quot; \         // 清方向位
    &quot;1:\tlodsl\n\t&quot; \                   // 取[esi]→eax.
    &quot;notl %%eax\n\t&quot; \                  // eax中每位取反。
    &quot;bsfl %%eax,%%edx\n\t&quot; \            // 从位0扫描eax中是1的第1个位，其偏移值→edx
    &quot;je 2f\n\t&quot; \                       // 如果eax中全是0，则向前跳转到标号2处。
    &quot;addl %%edx,%%ecx\n\t&quot; \            // 偏移值加入ecx(ecx是位图首个0值位的偏移值)
    &quot;jmp 3f\n&quot; \                        // 向前跳转到标号3处
    &quot;2:\taddl $32,%%ecx\n\t&quot; \          // 未找到0值位，则将ecx加1个字长的位偏移量32
    &quot;cmpl $8192,%%ecx\n\t&quot; \            // 已经扫描了8192bit位(1024字节)
    &quot;jl 1b\n&quot; \                         // 若还没有扫描完1块数据，则向前跳转到标号1处
    &quot;3:&quot; \                              // 结束。此时ecx中是位偏移量。
    :&quot;=c&quot; (__res):&quot;c&quot; (0),&quot;S&quot; (addr)); \
__res;})</code></pre></div><p>&#160; 3、光有工具还不够，要先生成超级块、inode位图和数据块才能运营整个文件系统，不是么？所以还要先建inode：</p><div class="codebox"><pre class="vscroll"><code>//// 为设备dev建立一个新i节点。初始化并返回该新i节点的指针。
// 在内存i节点表中获取一个空闲i节点表项，并从i节点位图中找一个空闲i节点。
struct m_inode * new_inode(int dev)
{
    struct m_inode * inode;
    struct super_block * sb;
    struct buffer_head * bh;
    int i,j;

    // 首先从内存i节点表(inode_table)中获取一个空闲i节点项，并读取指定设备的
    // 超级块结构。然后扫描超级块中8块i节点位图，寻找首个0bit位，寻找空闲节点，
    // 获取放置该i节点的节点号。如果全部扫描完还没找到，或者位图所在的缓冲块无效
    // (bh=NULL),则放回先前申请的i节点表中的i节点，并返回NULL退出(没有空闲的i节点)。
    if (!(inode=get_empty_inode()))
        return NULL;
    if (!(sb = get_super(dev)))
        panic(&quot;new_inode with unknown device&quot;);
    j = 8192;
    for (i=0 ; i&lt;8 ; i++)
        if ((bh=sb-&gt;s_imap[i]))
            if ((j=find_first_zero(bh-&gt;b_data))&lt;8192)
                break;
    if (!bh || j &gt;= 8192 || j+i*8192 &gt; sb-&gt;s_ninodes) {
        iput(inode);
        return NULL;
    }
    // 现在我们已经找到了还未使用的i节点号j。于是置位i节点j对应的i节点位图相应bit位。
    // 然后置i节点位图所在缓冲块已修改标志。最后初始化该i节点结构(i_ctime是i节点内容改变时间)。
    if (set_bit(j,bh-&gt;b_data))
        panic(&quot;new_inode: bit already set&quot;);
    bh-&gt;b_dirt = 1;
    inode-&gt;i_count=1;                           // 引用计数
    inode-&gt;i_nlinks=1;                          // 文件目录项连接数
    inode-&gt;i_dev=dev;                           // i节点所在的设备号
    inode-&gt;i_uid=current-&gt;euid;                 // i节点所属用户ID
    inode-&gt;i_gid=current-&gt;egid;                 // 组id
    inode-&gt;i_dirt=1;                            // 已修改标志置位
    inode-&gt;i_num = j + i*8192;                  // 对应设备中的i节点号
    inode-&gt;i_mtime = inode-&gt;i_atime = inode-&gt;i_ctime = CURRENT_TIME;
    return inode;
}</code></pre></div><p>&#160; 上述方法调用了get_empty_inode，核心思想是从inode_table中找空闲的inode，主要依靠判断i_count、i_dirt、i_lock这3个字段：</p><div class="codebox"><pre class="vscroll"><code>//// 从i节点表(inode_table)中获取一个空闲i节点项。
// 寻找引用计数count为0的i节点，并将其写盘后清零，返回指针。引用计数被置1.
struct m_inode * get_empty_inode(void)
{
    struct m_inode * inode;
    static struct m_inode * last_inode = inode_table;
    int i;

    do {
        // 在初始化last_inode指针指向i节点表头一项后循环扫描整个i节点表。如果last_inode
        // 已经指向i节点表的最后一项之后，则让其重新指向i节点表开始处，以继续循环寻找空闲
        // i节点项。如果last_inode所指向的i节点的计数值为0，则说明可能找到空闲i节点项。
        // 让inode指向该i节点。如果该i节点的已修改标志和锁定标志均为0，则我们可以使用该i
        // 节点，于是退出for循环。
        inode = NULL;
        for (i = NR_INODE; i ; i--) {
            if (++last_inode &gt;= inode_table + NR_INODE)
                last_inode = inode_table;
            if (!last_inode-&gt;i_count) {
                inode = last_inode;
                if (!inode-&gt;i_dirt &amp;&amp; !inode-&gt;i_lock)
                    break;
            }
        }
        // 如果没有找到空闲i节点（inode=NULL）,则将i节点表打印出来供调试使用，并停机。
        if (!inode) {
            for (i=0 ; i&lt;NR_INODE ; i++)
                printk(&quot;%04x: %6d\t&quot;,inode_table[i].i_dev,
                    inode_table[i].i_num);
            panic(&quot;No free inodes in mem&quot;);
        }
        // 等待该i节点解锁，如果该i节点已修改标志被置位的话，则将该i节点刷新，因为刷新时
        // 可能会睡眠，因此需要再次循环等待该i节点解锁。
        wait_on_inode(inode);
        while (inode-&gt;i_dirt) {
            write_inode(inode);
            wait_on_inode(inode);
        }
        // 如果i节点又被其他占用的话(i节点的计数值不为0了)，则重新寻找空闲i节点。否则
        // 说明已找到符合要求的空闲i节点项。则将该i节点项内容清零，并置引用计数为1，
        // 返回该i节点指针。
    } while (inode-&gt;i_count);
    memset(inode,0,sizeof(*inode));
    inode-&gt;i_count = 1;
    return inode;
}</code></pre></div><p>&#160; 用完后可以释放：注意看最后一行调用了memset，直接把整个inode节点存的数据全部清零！（这里可以对比后续的iput方法，只是执行了inode-&gt;i_count--，把引用计数减一，并未清空inode的任何数据！）</p><div class="codebox"><pre class="vscroll"><code>//// 释放指定的i节点
// 该函数首先判断参数给出的i节点号的有效性和课释放性。若i节点仍然在使用中则不能
// 被释放。然后利用超级块信息对i节点位图进行操作，复位i节点号对应的i节点位图中
// bit位，并清空i节点结构。
void free_inode(struct m_inode * inode)
{
    struct super_block * sb;
    struct buffer_head * bh;

    // 首先判断参数给出的需要释放的i节点有效性或合法性。如果i节点指针＝NULL，则
    // 退出。如果i节点上的设备号字段为0，则说明该节点没有使用。于是用0清空对应i
    // 节点所占内存区并返回。memset()定义在include/string.h中，这里表示用0填写
    // inode指针指定处、长度是sizeof(*inode)的内存快。
    if (!inode)
        return;
    if (!inode-&gt;i_dev) {
        memset(inode,0,sizeof(*inode));
        return;
    }
    // 如果此i节点还有其他程序引用，则不能释放，说明内核有问题，停机。如果文件
    // 连接数不为0，则表示还有其他文件目录项在使用该节点，因此也不应释放，而应该放回等。
    if (inode-&gt;i_count&gt;1) {
        printk(&quot;trying to free inode with count=%d\n&quot;,inode-&gt;i_count);
        panic(&quot;free_inode&quot;);
    }
    if (inode-&gt;i_nlinks)
        panic(&quot;trying to free inode with links&quot;);
    // 在判断完i节点的合理性之后，我们开始利用超级块信息对其中的i节点位图进行
    // 操作。首先取i节点所在设备的超级块，测试设备是否存在。然后判断i节点号的
    // 范围是否正确，如果i节点号等于0或大于该设备上i节点总数，则出错(0号i节点
    // 保留没有使用)。如果该i节点对应的节点位图不存在，则出错。因为一个缓冲块
    // 的i节点位图有8192 bit。因此i_num&gt;&gt;13(即i_num/8192)可以得到当前i节点所在
    // 的s_imap[]项，即所在盘块。
    if (!(sb = get_super(inode-&gt;i_dev)))
        panic(&quot;trying to free inode on nonexistent device&quot;);
    if (inode-&gt;i_num &lt; 1 || inode-&gt;i_num &gt; sb-&gt;s_ninodes)
        panic(&quot;trying to free inode 0 or nonexistant inode&quot;);
    if (!(bh=sb-&gt;s_imap[inode-&gt;i_num&gt;&gt;13]))
        panic(&quot;nonexistent imap in superblock&quot;);
    // 现在我们复位i节点对应的节点位图中的bit位。如果该bit位已经等于0，则显示
    // 出错警告信息。最后置i节点位图所在缓冲区已修改标志，并清空该i节点结构
    // 所占内存区。
    if (clear_bit(inode-&gt;i_num&amp;8191,bh-&gt;b_data))
        printk(&quot;free_inode: bit already cleared.\n\r&quot;);
    bh-&gt;b_dirt = 1;
    memset(inode,0,sizeof(*inode));
}</code></pre></div><p>&#160; 4、由于数据都是先存在高速缓存区，不会直接读写磁盘，所以此时要先在高速缓存区新建块，这里面就涉及到了上面的位图操作！</p><div class="codebox"><pre class="vscroll"><code>//// 向设备申请一个逻辑块。
// 函数首先取得设备的超级块，并在超级块中的逻辑块位图中寻找第一个0值bit位(代表一个
// 空闲逻辑块)。然后位置对应逻辑块在逻辑块位图中的bit位。接着为该逻辑块在缓冲区中取得
// 一块对应缓冲块。最后将该缓冲块清零，并设置其已更新标志和已修改标志。并返回逻辑块
// 号。函数执行成功则返回逻辑块号，否则返回0.
int new_block(int dev)
{
    struct buffer_head * bh;
    struct super_block * sb;
    int i,j;

    // 首先获取设备dev的超级块。如果指定设备的超级块不存在，则出错当机。然后扫描
    // 文件系统的8块逻辑位图，寻找首个0值bit位，以寻找空闲逻辑块，获取放置该逻辑块的
    // 块号。如果全部扫描完8块逻辑块位图的所有bit位(i &gt;= 8 或 j &gt;= 8192)还没找到0值
    // bit位或者位图所在的缓冲块指针无效(bh=NULL)则返回0退出(没有空闲逻辑块)。
    if (!(sb = get_super(dev)))
        panic(&quot;trying to get new block from nonexistant device&quot;);
    j = 8192;
    for (i=0 ; i&lt;8 ; i++)
        if ((bh=sb-&gt;s_zmap[i]))
            if ((j=find_first_zero(bh-&gt;b_data))&lt;8192)
                break;
    if (i&gt;=8 || !bh || j&gt;=8192)
        return 0;
    // 接着设置找到的新逻辑块j对应逻辑块位图中的bit位。若对应bit位已经置位，则出错
    // 停机。否则置存放位图的对应缓冲区块已修改标志。因为逻辑块位图仅表示盘上数据区
    // 中逻辑块的占用情况，则逻辑块位图中bit位偏移值表示从数据区开始处算起的块号，
    // 因此这里需要加上数据区第1个逻辑块的块号，把j转换成逻辑块号。此时如果新逻辑块
    // 大于该设备上的总逻辑块数，则说明指定逻辑块在对应设备上不存在。申请失败，返回0退出。
    if (set_bit(j,bh-&gt;b_data))
        panic(&quot;new_block: bit already set&quot;);
    bh-&gt;b_dirt = 1;
    j += i*8192 + sb-&gt;s_firstdatazone-1;
    if (j &gt;= sb-&gt;s_nzones)
        return 0;
    // 然后在高速缓冲区中为该设备上指定的逻辑块号取得一个缓冲块，并返回缓冲块头指针。
    // 因为刚取得的逻辑块其引用次数一定为1(getblk()中会设置)，因此若不为1则停机。
    // 最后将新逻辑块清零，并设置其已更新标志和已修改标志。然后释放对应缓冲块，返回
    // 逻辑块号。
    if (!(bh=getblk(dev,j)))
        panic(&quot;new_block: cannot get block&quot;);
    if (bh-&gt;b_count != 1)
        panic(&quot;new block: count is != 1&quot;);
    clear_block(bh-&gt;b_data);
    bh-&gt;b_uptodate = 1;
    bh-&gt;b_dirt = 1;
    brelse(bh);
    return j;
}</code></pre></div><p>&#160; 块用完后也需要释放，如下：先把位图对应的位置清0，再把高速缓存区对应的块释放；</p><div class="codebox"><pre class="vscroll"><code>//// 释放设备dev上数据区中的逻辑块block.
// 复位指定逻辑块block对应的逻辑块位图bit位
// 参数：dev是设备号，block是逻辑块号（盘块号）
void free_block(int dev, int block)
{
    struct super_block * sb;
    struct buffer_head * bh;

    // 首先取设备dev上文件系统的超级块信息，根据其中数据区开始逻辑块号和文件系统中逻辑
    // 块总数信息判断参数block的有效性。如果指定设备超级块不存在，则出错当机。若逻辑块
    // 号小于盘上面数据区第一个逻辑块的块号或者大于设备上总逻辑块数，也出错当机。
    if (!(sb = get_super(dev)))
        panic(&quot;trying to free block on nonexistent device&quot;);
    if (block &lt; sb-&gt;s_firstdatazone || block &gt;= sb-&gt;s_nzones)
        panic(&quot;trying to free block not in datazone&quot;);
    // 然后从hash表中寻找该块数据。若找到了则判断其有效性，并清已修改和更新标志，释放
    // 该数据块。该段代码的主要用途是如果该逻辑块目前存在于高速缓冲区中，就释放对应
    // 的缓冲块。
    bh = get_hash_table(dev,block);
    // 下面的代码会造成数据块不能释放。因为当b_count &gt; 1时，这段代码会仅打印一段信息而
    // 没有执行释放操作。
    if (bh) {
        if (bh-&gt;b_count != 1) {
            printk(&quot;trying to free block (%04x:%d), count=%d\n&quot;,
                dev,block,bh-&gt;b_count);
            return;
        }
        bh-&gt;b_dirt=0;
        bh-&gt;b_uptodate=0;
        brelse(bh);
    }
    // 接着我们复位block在逻辑块位图中的bit（置0），先计算block在数据区开始算起的数据
    // 逻辑块号(从1开始计数)。然后对逻辑块(区块)位图进行操作，复位对应的bit位。如果对应
    // bit位原来就是0，则出错停机。由于1个缓冲块有1024字节，即8192比特位，因此block/8192
    // 即可计算出指定块block在逻辑位图中的哪个块上。而block&amp;8192可以得到block在逻辑块位图
    // 当前块中的bit偏移位置。，不用担心偏移超出8191的范围。
    block -= sb-&gt;s_firstdatazone - 1 ;
    if (clear_bit(block&amp;8191,sb-&gt;s_zmap[block/8192]-&gt;b_data)) {
        printk(&quot;block (%04x:%d) &quot;,dev,block+sb-&gt;s_firstdatazone-1);
        panic(&quot;free_block: bit already cleared&quot;);
    }
    // 最后置相应逻辑块位图所在缓冲区已修改标志。
    sb-&gt;s_zmap[block/8192]-&gt;b_dirt = 1;
}</code></pre></div><p>&#160; 注意：上述的各种块操作，都是针对内存中的高速缓存区，并未直接操作磁盘！</p><p>&#160; &#160;5、（1）前面建好了block和inode，至此终于可以开始读写数据了，比如这里的write_inode函数：</p><div class="codebox"><pre class="vscroll"><code>//// 将i节点信息写入缓冲区中。
// 该函数把参数指定的i节点写入缓冲区相应的缓冲块中，待缓冲区刷新时会写入盘中。为了确定i节点
// 所在的设备逻辑块号（或缓冲块），必须首先读取相应设备上的超级块，以获取用于计算逻辑块号的
// 每块i节点数信息INODES_PER_BLOCK。在计算出i节点所在的逻辑块号后，就把该逻辑块读入一缓冲块
// 中。然后把i节点内容复制到缓冲块的相应位置处。
static void write_inode(struct m_inode * inode)
{
    struct super_block * sb;
    struct buffer_head * bh;
    int block;

    // 首先锁定该i节点，如果该i节点没有被修改或者该i节点的设备号等于零，则解锁该i节点，并退出。
    // 对于没有被修改过的i节点，其内容与缓冲区中或设备中的相同。然后获取该i节点的超级块。
    lock_inode(inode);
    if (!inode-&gt;i_dirt || !inode-&gt;i_dev) {
        unlock_inode(inode);
        return;
    }
    if (!(sb=get_super(inode-&gt;i_dev)))
        panic(&quot;trying to write inode without device&quot;);
    // 该i节点所在的设备逻辑块号＝（启动块+超级块）+i节点位图占用的块数+逻辑块位图占用的块数
    // +（i节点号-1）/每块含有的i节点数。我们从设备上读取i节点所在的逻辑块，并将该i节点信息复制
    // 到逻辑块对应i节点的项位置处。
    block = 2 + sb-&gt;s_imap_blocks + sb-&gt;s_zmap_blocks + (inode-&gt;i_num-1)/INODES_PER_BLOCK;
    if (!(bh=bread(inode-&gt;i_dev,block)))
        panic(&quot;unable to read i-node block&quot;);
    ((struct d_inode *)bh-&gt;b_data)
        [(inode-&gt;i_num-1)%INODES_PER_BLOCK] =
            *(struct d_inode *)inode;
    // 然后置缓冲区已修改标志，而i节点内容已经与缓冲区中的一致，因此修改标志置零。然后释放该
    // 含有i节点的缓冲区，并解锁该i节点。
    bh-&gt;b_dirt=1;
    inode-&gt;i_dirt=0;
    brelse(bh);
    unlock_inode(inode);
}</code></pre></div><p>&#160; 注意：上面的write函数并未直接把数据写入磁盘，而是先写入了缓存区，所以linux又提供了专门同步的接口，如下：这两个函数最终都调用了ll_rw_block方法向磁盘写数据！其中sys_sync还是个系统调用了！</p><div class="codebox"><pre class="vscroll"><code>//// 设备数据同步，这是个系统调用；
// 同步设备和内存高速缓冲中数据，其中sync_inode()定义在inode.c中。
// 把内存中高速缓存区的数写回到磁盘，需要调用磁盘的驱动代码
int sys_sync(void)
{
    int i;
    struct buffer_head * bh;

    // 首先调用i节点同步函数，把内存i节点表中所有修改过的i节点写入高速缓冲中。
    // 然后扫描所有高速缓冲区，对已被修改的缓冲块产生写盘请求，将缓冲中数据写入
    // 盘中，做到高速缓冲中的数据与设备中的同步。
    sync_inodes();        /* write out inodes into buffers */
    bh = start_buffer;
    for (i=0 ; i&lt;NR_BUFFERS ; i++,bh++) {
        wait_on_buffer(bh);                 // 等待缓冲区解锁(如果已经上锁的话)
        if (bh-&gt;b_dirt)
            ll_rw_block(WRITE,bh);          // 产生写设备块请求
    }
    return 0;
}

//// 对指定设备进行高速缓冲数据与设备上数据的同步操作
// 该函数首先搜索高速缓冲区所有缓冲块。对于指定设备dev的缓冲块，若其数据已经
// 被修改过就写入盘中(同步操作)。然后把内存中i节点表数据写入 高速缓冲中。之后
// 再对指定设备dev执行一次与上述相同的写盘操作。
int sync_dev(int dev)
{
    int i;
    struct buffer_head * bh;

    // 首先对参数指定的设备执行数据同步操作，让设备上的数据与高速缓冲区中的数据
    // 同步。方法是扫描高速缓冲区中所有缓冲块，对指定设备dev的缓冲块，先检测其
    // 是否已被上锁，若已被上锁就睡眠等待其解锁。然后再判断一次该缓冲块是否还是
    // 指定设备的缓冲块并且已修改过(b_dirt标志置位)，若是就对其执行写盘操作。
    // 因为在我们睡眠期间该缓冲块有可能已被释放或者被挪作他用，所以在继续执行前
    // 需要再次判断一下该缓冲块是否还是指定设备的缓冲块。
    bh = start_buffer;
    for (i=0 ; i&lt;NR_BUFFERS ; i++,bh++) {
        if (bh-&gt;b_dev != dev)               // 不是设备dev的缓冲块则继续
            continue;
        wait_on_buffer(bh);                     // 等待缓冲区解锁
        if (bh-&gt;b_dev == dev &amp;&amp; bh-&gt;b_dirt)
            ll_rw_block(WRITE,bh); //lowlevel
    }
    // 再将i节点数据吸入高速缓冲。让i节点表inode_table中的inode与缓冲中的信息同步。
    sync_inodes();
    // 然后在高速缓冲中的数据更新之后，再把他们与设备中的数据同步。这里采用两遍同步
    // 操作是为了提高内核执行效率。第一遍缓冲区同步操作可以让内核中许多&quot;脏快&quot;变干净，
    // 使得i节点的同步操作能够高效执行。本次缓冲区同步操作则把那些由于i节点同步操作
    // 而又变脏的缓冲块与设备中数据同步。
    bh = start_buffer;
    for (i=0 ; i&lt;NR_BUFFERS ; i++,bh++) {
        if (bh-&gt;b_dev != dev)
            continue;
        wait_on_buffer(bh);
        if (bh-&gt;b_dev == dev &amp;&amp; bh-&gt;b_dirt)
            ll_rw_block(WRITE,bh);
    }
    return 0;
}</code></pre></div><p>&#160; （2）同理，也有读read_inode的函数：</p><div class="codebox"><pre class="vscroll"><code>//// 读取指定i节点信息。
// 从设备上读取含有指定i节点信息的i节点盘块，然后复制到指定的i节点结构中。为了确定i节点
// 所在的设备逻辑块号（或缓冲块），必须首先读取相应设备上的超级块，以获取用于计算逻辑
// 块号的每块i节点数信息INODES_PER_BLOCK.在计算出i节点所在的逻辑块号后，就把该逻辑块读入
// 一缓冲块中。然后把缓冲块中相应位置处的i节点内容复制到参数指定的位置处。
static void read_inode(struct m_inode * inode)
{
    struct super_block * sb;
    struct buffer_head * bh;
    int block;

    // 首先锁定该i节点，并取该节点所在设备的超级块。
    lock_inode(inode);
    if (!(sb=get_super(inode-&gt;i_dev)))
        panic(&quot;trying to read inode without dev&quot;);
    // 该i节点所在的设备逻辑块号＝（启动块+超级块）+i节点位图占用的块数+逻辑块位图占用的块数
    // +（i节点号-1）/每块含有的i节点数。虽然i节点号从0开始编号，但第i个0号i节点不用，并且
    // 磁盘上也不保存对应的0号i节点结构。因此存放i节点的盘块的第i块上保存的是i节点号是1--16
    // 的i节点结构而不是0--15的。因此在上面计算i节点号对应的i节点结构所在盘块时需要减1，即：
    // B=（i节点号-1)/每块含有i节点结构数。例如，节点号16的i节点结构应该在B=（16-1）/16 = 0的
    // 块上。这里我们从设备上读取该i节点所在的逻辑块，并复制指定i节点内容到inode指针所指位置处。
    block = 2 + sb-&gt;s_imap_blocks + sb-&gt;s_zmap_blocks +
        (inode-&gt;i_num-1)/INODES_PER_BLOCK;
    if (!(bh=bread(inode-&gt;i_dev,block)))
        panic(&quot;unable to read i-node block&quot;);
    *(struct d_inode *)inode =
        ((struct d_inode *)bh-&gt;b_data)
            [(inode-&gt;i_num-1)%INODES_PER_BLOCK];
    // 最后释放读入的缓冲块，并解锁该i节点。
    brelse(bh);
    unlock_inode(inode);
}</code></pre></div><p>&#160; （3）inode用完后，并不是直接调用free_inode去清零inode节点的数据，而是先把i_count计数减一，如果计数是0了，再清零inode节点的数据，如下：</p><div class="codebox"><pre class="vscroll"><code>//// 放回(放置)一个i节点引用计数值递减1，并且若是管道i节点，则唤醒等待的进程。
// 若是块设备文件i节点则刷新设备。并且若i节点的链接计数为0，则释放该i节点占用
// 的所有磁盘逻辑块，并释放该i节点。
void iput(struct m_inode * inode)
{
    // 首先判断参数给出的i节点的有效性，并等待inode节点解锁，如果i节点的引用计数
    // 为0，表示该i节点已经是空闲的。内核再要求对其进行放回操作，说明内核中其他
    // 代码有问题。于是显示错误信息并停机。
    if (!inode)
        return;
    wait_on_inode(inode);
    if (!inode-&gt;i_count)
        panic(&quot;iput: trying to free free inode&quot;);
    // 如果是管道i节点，则唤醒等待该管道的进程，引用次数减1，如果还有引用则返回。
    // 否则释放管道占用的内存页面，并复位该节点的引用计数值、已修改标志和管道标志，
    // 并返回。对于管道节点，inode-&gt;i_size存放这内存也地址。
    if (inode-&gt;i_pipe) {
        wake_up(&amp;inode-&gt;i_wait);
        if (--inode-&gt;i_count)
            return;
        free_page(inode-&gt;i_size);
        inode-&gt;i_count=0;
        inode-&gt;i_dirt=0;
        inode-&gt;i_pipe=0;
        return;
    }
    // 如果i节点对应的设备号 ＝ 0，则将此节点的引用计数递减1，返回。例如用于管道操作
    // 的i节点，其i节点的设备号为0.
    if (!inode-&gt;i_dev) {
        inode-&gt;i_count--;
        return;
    }
    // 如果是块设备文件的i节点，此时逻辑块字段0(i_zone[0])中是设备号，则刷新该设备。
    // 并等待i节点解锁。
    if (S_ISBLK(inode-&gt;i_mode)) {
        sync_dev(inode-&gt;i_zone[0]);
        wait_on_inode(inode);
    }
    // 如果i节点的引用计数大于1，则计数递减1后就直接返回(因为该i节点还有人在用，不能
    // 释放)，否则就说明i节点的引用计数值为1。如果i节点的链接数为0，则说明i节点对应文件
    // 被删除。于是释放该i节点的所有逻辑块，并释放该i节点。函数free_inode()用于实际释
    // 放i节点操作，即复位i节点对应的i节点位图bit位，清空i节点结构内容。
repeat:
    if (inode-&gt;i_count&gt;1) {
        inode-&gt;i_count--;
        return;
    }
    if (!inode-&gt;i_nlinks) {
        truncate(inode);
        free_inode(inode);
        return;
    }
    // 如果该i节点已做过修改，则回写更新该i节点，并等待该i节点解锁。由于这里在写i节点
    // 时需要等待睡眠，此时其他进程有可能修改i节点，因此在进程被唤醒后需要再次重复进行
    // 上述判断过程(repeat)。
    if (inode-&gt;i_dirt) {
        write_inode(inode);    /* we can sleep - so do again */
        wait_on_inode(inode);
        goto repeat;
    }
    // 程序若能执行到此，则说明该i节点的引用计数值i_count是1、链接数不为零，并且内容
    // 没有被修改过。因此此时只要把i节点引用计数递减1，返回。此时该i节点的i_count=0,
    // 表示已释放。
    inode-&gt;i_count--;
    return;
}</code></pre></div><p>参考：</p><p>1、https://www.jianshu.com/p/9ef6542ced92&#160; Linux文件系统和inode</p><p>2、https://blog.csdn.net/YuZhiHui_No1/article/details/43951153&#160; &#160;Linux内核源码分析--文件系统</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sat, 08 Oct 2022 03:37:17 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=444&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[linux源码解读（二）：文件系统——高速缓存区]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=443&amp;action=new</link>
			<description><![CDATA[<p>用户的应用程序会经常读写磁盘文件的数据到内存，但是内存的速度和磁盘的速度理论上差了好几个数量级；为了更高效地解决内存和磁盘的速度差，linux也在内存使用了缓存区（作用类似于cpu内部为了解决寄存器和内存速度差异的的L1、L2、L3 cache）：如果数据要写入磁盘文件，先放在缓存区，等凑够了一定数量后再批量写入磁盘文件，借此减少磁盘寻址的次数，来提升写入效率（这里多说几句：比如U盘插上电脑后，如果要拔出，建议先卸载再拔出，而不是直接拔出，为啥了？U盘的数据也是先放入缓冲区的，缓冲区有自己的管理机制，很久没有使用的块可以给其他进程使用，如果是脏块则要进行写盘。缓冲在某些情况下才会有写盘操作，所以要拔出U盘时，应该先进行卸载，这样才会写盘，否则数据可能丢失，文件系统可能损坏。）；如果从磁盘读数据，也会先放入缓存区暂存，一旦有其他进程或线程读取同样的磁盘文件，这是就可以先从内存的缓存区取数据了，没必要重新从磁盘读取，也提升了效率！linux 0.11的缓冲区是怎么工作的了？</p><p>&#160; 在main.c的main函数中，有设置缓存区的大小，代码如下：内存不同，缓存区的大小也不同，linux是怎么管理和使用这些缓存区了的？</p><div class="codebox"><pre><code>void main(void)        /* This really IS void, no error here. */
{            /* The startup routine assumes (well, ...) this */
/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */
//前面这里做的所有事情都是在对内存进行拷贝
     ROOT_DEV = ORIG_ROOT_DEV;//设置操作系统的根文件
     drive_info = DRIVE_INFO;//设置操作系统驱动参数
     //解析setup.s代码后获取系统内存参数
    memory_end = (1&lt;&lt;20) + (EXT_MEM_K&lt;&lt;10);
    //取整4k的内存大小
    memory_end &amp;= 0xfffff000;
    if (memory_end &gt; 16*1024*1024)//控制操作系统的最大内存为16M
        memory_end = 16*1024*1024;
    if (memory_end &gt; 12*1024*1024) 
        buffer_memory_end = 4*1024*1024;//设置高速缓冲区的大小，跟块设备有关，跟设备交互的时候，充当缓冲区，写入到块设备中的数据先放在缓冲区里，只有执行sync时才真正写入；这也是为什么要区分块设备驱动和字符设备驱动；块设备写入需要缓冲区，字符设备不需要是直接写入的
    else if (memory_end &gt; 6*1024*1024)
        buffer_memory_end = 2*1024*1024;
    else
        buffer_memory_end = 1*1024*1024;
    main_memory_start = buffer_memory_end;</code></pre></div><p>&#160; 1、cpu有分页机制，硬件上以4KB为单位把内存分割成小块供程序使用；这个颗粒度是比较大的，有些时候可能会浪费比较多的内存，所以linux缓存区采用了1KB的大小来分割整个缓存区；假设缓存区有2MB，那么一共被分割成了2000个小块，这么多的缓存区该怎么管理了？</p><p>&#160; 每个缓存都有各自的属性，比如是否使用、数据是否更新、缓存数据在磁盘的位置、缓存的起始地址等，要想统一管理这么多的属性，最好的办法自然是构建结构体了；一个结构体管理1块（也就是1KB）的缓存区；假设这里有2000个缓存区，就需要2000个结构体，那么问题又来了：这个多的结构体，又该怎么去管理了？ </p><p>&#160; 参考前面的进程task结构体管理方式：用task数组来管理所有的进程task结构体，最大限制为64个进程，但是放在这里显然不适用：不同机器的物理内存大小是不同的，导致缓存区的block数量是不同的，但数组最大的缺点就是定长，无法适应不同的物理内存，那么这里最适合的只剩链表了，所以linux 0.11版本使用的结构体如下：</p><div class="codebox"><pre><code>struct buffer_head {
    char * b_data;            /* pointer to data block (1024 bytes):单个数据块大小1KB */
    unsigned long b_blocknr;    /* block number */
    unsigned short b_dev;        /* device (0 = free) */
    unsigned char b_uptodate;    /*数据是否更新*/
    unsigned char b_dirt;        /* 0-clean空现,1-dirty已被占用*/
    unsigned char b_count;        /* users using this block */
    /*如果缓冲区的某个block被锁，上层应用是没法从这个block对应的磁盘空间读数据的，这里有个漏洞：
    A进程锁定了某block，B进程想办法解锁，然后就能监听A进程从磁盘读写了哪些数据
    */
    unsigned char b_lock;        /* 0 - ok, 1 -locked:锁用于多进程/多线程之间同步，避免数据出错*/
    struct task_struct * b_wait;/*A正在使用这个缓存，并已经锁定；B也想用，就用这个字段记录；等A用完后从这里找到B再给B用*/
    struct buffer_head * b_prev;
    struct buffer_head * b_next;
    struct buffer_head * b_prev_free;
    struct buffer_head * b_next_free;
};</code></pre></div><p>&#160; 每个字段的含义都在注释了，这里不再赘述；既然采用了链表，解决了数组只能定长的缺点，但是链表本身也有缺点：无法直接找到目标实例，需要挨个遍历链表上的每个节点；还是假设有2000个块，好巧的不巧的是程序所需的block刚好在最后一个节点，那么需要遍历1999个节点才能到达，效率非常低，这又该怎么解决了？刚好这种快速寻址（时间复杂度O(1)）是数组的优势，怎么解决数组和链表各自的优势了？-----hash表！</p><p>&#160; linux 0.11版本采用hash表的方式快速寻址，hash映射算法如下：</p><div class="codebox"><pre><code>// hash表的主要作用是减少查找比较元素所花费的时间。通过在元素的存储位置与关
// 键字之间建立一个对应关系(hash函数)，我们就可以直接通过函数计算立刻查询到指定
// 的元素。建立hash函数的指导条件主要是尽量确保散列在任何数组项的概率基本相等。
// 建立函数的方法有多种，这里Linux-0.11主要采用了关键字除留余数法。因为我们
// 寻找的缓冲块有两个条件，即设备号dev和缓冲块号block，因此设计的hash函数肯定
// 需要包含这两个关键值。这两个关键字的异或操作只是计算关键值的一种方法。再对
// 关键值进行MOD运算就可以保证函数所计算得到的值都处于函数数组项范围内。
#define _hashfn(dev,block) (((unsigned)(dev^block))%NR_HASH)
#define hash(dev,block) hash_table[_hashfn(dev,block)]</code></pre></div><p>&#160; 映射的算法也很简单：每个buffer_head结构体都有dev和block两个字段，这两个字段组合起来本身是不会重复的，所以把这两个字段异或后模上hash表的长度，就得到了hash数组的偏移；现在问题又来了：这个版本的hash_table数组长度设定为NR_HASH=307，远不如buffer_head的实例个数，肯定会发生hash冲突，这个该怎么解决了？--这里就要用上链表变长的优点了：把发生hash冲突的bufer_head实例首位相接不久得了么？最终的hash_table示意图如下：hash表本身用数组，存储buffer_head实例的地址；如果发生hash冲突，相同hash偏移的实例通过b_next和b_prev链表首尾连接！</p><p><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202111/2052730-20211128163552687-1170315130.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160;当这个一整套存储机制建立后，怎么检索了？linux的检索方式如下：先通过dev和block号定位到hash表的偏移，再遍历该偏移处的所有节点，通过比对dev和block号找到目标buffer_head实例！</p><div class="codebox"><pre><code>//// 利用hash表在高速缓冲区中寻找给定设备和指定块号的缓冲区块。
// 如果找到则返回缓冲区块的指针，否则返回NULL。
static struct buffer_head * find_buffer(int dev, int block)
{        
    struct buffer_head * tmp;

    // 搜索hash表，寻找指定设备号和块号的缓冲块。
    for (tmp = hash(dev,block) ; tmp != NULL ; tmp = tmp-&gt;b_next)
        if (tmp-&gt;b_dev==dev &amp;&amp; tmp-&gt;b_blocknr==block)
            return tmp;
    return NULL;
}</code></pre></div><p>&#160; &#160;根据dev和block号找到缓存区的buffer_head并不代表万事大吉，因为该缓存区可能已经被其他进程/线程占用，当前线程如果一定要用这个缓存区，只能等了，所以最终查找缓存区的代码如下：这里增加了wait_on_buffer函数：</p><div class="codebox"><pre><code>//// 利用hash表在高速缓冲区中寻找指定的缓冲块。若找到则对该缓冲块上锁
// 返回块头指针。
struct buffer_head * get_hash_table(int dev, int block)
{
    struct buffer_head * bh;

    for (;;) {
        // 在高速缓冲中寻找给定设备和指定块的缓冲区块，如果没有找到则返回NULL。
        if (!(bh=find_buffer(dev,block)))
            return NULL;
        // 对该缓冲块增加引用计数，并等待该缓冲块解锁。由于经过了睡眠状态，其他任务可能会更改这个缓存区对应的dev和block号
        // 因此有必要在验证该缓冲块的正确性，并返回缓冲块头指针。
        bh-&gt;b_count++;
        wait_on_buffer(bh);
        if (bh-&gt;b_dev == dev &amp;&amp; bh-&gt;b_blocknr == block)
            return bh;
        // 如果在睡眠时该缓冲块所属的设备号或块设备号发生了改变，则撤消对它的
        // 引用计数，重新寻找。
        bh-&gt;b_count--;
    }
}</code></pre></div><p>&#160; wait_on_buffer函数实现：如果发现该缓存区已经上锁，那么调用sleep_on函数让出cpu，阻塞在这里等待；这个sleep_on函数传入的参数是二级指针，并且内部用了tmp变量保存临时变量；由于二级指针是全局的，所以如果有多个task等待同一个缓存区，sleep_on函数是通过先进后出的栈的形式唤醒等待任务的；参考1有详细的说明，感兴趣的小伙伴建议好好看看！</p><div class="codebox"><pre><code>//// 等待指定缓冲块解锁
// 如果指定的缓冲块bh已经上锁就让进程不可中断地睡眠在该缓冲块的等待队列b_wait中。
// 在缓冲块解锁时，其等待队列上的所有进程将被唤醒。虽然是在关闭中断(cli)之后
// 去睡眠的，但这样做并不会影响在其他进程上下文中影响中断。因为每个进程都在自己的
// TSS段中保存了标志寄存器EFLAGS的值，所以在进程切换时CPU中当前EFLAGS的值也随之
// 改变。使用sleep_on进入睡眠状态的进程需要用wake_up明确地唤醒。
static inline void wait_on_buffer(struct buffer_head * bh)
{
    cli();                          // 关中断
    while (bh-&gt;b_lock)              // 如果已被上锁则进程进入睡眠，等待其解锁
        sleep_on(&amp;bh-&gt;b_wait);
    sti();                          // 开中断
}</code></pre></div><p>&#160; &#160;先进后出的栈形式唤醒等待任务：<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202111/2052730-20211129202959305-1621767149.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; 接下来可能就是buffer.c中最重要的函数之一了：struct buffer_head * getblk(int dev,int block)，根据设备号和块号得到buffer_head的实例，便于后续使用对应的缓存区；</p><div class="codebox"><pre class="vscroll"><code>//// 取高速缓冲中指定的缓冲块
// 检查指定（设备号和块号）的缓冲区是否已经在高速缓冲中。如果指定块已经在
// 高速缓冲中，则返回对应缓冲区头指针退出；如果不在，就需要在高速缓冲中设置一个
// 对应设备号和块好的新项。返回相应的缓冲区头指针。
struct buffer_head * getblk(int dev,int block)
{
    struct buffer_head * tmp, * bh;

repeat:
    // 搜索hash表，如果指定块已经在高速缓冲中，则返回对应缓冲区头指针，退出。
    if ((bh = get_hash_table(dev,block)))
        return bh;
    // 扫描空闲数据块链表，寻找空闲缓冲区。
    // 首先让tmp指向空闲链表的第一个空闲缓冲区头
    tmp = free_list;
    do {
        // 如果该缓冲区正被使用（引用计数不等于0），则继续扫描下一项。对于
        // b_count = 0的块，即高速缓冲中当前没有引用的块不一定就是干净的
        // (b_dirt=0)或没有锁定的(b_lock=0)。因此，我们还是需要继续下面的判断
        // 和选择。例如当一个任务该写过一块内容后就释放了，于是该块b_count()=0
        // 但b_lock不等于0；当一个任务执行breada()预读几个块时，只要ll_rw_block()
        // 命令发出后，它就会递减b_count; 但此时实际上硬盘访问操作可能还在进行，
        // 因此此时b_lock=1, 但b_count=0.
        if (tmp-&gt;b_count)
            continue;
        // 如果缓冲头指针bh为空，或者tmp所指缓冲头的标志(修改、锁定)权重小于bh
        // 头标志的权重，则让bh指向tmp缓冲块头。如果该tmp缓冲块头表明缓冲块既
        // 没有修改也没有锁定标志置位，则说明已为指定设备上的块取得对应的高速
        // 缓冲块，则退出循环。否则我们就继续执行本循环，看看能否找到一个BANDNESS()
        // 最小的缓冲块。BADNESS等于0意味着b_block和b_dirt都是0，这块缓存区还没被使用，目标缓存区已经找到，可以跳出循环了
        if (!bh || BADNESS(tmp)&lt;BADNESS(bh)) {
            bh = tmp;
            if (!BADNESS(tmp))
                break;
        }
/* and repeat until we find something good */
    } while ((tmp = tmp-&gt;b_next_free) != free_list);
    // 如果循环检查发现所有缓冲块都正在被使用(所有缓冲块的头部引用计数都&gt;0)中，
    // 则睡眠等待有空闲缓冲块可用。当有空闲缓冲块可用时本进程会呗明确的唤醒。
    // 然后我们跳转到函数开始处重新查找空闲缓冲块。
    if (!bh) {
        sleep_on(&amp;buffer_wait);
        goto repeat;
    }
    // 执行到这里，说明我们已经找到了一个比较合适的空闲缓冲块了。于是先等待该缓冲区
    // 解锁（多任务同时运行，刚找到的缓存块可能已经被其他任务抢先一步找到并使用了，所以要再次检查）。如果在我们睡眠阶段该缓冲区又被其他任务使用的话，只好重复上述寻找过程。
    wait_on_buffer(bh);
    if (bh-&gt;b_count)
        goto repeat;
    // 如果该缓冲区已被修改，则将数据写盘，并再次等待缓冲区解锁。同样地，若该缓冲区
    // 又被其他任务使用的话，只好再重复上述寻找过程。
    while (bh-&gt;b_dirt) {
        sync_dev(bh-&gt;b_dev);
        wait_on_buffer(bh);
        if (bh-&gt;b_count)
            goto repeat;
    }
/* NOTE!! While we slept waiting for this block, somebody else might */
/* already have added &quot;this&quot; block to the cache. check it */
    // 在高速缓冲hash表中检查指定设备和块的缓冲块是否乘我们睡眠之际已经被加入
    // 进去(毕竟是多任务系统，有可能被其他任务抢先使用并放入has表)。如果是的话，就再次重复上述寻找过程。
    if (find_buffer(dev,block))
        goto repeat;
/* OK, FINALLY we know that this buffer is the only one of it&#039;s kind, */
/* and that it&#039;s unused (b_count=0), unlocked (b_lock=0), and clean */
    // 于是让我们占用此缓冲块。置引用计数为1，复位修改标志和有效(更新)标志。
    bh-&gt;b_count=1;
    bh-&gt;b_dirt=0;
    bh-&gt;b_uptodate=0;
    // 从hash队列和空闲队列块链表中移出该缓冲区头，让该缓冲区用于指定设备和
    // 其上的指定块。然后根据此新的设备号和块号重新插入空闲链表和hash队列新
    // 位置处。并最终返回缓冲头指针。
    remove_from_queues(bh);
    bh-&gt;b_dev=dev;
    bh-&gt;b_blocknr=block;
    insert_into_queues(bh);
    return bh;
}</code></pre></div><p>&#160; &#160;代码的整体逻辑并不复杂，但是有些细节想展开说说：</p><p>&#160; BADNESS(bh)：从表达式看，b_dirt左移1位后再和b_lock相加，明显b_dirt的权重乘以了2，说明作者认为缓存区是否被使用的权重应该大于是否被锁！但是实际使用的时候，会一直循环查找BADNESS小的缓存区，说明作者认为b_block比b_dirt更重要，也就是缓存区是否上锁比是否被使用了更重要，这个也符合业务逻辑！<br />// 下面宏用于同时判断缓冲区的修改标志和锁定标志，并且定义修改标志的权重要比锁定标志大。<br />//&#160; b_dirt左移1位，权重比b_block高<br />&#160; &#160;#define BADNESS(bh) (((bh)-&gt;b_dirt&lt;&lt;1)+(bh)-&gt;b_lock)<br />&#160; 循环停止的条件如下：tmp初始值就是free_list，这里的停止的条件也是tmp == free_list，说明free_list是个环形循环链表；所以整个do while循环本质上就是在free_list中找BADNESS值最小的buffer_head；如果找到BADNESS等于0（意味着b_block和b_dirt都为0，该缓存区还没被使用）的buffer_head，直接跳出循环！<br />&#160; &#160;while ((tmp = tmp-&gt;b_next_free) != free_list);<br /> 函数结尾处: 再次检查dev+block是否已经在缓存区了，如果在，说明其他任务捷足先登，已经使用了该缓存区，本任务只能重新走查找的流程；如果该缓存块还没被使用，先设置一些标志/属性位，再把该buffer_head节点从旧hash表和free_list队列溢出，再重新加入hash_table和free_list队列，作者是咋想的？为啥要重复干这种事了？</p><div class="codebox"><pre><code>/* already have added &quot;this&quot; block to the cache. check it */
    // 在高速缓冲hash表中检查指定设备和块的缓冲块是否乘我们睡眠之际已经被加入
    // 进去（毕竟是多任务，期间可能会被其他任务抢先使用并放入hash表）。如果是的话，就再次重复上述寻找过程。
    if (find_buffer(dev,block))
        goto repeat;
/* OK, FINALLY we know that this buffer is the only one of it&#039;s kind, */
/* and that it&#039;s unused (b_count=0), unlocked (b_lock=0), and clean */
    // 于是让我们占用此缓冲块。置引用计数为1，复位修改标志和有效(更新)标志。
    bh-&gt;b_count=1;
    bh-&gt;b_dirt=0;
    bh-&gt;b_uptodate=0;
    // 从hash队列和空闲队列块链表中移出该缓冲区头，让该缓冲区用于指定设备和
    // 其上的指定块。然后根据此新的设备号和块号重新插入空闲链表和hash队列新
    // 位置处。并最终返回缓冲头指针。
    /*将缓冲块从旧的队列移出，添加到新的队列中，即哈希表的头，空闲表的尾，这样能够迅速找到该存在的块，而该缓冲块存在的时间最长*/
    remove_from_queues(bh);
    bh-&gt;b_dev=dev;
    bh-&gt;b_blocknr=block;
    insert_into_queues(bh);
    return bh;</code></pre></div><p>&#160; 先来看看remove_from_queues和insert_into_queu函数代码：remove_from_queues没啥好说的，就是简单粗暴的从hash表和free_list删除，也是常规的链表操作，重点在insert_into_queu函数：</p><p> bh节点加入了free_list链表的末尾，直接减少了后续查询遍历链表的时间，这不就直接提升了查询效率么？<br /> bh节点加入hash表某个偏移的表头，后续通过hash偏移不就能第一个找到该节点了么？又省了遍历链表的操作！</p><div class="codebox"><pre class="vscroll"><code>//// 从hash队列和空闲缓冲区队列中移走缓冲块。
// hash队列是双向链表结构，空闲缓冲块队列是双向循环链表结构。
static inline void remove_from_queues(struct buffer_head * bh)
{
/* remove from hash-queue */
    if (bh-&gt;b_next)
        bh-&gt;b_next-&gt;b_prev = bh-&gt;b_prev;
    if (bh-&gt;b_prev)
        bh-&gt;b_prev-&gt;b_next = bh-&gt;b_next;
    // 如果该缓冲区是该队列的头一个块（每个hash偏移的头），则让hash表的对应项指向本队列中的下一个
    // 缓冲区。
    if (hash(bh-&gt;b_dev,bh-&gt;b_blocknr) == bh)
        hash(bh-&gt;b_dev,bh-&gt;b_blocknr) = bh-&gt;b_next;
/* remove from free list */
    if (!(bh-&gt;b_prev_free) || !(bh-&gt;b_next_free))
        panic(&quot;Free block list corrupted&quot;);
    bh-&gt;b_prev_free-&gt;b_next_free = bh-&gt;b_next_free;
    bh-&gt;b_next_free-&gt;b_prev_free = bh-&gt;b_prev_free;
    // 如果空闲链表头指向本缓冲区，则让其指向下一缓冲区。
    if (free_list == bh)
        free_list = bh-&gt;b_next_free;
}

//// 将缓冲块插入空闲链表尾部，同时放入hash队列中。
static inline void insert_into_queues(struct buffer_head * bh)
{
/* put at end of free list */
    bh-&gt;b_next_free = free_list;
    bh-&gt;b_prev_free = free_list-&gt;b_prev_free;
    free_list-&gt;b_prev_free-&gt;b_next_free = bh;
    free_list-&gt;b_prev_free = bh;
/* put the buffer in new hash-queue if it has a device */
    // 请注意当hash表某项第1次插入项时，hash()计算值肯定为Null，因此此时得到
    // 的bh-&gt;b_next肯定是NULL，所以应该在bh-&gt;b_next不为NULL时才能给b_prev赋
    // bh值。
    bh-&gt;b_prev = NULL;
    bh-&gt;b_next = NULL;
    if (!bh-&gt;b_dev)
        return;
    bh-&gt;b_next = hash(bh-&gt;b_dev,bh-&gt;b_blocknr);
    hash(bh-&gt;b_dev,bh-&gt;b_blocknr) = bh;
    bh-&gt;b_next-&gt;b_prev = bh;                // 此句前应添加&quot;if (bh-&gt;b_next)&quot;判断
}</code></pre></div><p>&#160; &#160;当一个block使用完后就要释放了，避免“占着茅坑不拉屎”；释放的逻辑也简单，如下：引用计数count--，并且唤醒正在等待该缓存区的其他任务；</p><div class="codebox"><pre><code>// 释放指定缓冲块。
// 等待该缓冲块解锁。然后引用计数递减1，并明确地唤醒等待空闲缓冲块的进程。
void brelse(struct buffer_head * buf)
{
    if (!buf)
        return;
    wait_on_buffer(buf);
    if (!(buf-&gt;b_count--))
        panic(&quot;Trying to free free buffer&quot;);
    wake_up(&amp;buffer_wait);
}</code></pre></div><p>&#160; 前面很多的操作，尤其是节点的增删改查都涉及到了hash表和链表，那么hash表和链表都是怎么建立的了？这里用的是buffer_init函数：hash表初始化时所有的偏移都指向null；</p><div class="codebox"><pre class="vscroll"><code>// 缓冲区初始化函数
// 参数buffer_end是缓冲区内存末端。对于具有16MB内存的系统，缓冲区末端被设置为4MB.
// 对于有8MB内存的系统，缓冲区末端被设置为2MB。该函数从缓冲区开始位置start_buffer
// 处和缓冲区末端buffer_end处分别同时设置(初始化)缓冲块头结构和对应的数据块。直到
// 缓冲区中所有内存被分配完毕。
void buffer_init(long buffer_end)
{
    struct buffer_head * h = start_buffer;
    void * b;
    int i;

    // 首先根据参数提供的缓冲区高端位置确定实际缓冲区高端位置b。如果缓冲区高端等于1Mb，
    // 则因为从640KB - 1MB被显示内存和BIOS占用，所以实际可用缓冲区内存高端位置应该是
    // 640KB。否则缓冲区内存高端一定大于1MB。
    if (buffer_end == 1&lt;&lt;20)
        b = (void *) (640*1024);
    else
        b = (void *) buffer_end;
    // 这段代码用于初始化缓冲区，建立空闲缓冲区块循环链表，并获取系统中缓冲块数目。
    // 操作的过程是从缓冲区高端开始划分1KB大小的缓冲块，与此同时在缓冲区低端建立
    // 描述该缓冲区块的结构buffer_head,并将这些buffer_head组成双向链表。
    // h是指向缓冲头结构的指针，而h+1是指向内存地址连续的下一个缓冲头地址，也可以说
    // 是指向h缓冲头的末端外。为了保证有足够长度的内存来存储一个缓冲头结构，需要b所
    // 指向的内存块地址 &gt;= h 缓冲头的末端，即要求 &gt;= h+1.
    while ( (b -= BLOCK_SIZE) &gt;= ((void *) (h+1)) ) {
        h-&gt;b_dev = 0;                       // 使用该缓冲块的设备号
        h-&gt;b_dirt = 0;                      // 脏标志，即缓冲块修改标志
        h-&gt;b_count = 0;                     // 缓冲块引用计数
        h-&gt;b_lock = 0;                      // 缓冲块锁定标志
        h-&gt;b_uptodate = 0;                  // 缓冲块更新标志(或称数据有效标志)
        h-&gt;b_wait = NULL;                   // 指向等待该缓冲块解锁的进程
        h-&gt;b_next = NULL;                   // 指向具有相同hash值的下一个缓冲头
        h-&gt;b_prev = NULL;                   // 指向具有相同hash值的前一个缓冲头
        h-&gt;b_data = (char *) b;             // 指向对应缓冲块数据块（1024字节）
        h-&gt;b_prev_free = h-1;               // 指向链表中前一项
        h-&gt;b_next_free = h+1;               // 指向连表中后一项
        h++;                                // h指向下一新缓冲头位置
        NR_BUFFERS++;                       // 缓冲区块数累加
        if (b == (void *) 0x100000)         // 若b递减到等于1MB，则跳过384KB
            b = (void *) 0xA0000;           // 让b指向地址0xA0000(640KB)处
    }
    h--;                                    // 让h指向最后一个有效缓冲块头
    free_list = start_buffer;               // 让空闲链表头指向头一个缓冲快
    free_list-&gt;b_prev_free = h;             // 链表头的b_prev_free指向前一项(即最后一项)。
    h-&gt;b_next_free = free_list;             // h的下一项指针指向第一项，形成一个环链
    // 最后初始化hash表，置表中所有指针为NULL。
    for (i=0;i&lt;NR_HASH;i++)
        hash_table[i]=NULL;
}</code></pre></div><p>&#160; 截至目前，前面围绕缓存区做了大量的铺垫，最终的目的就是和磁盘之间读写数据，那么linux又是怎么利用缓存区从磁盘读数据的了？bread函数代码如下：整个逻辑也很简单，先申请缓存区，如果已经更新就直接返回；否则调用ll_rw_block读磁盘数据；读数据是要花时间的，这段时间cpu没必要闲着，可以跳转到其他进程继续执行；等数据读完后唤醒当前进程，检查buffer是否被锁、是否被更新；如果都没有，就可以安心释放了！</p><div class="codebox"><pre><code>//// 从设备上读取数据块。
// 该函数根据指定的设备号 dev 和数据块号 block，首先在高速缓冲区中申请一块
// 缓冲块。如果该缓冲块中已经包含有有效的数据就直接返回该缓冲块指针，否则
// 就从设备中读取指定的数据块到该缓冲块中并返回缓冲块指针。
struct buffer_head * bread(int dev,int block)
{
    struct buffer_head * bh;

    // 在高速缓冲区中申请一块缓冲块。如果返回值是NULL，则表示内核出错，停机。
    // 然后我们判断其中说是否已有可用数据。如果该缓冲块中数据是有效的（已更新）
    // 可以直接使用，则返回。
    if (!(bh=getblk(dev,block)))
        panic(&quot;bread: getblk returned NULL\n&quot;);
    if (bh-&gt;b_uptodate)
        return bh;
    // 否则我们就调用底层快设备读写ll_rw_block函数，产生读设备块请求。然后
    // 等待指定数据块被读入，并等待缓冲区解锁。在睡眠醒来之后，如果该缓冲区已
    // 更新，则返回缓冲区头指针，退出。否则表明读设备操作失败，于是释放该缓
    // 冲区，返回NULL，退出。
    ll_rw_block(READ,bh);
    wait_on_buffer(bh);
    if (bh-&gt;b_uptodate)
        return bh;
    brelse(bh);
    return NULL;
}</code></pre></div><p>&#160; &#160;ll_rw_block：ll全称应该是lowlevel的意思；rw表示读或者写请求，bh用来传递数据或保存数据。先通过主设备号判断是否为有效的设备，同时请求函数是否存在。如果是有效的设备且函数存在，即有驱动，则添加请求到相关链表中；</p><p>&#160; 对于一个当前空闲的块设备，当 ll_rw_block()函数为其建立第一个请求项时，会让该设备的当前请求项指针current_request直接指向刚建立的请求项，并且立刻调用对应设备的请求项操作函数开始执行块设备读写操作。当一个块设备已经有几个请求项组成的链表存在，ll_rw_block()就会利用电梯算法，根据磁头移动距离最小原则，把新建的请求项插入到链表适当的位置处；</p><div class="codebox"><pre><code>void ll_rw_block(int rw, struct buffer_head * bh)
{
    unsigned int major;

    if ((major=MAJOR(bh-&gt;b_dev)) &gt;= NR_BLK_DEV ||
    !(blk_dev[major].request_fn)) {
        printk(&quot;Trying to read nonexistent block-device\n\r&quot;);
        return;
    }
    make_request(major,rw,bh);
}</code></pre></div><p>&#160; 该函数内部继续调用make_request生成request：函数首先判断是否为提前读或者提前写，如果是则要看bh是否上了锁。上了锁则直接返回，因为提前操作是不必要的，否则转化为可以识别的读或者写，然后锁住缓冲区；数据处理结束后在中断处理函数中解锁；如果是写操作但是缓冲区不脏，或者读操作但是缓冲区已经更新，则直接返回；最后构造request实例，调用add_request函数把实例添加到链表！</p><p>&#160; add_request函数用了电梯调度算法，主要是考虑到早期机械磁盘的移臂的时间消耗较大，要么从里到外，要么从外到里，顺着某个方向多处理请求。如果req刚好在磁头移动的方向上，那么可以先处理，这样能节省IO(本质是寻址)的时间；</p><div class="codebox"><pre class="vscroll"><code>/*
 * add-request adds a request to the linked list.
 * It disables interrupts so that it can muck with the
 * request-lists in peace.
 */
static void add_request(struct blk_dev_struct * dev, struct request * req)
{
    struct request * tmp;

    req-&gt;next = NULL;
    cli();
    if (req-&gt;bh)
        req-&gt;bh-&gt;b_dirt = 0;
    if (!(tmp = dev-&gt;current_request)) {
        dev-&gt;current_request = req;
        sti();
        (dev-&gt;request_fn)();
        return;
    }
    for ( ; tmp-&gt;next ; tmp=tmp-&gt;next)
        if ((IN_ORDER(tmp,req) || 
            !IN_ORDER(tmp,tmp-&gt;next)) &amp;&amp;
            IN_ORDER(req,tmp-&gt;next))
            break;
    req-&gt;next=tmp-&gt;next;
    tmp-&gt;next=req;
    sti();
}

static void make_request(int major,int rw, struct buffer_head * bh)
{
    struct request * req;
    int rw_ahead;

/* WRITEA/READA is special case - it is not really needed, so if the */
/* buffer is locked, we just forget about it, else it&#039;s a normal read */
    if ((rw_ahead = (rw == READA || rw == WRITEA))) {
        if (bh-&gt;b_lock)
            return;
        if (rw == READA)
            rw = READ;
        else
            rw = WRITE;
    }
    if (rw!=READ &amp;&amp; rw!=WRITE)
        panic(&quot;Bad block dev command, must be R/W/RA/WA&quot;);
    lock_buffer(bh);
    if ((rw == WRITE &amp;&amp; !bh-&gt;b_dirt) || (rw == READ &amp;&amp; bh-&gt;b_uptodate)) {
        unlock_buffer(bh);
        return;
    }
repeat:
/* we don&#039;t allow the write-requests to fill up the queue completely:
 * we want some room for reads: they take precedence. The last third
 * of the requests are only for reads.
 */
    if (rw == READ)
        req = request+NR_REQUEST;
    else
        req = request+((NR_REQUEST*2)/3);
/* find an empty request */
    while (--req &gt;= request)
        if (req-&gt;dev&lt;0)
            break;
/* if none found, sleep on new requests: check for rw_ahead */
    if (req &lt; request) {
        if (rw_ahead) {
            unlock_buffer(bh);
            return;
        }
        sleep_on(&amp;wait_for_request);
        goto repeat;
    }
/* fill up the request-info, and add it to the queue */
    req-&gt;dev = bh-&gt;b_dev;
    req-&gt;cmd = rw;
    req-&gt;errors=0;
    req-&gt;sector = bh-&gt;b_blocknr&lt;&lt;1;
    req-&gt;nr_sectors = 2;
    req-&gt;buffer = bh-&gt;b_data;
    req-&gt;waiting = NULL;
    req-&gt;bh = bh;
    req-&gt;next = NULL;
    add_request(major+blk_dev,req);
}</code></pre></div><p>&#160; &#160;add_request中定义了宏IN_ORDER，真正的电梯调度算法体现在这里了：read请求排在写请求前面，先处理读请求，再处理写请求；同一读或写请求先处理设备号小的设备请求，再处理设备号大的设备请求；同一读或写请求，同一设备，按先里面的扇区再到外面的扇区的顺序处理。</p><div class="codebox"><pre><code>/*
 * This is used in the elevator algorithm: Note that
 * reads always go before writes. This is natural: reads
 * are much more time-critical than writes.
 */
#define IN_ORDER(s1,s2) \
((s1)-&gt;cmd&lt;(s2)-&gt;cmd || ((s1)-&gt;cmd==(s2)-&gt;cmd &amp;&amp; \
((s1)-&gt;dev &lt; (s2)-&gt;dev || ((s1)-&gt;dev == (s2)-&gt;dev &amp;&amp; \
(s1)-&gt;sector &lt; (s2)-&gt;sector))))</code></pre></div><p>参考：</p><p>1、https://blog.csdn.net/jmh1996/article/details/90139485&#160; &#160; &#160;linux-0.12源码分析——缓冲区等待队列（栈）sleep_on+wake_up分析2</p><p>2、https://blog.csdn.net/ac_dao_di/article/details/54615951&#160; &#160;linux 0.11 块设备文件的使用</p><p>3、https://cloud.tencent.com/developer/article/1749826 Linux文件系统之 — 通用块处理层</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sat, 08 Oct 2022 03:21:09 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=443&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[深入理解Linux文件系统与日志分析]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=437&amp;action=new</link>
			<description><![CDATA[<p>引言</p><p>本章内容我们讲解了inode和block的关系，恢复xfs 、ext类型的文件以及日志文件的管理与分析<br />一、inode与block<br />1.inode和block概述</p><p>&#160; &#160; 文件数据包括元信息与实际数据 （元信息：包含属性的相关信息，实际数据：文件内容）<br />&#160; &#160; 文件是存储在硬盘上的，硬盘的最小存储单位叫做&quot;扇区”(sector)，每个扇区存储512字节。<br />&#160; &#160; 一般连续八个扇区组成一个&quot;块&quot;(block)，一个块是4K大小，是文件存取的最小单位。操作系统读取硬盘的时候，是一次性连续读取多个扇区，即一个块一个块的读取的。<br />&#160; &#160; block(块)</p><p>&#160; &#160; 连续的八个扇区组成一个block(4K)<br />&#160; &#160; 是文件存取的最小单位</p><p>&#160; &#160; inode(索引节点)</p><p>&#160; &#160; 中文译名为“索引节点”，也叫i节点<br />&#160; &#160; 用于存储文件元信息</p><p>文件数据包括实际数据与元信息（类似文件属性)。文件数据存储在&quot;块&quot;中，存储文件元信息（比如文件的创建者、创建日期、文件大小、文件权限等）的区域就叫做inode。因此，一个文件必须占用一个inode，并且至少占用一个 block。</p><p>inode不包含文件名。文件名是存放在目录当中的。Linux系统中一切皆文件，因此目录也是一种文件。</p><p>每个inode都有一个号码，操作系统用inode号码来识别不同的文件。Linux系统内部不使用文件名，而使用inode号码来识别文件。对于系统来说，文件名只是inode号码便于识别的别称，文件名和inode号码是一一对应关系，每个inode号码对应一个文件名。</p><p>所以，当用户在Linux系统中试图访问一个文件时，系统会先根据文件名去查找它对应的inode号码，通过inode号码，获取inode信息﹔根据inode信息，看该用户是否具有访问这个文件的权限;如果有，就指向相对应的数据block，并读取数据。</p><p>2.inode的内容</p><p>inode包含文件的元信息，具体来说有以下内容：</p><p>&#160; &#160; 文件的字节数 就是字节占了多少空间和文件大小<br />&#160; &#160; 文件拥有者的User ID<br />&#160; &#160; 文件的Group ID<br />&#160; &#160; 文件的读、 写、执行权限<br />&#160; &#160; 文件的时间戳<br />&#160; &#160; 文件类型<br />&#160; &#160; 链接数<br />&#160; &#160; 有关文件的其他数据.</p><p>2.2 Linux系统文件三个主要的时间属性</p><p>&#160; &#160; &#160; &#160; ctime(change time)<br />&#160; &#160; &#160; &#160; ◆最后一次改变文件或目录（属性)的时间<br />&#160; &#160; &#160; &#160; atime(access time)<br />&#160; &#160; &#160; &#160; ◆最后一次访问文件或目录的时间<br />&#160; &#160; &#160; &#160; mtime(modify time)<br />&#160; &#160; &#160; &#160; ◆最后一次修改文件或目录(内容)的时间</p><p>2.3目录文件结构<br />&#160; &#160; &#160; &#160; 目录也是一种文件<br />&#160; &#160; &#160; &#160; 目录文件的结构</p><p>&#160; &#160; 每个inode都有一个号码，操作系统用inode号码来识别不同的文件<br />&#160; &#160; Linux系统内部不使用文件名，而使用inode号码来识别文件<br />&#160; &#160; 对于用户，文件名只是inode号码便于识别的别称</p><p>3.inode的号码</p><p>用户通过文件名打开文件时，系统内部的过程</p><p>&#160; &#160; 1.系统找到这个文件名对应的inode号码<br />&#160; &#160; 2.通过inode号码，获取inode信息<br />&#160; &#160; 3.根据inode信息，找到文件数据所在的block，读出数据</p><p>查看inode号码的方法</p><p>&#160; &#160; ls -i命令:查看文件名对应的inode号码<br />&#160; &#160; ls -i aa.txt<br />&#160; &#160; stat命令:查看文件inode信息中的inode号码<br />&#160; &#160; stat aa.txt</p><p>4.inode的大小</p><p>&#160; &#160; &#160; &#160; inode也会消耗硬盘空间，每个inode的大小，一般是128字节或256字节<br />&#160; &#160; &#160; &#160; 格式化文件系统时确定inode的总数<br />&#160; &#160; &#160; &#160; 使用df -i命令可以查看每个硬盘分区的inode总数和已经使用的数量</p><p>5.inode的特殊作用</p><p>由于inode号码与文件名分离，导致Linux系统具备以下几种特有的现象:</p><p>&#160; &#160; 1.文件名包含特殊字符，可能无法正常删除。这时直接删除 inode，也可以删除文件<br />&#160; &#160; 2.移动文件或重命名文件，只是改变文件名，不影响inode号码<br />&#160; &#160; 3.打开一个文件以后，系统就以inode号码来识别这个文件，不再考虑文件名</p><p>&#160; &#160; 格式</p><p>&#160; &#160; 普通文件 find ./ -inum 52305140 -exec rm -i {} ;<br />&#160; &#160; find ./ -inum 52345140 -exec rm -rf {} \ 目录</p><p>&#160; &#160; find ./ -inum 50464299 -delete</p><p>6.链接文件</p><p>&#160; &#160; &#160; &#160; 为文件或目录建立链接文件<br />&#160; &#160; &#160; &#160; 链接文件分类<br />硬链接</p><p>ln 源文件目标位置</p><p>软链接</p><p>ln [-s] 源文件或目录... 链接文件或目标位置</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Thu, 22 Sep 2022 05:57:11 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=437&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[linux系统目录详解（很详细）]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=431&amp;action=new</link>
			<description><![CDATA[<p>test</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Thu, 22 Sep 2022 04:34:20 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=431&amp;action=new</guid>
		</item>
	</channel>
</rss>
