当前位置: 首页 > news >正文

Linux基础IO---缓冲区----文件系统----软硬链接

在 Linux 的 文件 I/O(输入 / 输出) 中,缓冲区(Buffer) 是用于暂存文件数据的内存区域,主要作用是减少磁盘 I/O 次数、提升读写效率。以下是文件 I/O 中缓冲区的核心内容:

一、文件 I/O 缓冲区的分类

根据缓冲区的位置和管理方式,可分为两类:

1. 内核缓冲区(Kernel Buffer)
  • 位置:位于操作系统内核空间,由内核直接管理。
  • 作用
    • 所有文件系统的读写操作(如通过 open/read/write 系统调用)都会经过内核缓冲区。
    • 读文件时,数据从磁盘先加载到内核缓冲区,再复制到用户空间;
    • 写文件时,数据先存入内核缓冲区(称为 脏数据),再异步写入磁盘。
  • 实现原理
    • 基于 页缓存(Page Cache):Linux 将内存划分为固定大小的页(通常 4KB),文件数据以页为单位缓存到内核内存中。
    • 异步写入机制:内核通过后台进程(如 pdflush/kswapd)定期将脏数据写入磁盘,或在内存不足时触发写入。
2. 用户空间缓冲区(User Space Buffer)
  • 位置:位于应用程序的用户空间(如通过 malloc 分配的内存)。
  • 作用
    • 由应用程序自行管理,用于暂存数据以减少系统调用次数。
    • 典型场景:标准 I/O 库(如 C 语言的 fread/fwrite)通过用户空间缓冲区实现 全缓冲行缓冲 或 无缓冲 的模式。

为什么要有缓冲区呢?

为了提高使用者效率->用空间换时间

  • 内核缓冲区 利用页缓存和异步写入减少磁盘 I/O;
  • 用户空间缓冲区 通过批量操作减少系统调用开销。
    理解缓冲区的工作原理(如脏数据同步时机、fsync 的作用)有助于在开发中平衡性能与数据可靠性,避免因缓冲区未刷新导致的数据丢失问题。

二、观察用户缓冲区(C语言)

观察下面这段代码的运行情况:

#include <stdio.h>
#include <unistd.h>
int main()
{//cprintf("hello printf\n");fputs("hello fputs\n", stdout);//systemwrite(1, "hello write\n", 12);fork();return 0;
}

执行结果: 

一切正常

但是当我们把结果重定向到log.txt中时:

发现 write调用只打印一次,printf和fput分别调用两次,并且再write后打印出来

这是为什么呢?

首先,缓冲区的缓冲方式有3种:

1.无缓冲

2.行缓冲

3.全缓冲

我们第一次执行程序的时候就是行缓冲,因为每一次打印都带了"\n",所以在代码执行完后,对应的数据就立刻被刷新到显示器上。

那么为什么输入到文件里就不是行缓冲了呢?

在 Linux 中,标准 I/O 库(如 printffputs)的缓冲模式会根据 输出设备类型 自动调整

默认输出到终端时,启用行缓冲

输出到非终端,如文件、管道、套接字时,启用全缓冲

  1. 行缓冲(Line Buffered)

    • 当输出到 终端(TTY) 时,默认启用行缓冲。
    • 缓冲区在以下情况刷新:
      • 遇到换行符 \n
      • 缓冲区满(通常 1024 字节);
      • 手动调用 fflush()
  2. 全缓冲(Fully Buffered)

    • 当输出到 非终端设备(如文件、管道、套接字)时,默认启用全缓冲。
    • 缓冲区仅在以下情况刷新:
      • 缓冲区满(通常 4KB/8KB);
      • 手动调用 fflush()
      • 程序正常退出(如 exit() 或 return)。
  3. 无缓冲(Unbuffered)

    • 标准错误输出 stderr 始终为无缓冲,确保错误信息立即显示。、

所以在全缓冲的情况下,程序没有手动调用fflush,两句打印也不至于塞满缓冲区,所以缓冲区刷新就是在程序退出的时候。

那么为什么是write先刷新出来呢,就算是退出的时候刷新缓冲区难到不应该是先printf和fputs吗?

