Linux文件(二)

1. 缓冲区

1.1 缓冲区的概念

缓冲区是操作系统或标准库在内存中预留的一块连续存储空间 ,专门用于暂存输入 / 输出数据。但它的核心作用 不是 "存数据",而是减少 CPU 与外设的直接交互次数------ 因为 CPU 运算速度(GHz 级)与外设读写速度(磁盘 MB/s 级、键盘 / 显示器 KB/s 级)相差数个数量级,直接交互会导致 CPU 大量时间浪费在 "等待外设响应" 上。

举个直观例子:用打印机打印 100 行文档时,若没有缓冲区,CPU 需逐行发送数据并等待打印机完成;有缓冲区时,CPU 可一次性将 100 行数据写入缓冲区,后续由打印机自行从缓冲区读取打印,CPU 可同时处理其他任务。

很多人误以为 "缓冲区只有一层",但实际上 Linux 中存在两层缓冲区,二者分工不同

层级 实现者 作用 典型场景
用户层缓冲区 C 标准库(如 Glibc) 减少用户程序调用系统调用的次数 printf、fwrite 等库函数
内核层缓冲区 操作系统内核 减少内核与硬件外设的直接交互 write、read 等系统调用

交互流程:当调用 printf("hello") 时,数据先写入 C 库的用户层缓冲区;满足刷新条件后,C 库调用 write 系统调用,将数据写入内核层缓冲区;内核再根据外设调度策略,将数据刷到磁盘 / 显示器等外设。

1.2 缓冲区的作用

1. 减少系统调用次数,降低 CPU 开销

系统调用(如 write)需要切换 CPU 状态(从用户态到内核态),每次切换耗时约 1~10 微秒。若没有用户层缓冲区,打印 1000 个字符需调用 1000 次 write;有缓冲区时,可一次性将 1000 个字符写入缓冲,再调用 1 次 write 即可,系统调用次数减少 99.9%。

2. 协调外设速度差异,提升用户体验

以显示器输出为例:若没有行缓冲,printf("hello") 会立即触发 write,但显示器刷新速度有限,可能导致 "字符逐个显示" 的卡顿;有行缓冲时,数据会暂存到缓冲,直到遇到 \n 或缓冲满,再一次性刷新到显示器,呈现 "整行输出" 的流畅效果。

3. 避免数据丢失,提升可靠性

内核层缓冲区会缓存数据并根据 "延迟写" 策略刷盘(如 Linux 的 Page Cache),即使突然断电,内核也会通过日志(如 ext4 的 journal)恢复缓冲区数据;若直接读写磁盘,断电时未完成的 IO 操作会导致数据损坏。

1.3 缓冲区刷新的三种类型

C 标准库(如 Glibc)将用户层缓冲区分为三类,核心区别是 **"何时将数据从用户缓冲刷到内核缓冲"**,这也是实际开发中最易踩坑的点。

(1)全缓冲(Full Buffer):满了才刷

触发条件: 缓冲区被填满(默认大小通常为 4096 字节或 8192 字节)、主动调用 fflush、进程正常退出。
**典型场景:**对磁盘文件的操作(如 fopen("file.txt", "w"))。

实战验证:

cpp 复制代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() 
{
	close(1); // 关闭stdout(fd=1)
	// 打开文件,fd=1指向log.txt(磁盘文件,全缓冲)
	int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	printf("hello world"); // 数据写入用户层缓冲(未填满,不刷新)
	close(fd); // 未主动fflush,缓冲数据丢失

	return 0;
}

运行结果:

运行后 log.txt 为空 ------ 因为 printf 写入的 11 个字符未填满全缓冲(默认 4096 字节),正常来说我们退出进程就会刷新缓冲区然后将缓冲区的数据写入到 log.txt 中,但是我们在退出进程前就用close将文件 log.txt 的文件描述符关闭了,这样就算进程退出后刷新缓冲区也无法找到 log.txt 进而也就无法将数据写入到文件了,此时缓冲区里的数据就丢失了。

(2)行缓冲(Line Buffer):遇换行就刷

触发条件: 遇到 \n、缓冲区填满、主动调用 fflush、进程正常退出。
**典型场景:**对终端的操作(如 stdout,默认指向显示器)。

实战验证:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>

int main() 
{
	printf("hello 1"); // 无\n,数据存行缓冲,不输出
	sleep(2); // 等待2秒,终端无内容
	printf("hello 2\n"); // 有\n,触发刷新,终端输出"hello 1hello 2"
	sleep(2);
	return 0;
}

运行结果:

**现象:**前 2 秒终端无输出,第 2 秒时 "hello 1hello 2" 一起显示 ------ 因为 hello 1 存于行缓冲,hello 2\n 触发换行,缓冲数据被一次性刷新。

(3)无缓冲(Unbuffered):写了就刷

触发条件: 数据写入后立即调用 write 刷到内核,无需等待。
**典型场景:**标准错误流 stderr(确保错误信息即时显示)。

实战验证:

cpp 复制代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() 
{
	close(2); // 关闭stderr(fd=2)
	// 打开文件,fd=2指向log.txt(无缓冲)
	int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	perror("hello world"); // perror输出到stderr,无缓冲,立即刷新
	close(fd);
	return 0;
}

运行结果:

运行后 log.txt 包含 "hello world: Success"------ 因为 stderr 无缓冲,perror 的数据直接刷到内核,无需 fflush。

1.4 场景注意

我们来看以下场景:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>

int main()
{
	//C函数调用
	printf("hello printf\n");
	fputs("hello fputs\n", stdout);
	//系统函数调用
	write(1, "hello write\n", 12);

	fork();
	return 0;
}

运行结果:

我们可以看到,在打印到终端的时候,是正常打印的,但是如果重定向打印到 log.txt 文件的时候就出现了奇怪的现象,C语言函数printf、fputs 都调用了两次,而系统函数write 则正常调用了一次,这是为什么呢?

这是因为我们在打印的时候,如果带了\n打印到终端就是行缓冲,遇到\n直接刷新。而如果是重定向打印到文件中,此时刷新方式就会变成全缓冲,也即除非用 fflush 函数等方法主动刷新,否则将缓冲区填满后才会刷新。

我们在调用C函数时,会将数据存入到用户缓冲区中,然后根据刷新条件写入到文件内核缓冲区中,此时操作系统就会帮我们处理好数据自主决定如何写到硬件中,相当于我们把快递交给了快递员后就默认已经能送到目的地了。在进程中,我们调用了fork函数创建了子进程,此时重定向到文件 log.txt ,刷新方式为全缓冲,我们知道创建的子进程会共享父进程的代码和数据,只有更改的时候才会发生写时拷贝,而其实父进程的缓冲区也会和子进程共享,这个缓冲区就是用户缓冲区,在进程结束后就会将用户缓冲区的数据刷新到 log.txt中,这其实就相当于对数据进行更改了,为了保证进程独立性,就会发生写时拷贝,此时父进程和子进程各自拥有自己的缓冲区,其中的数据是一样的,在进程结束后后父进程和子进程会将各自缓冲区中的数据写到 log.txt中,此时也就有了两份数据了。

那么为什么系统函数不会出现这种情况呢?那是因为系统函数会直接将数据写入到文件内核缓冲区,没有写入到用户缓冲区这一中间步骤,子进程和父进程共享空的用户缓冲区,也就不会发生写时拷贝,所以只有一份 write数据。

总结:

C 函数重复的原因:

重定向到文件时,stdout是全缓冲,printf/fputs的内容存在用户层缓冲区里;fork后父子进程共享用户缓冲,进程退出时刷新缓冲(属于 "修改操作"),触发写时拷贝 ------ 父子各有一份缓冲数据,最终都刷到文件,导致重复。
write正常的原因:

write是系统调用,直接写入内核层缓冲区,没有用户层缓冲的中间步骤;fork时内核缓冲由 OS 管理,不会触发用户层的写时拷贝,所以只输出一次。

以上我们谈论的都是打开的文件,接下来我们来谈谈存在磁盘中,未被打开的文件。

2 磁盘结构与地址映射

文件的底层存储依赖磁盘,而磁盘的物理结构和地址寻址方式,直接决定了文件存储的效率。理解这部分内容,是掌握文件系统的前提。

2.1 磁盘的物理结构

磁盘是计算机中唯一的机械设备,主要由盘片、磁头、磁头臂、主轴等部件组成:

盘片: 磁盘的核心存储介质,每个盘片有上下两个盘面,每个盘面对应一个磁头。
磁道: 盘片表面的同心圆轨道,从外圈到内圈依次编号(0 磁道开始)。
扇区: 磁道被划分的扇形区域,是磁盘存储的最小物理单位,默认大小为 512 字节。
**柱面:**所有盘片上半径相同的磁道构成的虚拟圆柱,是分区的最小单位。

磁头通过磁头臂的径向移动定位磁道,盘片通过主轴旋转定位扇区,二者配合实现数据的读写。

磁盘的存储结构图:

一个盘片有两个盘面,一个盘面如上图a所示。一个盘面中有很多圆,这些圆其实就是磁道。而一个磁道又有多个扇区,扇区是磁盘存储的最小物理单位,默认为512字节,每个磁道存储的扇区数相同。虽然不同磁道间的扇区看起来大小不同,但其实它们存储的数据大小是相同的(有些先进的磁盘可以做出改进但本文暂且不讨论)。

我们再来看看图b,图b中有三个盘片六个盘面,而柱面其实是一种抽象结构,我们可以理解为是六个盘面中所有半径相同的磁道构成一个虚拟圆柱,而这就是柱面。如图中柱面k,我们可以理解为是六个盘面中所有半径为k构成的圆柱体,同样有多少半径不同的磁道,就有多少柱面。

我们再来看看加上磁头和机械臂的图:

磁盘表面不断旋转用以定位扇区,而磁头则在半径方向不断移动用以定位磁道。六个盘面各有一个磁头,传动臂带动所有磁头同步沿径向移动(同步移动不代表一同修改数据),这样能保证所有磁头同时定位到同一柱面的不同磁道,这也是 "柱面是分区最小单位" 的原因。

2.2 磁盘的地址寻址方式

要访问磁盘上的某个扇区,需要明确其物理位置,主要有两种寻址方式:

(1)CHS 寻址(Cylinder-Head-Sector)

核心逻辑: 通过柱面号(C)、磁头号(H)、扇区号(S)三者定位扇区。
计算方式: 磁盘容量 = 磁头数 × 柱面数 × 每磁道扇区数 × 每扇区字节数。
**局限性:**支持的最大容量有限(传统 CHS 仅支持 8.4GB),无法满足大硬盘需求。

柱面是一个逻辑上的概念,其实就是每一面上,相同半径的磁道逻辑上构成柱面。所以,磁盘物理上分了很多面,但是在我们看来,逻辑上,磁盘整体是由"柱面"卷起来的,类似于山楂卷:

所以,磁盘的真实情况是:

磁道:

某一盘面的某一个磁道展开(一维数组):

柱面:

整个磁盘所有盘面的同一个磁道,即柱面展开(二维数组):

整盘:

所有的盘面的展开(三维数组):

整个磁盘不就是多张二维的扇区数组表,也就是可以看成三维数组。所以,寻址一个扇区:先找到在哪一个柱面(Cylinder) ,再确定在柱面内哪一个磁道(其实就是磁头位置,Head),再确定扇区(Sector),所以就有了 CHS 。​

(2)LBA 寻址(Logical Block Address)

核心逻辑: 将磁盘所有扇区按顺序编号,形成线性地址(从 0 开始),屏蔽物理结构差异。
**优势:**操作系统无需关注磁盘物理结构,仅通过线性地址即可访问扇区,磁盘固件自动完成 LBA 与 CHS 的转换。

我们学过C/C++的数组,所以在我们看来上面的三维数组其实全都是一维数组:

所以,每一个扇区都有一个下标,我们叫做LBA(Logical Block Address) 地址,其实就是线性地址。所以怎么计算得到这个LBA地址呢?​

(3)CHS 与 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

• "//": 表示除取整

示例:

假设某磁盘有:

盘面数(磁头数) = 4 个(对应磁头号 0、1、2、3)

每盘面磁道数 = 100 个(磁道号 0~99)

每磁道扇区数 = 60 个(扇区号 1~60)

第一步:计算 "单个柱面的扇区总数"

单个柱面包含 "所有盘面的同一磁道",所以:

单个柱面扇区总数 = 磁头数 × 每磁道扇区数 = 4 × 60 = 240 个

案例 1:CHS 转 LBA

已知某扇区的 CHS 地址是:

柱面号 C= 5

磁头号 H= 2

扇区号 S= 30

根据公式: LBA = C× 单个柱面扇区总数 + H× 每磁道扇区数 + (S - 1)
**代入数值:**LBA = 5×240 + 2×60 + (30 - 1) = 1200 + 120 + 29 = 1349

案例 2:LBA 转 CHS

已知某扇区的 LBA 地址是 2500,反向转换为 CHS 地址:
1. 计算磁头号 H:

磁头号 H = (LBA ÷ 单个柱面扇区总数) 的余数 ÷ 每磁道扇区数

先算:2500 ÷ 240 = 10 余 100(因为 240×10=2400,2500-2400=100)

再算:100 ÷ 60 = 1 余 40 → 磁头号 H=1
2. 计算柱面号 C: 柱面号 C = LBA ÷ 单个柱面扇区总数 = 2500 ÷ 240 = 10 → 柱面号 C=10
**3. 计算扇区号 S:**扇区号 S = (LBA ÷ 单个柱面扇区总数) 的余数 % 每磁道扇区数 + 1

即:100 % 60 + 1 = 40 + 1 = 41
**最终结果:**LBA=2500 对应的 CHS 地址是 柱面 10、磁头 1、扇区 41。

所以:从此往后,在磁盘使用者看来,根本就不关心CHS地址,而是直接使用LBA地址,磁盘内部自己转换。

所以:从现在开始,磁盘就是一个元素为扇区的一维数组,数组的下标就是每一个扇区的LBA地址。OS使用磁盘,就可以用一个数字访问磁盘扇区了。

2.3 文件存储的最小单位

磁盘的最小物理单位是扇区,但操作系统读写数据时不会逐个扇区操作(效率过低),而是将连续的扇区组合成 "块"(Block):

• 块是文件存取的最小逻辑单位,大小由格式化时指定(常见 4KB,即 8 个扇区)。

• 块的存在减少了 I/O 操作次数,同时便于内存缓存(连续数据的缓存命中率更高)。

前面我们说一个LBA地址对应一个扇区,而在操作系统重读写数据一次性操作一整个块(8个扇区),此时其实只需要这一块的首个扇区的LBA地址,就可以根据偏移量来操作整个块的所有扇区的数据了。

3. 文件系统

磁盘的空间很大,要如何进行管理呢?我们假设磁盘有500G的空间,那么我们不妨把它分为一个个分区进行管理,我们把这些分区管理好了就能管理好整个磁盘。但是一个分区的大小其实还是很大,我们可以把一个分区再划分为若干个块组,我们把这些组管理好了,就能管理好整个分区,进而管理好整个磁盘了,这其实就是分治思想。

3.1 EXT 文件系统的整体结构

EXT 文件系统将分区划分为多个大小相等的块组(Block Group),每个块组包含相同的结构,便于管理:

启动块(Boot Block): 位于分区最开头,大小 1KB,存储分区信息和启动程序,不受文件系统管理。
**块组(Block Group):**分区的核心管理单元,每个块组包含超级块、块组描述符表、块位图、inode 位图、inode 表、数据块 6 个部分。

3.2 块组的关键组成部分

注:该图未画出Boot Block,可参考上一张图。

超级块(Super Block)

作用:存储文件系统的全局信息,相当于文件系统的 "户口本"。

包含内容:块总数、inode 总数、空闲块数、空闲 inode 数、块大小、inode 大小、最近挂载时间、最近写入时间等。

冗余设计:超级块在多个块组中备份(第一个块组必存),防止单个块损坏导致整个文件系统失效。

块组描述符表(GDT)

作用:描述每个块组的属性,每个块组对应一个描述符。

包含内容:块组内块位图、inode 位图、inode 表的起始位置,以及该块组的空闲块数、空闲 inode 数等。

冗余设计:与超级块类似,在多个块组中备份,确保可靠性。

块位图(Block Bitmap)

作用:记录数据块的使用状态,每个比特位对应一个数据块(1 表示占用,0 表示空闲)。

优势:通过位图可快速查找空闲数据块,查询效率高(仅需遍历对应比特位)。

inode 位图(Inode Bitmap)

作用:记录 inode 的使用状态,每个比特位对应一个 inode(1 表示占用,0 表示空闲)。

与块位图配合,实现 inode 和数据块的快速分配与释放。

inode 表(Inode Table)

