
自律是普通人最大的武器
| 系列专栏 | 直达链接 |
|---|---|
| 🐧 Linux | 进入专栏 → |
| 🔤 C语言 | 进入专栏 → |
| 🔧 C++ | 进入专栏 → |
| 🐬 MySQL | 进入专栏 → |
| 🧩 算法刷题 | 进入专栏 → |
🏠 博客主页:虚无的纽扣
📦 作者仓库:github | gitee
引言
前两讲我们聊的重定向和文件缓冲区,讲的都是已经打开的文件。那如果一个文件没有被打开呢?没被打开的文件在哪里存着呢?
它存在磁盘里。那么磁盘是如何把数以万计的文件进行归类摆放,让我们能快速找到的呢?操作系统中存在一个文件系统,它负责将文件以目录的形式组织起来,并通过路径帮我们快速找到所需要的文件。这就像图书馆:几百万册书按分类摆进一排排书架,每本书有自己的编号,顺着索引就能找到它
1、理解硬件
磁盘的物理结构

上图是一块机械硬盘拆开之后的样子,亮闪闪的圆盘叫盘片,数据就写在它上面。每有一个盘片就有两个盘面,每有一个盘面就要有一个磁头,整个硬盘只有一个传动臂,所有的磁头都在传动臂的带动下同时移动,任意时刻所有磁头都停在各盘面相同编号的磁道上,这些磁道在逻辑上共同构成的结构,我们就称为柱面
磁盘的存储结构
![![[Linux/attachments/磁盘的存储结构.png|盘面上的磁道与扇区划分]]](https://i-blog.csdnimg.cn/direct/ea08e9c46a07412f921dc6e486657ddf.png)
盘片旋转时,磁头在一个固定半径的圆上划过盘面,这一圈圈同心圆就叫磁道。每条磁道再被切成一段一段,每一段就是一个扇区,扇区是磁盘存储数据的基本单位,512 字节
扇区是读写磁盘的基本单位的意思是,哪怕只修改一个扇区内的一个字节,也必须把这 512 个字节全部加载到内存里,修改完后再写回扇区

柱面是个逻辑概念
柱面是一个逻辑上的概念,其实就是每一面上相同编号的磁道逻辑上构成柱面。所以磁盘物理上分了很多面,但是在我们看来,逻辑上磁盘整体是由柱面卷起来的
所以磁盘的真实情况是:某一个盘面的一个磁道展开,构成一个一维数组;将一个柱面展开,即这个柱面上所有磁道展开,就是一个二维数组;整个磁盘有多个柱面,将整个磁盘展开,就构成了一个三维数组
一个盘面的一条磁道拉直 -> 一维数组
一个柱面上所有磁道拉直 -> 二维数组
整个磁盘所有柱面叠起来 -> 三维数组
CHS 定址
当我们要想将内容写到某一个扇区中,磁盘该如何移动?
先是磁臂带动磁头移动,定位到对应的柱面,然后选择读取某一个磁头所在盘面的数据,然后让盘片旋转,让扇区旋转到对应的磁头下面,这种定位方案叫做 CHS 定址,也就是 Cylinder Head Sector
CHS 定址就是先找到对应柱面 C,再找到对应磁头 H,最后找到对应扇区 S,就构成 CHS 定址,实际上就是获取了三个下标

LBA
磁盘上的每一个磁道,如果将它们拉直,就可以看作是一个线性结构,所以在我们定位扇区的时候只需要一个数字就能定位到对应的扇区,就是数组下标,这种地址叫做 LBA
CHS:先定柱面 C,再定磁头 H,最后定扇区 S 三个下标
LBA:整个磁盘拉直成一条线,一个下标就够
[0] [1] [2] [3] [4] [5] [6] [7] ... 数组下标
所以操作系统对磁盘的管理就变成了对数组的管理
简单来说,CHS 地址是磁盘内部实际的存储地址,而 LBA 地址是操作系统将 CHS 整个抽象为一个超长的一维数组,在进行读取和写入时,磁盘就要将 CHS 地址和 LBA 地址进行转换
2、引入文件系统
块
硬盘是典型的“块设备”,“块设备”支持随机读取,与“块设备”相对应的是字符设备,我们的键盘就是一种字符设备,不支持随机读取,只支持顺序读取
操作系统读取磁盘数据的时候,其实是不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个块
一般情况下连续八个扇区为一个块,大小为 4 KB,块是文件存取的最小单位
一个块 = 连续 8 个扇区 = 4 KB(文件存取的最小单位)
扇区 [0] [1] [2] [3] [4] [5] [6] [7]
+-------------------------------+
| 1 个块 = 4 KB |
+-------------------------------+
分区
操作系统会将整个磁盘划分为若干区域,以 Windows 观点来看,我们的磁盘往往被分成 C 盘、D 盘等盘符,这其实不是我们真的有这么多块磁盘,而是系统将一块磁盘划分为了这么多分区,分区实质上说就是对硬盘的一种格式化。但是 Linux 的设备都是以文件的形式存在,那是怎么分区的呢?
柱面就是分区的最小单位,我们可以利用柱面的号码进行分区
在分区下面,Linux 还会将一个分区划分为多个组,最终操作系统对磁盘的管理就转化成了对每个小组的管理,这种思想叫做分治思想
inode
我们知道文件 = 内容 + 属性,在 Linux 系统里,内容和属性是分开存储的
Linux 中任何正常文件,都要有自己的属性集合,每一个文件的属性在内存当中就是一个结构体,又是结构体,描述文件属性的结构体叫做 inode,一个 inode 占 128 字节,一个数据块会保存 32 个 inode(顺带说一句,这个 32 是按 128 字节的 inode 算的,ext2、ext3 是这样的;ext4 默认把 inode 扩到了 256 字节,一个数据块里就只放得下 16 个)
文件名是不会做为属性被保存在 inode 中的,因为文件名的类型为字符串类型,数据大小会根据实际文件名长度发生变动,就无法保证每一个 inode 的大小一致。所以为了识别文件,inode 里还有一个 inode 编号表示文件
所以当文件系统与内存进行交互的时候,就会一次性将 32 个文件的属性全部读到内存里,如果两个文件的 inode 存储位置很近,就意味着它们两的创建时间很接近,往往会有联系,这样的读取方式可以让我们在读取相邻文件时效率更高
块组的几个区域

一个块组里的区域主要有这几块:
data blocks:里面有很多很多个单位为 4 KB 的小数据块,我们的文件内容会被存储在这里block bitmap:记录着data blocks中哪个数据块已经被占用,哪个数据块没有被占用inode bitmap:表示inode table中哪个位置被占用,哪个位置没有占用inode table:存放所有inode的表GDT:块组描述符,描述块组的属性信息super block:超级块,描述的是整个文件系统的文件信息
在我们对文件进行删除的时候,就不用真的将 inode 和 data blocks 里的信息删掉,只需要将对应的位图的比特位置空就行了
GDT,块组描述符,描述块组属性信息,整个分区分成多少个块组就有多少个块组描述符,每个块组描述符存储一个块组的描述信息,如在这个块组中哪里开始是 inode table,从哪里开始是 data blocks,空闲的 inode 和数据块还有多少个等等,块组描述符在每个块组的开头都有一份拷贝
super block,超级块,描述的是整个文件系统的文件信息,一个组里有多少块,有多少 inode,就是文件系统的基本信息。因为每个组的大小构成都是相同的,只要知道了一个组里有多少块,就知道了有多少个组,根据文件系统的整体大小算出
super block 一旦被破坏,整个文件系统就不可用了。不是每一个块组里都有 super block,并且每一个有 super block 的组里的 super block 都是完全一样的
block bitmap 就很像书架上“已借出”的卡片,翻一下就知道哪一格还空着;而 super block 就是图书馆的馆藏总目录,馆里有多少书、分几个区,全记在里面
格式化与编号
文件系统的载体是分区
格式化,就是将整个文件系统中所有的操作信息全部清零
inode 和数据块的编号都是跨组的,意味着整个分区里,无论在不在一个组,一个编号只能被用来标识一个文件
3、目录、路径解析与挂载
目录也是一个文件
可是现在有一个问题,我们平常在找文件的时候都是用的文件名来查找,从来没用过 inode 来查找,那为什么我们又说文件名不会做为属性被保存在 inode 中,那我们到底是怎么找到对应文件的呢?
Linux 下如何看待目录?目录也是按照刚刚所讲的方式进行保存的,目录也有自己的 inode 和数据,而目录中的数据就保存了文件名与对应文件 inode 的映射关系,所以文件的文件名就保存在当前文件所属目录的数据内容当中
所以这就是为什么我们之前说必须指出文件路径,文件才能进行执行,就是因为操作系统要先打开文件所在的路径,读取对应目录里面的数据内容,得到文件名与 inode 的映射关系,才能在磁盘上找到文件的 inode 和数据块,才能进行执行
而我们想打开对应的目录,也必须打开上级目录,所以找到任何 Linux 文件都必须从 / 目录开始进行路径解析,直到找到对应的文件
路径缓存
但是这样效率是不是有点太低了?我们上一次解析时已经解析过的路径,访问另一个文件时又要解析一次,是不是有点太浪费了?
操作系统也觉得这样效率有点太低了,所以操作系统在进行路径解析的时候,会把我们历史访问的所有目录路径形成一颗多叉树进行保存,就相当于将已经解析过的路径都缓存起来,以后如果再打开相关路径时,如果前面已经被载入,就直接搜索这颗多叉树的路径,如果有就直接使用,没有就从头开始加载并加入这颗多叉树中
整棵树形节点同时也会隶属于 LRU 结构中,进行节点淘汰
整棵树形节点也同时隶属于哈希表,方便快速查找
更重要的是,这个树形结构整体构成了 Linux 的路径缓存结构,打开访问任何文件,都先在这棵树下根据路径进行查找,找到就返回属性 inode 和内容,没找到就从磁盘加载路径,缓存新路径
挂载
现在有一个新的问题,我们该怎么知道我们的文件被存储在哪个分区里呢?inode 不是不能跨分区吗?
我们的分区在进行格式化后我们依旧不能直接使用这个分区,我们的分区一定是要和某个特定的目录进行关联,通过进入这个目录,就相当于进入这个分区,这个操作被称为挂载
每个分区一定是会被挂载在某个路径下的,而我们在创建文件的时候,这个文件的路径中肯定有一段路径是挂载了某一个分区的,所以我们创建文件时就会在这个分区里创建,在我们后续使用时也会根据文件路径确定挂载的分区是哪个,从而到对应的分区去找到文件的 inode
确定文件属于哪个分区采用就近原则,离文件最近的有挂载分区的路径的分区就是文件对应的分区
每个文件的 struct file 中都有一个对应的 dentry 指针,指向自己的 dentry 对象,dentry 里保存的是当前文件在目录树中的位置,还有 inode
task_struct(进程)
|
+-- files_struct
|
+-- fd 数组 ----------> struct file
|
+--> dentry --> inode --> 数据块
|
+-- 同时挂在 dentry 缓存树上
(多叉树 + LRU + 哈希表)
4、软硬连接
软链接
ln -s,软连接
软连接是一个独立的文件,因为它有独立的 inode,软连接保存了目标文件的路径
软连接就相当于是 Windows 下的快捷方式,相当于给被连接的文件新增了一个访问方式
硬链接
ln,硬连接
硬连接的 inode 与源文件的 inode 是一样的,所以硬连接本质不是一个独立的文件,因为它没有独立的 inode。硬连接本质是一组新的文件名和目标 inode 的映射关系,数字 2 表示有两个文件名指向目标文件,叫做硬连接数
硬连接有什么用?对文件进行备份!以后想要对文件进行备份不用再把文件拷贝到指定目录里了,只需要建立一个硬连接就行
为什么普通文件初始的硬连接数为 1,而目录文件的初始硬连接数是 2 呢?因为我们之前讲过,每一个目录里都有一个隐藏文件 . 指向目录本身,这个 . 文件本质就是当前目录的硬连接;如果一个目录里还有一个子目录,子目录里的 .. 文件指向上级路径,这个也是目录的硬连接
硬连接只能给普通文件建立,更准确地说,是 Linux 不允许用户给目录建立硬连接,. 和 .. 是系统自己建的,不在此列
为什么呢?如果让用户能自己建立硬连接,很容易形成路径环问题:A 指向 B,B 又指回 A,路径解析就会陷进死循环
结语
这一篇我们从磁盘的盘面、磁道一路讲到了 inode 和软硬连接,整条线可以拆成三件事:磁盘负责存,文件系统负责组织,目录负责记名字
以后我会继续创作 Linux 方向的博客,如果觉得有帮助的话,希望各位大佬能三联关注支持一下,看到后我会进行回关和回访
1896



被折叠的 条评论
为什么被折叠?