那是因为write的缓冲区和另外两个的缓冲区不一样。

  1. write 直接操作内核缓冲区,数据立即进入内核管理的输出队列;
  2. 标准 I/O 函数依赖用户空间缓冲,数据需满足特定条件才会刷新到内核。

用户缓冲区和内核缓冲区并不一样。

用户缓冲区是C语言自带的,C接口的内容先写到用户缓冲区里并未刷新到内核缓冲区里。

此时创建了子进程,子进程继承父进程的数据和代码,所以也同时也拥有父进程的缓冲区。

当父子进程退出时满足全缓冲的刷新条件了,就开始刷新缓冲区,由于刷新缓冲区也是修改数据,那么就会触发写时拷贝,于是父子进程各自就有一份缓冲区,刷新了两份内容到内核缓冲区,所以导致最后打印两次到文件里。

先刷新到内核缓冲区然后再输出到文件,肯定比直接就在内核缓冲区然后写入到文件漫,所以write比另外俩快。

理解文件系统

我们之前讨论的都是被进程所打开的文件,那么操作系统中被打开的文件一般是少数,其他更多的文件是没有被打开的存在磁盘中的文件,接下来我们就来谈谈磁盘文件。

inode

磁盘文件由两部分组成:内容+属性。

文件的内容就是其中存放的数据,文件的的属性就是文件的大小、最近修改时间、权限等等,也成为元信息。

使用命令 ls -l 可以查看文件的元信息:

在 Linux 系统中,inode(索引节点) 是文件系统的核心数据结构,用于存储文件和目录的元信息(如权限、所有者、时间戳等),但不包含文件名和文件内容。

定义与作用
  • inode 是 Index Node 的缩写,是 Linux 文件系统中存储文件元数据的数据结构。
  • 每个文件和目录在文件系统中都有唯一的 inode 编号(inode number),用于标识和访问文件。

inode 包含以下关键信息:

  • 文件类型(普通文件、目录、符号链接、设备文件等)。
  • 权限信息(读 / 写 / 执行权限)。
  • 所有者和所属组(文件的用户 ID 和组 ID)。
  • 时间戳(创建时间、修改时间、访问时间)。
  • 文件大小使用的块数
  • 数据块指针(指向存储文件内容的数据块位置)。
  • 文件链接数(硬链接数量)。

为什么inode不包含文件名?

分离元数据与目录结构

  1. inode 专注存储文件元数据
    inode 的核心功能是存储文件的元信息(如权限、所有者、数据块指针等),这些信息是文件内容相关的核心属性,与文件在目录中的位置无关。

    • 例如:一个文件被移动到不同目录时,文件名会变化,但 inode 及其元数据保持不变。
    • 若 inode 存储文件名,当文件移动或重命名时,需要修改 inode 内容,这会增加磁盘 I/O 开销和实现复杂度。
  2. 目录作为 “文件名 - inode 映射表”
    目录本质是一种特殊文件,其数据块中存储的是 文件名与 inode 编号的映射关系

    • 例如:目录 /home/user 中包含文件名 file.txt,其对应 inode 编号为 1234。当访问 file.txt 时,系统先通过目录找到 inode 编号,再通过 inode 读取文件内容。
    • 这种分离设计使得文件名的修改(如重命名、移动)只需修改目录中的映射关系,而无需触及 inode,提升了灵活性和效率。

 通过命令 ls -i -l查看文件的inode编号:

磁盘的概念

磁盘的概念:

磁盘是用电磁原理记录数据的存储介质,通常由涂有磁性物质的盘片和读写装置(驱动器)组成。

磁盘可以作为输出设备也可以作为输入设备。

磁盘的存储构成:

对其中重要的部分:盘片、柱面、磁道和扇区,进行讲解。

盘片:

每个盘片的上下两个面都有存储信息,一个磁盘通常是多个盘片堆叠在一起的。通常磁盘容量越大盘片越多。

工作原理
  • 盘片通过马达驱动高速旋转(转速如 5400 RPM),磁头悬浮在盘片表面(距离约几纳米),通过改变磁性材料的极性来读写数据。
  • 每个盘片的每个面对应一个独立的磁头,多个磁头通过磁头臂联动同步移动。

