一、认识硬件——磁盘

1.1 物理构成

     磁盘是唯一的一个机械设备,也是一个外设

     以前的老式电脑上装的就是机械磁盘,现在由于用户对使用计算机的速度要求越来越高,现在我们普通人使用的电脑基本上都是用的SSD固态硬盘,SSD固态硬盘并没有像机械磁盘那样的机械运动,读写速度更快,且具有体积小、低功耗、耐用性好、无噪音等特点且未来还有很大的研究空间!所以在桌面领域几乎取代了机械磁盘

     但是企业级存储更倾向于使用机械硬盘,由于其成本低、容量大的特点更有利于进行大规模的数据存储,所以他其实很难被淘汰! 

1、写入磁盘工作原理:二进制序列会通过磁头的充放电(任何硬件都只认识二进制序列,因为本质上是用线连接的),将数据写到盘片上。

2、 一些特点:

(1)我们的计算机内部的信息流动是以电子或者光电信号的形式传递(非常快),而磁盘是机械运动相比之下速度很慢!!

(2)盘片高速旋转,磁头左右转动,磁头是一面一个且和盘面不接触!

(3)磁盘在设计的时候必须保证无尘环境且密封完好,因为有灰尘的话可能会导致盘面刮花造成数据丢失。

(4)内存是掉电易失存储介质,盘片是永久性存储介质。

 3、注:磁盘是有寿命的,大公司磁盘快报废的时候并不敢直接把磁盘给丢掉,因为里面存储了大量的用户数据(磁盘密封性好且不易被销毁),所以相关的安全部门对大公司的磁盘销毁工作时有严格的要求的

1.2 存储构成

磁头的左右摆动——>定位磁道(柱面) 

磁头不动的时候,盘片的旋转——>定位扇区  

所以磁盘被访问的最基本单位是扇区 ——> 512字节/4KB

——>因此我们可以把磁盘看做由无数个扇区构成的存储介质

——>所以我们要把数据存到磁盘,首先就是定位一个扇区:(1)先找到哪一面(定位磁头)(2)哪一个磁道(3)哪一个扇区

1.3 逻辑抽象

        我们把假设把磁带摊开,从逻辑上我们就可以把他看做是线性的我们就可以用一个数组把扇区组织起来,每个下标对应着一个扇区(我们把逻辑扇区地址叫做LBA地址)

问题1:那么我们如何通过下标找到我们要写入的扇区呢??

问题2: 为什么扇区大小不均匀的但是LBA地址是均匀??

——>因为磁道是从中间向外辐射的,所以里面的磁道有多少个扇区,外面的磁道就有多少个扇区,只不过里面磁道的扇区会小一点(扇区大小不均匀)   但其实可以通过调整密度来变得均匀(里面的01序列稠密一点,外面的稀疏一点) 

        现在的磁盘其实也是可以做到让外面扇区较大的多存点数据,但是这样的话相关的算法就得更改!! 

 1.4 回归硬件

 所以我们究竟是如何和硬件交互的呢???

不仅CPU有寄存器,其他外设也有寄存器,包括磁盘!!

CPU向磁盘发送IO方向的请求时——>

1、控制寄存器(告诉磁盘是打算读还是打算写) 

2、数据寄存器(告诉磁盘要写入哪些数据)

3、地址寄存器(告诉磁盘要写的LBA地址,磁盘自己通过CHS寻址)

4、结果寄存器(CPU获取IO请求是否成功的状态,比如可能空间不足写入失败) 

二、文件系统 

        通过逻辑抽象,我们可以把对扇区的地址抽象成线性的LBA地址,但是具体磁盘有多大呢?已经用了多少扇区?哪些扇区还没被使用?哪些扇区存的是属性?哪些扇区存的是内容?要往哪个扇区去写入??——>诸如以上问题,注定了我们的操作系统必须想办法把磁盘空间组织起来!

    首先是磁盘分区,我们可以把一个磁盘分成多个分区。

 而每个分区,都可以用以下的区域来表示

 Boot Block: 是文件系统中的一个特殊块,位于文件系统的起始位置。 它包含了引导加载程序(Boot Loader)所需的信息,用于引导操作系统的启动过程。

 Block group:每个分区被分成了一个个的block,该block的大小是由格式化确定的,不可被修改,每个block由可以细分为6个区域。

 Linux中,文件的属性和内容是分开存储的!!

 2.1 inode  

    inode:存储的是单个文件的全部属性(128字节) ,一般而言,一个文件只有一个inode!

 Linux系统里标记文件的唯一标识用的是inode,且文件的属性中不包含文件的名称!

