文件体系--底层架构(图文详解)

打印 上一主题 下一主题

主题 827|帖子 827|积分 2481

一、文件体系的底层存储与寻址 

当我们谈到文件体系的底层结构时,最关键的题目是:文件的数据与元数据(属性)怎样存储在磁盘上,以及体系是怎样定位这些数据的?在谈及文件体系之前,我们要先对储存文件的硬件有一些根本相识
首先,我们来看一下传统磁盘的基础结构 

我们可以看到这种传统的机械磁盘的构成,那么它是怎样运转的呢,又是怎样进行文件里的内容存储的呢?
首先,计算机是不是只能识别二进制语言啊,而这个二进制语言说起来又有些广泛,有人说二进制不就是0和1吗? 0和1又是什么呢?实在啊,0和1也只是相对的一种概念,它也可以由我们自己更换,好比阴和阳?水和火?这些相对的概念也可以称之为0和1,因此在磁盘中,磁盘由于具有磁性,所以就代表他有南北极,我们就可以近似的把它的磁性之分代表对应的0和1,通过修改它的磁场,是不是就可以到达对“0”和“1”的修改呢?也就可以轻松完成数据的存储。
那具体它又是怎样通过以上图片里的这些电子元件完成我们的目标的呢?以下是它的工作步调:

有同学看到这里就大概没太看懂,那我们来总结以下,大抵如下:

磁盘呢实在是一个里面结构,它每个面都是光滑的,都是可读可写的,我们家用的从前的DVD,只是单面且只读的,也就是在盘片高速旋转的过程中,磁头通过左右摇摆和前后移动来到达对应的位置然后改变它下面盘片的磁性,从而到达对数据进行更改存储,那么题目来了,它怎么知道我要改的地域是哪里,这么大一个盘片,怎么能精确的我所要更改的数据存储在哪个地域呢,因此我们来细致的讲解以下盘片的地域划分:

这里我们就要提到一些概念:
首先是磁道 ,一个大的盘面,可以由若干个同心圆由内向外扩散,这每一个圆圈都叫做一个磁道,而这个磁道由圆心开始﹐在同一表面上分别画出无数条半径﹐然後每两条半径所分割的磁轨﹐我们称为磁区(Sector),也就是扇区,所以终极经过层层确认,每个扇区内的那一小块就是我们要寻找的地方。
因此,传统磁盘内怎样找到一个指定位置的扇区?
也就是我们的“CHS定址法
a. 首先确定我们在第几个磁头(Header),也就是第几个盘片
b.找到磁头后确定对应的磁道(Cylinder)
c.找到磁道后确认在哪个扇区(Sector)
通过以上步调我们就可以找到文件所对应在磁盘的准确位置了,是不是很神奇,这是不是也意味着
文件的巨细归根结底就是占用多少个扇区的地方嘛!
可如今随着科技的发展,我们所接纳的已经不是这种很老的磁盘了,一般企业会接纳这种,由于它不会挪动,拿来做服务器刚刚好,一旦做成便携式笔记本,磁头和盘片一打仗直接就会造成不可避免的数据损失,我们如今所接纳的是固态硬盘(SSD),无需运动,而且有更高的效率。
那么话归正传,既然我们已经知道怎样在磁盘里寻找对应的文件地址了,是不是意味着每次我都要进行计算啊,这完全依赖于硬件配置啊,难道我作为一家企业要为每一个差别磁盘规格都编译一份操作体系吗,这肯定不可,因此我们要封装一下这个步调,让我们的操作体系不需要去依赖硬件才能找到地址,那我们的操作体系是怎样做的呢?
之前我们是不是说过,机械磁盘里盘面实在是链接在一起的,各人小学时应该都见过这个东西

它内部实在就是雷同两个盘片,中间用线将他们连接起来,反复播放,实在我们的磁盘也同理,n个盘片是不是也可以以线性的方式将他们连接到一起啊!
这是不是相当于将他们都整合成了一个近似的数组啊,这个数组里存放的是不是就是对应的扇区啊!
 也就是说我只需要存放对应位置的下标,然后进行一些列的计算就可以转换成机械硬盘所需要的CHS地址啊,而我操作体系内部就可以按照计算方式来进行管理,是不是很方便
假设我们当前一共有1000个扇区,10个磁道,那么此时我们的对应下标是500,那么如今我们要计算出在第几个磁头,然后转化成当前盘面的对应扇区下标,然后计算出在哪个磁道后计算出对应扇区,步调如下

通过如许的计算我们就可以成功找到对应的位置了
但是我们可以思索一下,一个扇区一个扇区的计算,对于操作体系的消耗是不是太繁琐了啊,由于扇区的单位实在是太小了,每次都要去计算出这个很小的精确位置十分消耗资源,因此操作体系在于磁盘交互的时候其根本单位是4kb,也就是八个扇区的巨细。
操作体系和文件体系通常利用“块(block)”作为根本分配和访问单位。一个块大概由若干扇区构成(比方4KB=8个512字节扇区)。文件体系通过块号(Block Number)来定位文件数据,从而屏蔽底层扇区的复杂定位。假设有N个连续的块可以利用,则文件体系在逻辑上将磁盘空间看成一个块数组(block0, block1, block2, ...)。通过块号,我们可以快速找到对应的扇区,进而从磁盘中读写数据。 如图所示

所以也意味着我们只需要一个起始地址,磁盘的总巨细,就可以知道磁盘每个单位的下标,然后通过计算就可以取到对应的CHS地址!
所以也就是今世方法“逻辑区块地址(Logical Block Address, LBA)”
LBA是非常单纯的一种定址模式﹔从0开始编号来定位区块,第一区块LBA=0,第二区块LBA=1,依此类推。这种定址模式取代了原先操作体系必须面临存储设备硬件构造的方式。最具代表性的首推CHS(cylinders-heads-sectors,磁柱-磁头-扇区)定址模式,区块必须以硬盘上某个磁柱、磁头、扇区的硬件位置所合成的地址来指定。CHS模式对硬盘以外的设备来说没什么作用(比方磁带或是网络存储设备),所以通常也不会用在这些地方。已往MFM(Modified Frequency Modulation, 改良调频式)和RLL(Run Length Limited)存储设备都曾利用CHS模式,ATA-1设备更将延伸CHS(Extended Cylinders-Heads-Sectors, ECHS)也派上了用场。
那仅仅到这里就完成我们文件体系的管理了吗?
我们的整个磁盘巨细但是有800GB啊,难道每次就对这800GB的磁盘巨细进行挨个查询吗,这显然也是不现实的,因此我们的操作体系接纳了“分治头脑
要管理好我们的磁盘,实在内存是很大的,这个时候我们接纳的就是分区,800GB分成若干个地域,有管理好200GB的机制,就可以以此类推管理好剩余地域,分区下面再分组,就完成了文件体系的管理
大型磁盘每每被分割为多个分区(Partition),每个分区作为独立的逻辑存储地域,安装(mount)为独立的文件体系。比方,一个1TB磁盘可以分成200GB、200GB、300GB、100GB等多个分区。如许做的目标是“分而治之”,简化管理和提升灵活性。
在类UNIX文件体系(如ext系列)中,分区内部还分成块组(block group)。块组内包含inode表、块位图、inode位图以及数据块地域。通过将文件体系分块组管理,可以提升文件体系的本地性和查找效率。

   如图所示,Linux ext2  文件体系,上图为磁盘文件体系图(内核内存映像肯定有所差别),磁盘是典型的块设备,硬盘分区被划分为一个个的block  。一个  block  的巨细是由格式化的时候确定的,而且不可以更改。比方  mke2fs  的  -b  选项可以设定block  巨细为  1024  、  2048  或  4096  字节。而上图中启动块(  Boot Block  )的巨细是确定的  那我们每个分组内的这些信息都代表什么呢?


  • Block Group:ext2文件体系会根据分区的巨细划分为数个Block Group。而每个Block Group都有着相同的结构构成。当局管理各区的例子
  • 超等块(Super Block):存放文件体系本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未利用的block和inode的数目,一个block和inode的巨细,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件体系的相关信息。Super Block的信息被破坏,可以说整个文件体系结构就被破坏了
  • GDT,Group Descriptor Table:块组描述符,描述块组属性信息
  • 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
  • inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
  • inode表:存放文件属性 如 文件巨细,全部者,最近修改时间等
  • 数据区:存放文件内容
 我们之前说过Linux内文件是由什么构成的?
文件 = 内容 + 属性 ,那么文件在磁盘中的存储就是文件的内容+文件的属性数据,本质上都是数据,存储在磁盘里,一个正常的文件,就要有他的对应的属性啊,这个属性就是inode,也就是一个正常文件就要有对应的inode属性聚集。
那么先说我们的inode,
inode表是存放文件元数据(属性)的地域。这里的inode是一个结构体,记录了文件的:


  • 文件巨细(以字节为单位)
  • 文件全部者和所属组ID
  • 文件权限和类型(平凡文件、目录、符号链接等)
  • 文件的时间戳(访问、修改、变革时间)
  • 指向文件数据块的索引指针(直接、间接、双重间接、三重间接指针)等信息
需要注意的是,在Linux的ext2/3/4文件体系中,inode并不存放文件名。文件名存储在目录的data block中,而inode指针是通过目录项(directory entry)将文件名映射到inode号(number)上。
inode Bitmap(inode位图)

inode位图(inode Bitmap)与块位图雷同,只不外它是标记inode的利用情况。


  • 1表示对应的inode已被利用(对应某个文件或目录)
  • 0表示该inode空闲可用
当需要创建新文件时,文件体系会在inode位图中查找空闲inode(即bit为0的位置),然后分配给新文件,并将该位清为1表示已占用。
Block Bitmap(块位图)

块位图(Block Bitmap)是用于记录本块组中哪些数据块已经被分配,哪些还空闲的位图数据结构。每一位(bit)对应一个block:


  • 1位代表该block已被占用
  • 0位代表该block空闲可用
当文件需要分配新的数据块时,文件体系会在对应块组的Block Bitmap中找到一个置0的bit,将其置1,并分配给文件。由于是位图结构,查找和修改都很高效,只需简单的位操作即可。
数据区(Data Area)

其次是我们的Data Blocks,这就是我们的数据区,也就是存放文件的内容的地方,里面的一个个根本单位就是4KB,实在就是我们磁盘最开始一个个小扇区单元块,完全同等。实际上我们在寻找文件时必须用inode号来寻找,inode是以分区为单位分配的而不是以分组的形式分配的,每个组只需要记录自己的起始inode和竣事inode即可,也就是在上层确定一个inode后直接在下面的分组确定自己在哪个分组里就行,但是题目是我们每个分组里不都是inode要从0开始吗,这也怎么区分呢?实在可以用inode号减去当前分组的起始inode,剩下的数字就可以进去bitinode检察是否被占用和后续利用了.
超等块(Super Block)

超等块(Super Block)是整个文件体系的“全局信息表”,其中记录了文件体系的关键信息,包括但不限于:


  • 文件体系总的block数目和可用block数目
  • 文件体系总的inode数目和可用inode数目
  • 每个block的巨细、每个inode的巨细
  • 文件体系创建时间、最近挂载/写入/查抄时间
  • 文件体系的特性标志(比方是否有日记扩展)
  • 查抄隔断(每隔多少次挂载或多少时间后需要进行fsck)等信息
在ext2中,超等块不但在分区开头的特定位置存储,还会在每个Block Group中存放一份副本(除非格式化时指定了不存放备份)。这些副本作为冗余信息存在,一旦主超等块破坏,可以从块组的副本中恢复文件体系的结构信息。
假如超等块信息被全部破坏,那么文件体系的结构信息就无法找回,整个文件体系相当于“失去了地图”。
但是到这里为止,各人有没有想过,inode编号的存在我们已经掌握了,但是你真正对文件进行操作的时候什么时候用inode号了,是不是都说以文件名的方式来进行操作啊,跟inode有什么关系呢?
那么到了这里我们就不得不谈一个相关的概念了:目录
我们说Linux里是不是一切皆文件啊,我要组织这些文件,那我目录是不是就担当这个角色呢,所以目录也有自己的内容+属性,他的属性是不是和inode差不多,内容呢,我们之前说过inode里不存文件名,实在文件名是存放到目录里的,我们通过目录里文件名对应的inode来进行寻找文件,实在是文件名和inode建立了一层映射关系,让我们可以通过目录直接分配对应的Inode来进行后续的管理啊,所以我们可以引申出一些结论
一个目录下不能建立同名文件的关系是什么,是不是无法区分对应的是哪个inode号啊,
而查文件的本质就是 文件名-> inode号
所以对一个文件没权限写的本质是什么,是不是没有访问inode的权利啊,找不到映射关系怎么写入呢?
所以我们每次访问是不是都要从目录里进行访问,那么我们的目录不也是文件吗?
目录名不也是一个inode吗,他也有对应的数据块啊,所以实际上我们在访问一个路径下的文件时,实在是对路径进行一个逆向剖析,先找到根目录,然后通过根目录的inode找到下一个目录的Inode,紧接着一步步在inode里找inode,最后找到文件的inode就可以进行操作了,所以根目录的inode一定是刚打开操作体系的时候就已经确定好了,
这些操作都是操作体系自己做的
这就是为什么我们在打开文件时都必须要有路径!
一个文件访问前都是先访问目录,这个目录就已经确定好分区了
所以目录是谁提供的呢,进程在启动时就已经携带了,都是由操作体系提供的
为了提高效率,Linux内核有dentry(directory entry)缓存,用于缓存最近剖析的路径信息。如许在多次访问同一路径时,无需重复进行层层目录查找,能显著减少体系开销。
这些组件是怎样协同工作的?


  • 查找文件时的步调
    当我们通过路径名(如/home/user/file.txt)访问文件时,操作体系会从根目录开始:

    • 根目录的inode号通常是固定的(好比2号)。内核根据根目录的inode从inode表中加载该inode信息(通过超块和GDT可以快速找到inode表位置)。
    • 根据该inode的块指针找到存放该目录内容的data block,读取其中的目录项(文件名与inode号的映射)。
    • 找到home目录名对应的inode号,接着打开home目录的inode,同理找到user目录inode号,再找到file.txt的inode号。
    • 一旦确定了file.txt的inode号,就通过inode表读取file.txt的inode信息,从而得到该文件的数据块指针,终极从数据区中读取该文件的内容。

  • 分配新文件时的步调
    当新建一个文件时,文件体系需要:

    • 从inode位图中找到一个空闲inode号,分配给新文件,并在inode表中初始化文件的属性信息。
    • 假如文件需要写入数据,为文件分配数据块。即在块位图中找到一个空闲块,将其标记为已占用,并写入文件数据内容到该块中。
    • 将新文件的名称与其inode号写入父目录的数据区(即父目录文件的data block中),使得路径剖析时能精确找到它。

  • 在内核内存映射中的对应关系
    在内核中,超等块信息会被读入内存的struct super_block,GDT会读入struct group_desc结构数组中,inode表的信息会被内核通过struct inode对象描述,目录项会通过struct dentry缓存,数据块会映射到页缓存中(page cache)以减少频仍磁盘I/O。
    虽然内存中结构体与磁盘上的数据结构不完全相同,但逻辑对应关系保持同等,内核通过特定的读写操作和缓冲机制(buffer head、page cache)来将磁盘数据结构映射和加载到内存中,进行缓存和同步。
下面将通过一个较为完整、详细的流程来描述当我们在用户步伐中利用 fwrite() 向一个新建文件中写入数据时,在底层发生了什么。从用户态的C标准库调用开始,一直到数据终极被写入到磁盘文件体系的实际块中,以及新文件创建过程中的元数据分配与更新流程。
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main() {
  4.     FILE *fp = fopen("newfile.txt", "w");
  5.     if (fp == NULL) {
  6.         perror("fopen");
  7.         return 1;
  8.     }
  9.     const char *data = "Hello, fwrite!\n";
  10.     size_t wrote = fwrite(data, 1, sizeof("Hello, fwrite!\n") - 1, fp);
  11.     if (wrote < sizeof("Hello, fwrite!\n") - 1) {
  12.         perror("fwrite");
  13.     }
  14.     fclose(fp);
  15.     return 0;
  16. }
复制代码
实行过程分解

1. 文件创建(fopen阶段)

当你实行 fopen("newfile.txt", "w") 时,底层流程大抵如下:

  • 用户态C库层面
    fopen()是C标准库函数,它并不直接完成文件创建,而是调用体系调用(如open())完成底层操作。此时:

    • fopen()会根据模式"w"决定调用open("newfile.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666)体系调用(精确标志大概略有差异)。
    • 若文件不存在,传入的 O_CREAT 标志会使内核实验新建文件。

  • 内核VFS层与文件体系层面
    内核吸收到open()体系调用后,会进行路径剖析:

    • 从根目录开始查找newfile.txt所在目录(好比当前工作目录),通过目录项(dentry)和inode查找目标目录的inode。
    • 找到该目录的inode后,内核在该目录的数据块中搜刮newfile.txt的目录项。假如文件不存在,内核就要创建一个新的inode来表示新文件。

  • inode与block分配(新文件诞生)
    在ext2文件体系中,新文件创建需要分配一个空闲的inode:

    • 内核通过对应块组的inode位图(inode bitmap)找到一个空闲的inode号,将其标记为已用。
    • 在inode表(inode table)中初始化该inode结构:设置文件类型为平凡文件、初始巨细为0、时间戳、权限(0666受umask影响),链接计数等。
    • 不需要立刻分配数据块,空文件初始巨细为0;数据块将在后续写入时分配(耽误分配计谋在ext4中更显着,在ext2中则大概立刻分配第一个块)。
    同时,内核需要在目标目录的数据块中添加一个新的目录项:
       

    • 目录的数据块是一个文件名到inode号的映射。
    • 内核找到该目录文件的数据块,在合适位置插入"newfile.txt"对应的目录项(包含文件名和新分配的inode号)。
    目录项更新后,newfile.txt就被记录在文件体系中,而且它有自己的inode,但文件内容还为空。

  • 返回文件描述符与FILE指针
    内核为此新文件分配一个file对象(struct file),并在进程的文件描述符表中加入一条指向该file对象的记录。open()体系调用返回一个整数文件描述符(fd)。
    C库的fopen()吸收到fd后,会为其分配一个FILE *结构,设置用户态缓冲区并返回给用户。至此,fp指针就有效地指向了一个新创建的空文件。
2. 写入数据(fwrite阶段)

当实行 fwrite(data, 1, len, fp) 时,发生了如下过程:

  • 用户态C库缓冲
    fwrite()首先将要写入的数据拷贝到fp所管理的用户态输出缓冲区(这是C标准库提供的缓冲区)。此时并未发生真正的内核写入。
    fwrite()的返回值表示成功写入用户态缓冲区的字节数。
    假如用户调用fflush(fp)大概缓冲区满、文件关闭(fclose)大概步伐竣事后,才会实际调用write()体系调用将数据从用户态缓冲写入内核态。
    若利用的是行缓冲或全缓冲,fwrite写入的数据大概先暂存在用户态,不立刻刷新到内核。
  • 刷入内核(write体系调用)
    当fclose(fp)或缓冲计谋触发刷新时,C库将调用write(fd, buffer, length)体系调用。
    内核处理:

    • 内核吸收write()体系调用,利用文件描述符找到对应的struct file对象。
    • struct file通过f_inode成员可找到对应的inode。
    • 文件体系(ext2)查抄该文件inode。假如当前文件巨细为0,还没有分配数据块,则需要为该文件分配数据块:

      • 内核通过Block Bitmap查找一个空闲数据块,将其标记为已用。
      • 在inode结构中更新i_block[]或间接块指针信息,指向分配的新数据块。

    • 将用户传递的写入数据(通过copy_from_user)拷贝到内核页缓存(page cache)对应的数据页中。页缓存是内核中缓存文件内容的地域,以减少频仍的磁盘IO。
    • inode的巨细被更新(增加写入的字节数),时间戳(mtime、ctime)更新。
    • 此时数据仍在内核页缓存中,还没有真正写入磁盘。

  • 缓存与耽误写回
    数据先写入内核的页缓存,这提高了性能,由于内核可以合并多次写入,将其在合适机会一次性写入磁盘。
    写回由内核的同步机制触发(pdflush或kworker线程)或在特定条件下(同步文件体系、文件关闭、缓冲区满、调用fsync())实行。
3. 文件体系同步与落盘

当内核需要将数据实际写入磁盘(大概是耽误一段时间后):

  • 块映射与缓冲区写入
    内核知道文件所在的块组、inode表和数据块位置。需要将数据缓存页中的内容写入对应的磁盘块:

    • 利用块设备驱动,将页缓存中的数据发出I/O哀求(通过I/O调度器)传给磁盘控制器。
    • 磁盘控制器将相应的扇区读写操作发给硬盘。

  • 元数据同步
    为了包管同等性,除了数据块本身外,对inode表、Block Bitmap、inode Bitmap的更改也需写回磁盘。比方:

    • 分配新inode和数据块时修改的位图需要同步到磁盘对应的块。
    • 修改inode表中的文件巨细和时间戳信息也会写回。
    在ext2文件体系中,这些元数据的更新没有日记,因此假如体系在更新中瓦解,大概需要fsck来恢复同等性。

4. 文件关闭(fclose阶段)

当你调用 fclose(fp):


  • fclose()会先调用fflush()将用户态缓冲中剩余数据写入内核。
  • 然后调用close()体系调用关闭文件描述符。
  • 内核将减少file对象的引用计数,假如是最后一个关闭该文件的进程,则file对象会被释放。
  • inode和dentry缓存保存一定时间,以便下次访问文件时加快速度,但终极也会被回收。
此时,从应用步伐的角度看,文件已经成功写入并关闭。数据终极会在一定时间内被写回磁盘(假如还未写回),从而持久地存储在分区对应的data block中。
总结底层过程


  • 路径剖析与inode分配:fopen与open体系调用通过内核的VFS层、dentry、inode找到目录并创建新文件的inode,分配其inode号,并在目录数据块中新建文件名->inode号的目录项。
  • 用户态I/O缓冲:fwrite仅将数据写入用户态缓冲区,不立刻到内核。当实行fflush或fclose或缓冲计谋触发时,才调用write()体系调用。
  • 内核缓冲(页缓存)与块分配:内核write()将数据写入内核的页缓存中。如需为文件增加空间,会通过Block Bitmap找到空闲块,将其分配给该文件的inode,并更新inode表。inode巨细、时间戳更新。这些修改还在内核内存中。
  • 耽误写回与磁盘同步:内核稍后会将页缓存数据块、更新后的位图块、inode表块写入磁盘中对应的扇区。假如体系调用fsync()或定期同步进程(比方sync)实行,就会逼迫立刻落盘。
  • 文件关闭:fclose()终极导致close()体系调用,内核释放文件描述符和相关数据结构引用。
通过上述过程,从调用fwrite()往新建文件中写入数据,到文件数据真正写入磁盘,中间经过了用户态缓冲—>内核态缓冲—>磁盘的多层抽象,期间涉及路径剖析、inode与block分配、目录项更新、位图修改和inode表修改等关键步调。这就是利用 fwrite 和新建文件这两个实行在底层全面展开的实际流程。
 
总体来说,这些元数据结构(超等块、GDT、位图、inode表)与数据区紧密协作,形成了ext2文件体系的“管理层”(元数据)与“内容层”(数据区),借助块组的分条理管理和位图的高效查找,使得文件体系在底层能够有效组织和管理海量文件与数据,包管文件读写、创建、删除和寻址的高效与可靠。 
 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

灌篮少年

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表