磁道:

  • 磁道是盘片表面以盘片中心为圆心的同心圆环,是盘片格式化后的逻辑划分。
特点
  • 磁道密度:单位长度内的磁道数量,外圈周长更长,但磁道密度(每英寸磁道数)与内圈相同。
  • 用途:磁道是盘片上数据存储的一级逻辑单位,进一步划分为扇区。

扇区:

  • 扇区是磁道上等角度划分的弧段,是机械硬盘最小的物理存储单元
  • 每个磁道被划分为若干扇区(如每个磁道有 512 个扇区),扇区之间以 间隙(Gap) 分隔,用于磁头识别扇区边界。

作用

操作系统读写数据时,必须以扇区为单位(即使只需读写 1 字节,也需读取整个扇区)

柱面:

  • 柱面所有盘片上相同磁道号的磁道集合。例如,所有盘片的 0 磁道共同构成 “0 柱面”,所有盘片的 1 磁道构成 “1 柱面”,依此类推。
  • 柱面是一个逻辑概念,用于简化磁头定位的计算(尤其是在分区时)
作用
  • 在传统分区方式(如 MBR 分区)中,分区的边界通常按柱面划分,便于磁头在多个盘片的同一磁道上连续读写数据,减少寻道时间。
  • 磁头定位逻辑:机械硬盘通过 “柱面号(磁道号)+ 磁头号(盘面号)+ 扇区号” 三维地址定位数据,其中:
    • 柱面号:确定磁头臂移动的水平位置(对应磁道);
    • 磁头号:确定读写哪个盘片的哪个面;
    • 扇区号:确定在磁道上的具体存储位置。

磁盘的寻址方式

对磁盘进行读写操作是,一般通过以下方式找到数据:

1.确定数据在哪个盘面。

2.确定数据在那个柱面。

3.确定数据再哪个扇区。

磁盘分区与格式化

磁盘的逻辑结构是线性的存储结构。就像被卷起来的磁带,把磁带拉长后就是一个线性的结构,圆形的磁盘其实逻辑结构也是线性的

磁盘分区、

一个扇区的大小通常是512字节。我们以512G的磁盘举例,该磁盘就可以分为10亿多个 扇区。

磁盘分区:

在Windows操作系统下磁盘的分区就是把一块磁盘分为为C盘和D盘等等,一旦划分成数个分区,不同的目录与文件就可以存储进不同的分区,分区越多,就可以将文件的性质区分得越细,按照更为细分的性质,存储在不同的地方以管理文件。

操作系统怎么分别不同的分区的?

通过一个分区的结构体:

struct partion
{int start;int end;
}

start和end分别指向分区的开头和结尾。将所有分区放在数组里 struct partion part[],得到这样一个分区表,这样方便操作系统访问。

每个分区也有更详细的划分

EXT2文件系统的存储方案

EXT2(Second Extended File System)是 Linux 系统早期广泛使用的文件系统,其设计目标是高效、可靠地管理磁盘存储。EXT2 的存储方案基于块存储索引节点(inode) 机制,将磁盘空间划分为多个逻辑单元进行管理

EXT2 文件系统的整体结构

EXT2 将磁盘划分为多个块组(Block Group),每个块组是文件系统的基本管理单元,包含独立的元数据和数据存储区域。

块组的核心组成部分:
  1. 引导块(Boot Block)

    • 位于磁盘起始位置(第 0 块),存储引导程序(如 GRUB),用于系统启动。
    • 仅第一个块组包含引导块,其他块组的引导块保留未用。
  2. 超级块(Super Block)

    • 存储文件系统的全局元数据,例如:
      • 块大小、inode 总数、块组数量、文件系统状态(是否挂载、是否干净)等。
    • 每个块组都有超级块的副本,以提高可靠性(如第一个块组的超级块损坏时,可从其他块组恢复)。
  3. inode 位图(inode Bitmap)

    • 每个比特位对应一个 inode,表示该 inode 是否已被占用(1 表示已用,0 表示空闲)。
  4. 块位图(Block Bitmap)

    • 每个比特位对应一个数据块,表示该块是否已被占用(1 表示已用,0 表示空闲)。
  5. inode 表(inode Table)

    • 存储所有 inode 的集合,每个 inode 对应一个文件或目录的元数据(如权限、所有者、修改时间、数据块指针等)。
  6. 数据块(Data Blocks)

    • 实际存储文件内容和目录数据的区域,按块大小(如 1KB、2KB、4KB)分配。

