前面博客讲解的是内存级文件管理,接下来介绍磁盘级文件管理
文件系统分为两部分
- 内存级文件系统 : OS加载进程 ,进程打开文件, OS为文件创建struct file 和文件描述符表 ,将进程与打开的文件相连, struct file 内还函数有指针表, 屏蔽了底层操作的差异,struct file中还有内核级文件缓冲区(提高效率)
- 磁盘级文件系统: 管理磁盘上的文件
磁盘结构
它通过磁性材料或闪存技术将数据持久化存储,即使在断电后数据也不会丢失
主要分为机械硬盘(HDD)和固态硬盘(SSD)
今天我们讲解机械硬盘
机械硬盘(HDD,Hard Disk Drive)
工作原理:
使用磁性材料涂覆的旋转盘片存储数据, 比如磁性南极代表1 ,磁性北极代表0(打比方)
通过磁头在盘片上移动来读写数据
定位扇区
CHS寻址模式
在硬盘的传统寻址模式中,数据是通过柱面(Cylinder)、磁头(Head)、扇区(Sector)的组合来寻址的,这种方式被称为CHS寻址(Cylinder-Head-Sector)
扇区: 磁盘存储数据的基本单位 ,512字节, 因此,磁盘是块设备
柱⾯上的每个磁道,扇区个数是⼀样的
注意: C H都是从0编码 ,S从1编码
真实过程 ⼀个细节:传动臂上的磁头是共进退的
LBA逻辑块寻址模式
LBA(Logical Block Addressing)逻辑块寻址模式。在 LBA 模式下,我们知道硬盘上的一个数据区域由它所在的磁头、柱面(也就是磁道)和扇区所唯一确定。
在LBA地址中,地址不再表示实际硬盘的实际物理地址(柱面、磁头和扇区)。LBA编址方式将 CHS这种三维寻址方式转变为一维的线性寻址,它把硬盘所有的物理扇区的C/H/S编号通过一定的规则转变为一线性的编号,系统效率得到大大提高,避免了烦琐的磁头/柱面/扇区的寻址方式。
在访问硬盘时,由硬盘控制器再这种逻辑地址转换为实际硬盘的物理地址。
OS只需要使⽤LBA就可以了!!LBA地址转成CHS地址,CHS如何转换成为LBA地址。谁做啊??磁盘 自己来做!固件(硬件电路,伺服系统)
LBA与C/H/S 之间的转换:
LBA =柱面号C*单个柱面的扇区总数 + 磁头号H*每个磁道扇区的数量+ 扇区号S-1
=柱面号*(磁头数*每个磁道扇区数)+磁头号H*每个磁道扇区的数量+ 扇区号S-1
柱面号C = LBA//(磁头数*每磁道扇区数) 就是单个柱面的扇区总数
磁头号H =(LBA% (磁头数* 每磁道扇区数)) // 每磁道扇区数
扇区号S =(LBA% 每磁道扇区数)+1
// 代表除法向下取整
所以:从此往后,在磁盘使用者看来,根本就不关心CHS地址,⽽是直接使⽤LBA地址,磁盘内部自己转换。所以: 从现在开始,磁盘就是⼀个 元素为扇区 的⼀维数组,数组的下标就是每⼀个扇区的LBA地址。OS使用磁盘,就可以⽤⼀个数字访问磁盘扇区了。
在磁盘如何管理文件
引入"块"概念
其实硬盘是典型的“块”设备,操作系统读取硬盘数据的时候,其实是不会⼀个个扇区地读取,这样 效率太低,⽽是⼀次性连续读取多个扇区,即⼀次性读取⼀个”块”(block)。
硬盘的每个分区是被划分为⼀个个的”块”。⼀个”块”的⼤⼩是由格式化的时候确定的,并且不可 以更改,最常⻅的是4KB,即连续⼋个扇区组成⼀个”块”。”块”是⽂件存取的最⼩单位。
注意:
- 磁盘就是⼀个三维数组,我们把它看待成为⼀个"一维数组",数组下标就是LBA,每个元素都是扇区
- 每个扇区都有LBA,那么8个扇区⼀个块,每⼀个块的地址我们也能算出来.
- 知道LBA:块号 = LBA / 8
- 知道块号:LAB=块号*8+n (n是块内第几个扇区 1<= n <=8)
引入"分区"概念
其实磁盘是可以被分成多个分区(partition)的,以Windows观点来看,你可能会有⼀块磁盘并且将 它分区成C,D,E盘。那个C,D,E就是分区。分区从实质上说就是对硬盘的⼀种格式化。但是Linux的设备 都是以文件形式存在,那是怎么分区的呢?
柱面是分区的最小单位,我们可以利⽤参考柱⾯号码的⽅式来进⾏分区,其本质就是设置每个区的起 始柱⾯和结束柱⾯号码。此时我们可以将硬盘上的柱⾯(分区)进⾏平铺,将其想象成⼀个⼤的平面,如下图所示:
柱面大小⼀致,扇区个位⼀致,那么其实只要知道每个分区的起始和结束柱⾯号,知道每 ⼀个柱面多少个扇区,那么该分区多⼤,其实和解释LBA是多少也就清楚了.
也就是OS管理磁盘,通过分治思想,将磁盘进行分区处理,但分出的区大小还有几百GB ,进一步对区分组 分成Block Group ,直接将每个组管理好即可.
Block Group 结构
- 文件 = 内容 + 属性
- Linux下文件的存储是属性和内容分离存储的
- 文件内容存储在(Block Group) 组 的 Data blocks中
- 文件属性也是数据 , linux中 文件的属性数据(比如文件的创建者、文件的创建日期、文件的大小等等)以数据结构 inode 的方式 存储 ⼀个inode,inode内有⼀个唯一的标识符,叫做inode号
- 一个文件一个inode ,inode大小有128字节 或者256字节, 我们同一用128字节 , inode又储存在 inodetable中
- 文件名属性并未纳入到inode数据结构内部
- 任何文件的内容大小可以不同,但是属性大小⼀定是相同的
- ls -l -i 可以打印出文件详细信息 包括文件的inode值
1. 我们已经知道硬盘是典型的“块”设备,操作系统读取硬盘数据的时候,读取的基本单位 是”块”。“块”又是硬盘的每个分区下的结构,难道“块”是随意的在分区上排布的吗?那要怎么找到“块”呢?
2. 还有就是上面提到的存储文件属性的inode,又是如何放置的呢?
文件系统就是为了组织管理这些的!!
ext2文件系统
我们想要在硬盘上储⽂件,必须先把硬盘格式化为某种格式的⽂件系统,才能存储⽂件。⽂件系统的⽬的就是组织和管理硬盘中的⽂件
在Linux系统中,最常见的是ext2系列的⽂件系统。其早期版本为ext2,后来⼜发展出ext3和ext4。 ext3和ext4虽然对ext2进行了增强,但是其核⼼设计并没有发⽣变化,我们仍是以较⽼的ext2作为 演示对象。
ext2⽂件系统将整个分区划分成若⼲个同样大小的块组(Block Group),如下图所示。只要能管理⼀个分区就能管理所有分区,也就能管理所有磁盘⽂件。
上图中启动块(Boot Block/Sector)的大小是确定的,为1KB,由PC标准规定,用来存储磁盘分区信息和启动信息,任何文件系统都不能修改启动块。启动块之后才是ext2文件系统的开始。
Block Group 块组内部构成
超级块(Super Block) :
存放文件系统本⾝的结构信息,描述整个分区的⽂件系统信息。记录的信息主要有:bolck 和 inode的总量, 未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了
超级块在每个块组的开头都有⼀份拷贝(第⼀个块组必须有,后⾯的块组可以没有)。为了保证⽂ 件系统在磁盘部分扇区出现物理问题的情况下还能正常⼯作,就必须保证⽂件系统的super block信 息在这种情况下也能正常访问。所以⼀个⽂件系统的super block会在多个block group中进行备份, 这些super block区域的数据保持⼀致。
GDT,Group Descriptor Table:块组描述符表
描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。每个块组描述符存储⼀个块组的描述信息,如在这个块组中从哪⾥开始是inode Table,从哪⾥开始是Data Blocks,空闲的inode和数据块还有多少个等等。
块位图(Block Bitmap)
Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
inode位图(inode Bitmap)
每个bit表示一个inode是否空闲可用。
i节点表 (inode table)
- 存放文件属性 , 如文件大小,所有者,最近修改时间等
- 当前分组所有Inode属性的集合
- inode编号以分区为单位,整体划分,不可跨分区
数据区(Data Blocks)
存放文件内容,也就是⼀个⼀个的Block。
补充: 普通文件的内容是普通数据 ,目录文件的内容是文件名和 inode号的映射关系
根据不同的文件类型有以下几个种情况:
- 对于普通文件,文件的数据存储在数据块中。
- 对于目录,该目录下的所有文件名和目录名存储在所在⽬录的数据块中,
- Block号按照分区划分,不可跨分区
- ls -l命令 文件名是从目录的Block中拿到的 ,看到的其它属性信息保存在该文件的inode中(inode 中无文件名)。
Linux路径解析
1. 拿到inode号就可以拿到文件内容存储位置
前面我们知道了文件内容存储在Block Group的数据区(Data Blocks)中,
除了文件名的文件属性存储在inode中 ,
而inode中有该文件内容存储在Data Block的哪个块的位置信息,也就是如果知道了该文件的inode号就知道了文件内容存储位置,就知道了文件内容.
2. 我们访问文件只知道文件名, 如何拿到该文件的inode号 进而拿到文件内容呢?
文件名不在inode中存储 ,在该文件所处的目录文件的Block中存储(且有文件名与该文件的inode的映射) ,所以我们只需要找到该文件所在的目录文件即可.
例如 :要访问makefile, 就必须打开lesson19(当前工作目录),然后才能获取makefile对应的inode进而对文件进行访问。
3. 目录文件怎么打开?
打开目录文件又依赖目录文件的inode ,又要打开:当前工作目录的上级目录
所以类似"递归",需要把路径中所有的⽬录全部解析,出口是"/"根目录。
实际上,任何⽂件,都有路径,访问目标文件,比如: /home/code/test/test/test.c 都要从根目录开始,依次打开每⼀个目录,根据目录名,依次访问每个目录下指定的目录,直到访问到test.c。这个过程叫做Linux路径解析。
4.结论:
所以,我们知道了:
- 访问文件必须要有文件存储的目录+文件名,这就是要写文件路径的原因,文件路径是进程给的(每一个进程都有一个CWD) .例如open("test.txt")这个命令就没有带路径也能执行;
- 根目录是固定的 , 固定文件名,inode号,无需查找,系统开机之后就必须知道
我们知道了,打开文件要先知道文件的inode号,inode号是如何找到文件内容的呢?下面我们解密
inode和datablock映射
inode先存储了12个直接块指针 ,直接指向存储文件数据的Block
第13个是一级间接块指针 ,一级间接块中不存文件数据,存储的是直接块的指针
第14 个是二级间接块指针 ,
第15个是三级间接块指针
inode这样设计,使得inode的大小得以固定
结论:
- 分区之后的格式化操作,就是对分区进行分组,在每个分组中写入SB、GDT、Block , Bitmap、Inode Bitmap等管理信息,这些管理信息统称: 文件系统
- 只要知道文件的inode号,就能在指定分区中确定是哪⼀个分组,进而在哪⼀个分组确定是哪⼀个inode
- 拿到inode文件属性和内容就全部都有了
像上面一样,打开文件,每次都要从根目录一步一步找文件,还是不断加载磁盘文件 ,效率会很低 ,因此 linux在内存中对路径结构进行缓存(内存缓存) ,用多叉树的结构 每个打开过的文件都有自己的dentry, dentry里包含了自己的inode号
普通文件也有dentry linux将所有已打开的文件创建对应的dentry ,对打开文件的管理转换成了对dentry的管理.
再次访问同级目录下文件, 会使用此目录dentry中的inode ,查找目录Block 中文件与inode的映射.
文件描述符 进程的关系
整体的认识在操作系统内打开一个文件
磁盘存在多种文件系统 ,linux默认将根目录在全局中找到根目录的dentry,当我们想打开文件时,进程会提供路径,然后linux系统会查根目录的dentry,进一步向下寻找(根据进程给的路径向下),打开whb的inode ,打开whb的block找到test.txt的inode号, 在磁盘找到test.txt的inode ,加载进来,在内核中形成dentry结构链接在whb下面 ,初始化他自己的dentry(存储test.txt的inode号), 初始化test.txt的struct file (用dentry初始化 struct file 的path ,将test.txt的内容拷贝到内核级缓冲区) , 将test.txt文件描述符存储在进程的文件描述符表中
1.创建内核数据结构 2.在磁盘中读取数据,加载数据到内存 3.构建映射关系
文件的内容加载进来 ,文件的inode加载进来 ,文件的路径关系添加到linux内部的路径缓存中(dentry多叉树)
下面,通过touch⼀个新文件来看看如何工作
[root@localhost linux]# touch abc
[root@localhost linux]# ls -i abc
263466 abc
有了inode,但是inode是按照区来编码的 ,怎么知道在哪个区里找呢?