通过ls -li 可以观察到文件的inode编号 

其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息 

 2.2 Data Block

     Data Block:存文件内容的区域,以块的形式呈现,常见的是4KB大小,一般而言一个块只有自己的数据!

     一个文件只有一个inode,但如果是个大文件可能会有很多个块。所以在inode结构体内部会有一个block数组,存储的是数据块的块号

问题:那难道是文件内容需要多少个数据块,block数组就需要有多大么???

——>并不是的,block数组内部除了一部分是直接索引(存储文件块号,可直接找到文件内容),还有一小部分是二级索引(该块号不会存储文件内容而是继续存储文件的块号)

2.3 Bitmap 

我怎么知道,哪些块被使用过,哪些块没被使用过呢???

块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用

inode那么多,我怎么知道要给该文件分配哪个呢??

inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。

问题1: 为什么我们下载一个文件需要很久,但是删除的时候却很快呢??

 ——>删除一个文件的时候,并不会把块(文件内容)清空,而仅仅只是把对应比特标志位给修改了,表明该块是空闲可用的。 

问题2:如果我们想恢复一个被删除的文件,要怎么办呢?? 

——> 如果我们不小心误删的一个文件,如果这个文件很重要,最好的办法就是什么都不做然后找专业的人去恢复(一般来说恢复其实就是得找到该文件的inode编号,比如通过Linux的日志信息找到被删文件的inode,但是具体怎么恢复得依靠一些专业的东西!),因为虽然内容还在,但是他的位图信息表明是可以被使用的,所以你如果乱操作可能会导致文件内容被覆盖

 2.4 GDT和超级块

GDT(Group Descriptor Table):块组描述符,描述块组属性信息(该block组的分配信息和使用情况)。

超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量, 未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个 文件系统结构就被破坏了 (整个分区的相关信息)

        因为超级快是在组里面存在的,但是记录了整个分区的信息,所以并不需要每个组都有, 其实理论上来说有一个就够了(太多会影响速度),但是由于他非常重要,一旦损坏就会造成文件系统崩溃,所以一般来说一个分区里面会有个别组有超级快,这样即使一个超级块崩了,也可以通过其他超级块区对文件系统做修正!

问题:超级块决定了文件系统能否正常运行,块又那么多,操作系统是如何定位超级块的呢??

 ——> 魔数,一个随机值,在超级块中的位置是确定的,只要我们操作系统去读该块的规定偏移量位置看看是否存在魔数,就可以确定该块是不是超级块!

2.5 格式化

       每个分区在使用之前,就必须提前将部分文件系统的属性信息提前设置到对应的分区中,方便我们后续使用这个分区或者分组!

       对磁盘做格式化可以帮助我们让磁盘恢复到完全没有被使用过的状态。 

 三、对目录的理解

3.1 新建和删除文件,系统做了什么?

 1、新建文件:系统会在某个路径下去创建,路径帮助我们确定在哪一个分区,读取了超级块后确定了一个block,然后通过查该block的GDT知道该分区的inode还有很多没有被使用,于是到inodebitmap位图结构里找到了没有用过的inode,然后分配给该文件,分配好之后把自己的属性往里面填,这样就算新建成功了!

   如果还打算写入的话,先确认一下要写入内容的大小,确定一下需要几个块,然后到blockbitmap里面找到没被使用的块,把块号填到inode结构体里面的block数组里,然后再把文件内容写进这些块里面

2、删除文件:先找到分区,再找到inode,然后把对应inodebitmap和blockbitmap的位置置0。其实文件还在,只不过对应的块可以被覆盖!

 我们会发现,无论是什么操作,最重要的就是如何去找到文件的inode,可以我们如何知道一个文件的inode呢??其实使用者压根没关心过inode,我们只关心文件名!

 3.2 目录文件

目录也是文件,也有自己的inode ,也是有内容+属性构成

 重点:目录文件的文件内容放的是文件的文件名+对应文件的inode映射关系!

 问题1:为什么一个目录下不能有同名文件??

——>因为文件名和inode是key和value的关系,key值必须唯一。

问题2:为什么没有w无法创建文件??

——>w意味着无法写,所以我们就无法将文件名和inode的映射关系写进去,因此无法创建文件

问题3:为什么没r无法查看文件??