group descriptor table
        

        描述的是整个分组的使用情况——比如我们block bitmap的起始地址, 使用了多少空间、inode bitmap的起始地址, 使用了多少空间。 inode table的起始地址、data block的数量, 空间块数量等等。 以及我们在分配inode的时候或者分配block的时候, 我们的inode或者data block的编号下一个要从哪里开始分配。这个也就解决了我们想要拿到块编号和inode编号来定位bitmap中比特位的问题。 

        这里之所以没有使用bitmap来直接计算block和inode的使用情况是因为计算的过程效率不如直接使用统计效率高。 所以这里直接使用了Group Descriptor Table。

注意:Boot Block启动块的大小是固定的,由格式化的时候确定的,并且不可以更改。

           磁盘分区并格式化后,每个分区的inode个数就确定了。

操作系统的对磁盘的管理方式——分而治之

对于512G的磁盘分区后一个区256G,再把打分区分成小分区比如10G,管理好小分区就能管理好大分区,管理好大分区就能管理好整个磁盘,这就是分治的思想。

如何创建一个文件?

1、首先遍历inode位图,找到空闲的inode。

2、在inode表中找到对应位图inode,将文件元信息填入inode结构中。

3.将该文件的文件名和inode号建立映射,然后将文件名和inode指针添加到目录的数据块中。

如何理解对文件写入信息?

通过文件的inode编号找到对应的inode结构。
通过inode结构找到存储该文件内容的数据块,并将数据写入数据块。
若不存在数据块或申请的数据块已被写满,则通过遍历块位图的方式找到一个空闲的块号,并在数据区当中找到对应的空闲块,再将数据写入数据块,最后还需要建立数据块和inode结构的对应关系。

一个文件使用的数据块和inode结构的对应关系,是通过一个数组进行维护的,该数组一般可以存储15个元素,其中前12个元素分别对应该文件使用的12个数据块,剩余的三个元素分别是一级索引、二级索引和三级索引,当该文件使用数据块的个数超过12个时,可以用这三个索引进行数据块扩充。

如何删除一个文件?

1、将该文件的inode在inode位图中对应的位置置为无效。

2、将该文件申请过的数据块在块位图当中置为无效

该操作并不会直接把数据全部置0,只是告诉操作系统这块磁盘“空出来了”,所以在其他文件写入内容前,其实都是可以恢复删除的文件的。

为什么删除文件很快,拷贝文件却很慢?

删除的原理刚才讲过了,其实根本不需要真的去删除数据,所以很快,拷贝是实打实的要先创建文件,然后对文件写入,inode申请数据块申请都不能少最后还要写入所以慢。

如何理解目录

Linux下一切皆文件,所以目录也是文件,只不过目录里的数据是存储在该目录下的文件名其对应的inode映射关系。

如何查看目录的inode呢?

其实很简单,去上级目录查找就行了,一直往上到根目录,从根目录递归返回所有目录的inode。

软硬链接

软链接:

什么是软连接?

其实软链接很想Windows系统下的快捷方式。

通过命令ln -s   目标文件  创建文件    的格式可以创建软链接

查看文件inode发现软链接的inode和被链接的文件不同,说明软链接时直接创建了一个新的文件,并且软链接文件的大小比原文件小得多。

 软链接文件内容和被链接文件完全一样:

硬链接

用命令 ln 目标文件 创建文件 的格式创建硬链接:

可以看见硬链接创建的文件inode和被链接文件一模一样,并且文件大小也一样。

同时注意:两个文件的硬链接数都变成了2

既然硬链接文件没有自己的inode代表他就不是一个独立的文件,硬链接文件其实是被链接文件的别名,该文件有几个硬链接硬链接数就是几。