作用:存储文件的元信息(属性),每个文件对应一个 inode。

inode 包含的属性:文件类型、权限(rwx)、所有者(uid)、所属组(gid)、文件大小、访问时间(Access)、修改时间(Modify)、创建时间(Change)、数据块指针等。

关键特性:inode 大小固定(128 字节或 256 字节),与文件内容分离存储;inode 编号唯一(以分区为单位,不可跨分区);文件名不存储在 inode 中,而是存储在目录的数据块中。

inode中包含了文件的诸多属性,我们知道每个文件的属性内容是不一样的,但类型是一样的,如都有自己的文件类型、权限、大小等,而inode就是一个包含了这些属性的结构体。

操作系统就是通过每个文件的inode编号找到磁盘中文件的inode结构体的存放位置,而inode结构体中又存放着该文件的数据块的指针,通过这些指针可以找到文件的数据。所以我们也就知道了有了文件的inode,就可以找到磁盘的文件的属性+ 内容了。

注:inode中不包括文件名,在linux中文件名不属于文件的属性,我们会在下面进行讲解。

我们可以通过 ls -i显示当前目录下的所有文件的inode编号:

数据块(Data Blocks)

作用:存储文件的实际内容,大小与块一致(常见 4KB)。

不同文件类型的存储方式:

普通文件:数据直接存储在数据块中。

目录:数据块存储该目录下的文件名与 inode 编号的映射关系。

链接文件:软链接存储源文件路径,硬链接无独立数据块(与源文件共享 inode)。

3.3 inode与数据块的映射关系

inode 通过数据块指针数组(i_block[EXT2_N_BLOCKS])关联数据块,EXT2 中该数组包含 15 个指针,采用 "直接 + 间接" 的分级映射方式,兼顾小文件效率和大文件存储:

12 个直接指针: 直接指向存储文件内容的数据块,适合小文件(≤48KB,12×4KB)。
1 个一级间接指针: 指向一个存储数据块编号的中间块,该中间块可存储 1024 个数据块编号(4KB/4 字节),对应 4MB 存储。
1 个二级间接指针: 指向一个存储一级间接块编号的中间块,对应 4GB 存储。
**1 个三级间接指针:**指向一个存储二级间接块编号的中间块,对应 4TB 存储。

4. 目录的本质

先说结论:在 Linux 中,目录也是文件,其核心作用是建立文件名与 inode 的映射关系,让用户可以通过文件名访问文件。

我们在上面说了,操作系统是通过每个文件的inode找到它存储在硬盘中的哪个位置,但我们平常运行我们的可执行程序或使用各种命令时(一切皆文件),都是使用的文件名啊,这又是怎么回事呢?其实这就好比我们之前学过的虚拟地址空间,在操作系统中自然也存在一张张表,记录着每个文件的文件名和它的inode的映射关系。而在linux中,目录也是文件(特殊文件),那么它自然也有自己的数据块,目录的数据块中存储的就是该目录下所有文件的文件名和inode的映射关系表,进而让用户能通过文件名来访问文件。

4.1 目录的存储结构

目录的属性: 与普通文件一样,目录有自己的 inode,存储目录的权限、所有者、大小等属性。
目录的数据块: 存储该目录下所有文件(包括子目录)的 "文件名 + inode 编号" 映射关系,格式为(文件名, inode编号)。

示例:使用ls -li命令可查看目录下文件的 inode 编号,使用stat /可查看根目录的 inode(通常固定为 2)。

4.2 目录的核心特性

同名文件限制:同一目录下不能有同名文件,因为映射关系是 "一对一"(文件名作为 key,inode 编号作为 value),避免查找冲突。

目录权限的意义:

r 权限:允许读取目录下的文件名列表(如ls命令)。

w 权限:允许在目录下创建、删除、重命名文件(需修改目录的数据块)。

x 权限:允许进入目录(如cd命令),本质是允许将目录路径写入环境变量 PWD。

4.3 路径解析

用户访问文件时通过路径(绝对路径或相对路径)指定位置,路径解析的过程就是从根目录出发,逐层查找目录的映射关系,最终找到目标文件的 inode:

解析流程: 以/home/nep/test.c为例,先访问根目录/(inode=2),从其数据块中找到home对应的 inode;再访问home目录的 inode,找到nep对应的 inode;最后访问nep目录的 inode,找到test.c对应的 inode,完成解析。
**递归终止条件:**根目录(/)没有上级目录,其 inode 编号固定,是路径解析的起点。

