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

[Linux系统编程——Lesson14.基础IO:系统文件IO]

目录

前言

本节重点

一、📖预备知识

二、 🧐理解"⽂件"

1️⃣狭义理解:聚焦 “磁盘中的物理文件”

2️⃣广义理解:Linux 系统的 “万物皆文件” 抽象

两者核心差异对比

2.2 🎈文件操作的本质对象与执行主体

1️⃣⽂件操作的归类认知

文件操作的归类认知(明确 “文件是什么”)

2️⃣系统⻆度

系统角度的文件操作(明确 “操作如何实现”)

三、🤔复习常见C语言的文件接口

3.1🎈 文件接口的说明

3.1.1 fopen函数

✍️写文件

📖读文件

四、🐧认识操作文件的系统文件I/O

🔥open函数

🔥 write函数

🔥 close函数

🧐系统调用的使用

五、🤔C语言文件接口与文件系统接口的关系

六、🔥文件描述符(fd)🔥

6.1 🤔什么是文件描述符

1️⃣fd 0、1、2去哪了

2️⃣文件描述符的作用

6.2、🧐如何理解操作系统下一切皆文件

总结

八、🔎理解struct file内核对象

1️⃣先理清核心逻辑:读写操作的 “两步拷贝” 本质

读取文件(read/fread)的完整链路

写入文件(write/fwrite)的完整链路

2️⃣关键理解:为什么必须经过 “文件缓冲区”?

3️⃣补充细节:用户空间与内核空间的隔离

九、✍️fd的分配规则

十、🧐操作系统如何管理文件

十一、🎈重定向

🔥 dup2函数

🔥追加重定向

🔥输入重定向

🎈重定向与进程替换之间会不会互相影响

🤔使用重定向来讲述标准错误存在的原因

🔑总结提炼

1️⃣基础认知:从概念到本质

2️⃣操作接口:C 语言与系统调用

3️⃣核心概念:文件描述符(fd)

4️⃣内核原理:从对象到管理

5️⃣实际应用:重定向

总结

结束语


前言

        在编程的世界里,文件输入输出(IO)是与操作系统交互的重要方式。无论你是开发应用程序、处理数据,还是管理系统资源,掌握文件IO操作都是必不可少的。本篇博客将带你深入了解C语言中的基础IO操作,从入门到精通,全面覆盖文件操作的方方面面。本文不仅介绍基础的文件读写操作,还会扩展到系统调用接口、文件描述符、重定向、软硬链接、动态库和静态库等内容。

本节重点

  • 复习C⽂件IO相关操作
  • 认识⽂件相关系统调⽤接⼝
  • 认识⽂件描述符,理解重定向
  • 对⽐fd和FILE,理解系统调⽤和库函数的关系
  • 理解⽂件和内核⽂件缓冲区
  • ⾃定义shell新增重定向功能
  • 理解Glibc的IO库

一、📖预备知识

通过之前的学习我们知道:文件=内容+属性

  • 所以对文件的操作都分为以下两种

    • 对文件的内容进行操作
    • 对文件的属性进行操作
  • 文件的基本概念文件由内容和属性组成,内容是数据,属性也是数据,默认存储在磁盘中。文件属性包含文件名、标识符、类型、大小、保护信息等。对文件的操作可分为对内容的操作(如读、写数据)和对属性的操作(如获取或修改文件的权限、大小等信息)。
  • 进程对文件的访问:进程访问文件前必须先打开文件,通过 CPU 执行进程中打开文件的函数来实现。由于 CPU 只能与内存交互,所以文件打开后会被加载到内存中。一个进程可以打开多个文件,进程与被打开文件的数量比为 1:n。
  • 操作系统对文件的管理:
    • 文件结构体与链表管理当文件加载到内存时,操作系统会为其创建一个文件结构体对象,其中包含文件的属性等信息。结构体中还有一个结构体指针字段,新打开文件时创建的结构体对象通过指针连接起来,形成链表,操作系统通过管理这个链表来实现对文件的管理。
    • 文件描述符与打开文件表在一些操作系统中,进程打开文件后会得到一个文件描述符,它是进程中打开文件表的下标。内核根据文件描述符访问相应的文件对象,从而实现对文件的操作。不同进程的文件描述符表中的指针可以指向相同的文件对象,以实现文件共享。
struct file
{// 文件属性// ...// 指针struct file* next;
}

这些知识是理解操作系统文件管理机制以及进程与文件交互的基础,为进一步学习文件系统的高级特性(如文件共享、保护、磁盘调度等)提供了支撑。

  • 文件按照是否被打开分为以下两种

    • 被打开的文件(在内存中)
    • 未被打开的文件(在磁盘中)
  • 这里我们讲述的文件操作本质上是进程与被打开文件的关系。