现在我们删除原文件log.txt.

我们发现硬链接数变成了1,分别尝试查看软硬链接的内容:

发现硬链接任然能够正常查看,但是软链接报错说找不到文件。

为什么硬链接创建的文件还能正常打开?

软链接打不开好理解,快捷方式指向的文件名都没了,没有文件名就找不到文件名对应的inode,所以就打不开。

那么硬链接呢?

其实硬链接的本质是在目录的数据块里增加一个文件名和inode的映射。

什么意思呢,一个inode可以有多个文件名和其创建映射,这些多出来的映射就是硬链接,我们删除一个文件,该inode就少一个映射,当链接inode的文件名全部删除也就是映射为0的时候文就被彻底删除了。对于文件来说,无论文件名怎么变,inode是创建文件的时候就固定好的。

对于软链接:

软链接作为快捷方式,拥有自己的inode,那么他的数据块里存的是什么内容呢?

软链接的数据块内存储的是他所链接的文件的路径。所以删掉软链接并不会影响被链接文件,删除被链接文件就会发生找不到文件的错误。

对于硬链接:

为什么文件夹的硬链接数是2?

这是因为每个目录文件下都有一个“.”目录文件

这个.就是当前目录本身的硬链接,我们可以查看inode确认:

我们继续观察还可以看到目录里还有一个".."文件夹,这个文件夹是上级目录

通过cd .. 操作进入上级目录可以发现上级目录的inode和..文件的inode相同,他的硬链接数是4

那么是哪里来的4个呢?

首先目录本身不说了,其次是自己目录下的.文件,还有两个硬链接是什么呢,就是其目录下的目录里的..文件。

也就是说一个目录的硬链接数-2(自己和.文件)剩下的数就是该目录下保存的目录数量!

注意:Linux不允许用户创建目录的硬链接,那我问你,这个 . 和..文件不就是目录的硬链接吗?

我只能回答系统可以任性...

那么为什么不让用户创建目录的硬链接呢?

  • 无限递归遍历:当系统遍历目录时(如执行 lsfind 命令),循环链接会使程序陷入死循环,无法终止,甚至耗尽系统资源。
  • 元数据混乱:目录的硬链接会增加其 “硬链接计数”(inode 中的 i_nlink 字段),但内核难以正确维护循环链接中的目录层次关系,可能导致文件系统元数据损坏

相关文章:

  • Python打卡训练营学习记录Day36
  • Spring AI 与 Python:AI 开发的新老势力对决与协作
  • git提交通用规范
  • 阿里云国际版香港轻量云服务器:CN2 GIA加持,征服海外网络的“速度与激情”!
  • 掌握聚合函数:COUNT,MAX,MIN,SUM,AVG,GROUP BY和HAVING子句的用法,Where和HAVING的区别
  • 成功解决ImportError: cannot import name ‘DTensor‘ from ‘torch.distributed.tensor‘
  • 题目 3327: 蓝桥杯2025年第十六届省赛真题-倒水
  • loss的范围
  • 读《Go语言圣经》记录(一)
  • uni-app学习笔记十三-vue3中slot插槽的使用
  • QML与C++交互2
  • 【电子通识】连接器的绝缘胶座和接触端子基础知识
  • 数据结构 栈的详细解析
  • Java——优先级队列(堆)
  • 明阳智慧能源社招校招入职测评 |iLogic言语逻辑数字、Talent5大五职业性格测评、TAS倍智人才测评考什么
  • JVM规范之栈帧
  • 我的第1个爬虫程序——豆瓣Top250爬虫的详细步骤指南
  • uni-app学习笔记十二-vue3中组件传值(对象传值)
  • 字节跳动GPU Scale-up互联技术白皮书
  • 【数据结构】实现方式、应用场景与优缺点的系统总结
  • 做计量检定的网站/百度推广开户费用多少
  • 哪个找房网站好/软文推广案例大全
  • 让其他公司做网站应注意什么/百度信息流推广技巧
  • 网站建设任务书/百度seo排名培训 优化
  • 山东建设厅官方网站临沂/网上营销型网站
  • 浅谈幼儿园网站的建设与管理制度/创意设计