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

Linux 文件系统与 I/O 编程核心原理及实践笔记

文章目录

  • 一、理解文件
    • 1.1 狭义理解
    • 1.2 广义理解
    • 1.3 文件操作的归类认识
    • 1.4 系统角度:进程与文件的交互
    • 1.5 实践示例
  • 二、回顾 C 文件接口
    • 2.1 hello.c 打开文件
    • 2.2 hello.c 写文件
    • 2.3 hello.c 读文件
    • 2.4 输出信息到显示器的几种方法
    • 2.5 stdin & stdout & stderr
  • 三、系统文件I/O
    • 3.1 一种传递标志位的方法
    • 3.2 接口介绍
    • 3.5 3-5 open函数返回值
    • 3.6 文件描述符 fd
      • 3.6.1 0 & 1 & 2
      • 3.6.2 文件描述符的分配规则:最小未使用下标优先
      • 3.6.3 应用场景:重定向的实现
      • 3.6.4 dup2 系统调用
  • 五、缓冲区
    • 5.1 什么是缓冲区
    • 5.2 为什么要引入缓冲区机制
    • 5.3 缓冲类型

一、理解文件

1.1 狭义理解

  • 文件在磁盘里
    *磁盘作为永久性存储介质,通过文件系统(如 EXT4、XFS)管理文件存储。文件系统将磁盘划分为inode(索引节点)和block(数据块)
    • inode:存储文件元数据(权限、所有者、修改时间等),每个文件唯一对应一个 inode。
    • block:存储文件实际数据,大小由文件系统决定(如 4KB)。
      注意:即使 0KB 的空文件也会占用inode 空间(不同文件系统 inode 大小不同,如 EXT4 默认 256 字节),但不占用数据块(block)。
  • 磁盘是外设(输入 / 输出设备)
    对磁盘文件的操作本质是IO(Input/Output),涉及内核与外设的数据交互(如通过 DMA 控制器读写磁盘)。

1.2 广义理解

Linux下一切皆文件
系统将硬件设备、进程信息、通信管道等抽象为文件,通过统一接口管理:

  • 硬件设备
    • 块设备:以块为单位读写(如硬盘 /dev/sda,文件类型 b)。
    • 字符设备:以字符流读写(如键盘 /dev/input/event0,文件类型 c)。
  • 虚拟文件系统
    • /proc:动态映射进程信息(如 /proc/self/exe 是当前进程二进制文件)。
    • /sys:暴露内核设备驱动细节(如 /sys/class/leds/ 控制 LED 灯)。
  • 进程通信
    • 管道文件(类型 p):mkfifo mypipe 创建命名管道。
    • 套接字文件(类型 s):/run/docker.sock 用于 Docker 进程通信。
      这种抽象屏蔽了底层差异,例如读写 /dev/tty1(终端设备文件)与读写普通文件使用相同 API。

1.3 文件操作的归类认识

文件 = 元数据(属性) + 数据内容

  • 元数据
    • 基础属性:权限(rwx)、所有者(uid/gid)、硬链接数(ls -l 第二列)。
    • 时间戳:修改时间(mtime)、状态改变时间(ctime)、访问时间(atime)。
    • 技术属性:inode 编号(ls -i)、文件大小(ls -l 第五列)、块数(ls -s)。
  • 数据内容
    分为文本(ASCII/UTF-8)和二进制(如可执行程序、图片),通过cathexdump等工具查看。
  • 操作分类
    • 内容操作:读写(read/write系统调用)、定位(lseek)、截断(truncate)。
  • 属性操作
    • 修改权限chmod(对应chmod系统调用)。
    • 更改所有者chown(对应chown系统调用)。
    • 查看元数据stat命令(对应stat系统调用,返回struct stat结构体)。

