初识Linux14Ext系列件系统
初识Linux(14)Ext系列⽂件系统
之前谈论的都是已打开文件在操作系统的中的管理,但是还有更多的文件没有被打开,被存在磁盘中,如何管理这些磁盘中的文件,就是本篇的学习目标。
1.理解硬件
磁盘、服务器、机柜、机房
**计算机中有许多重要的硬件,比如寄存器(由各种各样的
门电路逻辑电路
组合而成),而
机械磁盘是计算机中的唯一的一个机械设备。**
磁盘是一种外设,不同于现在的ssd卡,虽然早期的磁盘都相对 速度慢 ,同时单位容量的价格也更便宜,任然广泛运用于计算机中。
磁盘、二进制
计算机只认识二进制 是一种宏观表示,这个二进制的具体实现在不同的物理设备上有不同的具体体现。
比如高低电平,网卡上的电脉冲,或者是磁盘 上的NS磁性
所以,在磁盘中,改变0、1,其实是将N极改成S极,或者将s极改成n极,然后由磁性得到1或者0
磁头和磁盘是悬浮着(距离极小)的,外壳相对于内层一定是完全密封的,因为灰尘直径远大于磁头和磁盘的距离,灰尘在上方撞击可能导致数据丢失的问题。主轴能让磁盘旋转,永磁铁能让磁头沿半径移动,读取所有 磁道 上的内容
真实情况:
相邻磁极之间的磁性远大于内部的磁性,所以本质是用两个微磁体之间是否由磁性来确定1和0。
磁盘结构
侧面来看 ,磁盘可以分成三层或者两层或者四层等(下图是三层),每层两个面都能记录数据;
俯视来看,每个面上,不同的半径形成的是不同的磁道,一定距离的磁道构成一个 扇区 。
所有面上的相同半径构成了一个圆柱面,
扇区:是磁盘存储数据的基本单位,512字节,块设备(就算只访问这512字节当中的一个字节,也需要全部访问这个扇区,整个扇区是一块完整的存储结构)
从最外圈的磁道开始,编号为0,然后依次向内递增;盘面的编号也是从0开始的。
这一点在之后的计算中有用。
扇区的读写
能定位在任何磁道上,就能定位在任何扇区上。
如何定位⼀个扇区呢?
•
可以先确定磁头要访问哪⼀个柱⾯(磁道)(cylinder)
•
再定位磁头(哪一层)(header)
•
定位⼀个扇区(sector)
CHS地址定位
⽂件 = 内容+属性 都是数据,⽆⾮就是占据那⼏个扇区的问题!
( 我们认为不同半径的磁道所能容纳的扇区的数量是一样的,但是
靠近主轴的同⼼圆⽤于停靠磁
头,不存储数据 )
磁盘容量 = 柱面个数(磁道数)*磁头数(层数)每个磁道扇区数每个扇区字节数
就像一个三维坐标,只要知道了磁道位置/磁头位置以及第几扇区,就能定位扇区。
对早期的磁盘⾮常有效,知道⽤哪个磁头,读取哪个柱⾯上的第⼏扇区就可以读到数据了。
但是CHS模式⽀持的硬盘容量有限,不够长。
磁盘的逻辑结构
将磁道上的扇区化曲为直,得到LBA的概念。
LBA(Logic Block Address)是一种逻辑抽象,但是实际去找的时候是否还是需要去转换成CHS
LBA将扇区的变成一个一个独立的单元,像三维数组一样联系到一起,
整个盘:
同时,可以用一维数组来构建具体的LBA地址:
半径为r的一个盘面上的一个磁道:
由该磁道相同半径的磁道组成的柱面:
多个柱面组成整个逻辑结构:
一个三维数组就被我们抽象成一维的线性存储模式!!
***重点!!!
对于OS来说,知道LBA地址即可,LBA地址与CHS地址的转换由磁盘⾃⼰来做!固件(硬件电路,伺服系统)
注意:柱面号、磁头号通常都是从0开始编号的。这种编号方式是硬盘存储结构的一种约定俗成的规定,有助于硬盘控制器和操作系统对磁盘进行统一的寻址和数据管理。 所以,一个CHS地址的柱面号表示的是当前所用地址以外已经被完全占满的柱面。
每⼀个扇区都有⼀个下标,我们叫做LBA(Logical Block Address)地址,其实就是线性地址。所以 ,怎么计算得到这个LBA地址呢?
CHS转成LBA:
•
磁头数*每磁道扇区数 = 单个柱⾯的扇区总数
•
LBA = 柱⾯号C单个柱⾯的扇区总数 + 磁头号H每磁道扇区数 + 扇区号S - 1
•
即: LBA = 柱⾯号C(磁头数每磁道扇区数) + 磁头号H每磁道扇区数 + 扇区号S - 1*
•
扇区号通常是从1开始的,⽽在LBA中,地址是从0开始的
•
柱⾯和磁道都是从0开始编号的
•
总柱⾯,磁道个数,扇区总数等信息,在磁盘内部会⾃动维护,上层开机的时候,会获取到这些参
数。
LBA转成CHS:
•
柱⾯号C = LBA // (磁头数*每磁道扇区数)【就是单个柱⾯的扇区总数】
•
磁头号H = (LBA % (磁头数*每磁道扇区数)) // 每磁道扇区数
•
扇区号S = (LBA % 每磁道扇区数) + 1
•
“//”: 表⽰除取整
以上公式都不需要死记硬背,对于工科生来说,理解这个图就能理解换算办法。
2. 引⼊⽂件系统
操作系统读取硬盘数据的时候,其实是不会⼀个个扇区地读取,这样
效率太低,⽽是⼀次性连续读取多个扇区,即⼀次性读取⼀个”块”(block)。毕竟磁盘是外部设备,每次IO都有成本。
因此, 磁盘也是一种“块”设备。
硬盘的每个分区是被划分为⼀个个的”块”。⼀个”块”的⼤⼩是由格式化的时候确定的,并且不可以更改,最常⻅的是4KB,即连续⼋个扇区组成⼀个 ”块”。”块”是⽂件存取的最⼩单位。
所以,8个LBA数据构成一个“块”,即块号 = LBA // 8,LBA=块号*8+n(n是指该块中的哪个扇区)
或者 块号*8=该块的LBA起始地址
分区
对于win系统来说,我们有C盘D盘E盘F盘等等,其实这就是一个硬盘中分区的表示。
每个分区独立,可以装不同的文件系统,分区之间互不影响。
分治思想:管好一个组,就能管好所有的组。我们先研究操作系统如何管理分区中的一个组:
比如,我们将 group0 分成很多个 block group
data block所占空间远大于其他的几个模块
文件=内容+属性,类似于PCB或者struct files,inode就是管理磁盘上文件属性的结构体
Boot Sector也称作启动区,大小为1kb,任何文件系统都不能改变启动区的内容!!
启动区里存放了启动信息和分区信息。
宏观来说,inode存文件属性,data block存文件内容
inode里面有相关的映射内容,便于去data block中找具体的文件内容。
在linux系统下,文件名不会存在inode中。
我们认为,inode是128字节大小,一个块(win系统中叫簇)能存32个inode(4*1024 / 128)
每一个分组都有这些固定字段,我们依次介绍。
inode :
•
存放⽂件属性 如 ⽂件⼤⼩,所有者,最近修改时间等
•
当前分组所有Inode属性的集合
•
inode编号以分区为单位,整体划分,不可跨分区
我们还认为:
inode结构里还有个block数组,这个数组用于记录属于该inode对应文件内容所存储的块号
Data Blocks:
按照“块”的大小一个一个存储在内存中,一个块4字节,用于存储文件内容。
Bitmap:
inode Bitmap:
来显示inode是否存在的位图。比如该区域有8个inode,位图为0010 0001,则表示第1、6个已经有内容了。
一个块写进内存(一万个比特位的位图也才不到2kb,一个块4kb),所以不影响我们前文提到的内存要加载4kb来进行操作。整体修改完了再写出来即可。
Block Bitmap :同理,检测对应的data blocks是否合法的存在(是否已经被使用)。
Super Block和GDT
SB一般在每个分组的第一个字段中,存储的是该 分区
的文件系统信息(每个分区的文件系统可以不一样),但并不是每个分组中都有,也不是只有第一个分组有,这样能保证一个超级块出问题时,其他超级块能用正确的信息来修复。当所有的超级块都被破坏时,说明这个文件系统已经崩溃了。 该字段记录的包括不限于:总的inode数和block数,还能使用的inode数和block数,最近一次挂载时间、修改时间等。
struct ext2_super_block { __le32 s_inodes_count; /* Inodes count */ __le32 s_blocks_count; /* Blocks count */ __le32 s_r_blocks_count; /* Reserved blocks count */ __le32 s_free_blocks_count; /* Free blocks count */ __le32 s_free_inodes_count; /* Free inodes count */ __le32 s_first_data_block; /* First Data Block */ __le32 s_log_block_size; /* Block size */ __le32 s_log_frag_size; /* Fragment size */ __le32 s_blocks_per_group; /* # Blocks per group */ __le32 s_frags_per_group; /* # Fragments per group */ __le32 s_inodes_per_group; /* # Inodes per group */
GDT:块组描述符,描述当前块组中哪里到哪里是哪些内容。
创建一个文件可能(只写了hello world)的完整步骤:
先在inode bitmap中找一个可以用的位置,将0置1,加载对应的文件属性到 inode 中去,此时文件内容少,直接将内容放到inode指向的data block中去。
删一个文件,相当于是把inode Bitmap和Block Bitmap对应的比特值从0置1
所以删除一个文件是可以恢复的。
删除的本质其实是让文件无效,删除之后,如果想恢复,最好的办法就是什么都不做,避免被其他文件覆盖,覆盖了就真的无了
格式化
我们想要在硬盘上储⽂件,必须先把硬盘格式化为某种格式的⽂件系统,才能存储⽂件。⽂件系统的⽬的就是组织和管理硬盘中的⽂件。
格式化的本质是在一个分区中写入新的(空的)文件系统,两个bitmap要清0,GDT里的使用率要降到0,SB中未使用的block和inode要加到最大。
包括在win系统中,格式化也是对于每一个分区来说的!
从分区落实到分组,每个组里的inode和block的个数是动态计算后固定的,所以比例也是固定的。
inode和block会成比例分配!
比如一个inode分配十个block,这比例是固定的。
磁盘中存在inode被分配完的情况(都是小文件)。
一个组里面的inode的值是固定的,都是从a~b
一整个 分区(C盘,D盘) 的inode是一套
一整个 分区 的block也是一套。
inode与block的分配
每个分组占据了多少编号到多少编号的inode或者block都记录在GDT里
因此,每次只需要确定每个分组的起始inode值即可。block同理。
可以 跨组(Block group0,Block group1) 建立块号和inode的映射关系,inode和block的映射关系是在全局建立的。
但是inode和block的编号都是分区独立,分组分配的。
•
分区之后的格式化操作,就是对分区进⾏分组,在每个分组中写⼊SB、GDT、Block
Bitmap、Inode Bitmap等管理信息,这些管理信息统称: ⽂件系统
•
只要知道⽂件的inode号,就能在指定分区中确定是哪⼀个分组,进⽽在哪⼀个分组确定
是哪⼀个inode
•
拿到inode⽂件属性和内容就全部都有了, 可以通过Inode完成对文件的增删查改
根据inode找文件内容的大概流程:
查区间,inode减去起始编号,查对应的bitmap,从而查到对应的inode table中的数据,根据映射关系找到data block
3. inode和block的映射
一个inode128字节,一个指针4字节,就算全部存指针,也只能存32个块,一个块能存4kb,32*4 = 128kb,甚至不到1MB,这样的文件也太小了吧!!
一共有15个块指针,12个是直接块指针,还有三个间接块指针。
比如一级间接块索引,,指向的是一个不装文件数据、只存其他块号的块,一个块能存1024个块号,相当于通过这个一级索引能找1024*4kb=4mb的空间。同理,二级索引指向的全是
只存一级索引块,能装4gb,三级能装4tb。
所以,一个inode实际能装的大小远超过一个正常文件的大小,甚至大于整个磁盘的大小。
4. 目录文件(重点)
建议有能力的读者去看看这篇深度好文。
/就是一个目录文件,/user也是一个目录文件,文件夹就是目录,前者是win的概念,后者是linux的概念, 目录文件存放他下面的“子目录文件的文件名和inode的映射”以及普通文件的文件名和inode的映射,每个文件的inode和文件名的映射是存在其所在的文件夹中的。
在上层使用时,好像从来没有用过inode,都是直接操作文件名。文件名和inode的关系是什么??
正如上所述,在linux中,文件名不存储在inode中,文件名的信息是存在其所存在的目录
所以地址的意义就在此,/user/zhangsan/test.cc,在/中找user文件名(即“user”)对应的inode,找到这个inode在其对应的data block中找到zhangsan对应的inode,依次找到test.cc,将test.cc加载到内存中去…….
进程都有CWD的意义也在于此,通过CWD(再加上open等函数)就能拼成所需的完整路径,让操作系统永远都能获得一个完整路径。
为什么以inode作为比较、查找的对象?因为整数的查找添加等比字符串简单。
这样更高效。类似于GID、UID和USERNAME这种关系,字符串是给用户看的。
5. dentry(directory entry)
两个现象:
1,在文件夹中能直接进行tree命令
2,诸如下图这种情况,只能不断的访问磁盘文件系统吗?
文件系统是外设,速度很慢!
解决办法:引入结构体dentry。
将曾经访问过、经常访问的路径在内存中缓存起来,形成一棵多叉树,这样就不需要一直访问磁盘了!!!
6. 文件描述符、内存、进程与 目录缓存路径 的关系 (难点)
task_struct中的files_struct维护该进程打开的文件列表,列表中存的是一个个打开的file对象(经过VFS系统封装出来的,让所有文件都是file对象), 通过file对象,我们可以找到包括但不止于: 1 文件对应的操作表(就是将每个文件的具体操作封装成write,read等接口的函数指针数组), 2 文件内核缓冲区(缓冲够了直接由OS往外设发送), 3
f_path,f_path指向了一个包含dentry的结构体。再观察dentry的结构,有parent和child,由此说明dentry维护的 的确是一个树形结构。
dentry里还有一个inode对象,其对应的 inode值 就是此时file所对应的 inode值
file中前面两个包含的结构是在上一篇文章中学习过的,提出来只是为了复习。
所以当一个目录文件被打开后,不再需要去访问磁盘找data block从而找到子目录或文件,而是能通过file中的dentry直接找到子目录或文件 (dentry已经在内存中了!!!!)
注意:不是将整个结构树给塞进内存,而是将结构记录在dentry中,加载对应的文件的时候就能把该文件所对应的目录关系给加载到内存中去。
最终,就能通过dentry找到希望打开文件的inode了。
这个树形结构,整体构成了Linux的路径缓存结构,打开访问任何⽂件,都在先在这
棵树下根据路径进⾏查找,找到就返回属性inode和内容,没找到就从磁盘加载路径,添加dentry 结构,缓存新路径
7. 挂载mount(了解)
df -h 查看磁盘的挂载状态。
linux中, 必须将分区挂载到一个文件目录树上才能使用该分区 。
Mounted on : 挂载
使用指令建立好分区之后进行查找:
刚创建好是查不到也用不了的,要先挂载才能用
挂载和查看操作: