前言
在上篇文章基础IO中,我们了解了文件操作以及被打开的文件在操作系统中是如何管理起来的,那没有被打开的文件呢?
如此多的文件是如何存储的呢?
了解硬件
1. 磁盘
想必我们多多少少都听到过磁盘,服务器机柜和机房这些名词,那它们是干什么的呢?
很简单,我们被打开的文件在操作系统中被管理起来,而没有被打开的文件是存储在磁盘当中的。
- 机械磁盘,是计算机中唯一的机械设备
- 磁盘是外设
- 速度慢,容量大,价格便宜。
简单来说,磁盘就是用来存储数据的设备,是计算机的存储介质。
2. 磁盘的物理结构
我们知道了磁盘是计算机的存储介质,磁盘是外设,也是计算机中唯一的机械设备。
那磁盘当中有什么呢?

如上图所示,磁盘当中存在盘片、磁头、磁头臂等等
- 磁盘的盘片它有两面,每一面都类似于光盘;在盘面上存储着数据。
- 磁头:相当于笔;磁头和盘面虽然没有直接接触,但两者之间存在马达;(一旦盘片通电,盘片告诉旋转,磁头摆动,马达可以控制磁头摆动,控制盘片旋转)磁头通过感应磁场的变化读取数据,改变磁场的方向来写入数据。
在磁盘当中存在对应的硬件电路,通过硬件电路 + 系统,这样我们可以给磁盘方式二进制指令,让磁盘定位寻址到某一个盘面的某一个区间。
3. 磁盘的存储结构
简单了解了磁盘的物理结构,那磁盘是如何存储数据的呢?

可以看到,一个盘面上有多个同心圆,而每一个圆上存在着许多个片段,这些片段被称为扇区。
- 磁盘在寻址的时候,是以扇区为基本单位的;(不是
bit
也不是byte
) - 每一个扇区的大小是
512
字节,外磁道扇区和内磁道扇区是一样的,只是密度不同,大小都是512
字节。
当然磁盘也不是只存在一个盘面,而是有多个盘面;那也就存在多个磁头,每一个磁头对应一个盘面。
而我们磁头的运动也不是随机运动的,而是多个磁头同时移动。

我们知道扇区是从磁盘读取和写入的最小单位,大小通常为
512
字节那我们磁盘的容量该如何去计算呢?
- 磁头数:一个盘片一般有上下两面,一个盘面对应一个磁头;一个盘片就对应2个磁头;
- 磁道:磁道是从盘片外圈向内圈编号
0,1,2...
,靠近主轴的同心圆用来停靠磁头,不存储数据;- 柱面:磁盘中存在多个磁头,这些磁头都位于同一个磁道(共进退);此时磁道就形成了柱面。
- 扇区数:每一个磁道都被分为多个扇区,每一个磁道中的扇区是相同的。
- 圆盘数:就是磁盘的盘片数量。
磁盘容量 = 磁头数 * 磁道(柱面)数 * 扇区数 * 每一个扇区的大小。
那磁盘存在多个磁头(一个磁头对应一个盘面),每一个盘面上存在多个磁道,而每一个磁道上又存在多个扇区;那如何去确定一个扇区呢?
很显然,我们想要确定一个扇区,那我们就要知道这个扇区它是位于哪一个(柱面)的、这个扇区它是位于这个柱面中哪一个盘面的、以及这个扇区是位于这个盘面中的哪一个扇区。这样我们才能确定一个扇区。
所以我们只需要知道扇区位于哪一个柱面(
cylinder
)、哪一个磁头(head
)、哪一个扇区(sector
)就可以确定定位数据了。这种数据定位寻址方式也就是:
CHS
寻址方式。
4. 磁盘的逻辑结构
我们知道了磁盘的存储结构,那它和操作系统如何关联起来的呢?
我们在磁盘中,直到了扇区的柱面、磁头、扇区就可以确定这个扇区的位置;
而在操作系统的角度,它认为磁盘是盘面是一个线性的结构(就像二维数组那样,是线性的)
这样整个磁道就相当一个一维数组;多个磁道就构成了一个柱面,而整个柱面就相当于一个二维数组;多个柱面就构成了次磁盘的结构,所以整个磁盘在操作系统看来就是一个三维数组。
磁道
某一个磁道:

本质上就相当于一个一维数组。
柱面
多个磁道就构成了一个柱面:

本质上一个柱面就相当于一个二维数组。
磁盘
而多个柱面就构成了整个磁盘:

本质上就相当于三维数组。
所以,我们就可以将整个磁盘结构理解为一个三维数组。
在磁盘中我们寻找一个扇区要确定柱面、磁道、扇区;也就是CHS
地址。
现在我们要确定一个扇区,每一个扇区都存在数组下标,我们称之为LBA
地址。(就是线性地址)
所以操作系统只需要知道扇区的
LBA
地址,即可。而
LBA
转化为CHS
地址由磁盘自己来做(固件(硬件电路、伺服系统))。
5. CHS
地址和LBA
地址
CHS
转化为LBA
- 单个柱面的扇区总数 = 磁头数 * 每个磁道的扇区数
LBS
= 柱面号C
* 单个柱面的扇区总数 + 磁头号H
* 每个磁道的扇区总数 + 扇区号S
- 1
通常情况下,扇区号是从1
开始的,而LBA
中地址是从0
开始的。
柱面个磁道都是从0
开始编号的。
总柱面、磁道个数、扇区总数等,在磁盘内部会自动维护;在上层开机时,会自动获取这些参数。
LBA
转化为CHS
简单来说就是通过//
(取整操作)和%
操作。
- 柱面号
C
=LBA
//单个柱面扇区总数(磁头数 * 每个磁道的扇区总数) - 磁头号
H
= (LBA
%单个柱面扇区总数)// 每个磁道的扇区总数 - 扇区号
S
=LBA
% 每个磁道扇区数 + 1
所以在磁盘使用者看来,不需要关系什么CHS
地址,只需要使用LBA
地址,在磁盘内容就会自动转化成对应的CHS
地址。
文件系统
1. 块
硬盘就是一个典型的块设备,操作系统在读取数据时不会按照一个个扇区去读取,这样效率很低;而是一次连续读取多个扇,即一读取一个块。
而硬盘的每一个分区是被划分成一个个的块;每一个块的大小是格式化时确定的,并不可以修改,常见的块大小是4
KB也就是八个扇区。
块是文件读取的最小单位
在操作系统中,我们可以使用stat 文件名
指令查看相关信息

这里,知道了
LBA
地址,我们可以计算出哪一个块(块号 = LBA/8
);知道块号也可以计算出
LBA
地址(LBA = 块号*8 + n
)(n
表示块内第几个扇区)。
2. 分区
我们知道一个硬盘,我们是可以分成多个区的,就比如Windows
下C、D、E
盘就是进行分区操作。在Linux
下设备都是以文件形式存在,那如何理解分区呢?
**柱面是分区的最小单位:**我们可以利用参考柱面号码的方式进行分区,本质上就设置每个区的起始柱面号和结束柱面号。