1.4 系统角度:进程与文件的交互

  • 一切文件操作由进程触发
    内核通过 ** 文件描述符(File Descriptor, FD)** 标识进程打开的资源,FD0~1023 的整数(默认:0=stdin1=stdout2=stderr)。
    可通过ls -l /proc/$$/fd查看当前进程打开的文件($$为当前进程 PID)。
  • 系统调用 vs 库函数
    • 系统调用:内核提供的底层接口(如openread),需从用户态陷入内核态,开销较高但更直接。
    • 库函数:C 标准库封装的高层接口(如fopenfread),内部调用系统调用并提供缓存机制(如stdio的缓冲区)。
    • 示例fprintf(stdout, "hello") 最终会调用write(1, "hello", 5)系统调用。
  • 内核如何管理文件
    • 每个打开的文件对应内核中的 file结构体,记录文件位置、引用计数等。
    • 多个进程可通过不同 FD 指向同一file结构体(如父子进程共享文件),实现数据共享。

1.5 实践示例

  1. 查看文件元数据
stat test.txt  # 显示inode、权限、时间戳等详细信息
ls -li test.txt  # 查看inode编号和硬链接数

在这里插入图片描述

  1. 操作设备文件
echo "Hello zkp!" > /home/zkp/linux/25/6/7/file/test.txt  # 向文件写入信息
cat /home/zkp/linux/25/6/7/file/test.txt  # 查看文件内容

在这里插入图片描述

  1. 理解文件描述符
exec 3<> file.txt  # 在当前Shell中打开文件,FD=3可读可写
echo "test" >&3    # 通过FD=3写入文件
cat <&3            # 通过FD=3读取文件
exec 3>&-          # 关闭FD=3

二、回顾 C 文件接口

2.1 hello.c 打开文件

在这里插入图片描述
打开的myfile文件在哪个路径下?

  • 在程序的当前路径下,那系统怎么知道程序的当前路径在哪里呢?
    可以使用 ls /proc/[进程id]命令查看当前正在运行进程的信息:
    在这里插入图片描述
    其中:
  • cwd:指向当前进程运行目录的一个符号链接。
  • exe:指向启动当前进程的可执行文件(完整路径)的符号链接。
    打开文件,本质是进程打开,所以,进程知道自己在哪里,即便文件不带路径,进程也知道。由此OS就能知道要创建的文件放在哪里。

2.2 hello.c 写文件

在这里插入图片描述

2.3 hello.c 读文件

在这里插入图片描述

2.4 输出信息到显示器的几种方法

在这里插入图片描述

2.5 stdin & stdout & stderr

  • C 默认会打开三个输出流,分别是 stdin,stdout,stderr
  • 这三个流的类型都是 FILE*,而 fopen 返回值类型也是文件指针

三、系统文件I/O

我们知道,文件的权限分为 rwx,对应的标志位为 4,2,1。

3.1 一种传递标志位的方法

核心原理:位掩码(Bit Mask)
每个标志对应 唯一的二进制位(如第 0 位、第 1 位…),通过 位运算 组合 / 解析:

  • 设置标志:用 |(按位或)组合多个标志(如 FLAG_A | FLAG_B)。
  • 检查标志:用 &(按位与)判断某一位是否为 1(如 if (flags & FLAG_A))。
#include <stdio.h>// 定义权限标志位(与Linux系统保持一致)
#define PERM_READ   (1 << 2)  // 4: 读权限
#define PERM_WRITE  (1 << 1)  // 2: 写权限
#define PERM_EXEC   (1 << 0)  // 1: 执行权限// 解析权限并打印
void func(int perms) {printf("用户权限: ");printf(perms & USER_PERMS(PERM_READ)   ? "r" : "-");printf(perms & USER_PERMS(PERM_WRITE)  ? "w" : "-");printf(perms & USER_PERMS(PERM_EXEC)   ? "x" : "-");printf("\n");
}int main() {// 组合权限:用户有读写,组有读,其他用户无权限int perms = (PERM_READ | PERM_WRITE)printf("权限掩码(八进制): 0%o\n", perms);  // 输出: 0x6func(perms);return 0;
}

3.2 接口介绍

在这里插入图片描述
参数

  • pathname:要打开或创建的目标文件
  • flags:打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成 flags
    • O_RDONLY:只读打开
    • O_WRONLY:只写打开
    • O_RDWR :读,写打开
      这三个常量,必须指定一个且只能指定一个
    • O_CREAT:若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
    • O_APPEND:追加写

返回值

  • 成功:新打开的文件描述符
  • 失败:-1

open函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限;否则,使用两个参数的open

writereadcloselseek,类比c文件相关接口。

3.5 3-5 open函数返回值

在认识返回值之前,先来认识一下两个概念:系统调用和库函数

  • 上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
  • open close read write lseek都属于系统提供的接口,称之为系统调用接口

看下面这张图:
在这里插入图片描述
系统调用接口和库函数的关系,一目了然。
所以,可以认为,f# 系列的函数,都是对系统调用的封装,方便二次开发。

3.6 文件描述符 fd

  • 通过对open函数的学习,我们知道了文件描述符就是一个小整数

3.6.1 0 & 1 & 2

  • Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0,标准输出1,标准错误2。
  • 0,1,2对应的物理设备一般是:键盘,显示器,显示器
    所以输入输出还可以采用如下方式:
#include <stdio.h>
#include <sys/types.h>#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>int main()
{char buf[1024];ssize_t s = read(0, buf, sizeof(buf));if (s > 0) {buf[s] = 0;write(1, buf, strlen(buf));write(2, buf, strlen(buf));} return 0;
}

在这里插入图片描述

3.6.2 文件描述符的分配规则:最小未使用下标优先

  1. 默认初始状态
    进程启动时,内核会自动打开 3 个标准文件描述符:
    • 0:标准输入(stdin,默认关联键盘)
    • 1:标准输出(stdout,默认关联终端)
    • 2:标准错误(stderr,默认关联终端)
      因此,首次打开新文件时,文件描述符从 3 开始分配(依次递增:3、4、5…)。
  2. 关闭后复用
    如果进程主动关闭某个文件描述符(如 close(0)),后续调用 open 时,内核会 扫描文件描述符表,选择最小的未被使用的下标 分配给新文件。
    • 例:关闭 0 后,新打开的文件会优先占用 0;
    • 若同时关闭 0 和 2,新打开的文件会依次占用 0、2,再继续递增(如 3、4…)。

代码验证

#include <stdio.h>
#include <unistd.h>   // close
#include <fcntl.h>    // open, O_RDWR, O_CREATint main() {// 1. 初始打开:未关闭默认FD,从3开始int fd1 = open("test1.txt", O_RDWR | O_CREAT, 0644);printf("fd1: %d\n", fd1);  // 输出:3(0、1、2已占用)// 2. 关闭标准输入(FD=0),后续打开优先复用0close(0); int fd2 = open("test2.txt", O_RDWR | O_CREAT, 0644);printf("fd2: %d\n", fd2);  // 输出:0(最小未使用下标)// 3. 关闭标准错误(FD=2),后续打开优先复用2close(2); int fd3 = open("test3.txt", O_RDWR | O_CREAT, 0644);printf("fd3: %d\n", fd3);  // 输出:2(当前最小未使用下标)// 4. 继续打开,下一个最小未使用是3(0、2已用,1仍被stdout占用)int fd4 = open("test4.txt", O_RDWR | O_CREAT, 0644);printf("fd4: %d\n", fd4);  // 输出:3return 0;
}

运行结果

fd1: 3  
fd2: 0  
fd3: 2  
fd4: 3  

3.6.3 应用场景:重定向的实现

  1. 输出重定向示例
# 将命令输出写入文件(本质是修改FD=1的指向)
ls -l > output.txt

实现逻辑:

  • Shell 先关闭 FD=1(标准输出),再打开 output.txt,此时新文件会占用 FD=1;
  • 后续 ls 命令的输出会写入 FD=1(即 output.txt),而非终端。
  1. 代码模拟重定向
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>int main() {// 1. 关闭标准输出(FD=1)close(1); // 2. 打开新文件,会复用FD=1int fd = open("redirect.txt", O_WRONLY | O_CREAT, 0644);// 3. printf 会向 FD=1 写入(此时指向 redirect.txt)printf("Hello, Redirect!\n"); close(fd);return 0;
}

