【Linux操作系统】基础IO文件系统(理解硬件,理解文件系统,Inode,软硬链接)

一.理解硬件(初识)

1-1 磁盘、服务器、机柜、机房

  • 内存 -- 掉电易失存储介质
  • 磁盘 -- 永久性存储介质 -- SSD、U盘、flash卡、光盘、磁带
  • 机械磁盘是计算机中唯⼀的⼀个机械设备
  • 磁盘--- 外设
  • 容量⼤,价格便宜

磁盘的分类

  • 机械硬盘(HDD):依赖磁性盘片和机械臂,容量大、成本低、寿命长(对写入次数不敏感),但随机读写慢、怕震动。

  • 固态硬盘(SSD):依赖闪存芯片,无机械部件,随机读写快,但单位容量成本更高,且存在写入寿命限制(尤其是 TLC/QLC)。

  • 其他永久性存储:你笔记中提到的 U 盘、Flash 卡(本质也是闪存)、光盘(光学存储)、磁带(磁性顺序存储,备份场景仍在用)等,与磁盘并列而非下属关系。

永久性存储介质包括磁盘、光盘、磁带等,其中磁盘又分为机械硬盘和固态硬盘

服务器类型 常见存储 原因
冷存储/备份服务器 机械硬盘为主 容量大、成本低,访问频率不高
数据库/高频交易服务器 固态硬盘为主 需要极低延迟和高 IOPS
普通云服务器/虚拟化主机 混合(SSD 做缓存或系统盘,HDD 做数据盘) 平衡性能与成本
高性能计算/存储集群 全闪存阵列或 NVMe SSD 性能要求极高

所以:机械硬盘在大容量、低成本场景(如冷存储、备份)中仍占主流,但高性能服务器正越来越多地使用固态硬盘

  1. 磁盘的材料

    传统机械硬盘(HDD)的盘片表面涂有磁性材料(如铁氧化物或钴合金)。这些材料可以被磁化,且能长期保持磁性状态。

  2. 磁铁的作用

    • 写入数据 :磁盘的磁头是一个小电磁铁。通过改变电流方向,磁头产生不同方向的磁场,将盘片上微小区域的磁畴"定向"为北极(N)或南极(S),对应二进制的 0 和 1。

    • 读取数据:磁头靠近磁畴时,磁畴的磁场会感应出微弱的电流,通过检测电流方向判断存储的是 0 还是 1。

    • 磁铁的危险:强外部磁场(如强磁铁、MRI、电磁铁)会破坏磁盘上精心排列的磁畴方向,导致数据永久丢失。所以机房会强调"远离磁铁"。

  3. 题外话中的联系

    • "关于机房"------机房里的服务器硬盘对磁铁很敏感,因此有严格管理。

2.关于磁盘 ------ 磁铁"------强调磁盘依赖磁性存储,强磁铁是它的天敌。


  1. 服务器里装着磁盘

    绝大多数服务器(尤其是机房里的)都依赖硬盘来存储操作系统、应用程序和数据。这些硬盘主要分两种:

    • 机械硬盘(HDD):上面说的磁性存储,怕强磁铁。

    • 固态硬盘(SSD) :用闪存芯片,不依赖磁性,因此不怕磁铁(但怕强静电或掉电)。

  2. 服务器的运行环境(机房)特别强调"远离磁铁"

    • 因为很多服务器仍然使用大容量机械硬盘(成本低、适合冷数据)。

    • 机房人员维修、巡检时,如果带着强磁铁(比如磁性螺丝刀、名牌磁扣、扬声器磁铁),靠近运行中的服务器,可能导致正在读写的硬盘数据出错或直接损坏

    • 更严重的是,服务器里的磁盘通常组了 RAID(磁盘阵列),一块硬盘被磁化破坏,可能导致整个阵列降级甚至数据丢失。

磁盘是利用磁铁(电磁铁)来读写数据,但又惧怕外部强磁铁破坏数据。

注意高温条件,高温会让磁盘退磁

1-2 磁盘物理结构

1.盘片(Platter)

  • 形状与材料 :圆形薄片,通常由铝合金玻璃基盘 制成,表面涂覆极薄的磁性材料层(如钴铂铬硼合金)。

  • 作用 :实际存储数据的介质。数据以磁畴(微小磁性区域)的方向(N/S)记录为 0 或 1。

  • 数量:一个硬盘可能有 1 到 5 片盘片,垂直堆叠。

2. 主轴(Spindle)与主轴电机

  • 主轴:贯穿所有盘片中心的转轴。

  • 主轴电机:带动盘片以恒定高速旋转(常见 5400、7200、10000、15000 转/分钟)。

  • 作用:使盘片表面不同半径的区域依次经过磁头下方,实现读写。

3. 磁头(Head)

  • 结构 :每个盘片的每一面都有一个独立的浮动磁头,安装在磁头臂(Head Arm)末端。

  • 原理 :是一个微型电磁铁

    • 写入时:线圈通电产生磁场,改变下方磁畴的方向。

    • 读取时:磁畴的磁场在线圈中感应出微弱电流。

  • 飞行高度 :磁头不接触盘片,而是靠空气压力悬浮在盘片上方(现代硬盘约 1-5 纳米,比灰尘还小)。

4. 磁头臂(Actuator Arm)与音圈电机(Voice Coil Motor, VCM)

  • 形状:类似一个旋转的支架,所有磁头固定在末端。

  • 驱动装置音圈电机------利用电磁感应原理(类似扬声器线圈)控制磁头臂高速摆动。

  • 作用 :将磁头径向移动到指定磁道(寻道操作)。

5. 磁道(Track)、扇区(Sector)与柱面(Cylinder)

  • 磁道 :盘片表面上一圈圈的同心圆,是磁头不动时随着盘片旋转扫过的圆形路径。

  • 扇区 :每个磁道被划分成的弧段 ,是硬盘最小的可寻址存储单元(传统为 512 字节,现在常见 4096 字节)。

  • 柱面 :所有盘片相同半径位置的磁道在垂直方向构成的虚拟圆柱面。早期硬盘用"柱面-磁头-扇区"寻址。

6. 永磁体(强磁铁)------ 这是和你之前问题的关键联系

  • 存在位置 :在音圈电机的定子部分有一块或两块非常强大的永磁体

  • 作用:与音圈电机的线圈相互作用,推动磁头臂运动。

  • 矛盾点

    硬盘内部 本来就有一块极强的永磁体(注意:这块磁铁的磁场被严格屏蔽 在硬盘内部)。

    外部的强磁铁靠近时,会:

    • 干扰音圈电机的运动,导致磁头定位错误(敲盘、异响)。

    • 直接破坏盘片上磁性材料层的磁畴方向(数据彻底损坏)。

7. 控制电路板(PCB)

  • 位置:硬盘底部(反面)。

  • 主要芯片

    • 控制器芯片(处理协议、寻址、缓存)。

    • 预放大器(靠近磁头,放大读写信号)。

    • 电机驱动芯片(控制主轴电机和音圈电机)。

  • 注意:电路板本身不依赖磁存储,但控制电磁元件。

1-3 磁盘的存储结构

一、整体层次(从大到小)

bash 复制代码
整块硬盘 → 盘片 → 面 → 柱面 → 磁道 → 扇区 → 字节

二、每一层的详细解释

1. 盘片(Platter)

  • 物理上的圆形薄片,双面均可存储数据。

  • 每张盘片有上表面下表面,各对应一个磁头。

2. 面(Side / Head)

  • 每个盘面就是一个存储面

  • 磁头号来标识(Head 0, Head 1, ...)。

  • 一个硬盘的总容量 = 所有面的容量之和。

3. 磁道(Track)

  • 盘片上一圈圈的同心圆,不是螺旋线(和光盘不同)。

  • 从外圈到内圈编号:0 号磁道(最外圈)、1 号磁道、......

  • 同一磁道上的所有扇区,磁头不需要移动就能读写(只靠盘片旋转)。

4. 柱面(Cylinder)

  • 所有盘片上相同半径的磁道在垂直方向上构成一个虚拟圆柱面。

  • 例如:

    盘片1上表面磁道10 + 盘片1下表面磁道10 + 盘片2上表面磁道10 + ...... = 柱面10

  • 早期寻址方式(CHS寻址):用 柱面号 + 磁头号 + 扇区号 唯一确定一个扇区。

5. 扇区(Sector)

  • 磁盘的最小物理存储单元

  • 传统大小:512 字节

    现代大容量硬盘:4096 字节(4K 扇区)

  • 每个扇区包含两部分:

    • 数据区:实际存储的用户数据

    • 头部信息:扇区号、校验码(ECC,用于纠错)

  • 扇区之间由**间隙(gap)**隔开,用于区分边界。

6. 簇(Cluster,也叫"块")

  • 操作系统的最小存储单元(不是物理层的,是文件系统层的)。

  • 一个簇 = 连续多个扇区(例如 8 个扇区 × 512 字节 = 4 KB)。

  • 即使文件只有 1 个字节,也要占用整个簇(会产生内部碎片)。

如何定位⼀个扇区呢?

  • 可以先定位磁头(header)
  • 确定磁头要访问哪⼀个柱⾯(磁道)(cylinder)
  • 定位⼀个扇区(sector)
  • CHS地址定位

fdisk 里看到的 /dev/loop2/dev/vda 属于磁盘逻辑地址(LBA) ,不是内存意义上的虚拟地址。

它们之所以"虚拟",是因为 loop 设备把文件伪装成磁盘vda 把宿主机文件/分区伪装成虚拟机磁盘

⽂件 = 内容+属性 都是数据,⽆⾮就是占据那⼏个扇区的问题!能定位⼀个扇区了,能不能定位多个扇区呢?

扇区是从磁盘读出和写⼊信息的最⼩单位,通常⼤⼩为 512 字节。

  • 磁头(head)数:每个盘⽚⼀般有上下两⾯,分别对应1个磁头,共2个磁头
  • 磁道(track)数:磁道是从盘⽚外圈往内圈编号0磁道,1磁道...,靠近主轴的同⼼圆⽤于停靠磁头,不存储数据
  • 柱⾯(cylinder)数:磁道构成柱⾯,数量上等同于磁道个数
  • 扇区(sector)数:每个磁道都被切分成很多扇形区域,每道的扇区数量相同
  • 圆盘(platter)数:就是盘⽚的数量
  • 磁盘容量=磁头数 × 磁道(柱⾯)数 × 每道扇区数 × 每扇区字节数
  • 细节:传动臂上的磁头是共进退的(这点⽐较重要,后⾯会说明)

柱⾯(cylinder),磁头(head),扇区(sector),显然可以定位数据了,这就是数据定位(寻址)⽅式之⼀,CHS寻址⽅式。

1-4 磁盘的逻辑结构

理解过程

磁带上⾯可以存储数据,我们可以把磁带"拉直",形成线性结构

那么磁盘本质上虽然是硬质的,但是逻辑上我们可以把磁盘想象成为卷在⼀起的磁带,那么磁盘的逻辑存储结构我们也可以类似于:

这样每⼀个扇区,就有了⼀个线性地址(其实就是数组下标),这种地址叫做 LBA

真实过程

细节:传动臂上的磁头是共进退的

柱⾯是⼀个逻辑上的概念,其实就是每⼀⾯上,相同半径的磁道逻辑上构成柱⾯。

所以,磁盘物理上分了很多⾯,但是在我们看来,逻辑上,磁盘整体是由"柱⾯"卷起来的。

也可以看成山楂片一卷一卷的(柱面的效果)

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

磁道:

某⼀盘⾯的某⼀个磁道展开(即:⼀维数组):

柱⾯:

整个磁盘所有盘⾯的同⼀个磁道,即柱⾯展开:

柱⾯上的每个磁道,扇区个数是⼀样的
这也就是⼆维数组


整盘:

整个磁盘不就是多张⼆维的扇区数组表(三维数组)

所有,寻址⼀个扇区:先找到哪⼀个柱⾯(Cylinder) ,在确定柱⾯内哪⼀个磁道(其实就是磁头位置,Head),在确定扇区(Sector),所以就有了 CHS 。

类比之前学过C/C++的数组,在我们看来,其实全部都是⼀维数组:

所以,每⼀个扇区都有⼀个下标,我们叫做LBA(Logical Block Address) 地址,其实就是线性

地址。问题来了怎么计算得到这个LBA地址呢?

操作系统只需要使用LBA(逻辑块地址)来读写磁盘,不需要关心物理细节;LBA与CHS(柱面/磁头/扇区)之间的转换工作由磁盘自身的固件和伺服系统完成,而不是由操作系统来做。具体来说,磁盘内部通过固定公式或映射表将LBA转换成CHS地址(柱面号 = LBA ÷ (每柱面磁头数 × 每磁道扇区数),磁头号和扇区号依此类推),反过来也能将CHS转换回LBA,从而控制磁头寻道、读写数据。这个过程由磁盘硬件电路和伺服系统自动完成,对操作系统完全透明。

1-5 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
  • "//": 表⽰除取整所以:从此往后,在磁盘使⽤者看来,根本就不关⼼CHS地址,⽽是直接使⽤LBA地址,磁盘内部⾃⼰转换。

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

设定磁盘参数(举例)

假设某磁盘的固件中记录如下参数:

  • 每磁道扇区数(SPT)= 32

  • 磁头数(HPC,即每柱面的磁头数)= 8

  • 单个柱面扇区总数 = 磁头数 × 每磁道扇区数 = 8 × 32 = 256

这些参数在磁盘内部维护,主机开机时通过 ATA/SCSI 命令获取。


CHS -->LBA(已知柱面、磁头、扇区,求 LBA)

已知 CHS = (C=3, H=7, S=9)

应用公式:

bash 复制代码
LBA = (C × (HPC × SPT)) + (H × SPT) + (S - 1)

代入:

bash 复制代码
LBA = (3 × 256) + (7 × 32) + (9 - 1)
    = 768 + 224 + 8
    = 1000

结果:CHS(3,7,9) → LBA = 1000


LBA -->CHS(已知 LBA = 1000,求柱面、磁头、扇区)

已知:

  • HPC × SPT = 256(单个柱面扇区总数)

  • SPT = 32(每磁道扇区数)

应用公式:

bash 复制代码
柱面号 C = LBA // (HPC × SPT) = 1000 // 256 = 3(整除)
余数 = 1000 % 256 = 232

磁头号 H = 余数 // SPT = 232 // 32 = 7(整除)
扇区号 S = (余数 % SPT) + 1 = (232 % 32) + 1 = 8 + 1 = 9

结果:LBA 1000 → CHS(3, 7, 9)


二.⽂件系统

2-1引言:

  • 在操作系统的文件系统设计中,文件有两种存在形态:一种是未打开的文件,安静地存储在磁盘上;另一种是打开的文件,被某个进程加载到内存中并处于活动状态。这两种形态的分工非常明确。

  • 磁盘中的文件以数据为中心。磁盘上存储的是文件的实际内容(比如文本、图片、二进制代码)以及固定的元数据(比如文件大小、所有者、时间戳、数据块指针)。磁盘不关心谁在访问这个文件、访问到哪一位置,也不记录任何运行时状态。它的任务就是持久、完整地保存文件的数据,即使断电也不会丢失。内存中的文件则完全不同。当进程通过 openfopen 打开一个文件后,操作系统会在内核中创建一系列数据结构(如 struct filestruct dentry、缓存的 struct inode),这些结构强调的是属性与方法。这里的"属性"指的是运行时属性,比如当前文件读写偏移量(f_pos)、打开模式(只读/只写/读写)、标志位(追加、非阻塞)等;这里的"方法"指的是指向文件操作函数的指针(如 readwriterelease),内核通过这些函数指针调用具体的驱动代码来完成读写。内存中并不会完整保存该文件的所有数据,而只会通过页缓存(Page Cache)暂存当前正在使用的部分数据块。真正强调的是如何操作这个文件、当前操作到了哪里。 这两者通过缓冲区(页缓存)和文件系统关联:磁盘上的数据块被按需加载到内存的页缓存中,而内存中的 struct filestruct inode 则管理着对这些缓存数据的访问方式与状态。当文件被关闭时,内存中的运行时属性与方法结构会被释放,而磁盘上的数据依然完好无损。

  • 因此,总结来说:磁盘中的文件更强调数据------负责永久保存内容;内存中的文件更强调属性和方法------负责管理打开后的访问状态与操作接口。 这也是为什么操作系统可以高效管理上百万个未打开文件(只占磁盘空间,不占内存结构),而同时只能维护有限数量的打开文件(每个打开文件都要占用内核内存


    在操作系统中,文件分为打开的文件普通未打开的文件 两类。打开的文件属于内存级文件,其属性与方法的实现由内核中的 struct file 结构体负责,它们需要被操作系统管理;普通未打开的文件位于磁盘上,未被加载到内存,同样也需要被文件系统管理。程序是磁盘上的静态文件,当它被加载到内存中执行时就成为进程,而文件系统则是负责对所有这些文件(无论是打开的还是未打开的)进行组织、存储和管理的功能模块。

其中我们选取 stat 中展示的一部分信息,将内存和磁盘之间交互时的基本单元大小 4kb 叫 Blocks,这里的 IO Block:4096 就是 8 × 512


2-2再说磁盘

1. 内存的基本单位

  • C 语言中访问内存的基本单位是 1 字节(1 byte)

  • 操作系统角度认为内存的基本单位是 4 KB

  • 操作系统把内存看作一个数组,每个元素是一个 4 KB 的页框(页帧)

  • 操作系统分配、加载内存时都以 4 KB 为单位

2. 语言层面与操作系统的配合

  • malloc(1 byte) 不会让操作系统直接分配 1 字节

  • C 运行库会预先向操作系统申请一块内存(比如通过 brkmmap 得到 4 KB 甚至更大),再从中"切"出 1 字节返回给用户

  • 当用户申请超过已缓存的空间时,C 库再向操作系统申请新的内存块

3. 磁盘的基本单位

  • 磁盘读写的最小单元是 扇区

  • 绝大多数磁盘的一个扇区是 512 字节(注:现在也有很多 4 KB 原生扇区)

  • 虽然内圈扇区物理长度更短,但通过提高比特密度,每个扇区仍然是 512 字节

4. 内存与磁盘的交互

  • 内存 ↔ 磁盘 的数据传输称为 I/O(Input/Output)

  • 这个过程不是纯硬件 直接传输,而是通过 文件系统 + 操作系统 来完成

  • 内存与磁盘之间交互的基本单位通常是 4 KB

  • 因为磁盘一个扇区是 512 字节,所以 4 KB = 8 个连续扇区

  • 将数据存储到磁盘 → 把数据写入这个"磁盘数组"的某个位置

  • 找到磁盘特定扇区 → 找到磁盘数组中的特定下标

  • 对磁盘的管理 → 对这个大数组的管理(文件系统 + 驱动)

存与磁盘之间的数据读写(I/O)必须经过文件系统,读操作为 input(磁盘 → 文件系统 → 内存),写操作为 output(内存 → 文件系统 → 磁盘),传输基本单位为 4KB(对应 8 个 512 字节的扇区)。


1. 机械硬盘的物理结构对操作系统"不友好"

  • 机械硬盘是圆形的,有盘面、磁道、扇区等复杂的几何参数。

  • 如果操作系统直接按照这些物理参数来设计读写接口(比如 read(盘面, 磁道, 扇区)),那么读写磁盘的代码就必须硬编码这些物理概念。

2. 硬编码会导致强耦合

  • 一旦操作系统代码中写死了"盘面、磁道、扇区"这些参数,就意味着操作系统与机械硬盘的物理结构深度绑定

  • 当你把机械硬盘换成固态硬盘(SSD)时,问题就来了:SSD 没有盘面、磁道、扇区,它的内部是闪存芯片和 FTL 映射层。

  • 如果操作系统接口还是按老参数设计,那为了支持 SSD,就必须重写操作系统中所有与磁盘读写相关的代码,代价极大。

3. 这种强耦合是不合理的

  • 操作系统不应该因为底层存储介质的改变而需要大规模修改。

  • 更好的设计是:操作系统只与一个抽象的"块设备"打交道,而把不同硬件的差异交给驱动或设备自身的固件去处理。


做法:抽象与分层

层面 职责
操作系统 只认识 LBA(逻辑块地址) ,把磁盘看作一个 线性扇区数组 ,接口类似 read(设备, LBA, 数据)
设备驱动 / 固件 负责把 LBA 翻译成具体硬件的物理参数(机械硬盘的 CHS,或 SSD 的闪存地址)

效果:

  • 换硬盘时,操作系统代码一行都不用改

  • 只需要更换或加载对应的驱动程序即可。

总结

操作系统不应该基于某种具体硬件的物理结构(如磁道/扇区)来设计读写接口,而应该通过抽象(如 LBA 模型)来屏蔽底层差异,否则硬件一换,操作系统就得重写,这是典型的强耦合反模式。


为什么交互单位是 4 KB 而不是 512 字节

  • 局部性原理:一次读 4 KB 可以覆盖更多未来可能访问的数据

  • 减少 I/O 次数:传输同样数据量,大块 I/O 效率更高

  • 与内存页大小对齐:避免额外的拆分与合并开销

  • 磁盘硬件本身也支持一次读多个连续扇区

2-3"块"概念

其实硬盘是典型的"块"设备,操作系统读取硬盘数据的时候,其实是不会⼀个个扇区地读取,这样效率太低,⽽是⼀次性连续读取多个扇区,即⼀次性读取⼀个"块"(block)。
硬盘的每个分区是被划分为⼀个个的"块"。
⼀个"块"的⼤⼩是由格式化的时候确定的,并且不可以更改,最常⻅的是4KB,即连续⼋个扇区组成⼀个 "块"。"块"是⽂件存取的最⼩单位。

注意:

  • 磁盘就是⼀个三维数组,我们把它看待成为⼀个"⼀维数组",数组下标就是LBA,每个元素都是扇区
  • 每个扇区都有LBA,那么8个扇区⼀个块,每⼀个块的地址我们也能算出来。
  • 知道LBA:块号 = LBA/8
  • 知道块号:LAB=块号*8 + n. (n是块内第⼏个扇区)

2-4引⼊"分区"概念

其实磁盘是可以被分成多个分区(partition)的,以Windows观点来看,你可能会有⼀块磁盘并且将它分区成C,D,E盘。那个C,D,E就是分区。分区从实质上说就是对硬盘的⼀种格式化。但是Linux的设备都是以⽂件形式存在,那是怎么分区的呢?柱⾯是分区的最⼩单位,我们可以利⽤参考柱⾯号码的⽅式来进⾏分区,其本质就是设置个区的起始柱⾯和结束柱⾯号码。 此时我们可以将硬盘上的柱⾯(分区)进⾏平铺,将其想象成⼀个⼤的平⾯,如下图所⽰:

注意:

柱⾯⼤⼩⼀致,扇区个位⼀致,那么其实只要知道每个分区的起始和结束柱⾯号,知道每

⼀个柱⾯多少个扇区,那么该分区多⼤,其实和解释LBA是多少也就清楚了

2-5深入理解 inode 与 Ext 系列文件系统

之前我们说过 ⽂件=数据+属性 ,我们使⽤ ls -l 的时候看到的除了看到⽂件名,还能看到⽂件元

数据(属性)。

解释这7列

  • 模式
  • 硬链接数
  • 文件所有者
  • 大小
  • 最后修改时间
  • 文件名

用户在命令行输入 ls -l 后,bash 会先解析命令,然后通过 fork 创建子进程,子进程再通过 exec 执行 ls -l。执行时,ls -l 通过系统调用将文件的属性信息从磁盘读到内核,再由内核复制到用户空间,最终显示出来。此外,解释中也提到了除了 ls -l,还可以用 stat 命令查看更详细的文件信息。展现了用户态命令如何通过内核获取磁盘上的文件属性这一完整过程。

其实信息除了通过ls -al(更详细)/ls -l这种方式来读取,还有一个 stat 命令能够查看更详细的信息。

其中我们选取 stat 中展示的一部分信息,将内存和磁盘之间交互时的基本单元大小 4kb 叫 Blocks,这里的 IO Block:4096 就是 8 × 512


Inode

⽂件数据都储存在"块"中,那么很显然,我们还必须找到⼀个地⽅储存⽂件的元信息(属性信息),⽐如⽂件的创建者、⽂件的创建⽇期、⽂件的⼤⼩等等。这种储存⽂件元信息的区域就叫做inode,中⽂译名为"索引节点"

每⼀个⽂件都有对应的inode,⾥⾯包含了与该⽂件有关的⼀些信息。

注意:

  • Linux下⽂件的存储是属性和内容分离存储的
  • Linux下,保存⽂件属性的集合叫做inode,⼀个⽂件,⼀个inode,inode内有⼀个唯⼀

的标识符,叫做inode号

所以⼀个⽂件的属性inode⻓什么样⼦呢?

cpp 复制代码
struct ext2_inode {
__le16 i_mode;                    /* File mode */
__le16 i_uid;                    /* Low 16 bits of Owner Uid */
__le32 i_size;                        /* Size in bytes */
__le32 i_atime;                       /* Access time */
__le32 i_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;



/*
* Constants relative to the data blocks
*/
#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)
备注:EXT2_N_BLOCKS = 15

⽂件名属性并未纳⼊到inode数据结构内部

inode的⼤⼩⼀般是128字节或者256


ext2文件系统

我们想要在硬盘上储⽂件,必须先把硬盘格式化为某种格式的⽂件系统,才能存储⽂件。⽂件系统的⽬的就是组织和管理硬盘中的⽂件。在Linux 系统中,最常⻅的是 ext2 系列的⽂件系统。其早期版本为 ext2,后来⼜发展出 ext3 和 ext4。ext3 和 ext4 虽然对 ext2 进⾏了增强,但是其核⼼设计并没有发⽣变化,我们仍是以较⽼的 ext2 作为演⽰对象。ext2⽂件系统将整个分区划分成若⼲个同样⼤⼩的块组 (Block Group),如下图所⽰。只要能管理⼀个分区就能管理所有分区,也就能管理所有磁盘⽂件。

从磁盘到分区:操作系统的抽象与管理

磁盘(如 800G 的机械硬盘)在物理上是一个复杂的圆形磁性介质。为了方便管理,操作系统首先将其抽象为一个线性的"数组",每个元素的大小为 512 字节(即一个扇区)。

然而,800G 的数组过于庞大,直接管理非常低效。因此,操作系统会对其进行分区(例如划分为 300G、300G、200G)。这就是我们电脑上 C盘、D盘、E盘的由来。分区的本质,是将一块大的物理磁盘,逻辑上切分成多个独立管理的区域。

格式化:写入文件系统

分区只是划分了空间范围(好比划定了省份的边界)。要真正让这块空间能够存储文件,还需要对其进行格式化 。格式化的本质就是在分区内写入一个文件系统

文件系统包含两大部分:

  • 数据:描述该分区自身的属性,如文件系统类型、总容量、已用空间等。

  • 方法:操作系统操作该文件系统的一系列函数和规则,如创建文件、删除文件、查找文件的具体步骤。

不同的分区可以使用不同的文件系统,就像不同省份可以因地制宜地制定不同的管理政策。Linux 系统支持多种文件系统(如 Ext2、Ext3、Ext4、XFS、Btrfs 等)。本篇文章以最常见的 Ext 系列文件系统为例进行探讨。

块组:分而治之的核心单元

格式化后的分区(例如 300G)依然很大。为了进一步精细化管理,Ext 文件系统会将其划分为若干个块组,每个块组的大小通常为 30G 左右。

每个块组都是一个自包含的、结构完全相同的小型文件系统。我们只需要彻底理解一个块组的管理方式,就能推及整个分区。这个结构,就是文件系统的核心布局。

关键概念:块

文件系统操作磁盘的基本单位不是扇区(512字节),而是 。块是多个连续扇区的集合,其大小在格式化时确定(通常为 4KB,即连续 8 个扇区)。mke2fs 命令的 -b 选项可以设置块大小(可为 1024、2048 或 4096 字节)。

4块组的内部结构:管理文件的基石

一个典型的 Ext 块组由以下几部分组成。你可以通过 dumpe2fs 命令查看磁盘的完整布局。

组成部分 作用 类比
超级块 描述整个文件系统的全局信息。 国家的宪法(全局属性)。
块组描述符表 描述每个块组自己的元信息(如块位图、inode位图的位置)。 每个省的国土规划。
块位图 以位图形式记录数据区中哪些块已被使用,哪些是空闲的。 一张标记了所有土地是否被占用的地图。
inode位图 以位图形式记录inode表中哪些 inode 已被使用,哪些是空闲的。 一张标记了所有人(inode)是否在岗的名单。
inode表 存放每个文件的属性(inode)。 存放所有人档案信息的档案室。
数据区 存放文件的实际内容 存放实际货物的仓库。

1 .超级块

这是文件系统的"心脏",存储了整个文件系统的关键信息,包括:

  • inode/块的总数、空闲数

  • 块和 inode 的大小

  • 文件系统的挂载时间、最近一次写入数据的时间、最近一次校验的时间等。

  • 存放⽂件系统本⾝的结构信息,描述整个分区的⽂件系统信息。记录的信息主要有:bolck 和 inode的总量,未使⽤的block和inode的数量,⼀个block和inode的⼤⼩,最近⼀次挂载的时间,最近⼀次写⼊数据的时间,最近⼀次检验磁盘的时间等其他⽂件系统的相关信息。
  • Super Block的信息被破坏,可以说整个⽂件系统结构就被破坏了超级块在每个块组的开头都有⼀份拷⻉(第⼀个块组必须有,后⾯的块组可以没有)。 为了保证⽂件系统在磁盘部分扇区出现物理问题的情况下还能正常⼯作,就必须保证⽂件系统的super block信息在这种情况下也能正常访问。所以⼀个⽂件系统的super block会在多个block group中进⾏备份,这些super block区域的数据保持⼀致。
cpp 复制代码
struct ext2_super_block {
__le32 s_inodes_count;                                /* Inodes count */
__le32 s_blocks_count;                        /* Blocks count */
__le32 s_r_blocks_count;                        /* Reserved blocks count */
__le32 s_free_blocks_count;                            /* Free blocks count */
__le32 s_free_inodes_count;                                /* Free inodes count */
__le32 s_first_data_block; /* First Data Block */
__le32 s_log_block_size;                        /* Block size */
__le32 s_log_frag_size;                        /* Fragment size */
__le32 s_blocks_per_group; /* # Blocks per group */
__le32 s_frags_per_group; /* # Fragments per group */
__le32 s_inodes_per_group; /* # Inodes per group */
__le32 s_mtime;                    /* Mount time */
__le32 s_wtime;                        /* Write time */
__le16 s_mnt_count;                    /* Mount count */
__le16 s_max_mnt_count;                        /* Maximal mount count */
__le16 s_ma

2.GDT(Group Descriptor Table):块组描述符表

块组描述符表,描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。每个块组描述符存储⼀个块组 的描述信息,如在这个块组中从哪⾥开始是inode Table,从哪⾥开始是Data

Blocks,空闲的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];
}

3. 块位图(Block Bitmap)与 inode 位图(inode Bitmap)

Block Bitmap中记录着Data Block中哪个数据块已经被占⽤,哪个数据块没有被占⽤

Inode位图(Inode Bitmap) 每个bit表⽰⼀个inode是否空闲可⽤。

这两个位图是管理空间分配的核心数据结构。它们使用一个比特位(0或1)来标记一个资源(数据块或 inode)的占用状态。

  • 1:表示该资源已被占用。

  • 0:表示该资源空闲可用。

例如,当我们要创建一个新文件时,文件系统会先扫描 inode 位图,找到第一个为 0 的位,将其占用(置为 1),并找到对应的 inode 来存放文件属性。同时,它还会根据文件大小,在块位图中找到若干个为 0 的位,占用它们,并让 inode 中的指针指向这些数据块。

4. inode 表:存放"属性"

这里存放的是文件属性 。请注意,文件名并不存放在 inode 中,而是存放在目录文件中。

  • 文件 = 属性(inode) + 内容(数据块)

  • 属性也是数据,同样占用空间。因此,即使是一个空文件,也会占用一个 inode(通常 128 或 256 字节)和零个或多个数据块。

  • inode 中包含的关键信息有:文件大小、权限(读/写/执行)、所有者(UID/GID)、时间戳(访问、修改、状态改变)以及指向数据块位置的指针

  • 当前分组所有Inode属性的集合

  • inode编号以分区为单位,整体划分,不可跨分区

5. 数据区(Data Block):存放"内容"

这是块组中最大的部分,用于存放文件的实际数据

  • 文件内容:一个普通文件的内容就存放在这里。

  • 目录内容 :一个目录也是一个文件,它的内容就是一张表,记录着该目录下所有文件的文件名inode 编号的映射关系。

  • 数据区:存放⽂件内容,也就是⼀个⼀个的Block。根据不同的⽂件类型有以下⼏种情况:

  • 对于普通⽂件,⽂件的数据存储在数据块中。

  • 对于⽬录,该⽬录下的所有⽂件名和⽬录名存储在所在⽬录的数据块中,除了⽂件名外,ls -l命令看到的其它信息保存在该⽂件的inode中。

  • Block 号按照分区划分,不可跨分区

6.inode和datablock映射(弱化)

  • inode内部存在 __le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */ , EXT2_N_BLOCKS =15,就是⽤来进⾏inode和block映射的
  • 这样⽂件=内容+属性,就都能找到了。
cpp 复制代码
struct ext2_inode {
__le16 i_mode;                    /* File mode */
__le16 i_uid;                    /* Low 16 bits of Owner Uid */
__le32 i_size;                        /* Size in bytes */
__le32 i_atime;                /* Access time */
__le32 i_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 的⼤⼩通常是 128 字节 或 256 字节

为了说明问题,我们将上图简化:

创建⼀个新⽂件主要有以下4个操作:

  1. 存储属性

内核先找到⼀个空闲的i节点(这⾥是263466)。内核把⽂件信息记录到其中。

  1. 存储数据

该⽂件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第⼀块

数据复制到300,下⼀块复制到500,以此类推。

  1. 记录分配情况

⽂件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。

  1. 添加⽂件名到⽬录

新的⽂件名abc。linux如何在当前的⽬录中记录这个⽂件?内核将⼊⼝(263466,abc)添加到

⽬录⽂件。⽂件名和inode之间的对应关系将⽂件名和⽂件的内容及属性连接起来。


7. 如何找到一个文件?

当你在 Linux 中访问 /home/user/file.txt 时,文件系统的寻址过程如下:

  1. 从根目录开始:从超级块中找到根目录"/"的 inode 编号(通常是 2)。

  2. 解析目录 :读取根目录的 inode,找到其数据块。在数据块中查找"home"目录项,获取 /home 的 inode 编号。

  3. 逐级查找 :重复上述过程,读取 /home 的 inode 和数据块,找到"user"的 inode 编号。

  4. 定位目标文件 :读取 /home/user 的 inode 和数据块,找到 file.txt 的 inode 编号。

  5. 读取文件 :读取 file.txt 的 inode,获取其属性(大小、权限等)和指向数据块指针。

  6. 获取内容:根据数据块指针,从数据区读取文件的实际内容。

找到文件:inode 编号 -> 分区特定的 BlockGroup -> inode -> 属性 -> 内容


inode 如何找到数据块:直接指针与间接寻址

1. 核心回顾:文件 = 属性 + 内容,分开存储

  • 属性 (inode):存放在 inode 表 中,每个 inode 固定大小(Ext 系列通常是 128 字节或 256 字节)

  • 内容 :存放在 数据块 中,每个数据块固定大小(通常是 4KB)

重要:一个文件或目录对应一个 inode。文件名不在 inode 中,而是存放在目录文件的数据块里。

2. inode 中的数据块指针数组:block[ ]

每个 inode 中包含一个数组 ,用于指向该文件所占用的数据块。以常见的 Ext2/Ext3/Ext4 实现为例,这个数组通常有 32 个元素(具体数量因文件系统版本和格式参数而异,这里用 32 举例说明原理):

指针类型 数组下标范围 说明
直接指针 block[0] ~ block[11] 直接指向存储文件内容的数据块
间接指针 block[12] 指向一个间接块,该块内存放多个指向数据块的指针
双重间接指针 block[13] 指向一个双重间接块,该块指向多个间接块
三重间接指针 block[14] 指向一个三重间接块,用于超大文件

3. 举例说明:小文件如何通过 inode 找到数据块

  • 每个数据块大小 = 4KB

  • 文件需要占用 2 个数据块

步骤:

  1. 创建文件时,文件系统分配一个空闲的 inode(比如 inode 编号 = 1)

  2. 文件系统把文件的属性(大小、权限、时间戳等)写入 inode 1

  3. 文件系统从数据区分配两个空闲数据块(比如数据块编号 = 2 和 3)

  4. 文件系统将这两个数据块编号填入 inode 1 的 block 数组中:

    • block[0] = 2

    • block[1] = 3

    • 其他 block[2] ~ block[31] 为 0(表示未使用)

结果 :inode 1 通过 block[0]block[1] 就能找到文件内容所在的物理数据块。


4. 大文件问题:数据块不够怎么办?

上述例子中,直接指针只有 12 个,最大能表示:

12 × 4KB = 48KB

如果文件超过 48KB(比如 1GB,甚至 10TB),直接指针显然不够用。文件系统的解决方案是:

间接指针

  • block[12] 指向一个间接块 ,这个间接块本身是一个数据块(4KB),但它里面存放的不是文件内容,而是其他数据块的编号

  • 每个数据块编号假设占 4 字节,那么一个 4KB 的间接块可以存放:

4KB ÷ 4B = 1024 个 数据块指针

通过一级间接指针能访问的额外容量

1024 × 4KB = 4MB

双重间接指针

  • block[13] 指向一个双重间接块 ,这个块里存放的是多个间接块的编号。

  • 双重间接能访问的容量:

    1024(间接块数)× 1024(每个间接块的指针数)× 4KB = 4GB

三重间接指针

  • block[14] 用于更大文件,理论上支持 TB 甚至 PB 级别。

整体结构类似 B+ 树:直接指针 → 间接指针 → 双重间接 → 三重间接,按需使用。


5. 真正标识文件的不是文件名,而是 inode 编号

由于 inode 包含了文件的所有属性以及指向数据块的指针,操作系统在内部真正用来标识文件的是 inode 编号

  • 文件名 只是给人看的,它存放在目录文件的数据块中,与 inode 编号形成映射。

  • 目录文件的内容本质上是一个表格:

    文件名 inode 编号
    test.txt 12345
    a.log 67890

当你打开 test.txt 时:

  1. 系统在目录中找到文件名对应的 inode 编号(12345)

  2. 根据 inode 编号找到 inode 表里的 inode 12345

  3. 通过 inode 中的 block[] 数组找到数据块

  4. 读取文件内容


⽬录与⽂件名

问题

  • 我们访问⽂件,都是⽤的⽂件名,没⽤过inode号啊?
  • ⽬录是⽂件吗?如何理解?

答案:

  • ⽬录也是⽂件,但是磁盘上没有⽬录的概念,只有⽂件属性+⽂件内容的概念。
  • 目录有自己的 inode 和自己的 data block。目录的 data block 中存储的是文件名 -->inode 编号的映射关系,两者成对存储,互相绑定。通过文件名可以高效地找到对应的 inode 编号(正向查找),但反过来通过 inode 编号找文件名则需要遍历目录内容,没有直接的逆向索引。

如何理解删除文件

计算机中删除一个文件并不是真正的删除,而是把那块空间标识为无效。就像图书馆里有一本书被同学借走了,管理员在借书卡上写上他的名字,表示这本书被占用;还书时,管理员只需要把借书卡上的名字划掉,表示这本书现在是空闲的,可以被下一个人借,而不需要把书烧掉或把书上的字擦掉。

删除文件时,不需要把 inode 属性清空,不需要把 inode 对应的数据块清空,只需要做三件事:第一,把块位图中对应的比特位由 1 改为 0,表示这些数据块现在是空闲的,可以被覆盖。第二,把 inode 位图中对应的比特位由 1 改为 0,表示这个 inode 现在是空闲的,可以被重新分配。第三,把所在目录的 data block 中,该文件名与 inode 编号的映射关系删除。

此时文件占用的空间就是无效的,当下一次再新建文件时,系统就可以直接把这些标记为 0 的空间覆盖使用,就像新同学借书时在借书卡上写上自己的名字,把原来划掉的名字覆盖掉。

按上面这样说,删除后的文件当然可以恢复。Windows 下的回收站就是一个特殊的目录,当你删除文件时,系统并没有真正执行上述的位图修改,而是把文件移动到回收站目录下,把原目录数据块中的映射关系移动到回收站目录下的数据块中。所以回收站里的文件可以轻松还原,就像借书卡上的名字只是被划掉但还能看清。

Windows 下就算把回收站的内容清空,文件也是能恢复的,因为此时只是把位图标记为 0,真正的数据还在磁盘上。Linux 下如果要恢复删除的文件,是有一些恢复工具的,但恢复过程中可能会创建各种临时文件、日志文件,这些新文件有可能恰好覆盖掉你想恢复的那块数据空间,导致恢复失败,就像新同学恰好借走了同一本书,在借书卡上写上了自己的名字,把原来的名字彻底盖住了。

如果想自己动手恢复删除的文件,就需要更深入地了解文件系统原理,比如直接分析块位图和 inode 位图,或者使用专业工具逐扇区扫描。

**总结:**删除文件只是把"这块空间有人用"的标记擦掉,而不是把内容擦掉。数据还在,只是允许被覆盖。


三.软硬件链接

硬链接是通过 inode 引用另外一个文件,软链接是通过名字引用另外一个文件。

软链接

创建的软链接示例中,soft.link 是一个独立的文件,它拥有自己的 inode(编号为 787508),这与原文件 test.c 的 inode(编号为 787190)完全不同。soft.link 的类型标识为 l(表示 link),权限为 rwxrwxrwx(所有人都有读写执行权限,但实际权限取决于原文件),大小只有 6 字节,这 6 字节存储的正是它指向的目标路径字符串 test.c。当你访问 soft.link 时,操作系统会根据它数据块中存储的路径,自动重定向到原文件 test.c。这就类似于 Windows 下的快捷方式:桌面上的快捷方式图标只是一个指向真实程序的路径,删除快捷方式不会影响原文件,但如果删除原文件,软链接就会变成一个失效的"断链"。软链接可以跨文件系统创建,也可以指向目录,这是它相比硬链接的一个优势。总之,软链接是一个独立的、存储路径信息的文件,它的作用就是为用户提供一个便捷的访问入口,指向磁盘上另一个文件或目录。


硬链接

首先执行了 ln test.c hard.link,给 test.c 创建了一个硬链接。创建完成后,ls -li 的输出显示 test.chard.link 的 inode 编号都是 787190 ,这说明两者本质上指向的是同一个文件实体,硬链接并不是一个独立的文件,而只是给同一个 inode 起了第二个名字。同时,第三列的硬链接数从原来的 1 变成了 2,表示现在有两个文件名指向这个 inode。接着你执行 rm test.c 删除了原文件,再 ls -li 时可以看到 hard.link 依然存在,inode 仍然是 787190,只是硬链接数从 2 减为了 1。这意味着文件的实际内容并没有被删除,因为还有一个 hard.link 在引用这个 inode,只有硬链接数减到 0 时,系统才会真正释放 inode 和对应的数据块。而图片中的软链接 soft.link 则变成了断链,因为它指向的 test.c 已经不存在了。总之,硬链接的本质是在目录的数据块中给同一个 inode 创建多个别名,所有别名共享同一份数据,删除其中一个别名只是减少引用计数,数据不会被真正清除。


硬链接的应用

观察第三列(硬链接数):

  • 普通文件 file 的硬链接数是 1

  • 目录 dir 的硬链接数是 2

普通文件 file 的硬链接数是 1,表示当前目录中只有一组 file 这个文件名和它的 inode 的映射关系。没有其他名字指向这个 inode,所以是 1。

发现:

  • .(当前目录)的 inode 是 1052194 ,和 dir 的 inode 一模一样

  • 这意味着 . 就是 dir 的别名

所以 dir 的硬链接数是 2,是因为:

  • 一个名字是 dir(在父目录的数据块中)

  • 另一个名字是 .(在 dir 自己的数据块中)

这就是目录硬链接数为 2 的原因

在目录下再创建一个子目录,硬链接数如何变化?

  • dir 的硬链接数从 2 变成了 3

  • 新建的 other 目录的硬链接数是 2

为什么 dir 的硬链接数变成了 3?

因为 other 目录下有一个 ..(上级目录),这个 .. 指向的就是 dir

所以现在指向 dir 这个 inode 的名字有三个:

  1. dir(在父目录的数据块中)

  2. .(在 dir 自己的数据块中)

  3. ..(在 other 的数据块中)

因此 dir 的硬链接数变成了 3。

为什么 other 的硬链接数是 2?

other 是新创建的空目录,它的硬链接数 2 来自:

  1. other(在 dir 的数据块中)

  2. .(在 other 自己的数据块中)

.. 是指向 dir 的,不是指向 other 的,所以不计入 other 的硬链接数。

录类型 硬链接数 解释
新建的空目录 2 目录名 + .
包含 N 个子目录的目录 2 + N 目录名 + . + 每个子目录里的 ..

普通文件的硬链接数:默认是 1,每多一个硬链接就 +1。

硬链接的典型应用场景

硬链接最常见的应用就是方便路径转换

  • . 让我们可以用 ./xxx 访问当前目录

  • .. 让我们可以用 ../xxx 访问父目录


文件的ACM

访问时间 Access 文件最近一次被访问的时间 读取文件内容时

修改时间 Modify 文件内容最后被修改的时间 修改文件内容时

改变时间 Change 文件属性最后被修改的时间 修改 inode 中的属性时

  • 如果文件不存在,会创建一个空文件

  • 如果文件已存在,会刷新文件的 Access、Modify、Change 三个时间到当前时间

结果:三个时间都变成了当前时间。

vim 编辑文件:三个时间都会变化

vim 打开文件并写入内容:

结果:

  • Access:更新(因为访问了文件)

  • Modify:更新(因为修改了内容)

  • Change:更新(因为文件大小等属性变了)

注意 :即使你只是 vim 打开文件,不写入任何内容就保存退出,三个时间也会被刷新(不同 Linux 版本可能略有差异)。

chmod 修改权限:只有 Change 更新

结果:

  • Access:不变(没有读取文件内容)

  • Modify:不变(没有修改文件内容)

  • Change:更新(文件属性改变了)

因为 chmod 只修改 inode 中的权限位,不涉及文件内容,所以只有 Change 时间变化。

cat 读取文件:Access 不一定更新

你可能会发现 Access 时间没有变化

原因:Linux 2.6 内核之后,为了提升性能,不再每次访问文件都立即更新 Access 时间。因为访问文件是非常高频的操作,如果每次都写磁盘更新 Access 时间,会严重影响系统效率。不同的 Linux 版本策略不同,有的需要多次访问才会刷新,有的甚至默认关闭了 Access 时间的实时更新


三个时间的实际应用:make 的编译检测

make 工具通过比较文件的修改时间(Modify) 来判断是否需要重新编译:

  • 源文件(如 file.c)的修改时间晚于目标文件(如 file.o 或可执行程序)→ 需要重新编译

  • 源文件的修改时间早于目标文件 → 不需要重新编译

注意vim 即使没有修改内容,只要保存了,Modify 时间也会更新,所以 make 会认为文件被修改了,从而触发重新编译。

记忆:

Access 记录访问,Modify 记录内容修改,Change 记录属性修改。make 通过 Modify 时间判断是否需要重新编译。Linux 为了性能,不再每次访问都实时更新 Access 时间。

相关推荐
Donk_672 小时前
Shell 数组实践
linux·算法·bash
XMAIPC_Robot2 小时前
电力设备RK3568/RK3576+FPGA,多系统混合部署Linux+RTOS RT-THREAD,强实时性
linux·运维·fpga开发
aashuii2 小时前
linux测试lsquic
linux·运维·服务器
认真的薛薛2 小时前
Linux运维:Jenkins部署
linux·运维·jenkins
o丁二黄o2 小时前
Gemini镜像站办公效能深度解析:多模态链式调用与自动化工作流构建指南
运维·人工智能·自动化
叶半欲缺2 小时前
密码忘记了吗?Linux单用户模式下修改密码!
linux·运维·服务器
handler013 小时前
【Linux 网络】一文读懂 HTTP 协议
linux·c语言·网络·c++·笔记·网络协议·http
铅笔小新z3 小时前
【Linux】进程信号
linux·服务器