——>r意味着无法读,所以我们也看不到文件名和inode的映射关系,找不到inode就找不到文件

问题4:为什么没有x无法进入目录??

——>x意味着没有操作权限,要进入一个目录必须cd目录名,并且将当前目录名更新到环境变量PWD中,所以无法进行该操作的话我们就无法进入

 问题5:通过读取目录文件内容可以找到文件的inode,那么目录的inode如何查找呢??

——>你当前的目录其实也是别的目录的子目录,所以就得递归往上找,一直找到根目录,然后再从根目录往下找回来,读取每个目录的数据块,直到找到该目录的inode.所以访问文件必须带路径!

3.3 dentry缓存(扩展)

       找目录的inode要递归向上找到根目录,然后再找回来,难度不会很慢么??确实会的,所以Linux提供了dentry缓存,将常用文件的inode信息缓存起来!! 

        dentry缓存,简称dcache,是Linux为了提高目录项对象的处理效率而设计的。它是一个slab cache,保存在全局变量dentry_cache中,用于保存目录项的缓存。dentry结构是一种含有指向父节点和子节点指针的双向结构,多个这样的双向结构构成一个内存里面的树状结构,也就是文件系统的目录结构在内存中的缓存了。

linux 内核 目录项高速缓存 dentry cache 简介-CSDN博客

四、软硬链接 

建立方式:

4.1 如何理解硬链接?

硬链接不是一个独立的文件,因为他没有独立的inode!!

——>所谓的建立硬链接,本质上就是在特定目录的数据块中新增文件名和指向的文件inode编号的映射关系(有点像起别名的感觉,可以让多个文件名指向一个inode)。 

 

问题1:为什么dir的引用计数是2??

——>因为 .  是dir的一个硬链接

问题2:为什么dir的上级目录引用计数是3?

——>因为 dir中的 ..是上级目录的一个硬链接 

总结:

(1)无论他的引用计数是多少,-2就是他当前子目录的数目(比如21,那么他的子目录就是19)

(2)所以删除文件并不是直接把该文件的位图清空,本质上是先把目录里该文件的inode映射关系去掉,然后再到inode里面把引用计数--   引用计数为0时才是把文件的位图清空。

问题3:为什么Linux不允许对目录建立硬链接??(重点!!)

 ​​​​​​

——>为了防止出现环的题,b比方说我们要找一个目录的inode,会向上索引找到根目录,再向下找回来,如果恰好这个过程中出现了 上级目录的硬链接,那么就会回退回去,造成死循环!

问题4:可是目录内部不是有. 和 .. 的硬链接吗??(重点!!)

——> (1) . 和 .. 是操作系统创建的,他不让你创建是为了你好,担心你创建之后出现环的问题,其实. 和..按照道理也会有环的问题,但是操作系统提前规定好了 .和..不会被做搜索,这是强制规定的!所以不会有环的问题! (2)其实操作系统干嘛要多此一举搞个. 和 ..呢??不就是为了方便让用户使用相对路径么??  由于目录文件的inode需要递归向上索引才能找到,所以我们总是需要给想要找的文件加上绝对路径,现在操作系统给我们. 和 .. ,我们就可以用相对路径了!    

硬链接应用场景:通常用来做路径定位!!可以通过硬链接进行目录切换!(不常用) 

4.2 如何理解软链接?

      软连接是一个独立的文件,有独立的inode,也有独立的数据块 ,他的内容里面保存的是指向的文件的路径。(相当于windows的快捷方式)

 

 应用场景:当一个可执行文件在路径深处时,我们要找到他比较麻烦,如果我们在当前路径使用,就可以在当前路径建立一个该文件的软连接。(可执行程序随便放都行,只要有软链接,较常用)

 五、文件系统和内存系统的关联

5.1 硬件和内存交互的基本单位

物理内存是以4KB为基本单位的(由操作系统决定的)    

物理内存交互的单位叫做页框,而磁盘交互的单位叫做页帧

 问题:为什么是4KB而不是要多少给多少呢??难道我访问100字节不比访问4KB快吗??

——>理论上是这样的,但是100字节可能在4KB的不同位置,需要访问的话还需要对磁盘做更精细的计算(磁盘速度太慢了),甚至是文件系统也需要更精细的方法,操作系统文:你能保证你后几秒不用上下文数据吗??反正4kB和100字节效率差不是很多,我都给你拿过来,说不定你用得上呢!(根据局部性原理:程序倾向于访问近期或者近邻的数据,这是计算机性能优化的重要原则,合理运用可以整体提速!)

 总结:(1)硬件:减少IO的次数——减少访问外设的次数