4.4 路径缓存

我们是否每次访问一个文件都要从根目录开始解析,一次次进行IO操作,来查找对应的信息呢?其实不是的,这样做会有很大损耗,操作系统对此做出了处理。

频繁的路径解析会产生大量磁盘 I/O(效率低下),Linux 内核通过 dentry(目录项)结构体缓存路径信息:

dentry 的作用: 将已解析的路径(文件名与 inode 的映射关系)存储在内存中,形成树形结构,下次访问时直接从缓存查找,无需重复遍历磁盘。
dentry 的特性:

• 每个文件(包括目录)对应一个 dentry 结构体,包含父目录指针、子目录列表、inode 指针等。

• 加入 LRU(最近最少使用)淘汰机制,当内存不足时,回收不常用的 dentry。

• 支持哈希查找,进一步提升路径查询效率。

在有dentry之前,我们要访问一个文件,每次都要从根目录开始路径解析,每往下解析到一个目录时都要涉及到IO操作到磁盘中寻找信息,这样效率会很低下。在有了dentry之后,我们除了首次访问某个文件还要从根目录开始解析,之后都会将解析的路径数据存储在dentry中,方便我们重复使用。当然操作系统不会无限的记录使用过的路径信息,在记录到一定程度后,会按需删除使用最少的路径信息,保证内存有足够的空间使用。

5. 文件的增删查改

文件的所有操作,本质都是对 inode、数据块、目录映射关系的修改,结合文件系统的结构,我们可以清晰理解其底层逻辑。

5.1 创建文件(touch)

1. 分配 inode: 遍历 inode 位图,找到空闲 inode(比特位为 0),将其标记为占用(置 1),并写入文件属性(权限、所有者、创建时间等)。
2. 分配数据块(若文件有内容): 遍历块位图,找到空闲数据块(比特位为 0),标记为占用(置 1),将数据写入数据块。
3. 建立映射关系: 在当前目录的数据块中,添加 "文件名 + 新 inode 编号" 的映射条目。
**4. 关联 inode 与数据块:**将数据块的编号写入 inode 的i_block数组中。

5.2 删除文件(rm)

1. 解除目录映射: 从所在目录的数据块中,删除该文件的 "文件名 + inode 编号" 映射条目。
2. 标记空闲状态: 将 inode 位图中该文件对应的比特位置 0,块位图中对应数据块的比特位置 0。
**3. 无需清空数据:**文件的属性和内容采用 "覆盖写" 方式,删除时仅需标记空闲,无需清空数据(后续新文件会覆盖原有数据),因此删除速度极快。

5.3 查找文件(ls/cat)

1. 路径解析: 从根目录出发,逐层解析路径,找到目标文件的 inode 编号(会查看dentry)。
2. 验证 inode 有效性: 查看 inode 位图,确认该 inode 处于占用状态(比特位为 1)。
3. 访问文件属性: 从 inode 表中读取文件的元信息(如stat命令)。
**4. 访问文件内容:**通过 inode 的i_block数组找到数据块,读取其中的内容(如cat命令)。

5.4 修改文件(echo/vi)

1. 修改属性(如chmod): 直接修改 inode 表中该文件对应的属性字段,同时更新 inode 的 Change 时间。
2. 修改内容(如echo "test" > test.c):

• 若原有数据块足够:直接覆盖数据块中的内容,更新 inode 的 Modify 时间和 Change 时间。

• 若原有数据块不足:分配新的数据块,将新数据写入,更新 inode 的i_block数组,同时更新相关时间字段。

6. 挂载分区

我们在上面说了,磁盘管理内存会将内存分区管理,同时inode编号不跨区,如a分区有5000号inode,b分区也可以有5000号inode,但它们不是一个inode,那么一个inode编号怎么知道自己在哪个分区呢?这里就需要引入挂载的概念。

Linux 支持多个磁盘分区,每个分区可格式化为不同的文件系统(如 EXT4、XFS),挂载机制让这些分区可以被统一访问:

挂载的本质: 将一个分区的文件系统与目录树中的某个目录(挂载点)关联,访问该目录时,实际访问的是分区的文件系统。
关键概念:

• 挂载点:必须是一个空目录,挂载后该目录的原有内容被隐藏,卸载后恢复。

• 循环设备(loop device):允许将文件(如磁盘镜像)作为块设备挂载,如mount -t ext4 ./disk.img /mnt/mydisk。

• 分区识别:通过路径前缀判断文件所在分区(如/mnt/mydisk下的文件属于挂载的disk.img分区)。

7. 软硬链接

Linux 支持软硬链接两种机制,通过不同的方式关联文件,满足不同的使用场景。

7.1 软链接(Symbolic Link)

本质: 创建一个独立的文件(有自己的 inode),其数据块中存储的是源文件的路径(绝对路径或相对路径)。
特性:

• 是独立文件,有自己的 inode 和属性,大小为源文件路径的长度。

• 源文件删除后,软链接失效(变成 "死链接")。

• 可以跨分区创建,支持链接目录。

• 用途:快捷方式(如桌面图标指向程序)、简化长路径访问。

软链接文件一般就是对应路径文件的一种快捷访问方式。其中在Windows系统中,我们桌面上软件图标就是访问对应程序的快捷方式,本质其实就是一个软连接文件。

在Linux中,我们可以通过指令ln -s 文件名 软链接名设置软连接:

我们可以发现link_code6的权限的最前方是 l 而不是**-**,这代表它不是普通文件,同时 code6和link_code6的 inode编号也不同,证明了它是一个独立的文件。

我们可以通过指令 unlink 软连接名取消对应的软连接,并且如果删除软连接所指向的文件,那么该软连接文件也就没有意义了。

7.2 硬链接(Hard Link)

本质: 在目录中添加一个新的 "文件名 + inode 编号" 映射条目,与源文件共享同一个 inode。
特性:

• 无独立 inode,不是独立文件,与源文件完全等价。

• 硬链接数记录在 inode 的i_links_count字段,创建时加 1,删除时减 1,仅当硬链接数为 0 时,inode 和数据块才被标记为空闲。

• 不能跨分区创建(inode 编号以分区为单位,跨分区无意义)。

• 用途:文件备份(修改一个,另一个同步更新)、目录的.和..(.是当前目录的硬链接,..是父目录的硬链接)。

在Linux中,我们可以通过指令ln 文件名 硬链接名 建立对应的硬链接,同样我们也能通过unlink 硬链接名取消对应的硬链接。:

我们可以发现 link_code6的inode编号和 code6的完全相同,这就代表了它不是一个独立的文件,最多算作一个"别名",每创建一个硬链接,对应的硬链接数就会加一,如图中数量为2。

普通文件是这样,我们知道目录也是文件,那目录与普通玩家有什么不同呢?我们来看看:

为什么我们刚创建这个目录test1,它的硬链接数就是2呢?答案很简单,我们先进入test1目录:

谜底揭晓了:当前目录下还存在一个隐藏文件 **.**指向当前目录,所以硬链接数为2,如果我们在test01下再创建一个子目录,就会发现它的硬链接数会变成3:

答案我们也不难猜到,其实就是test01目录下存在一个**..**的隐藏目录也指向了test1,所哟test1的硬链接数就变为了3:

7.3 软硬链接对比

结语

好好学习,天天向上!有任何问题请指正,谢谢观看!

相关推荐
一个平凡而乐于分享的小比特6 小时前
U-Boot 和 Linux 内核的关系及设备树详解
linux·设备树·uboot
Sleepy MargulisItG6 小时前
【Linux网络编程】UDP Socket
linux·网络·udp
QT 小鲜肉7 小时前
【Linux命令大全】001.文件管理(理论篇)
linux·数据库·chrome·笔记
VekiSon7 小时前
Linux系统编程——进程进阶:exec 族、system 与工作路径操作
linux·运维·服务器
博语小屋7 小时前
Socket UDP 网络编程V2 版本- 简单聊天室
linux·网络·c++·网络协议·udp
一个平凡而乐于分享的小比特7 小时前
Linux 内核设计中的核心思想与架构原则
linux·架构·linux设计思想
BullSmall7 小时前
Shell脚本波浪号避坑指南
linux·bash
luoyayun3617 小时前
Linux下安装使用Claude遇到的问题及解决方案
linux·claude
[J] 一坚7 小时前
实用shell脚本学习分享一
linux·运维·编辑器