二、 🧐理解"⽂件"

1️⃣狭义理解:聚焦 “磁盘中的物理文件”

狭义定义将文件限定在磁盘这一具体载体上,核心是明确文件的物理存储属性和操作本质。

  • 存储位置与特性:文件仅存在于磁盘中,而磁盘作为永久性存储介质,决定了文件存储的永久性(断电后数据不丢失)。
  • 设备属性与操作本质:磁盘属于计算机外设,兼具输入和输出功能。因此,对磁盘文件的所有操作(如读文件、写文件),本质都是对这个外设的输入(从磁盘读数据到内存)或输出(从内存写数据到磁盘)操作,统称为 IO 操作

2️⃣广义理解:Linux 系统的 “万物皆文件” 抽象

广义定义突破了物理载体限制,是 Linux 系统的核心设计思想,核心是通过抽象统一设备与文件的操作方式。

  • 抽象范围:在 Linux 中,不仅磁盘文件是 “文件”,键盘、显示器、网卡、打印机等硬件设备,甚至进程间通信的管道、网络套接字等,都被抽象成 “文件” 的形式。
  • 抽象目的:将所有设备和资源统一为 “文件” 后,操作系统可以用一套相同的接口(如 open、read、write 等系统调用)来操作它们,无需为不同设备单独设计操作逻辑,极大简化了程序开发和系统管理。

两者核心差异对比

对比维度狭义理解广义理解(Linux)
适用范围仅磁盘中的物理文件磁盘文件、硬件设备、通信资源等
核心视角物理存储载体(磁盘)系统资源抽象统一
操作本质对外设(磁盘)的 IO 操作对统一 “文件接口” 的调用

🔑这些概念是理解 Linux 文件系统的关键:狭义理解帮你认清文件的物理本质,广义理解则帮你掌握 Linux 系统 “用文件管理一切” 的高效设计逻辑,为后续学习系统调用、设备驱动等知识打下基础。

2.2 🎈文件操作的本质对象与执行主体

1️⃣⽂件操作的归类认知

  • 对于 0KB 的空⽂件是占⽤磁盘空间的
  • ⽂件是⽂件属性(元数据)和⽂件内容的集合(⽂件 = 属性(元数据)+ 内容)
  • 所有的⽂件操作本质是⽂件内容操作和⽂件属性操作

文件操作的归类认知(明确 “文件是什么”)

这部分核心是建立对文件的完整认知,打破 “空文件不占空间”“文件就是内容” 的常见误区。

①空文件的空间属性:0KB 的空文件虽无实际内容,但仍占用磁盘空间。这部分空间主要用于存储其文件属性,而非内容。

②文件的构成要素:文件是两部分的集合,缺一不可。

  • 文件属性(元数据):描述文件的信息,如文件名、大小、创建时间、存储路径、权限等。
  • 文件内容:文件实际承载的数据,如文档中的文字、图片的像素信息等。

③文件操作的本质归类:所有对文件的操作,最终都可拆解为两类。

  • 对文件内容的操作:如打开文件后读写数据、修改文本内容等。
  • 对文件属性的操作:如重命名文件、修改文件权限、更改文件保存路径等。

2️⃣系统⻆度

  • 对⽂件的操作本质是进程对⽂件的操作
  • 磁盘的管理者是操作系统
  • ⽂件的读写本质不是通过 C 语⾔ / C++ 的库函数来操作的(这些库函数只是为⽤⼾提供⽅便),⽽ 是通过⽂件相关的系统调⽤接⼝来实现的

系统角度的文件操作(明确 “操作如何实现”)

这部分从操作系统底层逻辑出发,解释了文件操作的执行路径,破除 “库函数直接操作文件” 的误解。

①操作主体定位:对文件的所有操作,本质上都是进程在发起和执行。进程是操作系统中资源分配和调度的基本单位,只有进程才能向系统请求操作文件。

②磁盘管理主体:磁盘作为存储硬件,并非由应用程序直接管理,其管理者是操作系统。操作系统统一负责磁盘空间的分配、回收和文件的存储管理。

③操作接口的核心:文件的读写等操作,并非由 C/C++ 的库函数(如fopenfwrite)直接完成。

  • 库函数的作用:为用户提供更简洁、易用的编程接口,降低开发难度。
  • 实际执行路径:库函数会在内部调用操作系统提供的文件相关系统调用接口(如 Linux 中的openwrite),由操作系统通过这些接口去操作磁盘,最终完成文件读写

三、🤔复习常见C语言的文件接口

3.1🎈 文件接口的说明

3.1.1 fopen函数
FILE *fopen(const char *path, const char *mode);

功能fopen 函数是 C 标准库中用于打开文件的函数。