(2)软件:基于局部性原理而产生的预加载——整体提速

5.2 操作系统如何管理内存

       虚拟地址是操作系统提供的,我们用户只能看到纯的虚拟地址,具体这个虚拟地址具体映射到物理内存的哪个位置,我们并不关心也并不知道,这是操作系统帮我们决定好的,所以操作系统必然可以看得到物理地址!!

        那么操作系统要如何去管理我们的内存呢??

 Page的配置不能太大,因为这样的话会占据大量的空间。

 flag:检查当前page的状态(比如说当前检测到我们要访问的内容并没有被加载到内存中,所以这个时候就会发生缺页中断) 

count:引用计数(可以通过引用计数知道当前有多少个进程在共享我这个page,当有进程需要去修改内部的数据的时候,就会发生写时拷贝)

所有申请内存的工作,本质上都是访问Page数组!!(根据一个随机地址判断这个地址属于哪个Page的方法:1KB=2^10字节  所以4KB=2^12字节  恰好在16进制地址中就是后三位数字,所以我们任何一个地址只要 & 0xFFFF F000 ,就可以找到该地址对应的Page) 

5.3 文件页缓冲区

在Linux中,我们每一个进程打开的每一个文件都具有自己的inod和文件页缓冲区!!

      在我们的file结构体狸猫有一个address_spqce结构体,里面又有一个page_tree,里面又有一个 radix_tree_node节点 

 page_tree这个结构有点类似于B树。他不断延伸最后会找到我们想要寻找的Page结构体!

内存管理,并不是直接让那个我们去访问page数组,而是通过一些配套的算法。 

5.4 字典树的理解

      以上是一颗相对简单的字典树(组合式的KV模型),比方说我们想要找cba,我们就可以按照这颗树索引下去,找到我们想要找到的内容(某个对象数据) 

 为什么操作系统要用字典树这个结构来管理我们的page呢??

——>因为文件的内容按照4KB是有偏移量的。(虽然我们的块是4KB大小,但是我们不一定是在开头去读取或写入,而操作系统通过字典树这种组合式的KV结构来帮助我们定位具体的位置)

5.5 串联进程管理、文件管理、内存管理

         首先我们创建了一个进程,创建了一个PCB结构体,然后还会顺便创建一张文件描述符表,我们调用C接口去打开写入文件的时候,该文件会在该文件描述符表中分配一个位置,然后创建一个file结构体,里面分配了inode,但是文件的内容会先写入在C层的缓冲区, 当满足刷新策略的时候,再通过一些方式将其刷新到我们的page中(内核缓冲区)。然后最后再刷新到磁盘中。所以这个过程数据被拷贝了3次

      当我们的进程将数据交给内存后,其实他就不管了,所以我们的操作系统必须关心内存要如何刷新到磁盘上,且可能同一时间有大量的IO请求,因此我们的操作系统也要关心先执行哪个请求。所以这就涉及到IO子系统(操作系统和驱动做交互的系统) 

  我们的IO请求会将相关的一些需求和配置写到一个request结构体里,然后再由操作系统做管理

——>因为我们的不同的IO请求可能是向不同的扇区写入,所以我们肯定希望比较接近的扇区在一起被写入,这样减少磁头定位能够提高整体效率,所以我们会用一个队列把request管理起来,然后设计一些IO排序和IO合并算法,来给IO请求整体提速!!

     

       在我们开机的时候,因为物理内存经常需要跟磁盘做交互,所以会提前把一些访问物理内存所需要的区域会被预先加载进去,尤其是文件系统的相关功能。

——>说明操作系统真的帮助我们做了很多事情!! 

 5.6 slab分配器和伙伴系统(扩展)

从我们创建进程开始,或者是申请文件,我们就会有各种各样的结构体不断地被创建和释放

      所以当我们的内核数据结构想要释放的时候,操作系统并不会直接释放,而是会把一些高频使用的内核数据结构体通过某些机制暂时缓存起来,当你下次需要的时候你就不需要跟内存模块做申请,而是直接把之前缓存里面的内核数据结构拿出来初始化即可!

slap分配器:Linux 内核 | 内存管理——slab 分配器 - 知乎 (zhihu.com) 

伙伴系统:一篇看懂!伙伴系统之伙伴系统概述--Linux内存管理 - 知乎 (zhihu.com)

 

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