运行后,redirect.txt 会包含 Hello, Redirect!,而非终端输出

注意事项

  • 关闭默认描述符(如 close(1))后,若后续代码依赖标准输出(如 printf),会导致输出丢失或异常。
  • 建议使用 dup2 实现重定向(安全关闭旧描述符,避免冲突)。

重定向的本质
在这里插入图片描述

3.6.4 dup2 系统调用

在这里插入图片描述

示例:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>int main() {// 1. 打开文件(获取新的文件描述符,如3)int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);if (fd == -1) {perror("open failed");return 1;}// 2. 将标准输出(FD=1)重定向到 fd 指向的文件if (dup2(fd, 1) == -1) {perror("dup2 failed");close(fd);return 1;}// 3. 此时 printf 会写入 output.txt,而非终端printf("Hello, dup2!\n");// 4. 关闭 fd(注意:标准输出仍指向 output.txt)close(fd);// 5. 验证:继续向标准输出写入fprintf(stdout, "This will also appear in output.txt\n");return 0;
}

printf是C库当中的IO函数,一般往stdout中输出,但是stdout底层访问文件的时候,找的还是fd:1,但此时,fd:1下标所表示内容,已经变成了myfifile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。

五、缓冲区

5.1 什么是缓冲区

缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。

5.2 为什么要引入缓冲区机制

读写文件时,如果不会开辟对文件操作的缓冲区,直接通过系统调用对磁盘进行操作(读、写等),那么每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调用,执行一次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的CPU时间,频繁的磁盘访问对程序的执行效率造成很大的影响。

为了减少使用系统调用的次数,提高效率,我们就可以采用缓冲机制。比如我们从磁盘里取信息,可以在磁盘文件进行操作时,可以一次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度

又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。

5.3 缓冲类型

标准I/O提供了3种类型的缓冲区。

  • 全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/O系统调用操作。对于磁盘文件的操作通常使用全缓冲的方式访问。
  • 行缓冲区:在行缓冲情况下,当在输入和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。当所操作的流涉及一个终端时(例如标准输入和标准输出),使用行缓冲方式。因为标准1/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行I/0系统调用操作,默认行缓冲区的大小为1024。
  • 无缓冲区:无缓冲区是指标准I/O库不对字符进行缓存,直接调用系统调用。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。

除了上述列举的默认刷新方式,下列特殊情况也会引发缓冲区的刷新:

  1. 缓冲区满时;
  2. 执行flush语句;

相关文章:

  • 关于队列的使用
  • 6.7本日总结
  • python打卡day47
  • 第1讲、包管理和环境管理工具Conda 全面介绍
  • 理想汽车5月交付40856辆,同比增长16.7%
  • 《开篇:课程目录》
  • 33、原子操作
  • Vue中渲染函数的使用
  • Java编程中常见的条件链与继承陷阱
  • 华为云Flexus+DeepSeek征文|华为云一键部署知识库搜索增强版Dify平台,构建智能聊天助手实战指南
  • 【PhysUnits】15.17 比例因子模块 (ratio.rs)
  • 【在线五子棋对战】二、websocket 服务器搭建
  • 僵尸进程是什么?怎么回收?孤儿进程?
  • Spring Cloud Hystrix熔断机制:构建高可用微服务的利器
  • 今天对C语言中static和extern关键字的作用认识又深刻了
  • 174页PPT家居制造业集团战略规划和运营管控规划方案
  • SQLMesh实战:用虚拟数据环境和自动化测试重新定义数据工程
  • 高频 PCB 技术发展趋势与应用解析
  • Python 基础核心语法:输入输出、变量、注释与字符串操作
  • 数据通信与计算机网络——数字传输
  • 公司内部网站一般都怎么维护/谷歌aso优化
  • python官方网站/北京全网推广
  • 做平台外卖的网站需要什么资质/网站搭建外贸
  • 北京制作网站报价/获客软件排名前十名
  • 邢台哪儿能做网站/足球世界排名一览表
  • 建设网站是主营成本吗/app推广实名认证接单平台