3. inode
我们知道文件 = 文件内容 + 文件属性,我们使用ls -l
指令也可以查看这些属性(我们也可以使用stat
指令查看文件的更多属性

这些属性有:模式、文件权限、硬链接数、文件所有者、所有组、文件大小、最后修改时间、文件名等。

这里第一列属性表示的就是文件的inode
编号
那这些属性从哪里来呢?很显然是从磁盘中来。
那我们知道,文件内容存储在块中;很显然我们也要将文件属性存储起来;所以存储文件属性的区域就叫做inode
,
简单来说inode
中存储了文件所有属性。(注意:文件名不会作为属性存储在inode
中)
那么也就是说Linux
操作系统下,文件内容和文件属性是分开存储的;那在inode
中就必然存储了文件内容存放的位置。
c
struct ext2_inode {
__le16 i_mode; /* File mode */
__le16 i_uid; /* Low 16 bits of Owner Uid */
__le32 i size; _ /* Size in bytes */
_ /* Access time */
__le32 i
__le32 i atime;
_ctime; /* Creation time */
__le32 i_mtime; /* Modification time */
__le32 i_dtime; /* Deletion Time */
__le16 i_gid; /* Low 16 bits of Group Id */
__le16 i_links_count; /* Links count */
__le32 i_blocks; /* Blocks count */
__le32 i_flags; /* File flags */
union {
struct {
__le32 l_i_reserved1;
} linux1;
struct {
__le32 h_i_translator;
} hurd1;
struct {
__le32 m_i_reserved1;
} masix1;
} osd1; /* OS dependent 1 */
__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
__le32 i_generation; /* File version (for NFS) */
__le32 i_file_acl; /* File ACL */
__le32 i_dir_acl; /* Directory ACL */
__le32 i_faddr; /* Fragment address */
union {
struct {
__u8 l_i_frag; /* Fragment number */
__u8 l_i_fsize; /* Fragment size */
__u16 i_pad1;
__le16 l_i_uid_high; /* these 2 fields */
__le16 l_i_gid_high; /* were reserved2[0] */
__u32 l_i_reserved2;
} linux2;
struct {
__u8 h_i_frag; /* Fragment number */
__u8 h_i_fsize; /* Fragment size */
__le16 h_i_mode_high;
__le16 h_i_uid_high;
__le16 h_i_gid_high;
__le32 h_i_author;
} hurd2;
struct {
__u8 m_i_frag; /* Fragment number */
__u8 m_i_fsize; /* Fragment size */
__u16 m_pad1;
__u32 m_i_reserved2[2];
} masix2;
} osd2; /* OS dependent 2 */
};
#define EXT2_NDIR_BLOCKS 12
#define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS
#define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1)
#define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1)
#define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1)
仔细观察我们会发现,在inode
中i_block
数组大小为15
啊,那也就是说,我们一个文件大小最多只有15
个块吗?也不对啊
这里留下一个疑问,在后序ext2
文件系统中再探讨inode
和datablock
的映射关系。
ext2
文件系统
我们想要在硬盘是存储文件,就必须把硬盘格式化成某种格式的文件系统,才能进行存储。而文件系统它的作用就是管理硬盘中的文件。
在Linux
操作系统中,常见的就是ext
系统的文件系统,早期版本ext2
,后来又存在了ext3
和ext4
等;
现在我们来了解ext2
文件系统:
1. 了解ext2文件系统
ext2
文件系统将整个分区划分成若干个同样大小的块组Block Group
,如下图;这样只要管理一个分区就可以管理所有的分区,也就可以管理所有的磁盘文件了。

上图中的Boot Sector
(启动块)的大小是确定的,1
KB,由PC规定,用来存储磁盘分区信息和启动新的,任何文件系统都不能修改Boot Sector
。
启动块之后才是ext2
文件系统的开始。
2. Block Group
ext2
文件系统将根据分区大小划分成数个Block Group
(块组);
这样管理好每一个块组就可以管理好整个分区。
3. 块组内部组成
通过上图我们可以发现,块组内容有非常多的内容,那这些内容都是什么呢?
Data Block
数据块:存放文件内容,也就是一个一个的Block
。
- 对于普通文件,文件的数据存储在内存块中。
- 对于目录,该目录下的所有文件名和目录名存储在该目录的数据块中。
Block
号按照分区划分,不可夸分区
Inode Table
节点表;我们知道文件内容和文件属性是分开存储的;
文件内容存储在Data Block中,而文件属性就存放在这里的Inode Table(inode表)中。
- Inode Table中存储文化的属性(文件大小,所以这,最近修改时间,权限等等)
- Inode Table中存储的是当前块组中是所有文件的
Inode
属性 Inode
编号以分区问单位,整体划分,不可跨分区
也就是说,在一个(分区)文件系统中,我们只需要知道inode
编号就可以找到该文件了。
Block Bitmap
Block Bitmap也就是块位图;
当我们进行写入数据时,我们是如何知道当前分区当前块有没有位置写入呢?总不能遍历一次Inode Table
吧?
很显然不是,在每一个块组中都存在一个Block Bitmap
块位图,其中记录着Data Block
中哪一个数据块已经被占用了,哪一个数据块没有被占用。
Inode Table
Inode Table
也就是inode
位图;
其中记录了当前块组中,Inode Table
的使用情况。
那这样我们就可以理解:当我们下载数据(电影/游戏),它下载的非常慢;而当我们删除数据时,我们会发现删除的特别快;
所以数据真的被删除了吗?
很显然没有,在删除数据时,我们只需要修改
Block Bitmap
和Inode Bitmap
即可,并不需要将数据删除掉。(这也是误删数据时,可以恢复的原因;如果真的将数据删除(inode和数据块)删除是无法修复的)
GDT
Group Descriptor Table块组描述符表;
其中记录了块组的属性信息,一个块组对应一个块组描述符表。
在块组描述符表(GPT
)中记录了描述当前块组的所有信息;比如:从哪里开始是inode Table
,从哪里开始是Data Block
,当前有多少inode
和数据块还没有被占用等等。
cpp
struct ext2_group_desc
{
__le32 bg_block_bitmap; /* Blocks bitmap block */
__le32 bg_inode_bitmap; /* Inodes bitmap */
__le32 bg_inode_table; /* Inodes table block*/
__le16 bg_free_blocks_count; /* Free blocks count */
__le16 bg_free_inodes_count; /* Free inodes count */
__le16 bg_used_dirs_count; /* Directories count */
__le16 bg_pad;
__le32 bg_reserved[3];
};
Super Block
Super Block
超级块,其中存储的是当前整个分区的文件系统信息。
记录信息有:block 和 inode 的总量,未使用的block和inode数量,一个block和inode的大小,最近异常挂载时间等等。
当Super Block中的信息被破坏,可以认为整个文件系统结构就被破坏了。
看到这里,可以有疑问,GDT
描述块组的信息,存储在块组里可以理解;那Super Block
描述整个分区的信息,不应该存放在分区中吗?
超级块在每一个块组开头都有一份拷贝(第一个块组必须有,后面的块组可以没有);
因为当
Super Block
在信息被破坏时,文件系统结构就被破坏了,如果将块组只存储在分区中,那磁盘存储Super Block
的扇区出现物理问题无法正常工作,那整个文件系统是不是就无法访问了。所以为了保证文件系统在磁盘部分扇区出现物理问题的情况下可以正常工作,就要保证文件系统的
Super Block
信息在这种情况下能够正常访问,所以一个文件系统中的Super Block
就会备份,这些Super Block
区域的数据是一致的。
4. inode和datablock的映射关系
我们知道文件内容和文件属性是分开存放的,文件属性以inode的形式存放在inode表当中;而文件内容则存放在Data Blcok数据块当中。
我们还知道,在一个分区(文件系统)中,我们根据文件的inode
编号就可以找到文件,很显然找到的是文件的inode
,那我们根据文件的inode
如何找到文件内容呢?
在文件的inode
中,存放了文件内容的存储位置,在上述中inode
源码中也确实存在i_block
,但是我们可以发现,这个i_block
大小是固定的15
,那我们文件大小只有15
个块吗,那也太小了?
在源码中i_block
看起来只能指向15
个块,实则不然;
这15
个块又分为三部分:12
个直接块指针,1
个一级间接块索引表指针,1
个二级间接块索引表指针,1
个三级间接块索引表指针。

那我们就可以将i_block
理解成上图所示,12
个直接块指针就直接指向块数据;
一级块索引表指针,指向一张表,这个表中每一个指针都指向一个块数据;
二级块索引表指针,指向一张表,这张表中每一个指针都指向一个一级块索引表;
三级块索引表指针,指向一张表,这张表中每一个指针都指向一个二级块索引表。
到这里,本篇文章内容就结束了,感谢各位大佬的支持