参数

  • path:被打开文件的路径和文件名,若只有文件名则默认在当前工作目录搜索这个文件。
  • mode:被打开为文件以什么样的模式打开。

✍️写文件

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);//写入二进制数据
​ int fprintf(FILE *stream, const char *format, …);    //写入格式化文本文件
​ int sprintf(char *str, const char *format, …);    //内存中拼接字符串(慎用)
​ int snprintf(char *str, size_t size, const char *format, …);    //内存中安全拼接字符串
FILE* fp = fopen("log.txt","w");

log.txt文件以w(只写)的方式打开,若文件不存在则在当前工作目录下创建该文件,通过代码的测试我们发现确实可以创建文件。

🔔示例代码

#include <stdio.h>#define LOG "log.txt"int main()
{// 以 ' w' (只写)的方式打开文件FILE* fp = fopen(LOG,"w"); if(fp == NULL){perror("fopen");return 1;}// 对文件进行操作const char* msg = "Hello World!";int cnt = 5;while(cnt){fprintf(fp,"%s: %d\n",msg,cnt);cnt--;}// 关闭文件fclose(fp);return 0;
}

    打开的log.txt⽂件在哪个路径下
    • 在程序的当前路径下,那系统怎么知道程序的当前路径在哪⾥呢?
    可以使⽤ls /proc/[进程id] -l命令查看当前正在运⾏进程的信息:

    其中:
    • cwd:指向当前进程运⾏⽬录的⼀个符号链接。
    • exe:指向启动当前进程的可执⾏⽂件(完整路径)的符号链接。
    打开⽂件,本质是进程打开,所以,进程知道⾃⼰在哪⾥,即便⽂件不带路径,进程也知道。由此OS就能知道要创建的⽂件放在哪⾥。

    • 使用w的方式下打开文件,会先将文件中所有的内容清空。在下面的代码中,我们先向文件中写入一段数据,通过下图可以看到这段数据确实写入文件中了,再将文件以w的方式打开什么都不做,再将文件关闭,我们可以发现文件中的内容消失了,这里可以证明以w的方式打开文件,会先将文件中所有的内容清空。

    FILE* fp = fopen("fortest.txt","a");
    
    • 使用a(追加)方式打开文件,不会清空文件中的内容,会从文件的结尾处开始写入。下面的代码中,我们向文件中写入一段数据,通过下图可以看到这段数据确实写入文件中了,向文件中多写入几段数据,也是写入到了文件中,再将文件以a的方式打开什么都不做,再将文件关闭,我们可以发现文件中的内容并没有消失了,这里可以证明以a的方式打开文件,不会将文件中的内容清空,并且会在文件中的结尾处开始写入。

    📖读文件

    ​
    size_t fread (void *ptr, size_t size, size_t nmemb, FILE *stream);// 从文件流读取二进制或文本数据(无格式)
    int fscanf (FILE *stream, const char *format, …);    //从文件流读取格式化数据
    int sscanf (const char *str, const char *format, …);    //从字符串读取格式化数据​
    FILE* fp = fopen("log.txt","r");
    
    #include <stdio.h>#define LOG "log.txt"
    int main()
    {// 打开文件FILE* fp = fopen(LOG,"r");if(fp == NULL){perror("fopen");return 1;}// 对文件进行操作const char* msg = "Hello World!";char str[1024];while(fscanf(fp,"%s ",str) != EOF){fprintf(stdout,"%s ",str);}// 关闭文件fclose(fp);return 0;
    }

    上面我们只是简单回忆一下C语言的文件操作


    四、🐧认识操作文件的系统文件I/O

    • 文件操作并不是只存在于C语言的,Java,python等编程语言中同样存在文件操作,只不过方法不同而已。其实我们可以尝试在系统层面统一看待文件操作。
    • 操作系统提供了许多系统调用接口,而我们在语言层面看到的例如fopen,fwrite等还有其他语言相关函数都是对操作系统提供的文件相关的系统调用接口做了不同程度的封装而已。

    也就是说:不同语言的文件操作接口千变万化,但是对于操作系统来说都是同一个接口调用的 。

    🔥open函数

       open是操作系统为语言层提供的系统调用接口,其作用是打开文件。可以根据参数来指定文件打开的方式、文件以何种权限被打开等等。fopen以及各类语言的文件操作中,底层必定使用了open系统调用接口。

    函数形式

    int open(const char *pathname, int flags);
    int open(const char *pathname, int flags, mode_t mode);
    

    函数参数

    ①pathname:指定打开的文件所在路径;

    ②flags:指定以何种形式打开文件;

    选项作用
    O_RDONLY只读
    O_WRONLY只写
    O_RDWR即读又写
    O_CREAT没有文件进行创建
    O_TRUNC有文件将文件内容清空
    O_APPEND在文件末尾追加

    以上选项实际上就是个宏定义,实际上就是八进制的数字:

    • 使用比特位对应的不同的数值来表示不同的含义,因为是在不同的比特位上,所以可以通过按位或|将选项进行叠加使用,比如O_WRONLY|O_CREAT表示以写的方式打开文件,如果没有文件就创建新文件。

    ③mode:指定文件被创建时的权限;
    ④返回值:

    • 成功返回大于等于0的值;
    • 失败返回-1;

    🔔示例代码

    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>  
    #define LOG "log.txt"int main()
    {umask(0); // 将系统默认的掩码置0int fd = open(LOG, O_CREAT | O_WRONLY | O_TRUNC, 0666);if(fd == -1){printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));return 1; // 出错时应该返回非0值}const char* msg = "aaabbb ";int cnt = 5;while(cnt--){write(fd, msg, strlen(msg));}close(fd); // 完成写入后应该关闭文件描述符return 0;
    }

    🔥 write函数
    ssize_t write(int fd, const void *buf, size_t count);
    

    功能:write函数是系统调用中用来向文件中输入的函数。

    参数

    • fd:指定要写入文件的文件描述符。
    • buf:指向要写入文件的数据的指针。
    • count:代表写入文件数据的字节数。
    🔥 close函数
    int close(int fd);
    

    功能:close函数是系统调用中用于关闭一个打卡文件的函数

    参数

    • fd:指定要关闭的文件的文件描述符。

    🧐系统调用的使用

    int fd = open("fortest.txt",O_WRONLY|O_CREAT);
    
    • 将文件以只写的方式打开,若文件不存在则创建文件的方式创建文件,我们发现没有加上创建文件的权限,会导致创建出来的文件权限是乱码,所以若是打开文件不存在需要创建,需要向函数中传入文件权限。

    • 向函数中传入文件权限,可以发现文件确实被创建出来了,但是我们传入文件的权限是0666,但创建出来的文件的权限确实0664,这是因为存在权限掩码,会将传入的文件权限进行过滤,若不想使用系统中的权限掩码,可以使用umask函数在该进程中设置一个权限掩码,那么创建出来的文件的权限只受进程的权限掩码的影响。

    int fd = open("fortest.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    
    • 只写的方式打开文件,若文件不存在则创建文件,打卡文件时将文件内的内容全部清空。我们在此文件中写入一段字符串,并将字符串中的’\0’也写入到文件中,当运行进程后打开文件,我们发现除了我们写入的字符串外还有一个^@,这实际上就是’\0’,由此我们可以知道’\0’是C语言的标准,并不是文件的标准,所以在向文件中写入字符串时,不需要将’\0’写入到文件中。当我们再一次打开文件后,什么都不做就关闭文件,我们发现文件中的内容消失了,这也就证明了以标志O_TRUNC打开文件时,会将文件内所有的内容清除。

    int fd = open("fortest.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
    
    • 追加的方式打开文件,若文件不存在则创建文件。我们在此文件中写入一段字符串,我们发现确实向文件中写入了一段字符串。当我们多次向文件中写入字符,可以发现文件中的内容越来越多,可见这时候我们打开文件时,并没有清空文件的内容,而是在文件的结尾处开始继续写入字符串。这也就证明了以标志O_APPEND打开文件时,不会将文件内容清除,而是在为文件结尾处继续写入。

    五、🤔C语言文件接口与文件系统接口的关系

            通过上面文件接口的使用和系统调用的使用,我们发现下面C标准库中的fopen的功能和系统调用中的open功能一样,我们知道在语言层面上是无法访问磁盘的,所以C标准库中的文件操作函数实际上底层是封装了系统调用的文件操作接口的。

    // C标准库
    FILE* fp = fopen("fortest.txt","w");  
    
    // 系统调用
    int fd = open("fortest.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    

    // C标准库
    FILE* fp = fopen("fortest.txt","a");
    
    // 系统调用
    int fd = open("fortest.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
    

    六、🔥文件描述符(fd)🔥

    6.1 🤔什么是文件描述符

            上一小节中,我们知道在open函数打开一个文件时,如果成功,会返回一个大于等于0的数字;失败时,则返回-1。其实所谓的大于等于0的数字,就是文件描述符

    • 我们可以创建多个文件,并将这些文件描述符打印出来。

    可见,文件描述符就是从0开始的小整数,且文件描述符是随着打开文件的操作递增的。那么现在有两个问题❓:

    • 为什么文件描述符看起来好像是从3开始的,有什么特殊含义吗?
    • 文件描述符有什么作用?

    我们首先来解决第一个疑问。

    1️⃣fd 0、1、2去哪了

    早在C语言阶段,我们就知道,一个程序运行时,有3个文件流是默认被打开的。它们就是:

    • 标准输入流stdin(一般对应的物理设备是键盘);
    • 标准输出流stdout(一般对应的物理设备是显示器);
    • 标准错误流stderr(一般对应的物理设备是显示器);

    而此刻我们也应该能猜到,0、1、2这三个文件描述符其实是被这三个文件所占据。这也印证了Linux下一切皆文件,向显示器打印其实就是向文件中写入

    设备标识符文件描述符
    标准输入键盘stdin0
    标准输出显示屏stdout1
    标准错误显示屏stderr2

    下面使用代码验证一下在我们没有打打开标准输入/输出/错误的情况下,能不能对这几个文件进行操作,并验证文件描述符是否与之对应。

    • 通过对下面代码的运行我们发现在我们没有打打开标准输入/输出/错误的情况下,可以对这几个文件进行操作,并且文件描述符与上述的一 一对应,再打开一个文件并输出它的文件描述符也确实是从3开始的。

    2️⃣文件描述符的作用
    • 我们知道文件其实是文件=内容+属性,属性包含文件的大小、文件创建的时间、上一次修改的时间、文件的权限等。OS在管理文件时,需要在内存中创建相应的数据结构来描述文件,这就是file结构体,表示一个已经打开的文件对象。所以OS在管理文件时,只要找到了该文件所对应的struct_file对象,就能找到该文件的内容和属性。
    • 运行起来的程序我们把它叫做进程,当一个进程执行open调用,必须先让进程与要操作的文件关联起来。每个进程都有一个指针 *file,该指针指向一张文件描述符表files_struct这张表中最重要的部分就是一个指针数组。数组的每个元素都是一个指向打开文件的指针。
    • 所以,本质上,文件描述符(fd)就是该数组的下标,所以,只要拿到了文件描述符,就能找到对应的文件!
       

    小结

    1. 文件描述符的本质就是数组的下标
    2. 操作系统不希望进程和文件的耦合度太高了,所以为进程创建了一个文件描述符表,这个表中可以存储文件结构体对象的地址,通过文件描述符表将进程与文件联系起来。

    6.2、🧐如何理解操作系统下一切皆文件

            在硬件层中,每一个硬件至少会有读/写的方法,并且每个硬件的读写方法都不可能相同,在操作系统打开硬件的时候,会为其创建一个结构体对象,这个对象中存在两个函数指针,用于指向硬件的读写方法,当我们想调用硬件的读写方法时,可以通过函数指针来调用,而不是通过底层的读写方式来调用,那么就完成了对硬件读写操作的统一。

    学习过面向对象语言的同学看下面这幅图会有什么想法呢🤔

    • 大家可能会想到多态,Linux操作系统底层使用C语言编写的,C语言本身并不直接支持面向对象编程中的继承和多态,但是可以通过结构体和函数指针模拟了继承和多态的行为。

    总结

            “一切皆文件” 不是说所有东西都是磁盘里的普通文件,而是操作系统用文件这个统一的 “壳”,把硬件、网络、管道等所有资源包起来,再通过 “结构体 + 函数指针” 模拟多态,让程序用一套接口操作所有资源 —— 本质是 “用抽象简化复杂,用统一降低成本”。

    八、🔎理解struct file内核对象

            当一个进程运行时,操作系统会为其创建一个task_struct,操作系统还会为每个进程创建一个files_struct,files_struct中有一个进程文件描述符表,文件描述符表中有一个数组用来存储被打开文件的结构体对象的地址,结构体对象中有一个字段是用来指向文件所有属性的,有一个字段是用来指向文件的操作方法集的,还有个字段是用来指向文件缓冲区的。

    1️⃣先理清核心逻辑:读写操作的 “两步拷贝” 本质

    无论是读还是写,数据都要经过两次拷贝,核心是通过内核的文件缓冲区作为中间桥梁,而不是用户程序直接操作磁盘。这是为了减少磁盘 IO 次数、提升效率。

    读取文件(read/fread)的完整链路

    定位文件:从 fd 找到文件结构体

    • 进程调用 read(fd, buf, len) 时,操作系统先找到该进程的 task_struct(进程控制块,存储进程所有信息)。
    • 通过 task_struct 中的指针找到 files_struct,里面的 “文件描述符表” 是一个数组,fd 就是数组下标。
    • 用 fd 找到数组中对应的 “文件结构体”(如 Linux 的 struct file),该结构体里的指针直接指向内核的 “文件缓冲区”。

    数据拷贝:从文件缓冲区到用户缓冲区

    • 若文件数据已在 “文件缓冲区”(内核空间),直接将数据拷贝到用户定义的 buf(用户空间)。
    • 若数据不在缓冲区(缺页中断),操作系统会触发磁盘 IO,先把磁盘上的文件数据加载到 “文件缓冲区”,再执行上述拷贝步骤。

    写入文件(write/fwrite)的完整链路

    定位文件:与读取流程完全一致

    • 同样通过 fd → files_struct → task_struct → 找到 “文件结构体” 和对应的 “文件缓冲区”,确保操作的是目标文件。

    数据拷贝:从用户缓冲区到文件缓冲区,再到磁盘

    • 先将用户 buf 中的数据拷贝到内核的 “文件缓冲区”(此时数据还在内存,未到磁盘)。
    • 操作系统会通过 “页缓存刷新策略”(如定时刷新、缓冲区满时刷新),将 “文件缓冲区” 中的数据批量写入磁盘,完成真正的持久化。

    2️⃣关键理解:为什么必须经过 “文件缓冲区”?

     “读写都需要加载文件到缓冲区”,这背后是操作系统的核心优化逻辑,直接操作磁盘会有巨大性能问题:

    • 减少磁盘 IO 次数:磁盘是机械 / 电子设备,读写速度远慢于内存(差距可达万倍)。通过缓冲区批量处理数据(比如积累一定量再写磁盘),能大幅减少对磁盘的调用,提升整体效率。
    • 保护磁盘数据安全:若直接修改磁盘,一旦过程中发生断电、程序崩溃,数据可能损坏。缓冲区可以作为 “临时中转”,确保数据在内存中处理完整后,再安全写入磁盘,降低出错风险。
    • 实现数据共享与缓存:同一个文件被多个进程读取时,只需加载一次到 “文件缓冲区”,所有进程可共享该缓冲区数据,无需重复从磁盘加载,节省内存和 IO 资源。

    3️⃣补充细节:用户空间与内核空间的隔离

    需要注意的是,用户定义的 buf(用户缓冲区)位于 “用户空间”,而操作系统的 “文件缓冲区” 位于 “内核空间”。

    • 两者是严格隔离的,用户程序不能直接访问内核空间的缓冲区,必须通过 read/write 这类系统调用,由操作系统完成数据拷贝。
    • 这一隔离是为了系统安全:防止用户程序误操作内核数据,导致操作系统崩溃或数据泄露。

    总结来说,文件读写的核心是 “先定位,再拷贝,靠缓冲区优化”——fd 和一系列结构体负责 “定位” 目标文件,两次数据拷贝完成用户与磁盘的交互,而 “文件缓冲区” 则是平衡速度、安全与效率的关键设计。

    九、✍️fd的分配规则

    • 操作系统默认在进程运行时,将文件描述符为0、1、2的文件打开,我们可以直接使用0、1、2进行数据的访问
    • 文件描述符的规则就是遍历文件描述符表中的数组,寻找数组中下标最小且该位置没有被使用的位置,用来分配给指定的被打开文件。

    下面的代码中,我们分别将文件标识符为0和2的文件关闭后,在新打开一个文件,我们发现新打开文件的文件描述符确实分别为0和2,符合fd的分配规则。
     

    十、🧐操作系统如何管理文件

    操作系统管理的底层逻辑一律是:先描述,再组织

    • Linux下用struct files_struct来描述一个进程打开的文件信息:

    • 其中有两个数组是需要特别关注的:struct file ** fdstruct file * fd_array[NR_OPEN_DEFAULT],他们两个的作用是一样的,类似于动态顺序表和静态顺序表,其中一级指针类似于静态顺序表常用于打开文件少的时候来减少空间的开辟,二级指针类似于动态顺序表常用于打开文件多。该数组每个位置的下标就是对应着已经打开文件的文件描述符,open系统调用接口的返回值就是数组对应的下标。
    • 这两个结构体指针都指向struct file *的数组,struct flie是专门用来记录每个文件的各种属性的:

    • 该结构体又包含其他很多的结构体,其中struct list_head f_list是用来记录所有系统打开的文件

    • 就是一个双向指针,通过该双向指针可以实现所有文件属性的查找f_count是文件的引用引用计数,记录有多少个进程打开这个文件,f_flags用来记录文件被打开的方式/选项,就是open中的flagf_mode记录文件的权限,f_error记录操作文件的错误码,还有一个比较重要的就是struct address_space *f_mapping在后面的内存管理中会终点介绍。总而言之struct file中存储着打开的文件中的各种信息。

    示意图如下所示:


    十一、🎈重定向

    重定向通常指的是改变数据输入或输出的流向,从默认的设备(如键盘或屏幕)改为文件或其他设备。

    • 在上面讲述fd的分配规则时,我们分别将文件标识符为0和2的文件关闭后,在新打开一个文件,这里我们将表示文件标识符为1的文件关闭后,然后再新打开一个文件,最后输出一段数据,运行进程观察现象,我们发现本应该显示在显示屏上的数据,最后却写入到了这个被新打开的文件中?
    • 这是因为printf只认stdout,stdout的描述符又为1,与其说printf只认stdout,不如说printf只认文件描述符为1的文件,printf并不会管文件描述符表中的数组内容是如何变化的,它只会将数据写入到文件描述符为1的文件中。这里我们将标准输入关闭,新打开的文件文件描述符就是1,所以printf将数据写入到新文件中,并且新打开文件的文件描述符为1,符合fd的分配规则。

    • 这里我们以只写并且打开文件会清空文件内容的方式打开文件,我们将默认的输出设备(显示屏)修改为了这个新打开的文件,这样本该输出到显示屏的数据却写入到了文件中,由于这个文件被打开时会被清空内容,所以这里的重定向我们称之为输出重定向。
    常⻅的重定向有: > , >> , <

    那重定向的本质是什么呢

    重定向的本质是修改文件描述符表中特殊下标的指向,使其指向其他的文件或资源。当上层读写函数使用这些特殊文件描述符时,它们就会读写重定向后的文件或资源。

    上面我们是先将文件标识符为1的文件先关闭后,再打开一个新的文件后,才让这个文件的文件标识符变为1,而系统调用中有一个函数dup,dup函数能够使文件标识符表中为fd1为下标的内容直接覆盖到另一个文件标识符表中为fd1为下标的内容。

    🔥 dup2函数

    int dup2(int oldfd, int newfd);
    
    • 参数oldfd为要复制的文件描述符。
    • 参数newfd为目标文件描述符。如果 newfd 已经打开,则它会被关闭,除非 newfd 和 oldfd 相同。
    • dup2系统调用的作用是,将newfd文件描述符所对应的文件进行关闭,然后make一个新的fd——oldfd注意这里有点奇怪,新创建的fd反而叫做oldfd)。在files_struct数组中,将下标为oldfd所在的内容复制到newfd中。

    如下:

      int fd = open(LOG, O_CREAT | O_WRONLY | O_TRUNC, 0666);dup2(fd,1);
    

    🔔示例代码

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #define LOG "log.txt"int main()
    {umask(0); // 将系统默认的掩码置0int fd = open(LOG, O_CREAT | O_WRONLY | O_TRUNC, 0666);// 省略检查的步骤...dup2(fd,1);printf("hello world!\n"); // 本来应该向屏幕上打印,现在转为向LOG中打印printf("hello world!\n");printf("hello world!\n");printf("%d\n",fd);return 0;
    }
    

    🔥追加重定向

    • 这里我们以只写并且向文件中写入数据时会从文件的尾部开始写入的方式打开文件,我们将默认的输出设备(显示屏)修改为了这个新打开的文件,这样本该输出到显示屏的数据却写入到了文件中,由于向文件中写入内容时是向文件中的尾部开始写入,所以这里的重定向我们称之为追加重定向。

    🔥输入重定向

    • 既然能够覆盖stdout让printf将数据写入被打开文件,那么我们也能够覆盖stdin让scanf/fgets函数从文件中读取数据。以下面的代码为例,fread本应该从键盘中读取数据,因为被文件覆盖了,导致最终从文件中读取数据,我们称之为输入重定向。

    🎈重定向与进程替换之间会不会互相影响

    首先给出结论重定向与进程替换之间是不会互相影响的,重定向影响的是进程的文件描述符表文件结构体对象,而进程替换影响的是进程的进程地址空间、进程的页表和物理内存,所以重定向与进程替换之间是不会互相影响的。

    🤔使用重定向来讲述标准错误存在的原因

    在以前的学习过程中,我们会常常使用到标准输入和标准输出,很少使用到标准错误,并且标准输出和标准错误的设备都是显示屏,接下来我就为大家讲解这样做的原因。

    • 观察下面的代码,我们分别向标准输出和标准错误中输出一条信息,运行程序我们发现打印了两条信息,当我们使用输出重定向将运行结果写入到文件中时,我们发现输出到标准错误中的信息输出到了显示屏上,输出到标准错误中的信息写入到了文件中,因为标准重定向是向fd为1的文件中写入。
    • 接下来我们使用./myprocess > fortest.txt 2>&1将标准输入和标准错误都重定向到文件中,查看文件发现确实都写进去了,一个命令就可以是标准输入和标准输出都重定向到一个文件中,那么我们也可以使用一个命令将标准输入和标准输出分别重定向到两个不同的文件中,通过下图发现也确实分别将信息写入到了两个文件中,到这里就能就是标准错误存在的原因了,标准错误的存在就可以将标准信息和错误信息分别写入到两个文件中,这样可以方便我们查看错误信息,方便排查代码错误。


    🔑总结提炼

    1️⃣基础认知:从概念到本质

    • 文件的双重理解:先明确狭义的 “磁盘物理文件”,再延伸到 Linux “万物皆文件” 的抽象概念,通过对比两者差异建立基础认知。
    • 文件操作的核心:从 “是什么”(归类认知文件类型)和 “怎么做”(系统角度实现逻辑)两个维度,拆解文件操作的本质对象与执行主体。

    2️⃣操作接口:C 语言与系统调用

    • C 语言文件接口:聚焦fopen、读 / 写文件等常用接口,掌握用户层操作文件的基础工具。
    • Linux 系统文件 I/O:重点讲解openwriteclose三个核心系统调用,理解内核层操作文件的底层接口,以及系统调用的实际使用方式。
    • 接口关系:明确 C 语言接口与系统接口的关联,厘清用户层与内核层操作的衔接逻辑。

    3️⃣核心概念:文件描述符(fd)

    • 基本定义与关键问题:解释 fd 是什么,解答 “fd 0、1、2 去哪了”(对应标准输入、标准输出、标准错误),说明 fd 的作用(作为内核操作文件的标识)。
    • 分配规则:单独强调 fd 的分配逻辑,是理解文件操作的关键前提。
    • 延伸理解:结合 fd 进一步阐释 “操作系统下一切皆文件” 的抽象理念,强化对 Linux 设计思想的认知。

    4️⃣内核原理:从对象到管理

    • struct file 内核对象:通过 “两步拷贝”(用户空间 - 内核缓冲区 - 磁盘 / 设备)拆解读 / 写文件的完整链路,明确文件操作的底层流程。
    • 关键细节:解释 “文件缓冲区” 存在的必要性,以及 “用户空间与内核空间隔离” 的安全设计,理解内核操作的底层逻辑。
    • 文件管理逻辑:专门讲解操作系统如何管理文件,从内核角度补充文件系统的管理机制。

    5️⃣实际应用:重定向

    • 核心工具与场景:围绕dup2函数,介绍追加重定向、输入重定向等常用场景,掌握实际开发中文件流向控制的方法。
    • 关联问题:解答 “重定向与进程替换是否互相影响”,并结合重定向说明 “标准错误存在的原因”,将概念与实际问题结合。

    总结

            整体内容遵循 “概念→接口→核心标识→内核原理→实际应用” 的逻辑,从浅到深覆盖 Linux 文件系统的核心知识。重点在于理解 “文件抽象”“fd 作用”“内核操作链路” 三大核心,以及 C 语言接口与系统调用的关联、重定向的实际使用。


    结束语

    以上就是我对于【Linux系统编程】基础IO:系统文件IO的理解

    感谢你的三连支持!!!

    http://www.dtcms.com/a/490748.html

    相关文章:

  • golang的一些技巧
  • 高性能 Go 语言带 TTL 的内存缓存实现:精确过期、自动刷新、并发安全
  • ML.NET机器学习框架基本流程介绍
  • Day32_【 NLP _2.RNN及其变体 _(2) LSTM】
  • 重庆建站模板代理怎么做p2p网站
  • iis配置网站是什么网站建设方案书阿里云模板
  • 【计算机视觉】SAM 3 技术深潜:从“分割万物”到“理解概念”的范式转移
  • 「深度学习笔记3」概率论深度解析:从不确定性到人工智能的桥梁
  • 齐河专业企业网站建设做网站引流到天猫
  • 技术贴!【谷歌浏览器】实用工具推荐之谷歌浏览器(Google Chrome)离线纯净版完全安装指南:告别广告与捆绑骚扰
  • Centos7 自建Umami-开源免费的网站访问流量统计分析平台
  • 申威架构安装Java 11 RPM包教程:java-11.0.7-swjdk-11u-8.ky10.sw_64.rpm详细安装步骤
  • 【STM32项目开源】基于STM32的人体健康监测系统
  • 一个做礼品的网站国外网站用什么dns
  • 东莞 网站建设网站定制制作公司
  • Python 线程 类比c++【python】
  • 舆情监测的底层逻辑与技术方法探析
  • 谈谈redis的持久化
  • 网站建设进度深圳网站建设制作营销
  • SSM高校学生社团管理系统n4pcu(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • 强化学习_Paper_2000_Eligibility Traces for Off-Policy Policy Evaluation
  • Kubernetes秘钥与配置管理全解析
  • Python 匿名函数、map、filter、sort 用法详解
  • wordpress 4.0 伪静态seo优化一般优化哪些方面
  • 上海自助模板建站wordpress被黑
  • 数据可视化延迟实时大屏优化:WebSocket增量传输+Canvas渲染数据延迟压缩至300ms
  • TimerFd Epoll
  • 百度网盘怎么实现不限速的高速下载?
  • UltraEdit做网站教程定制开发网站如何报价单
  • 《彻底理解C语言指针全攻略(5)--指针和函数专题》