欢迎光临
我们一直在努力

Linux篇(六):文件 IO 深度剖析:从标准流、系统调用到内核底层全貌拆解

在 Linux 开发、C/C++ 后端、嵌入式编程中,文件 IO 是绕不开的核心基础。很多开发者只会简单调用 printf、fopen、open,却分不清标准库缓冲 IO和原生系统调用 IO的本质区别,不懂 stdin/stdout/stderr 三个标准流的内核承载逻辑,也不理解文件描述符、重定向、进程文件表、内核文件结构体之间的关联。

本文结合完整内核流程图,自上而下梳理 Linux 文件 IO 全链路:先从应用层标准输入输出流讲起,再拆解底层 open/read/write/close 系统调用,逐层穿透进程文件描述符表、内核文件结构体、磁盘 inode 缓冲模型,同时理清文件打开模式、缓冲区机制、dup2 重定向原理,一次性打通「用户层代码→标准库封装→操作系统内核→磁盘文件」的完整数据流,帮你彻底吃透 Linux IO 的底层逻辑。

一、系统视角:一切皆文件,文件操作的本质

Linux 核心哲学是一切皆文件,对普通文件、管道、终端、套接字的操作,底层全部统一为文件 IO 接口。

1.从系统层级划分,文件操作分为两层:

1.库层 IO(内存级缓冲文件)

C 标准库 stdio.h 提供 fopen/fread/fwrite/printf 等,封装缓冲层,上层语言(C++/Python/Java)的输入输出都基于这套封装实现,提升读写效率;

#include<stdio.h>
#include<string.h>

using namespace std;

//C语言
int main()
{
FILE *fp=fopen("myfile","w");
if(fp==NULL)
{
perror("file error\\n");
exit(1);
}
const char *msg="hello linux\\n";
int count=4;
while(count–)
fwrite(msg,strlen(msg),1,fp);
fclose(fp);
return 0;
}


2.系统调用 IO(磁盘级文件)

内核原生接口 open/read/write/close,无用户态缓冲,是标准库底层真正和操作系统交互的基础。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
int fd = open("myfile1", O_CREAT | O_WRONLY, 0666);

if(fd < 0)
{
perror("file error\\n");
exit(1);
}

const char *msg = "hello linux\\n";
int count = 4;

while(count–)
{
write(fd, msg, strlen(msg));
}

close(fd);

return 0;
}

所有对文件的读写,最终都会下沉到系统调用;标准库只是对系统调用做了缓冲封装,二者核心操作对象完全不同:

1、库层操作 FILE* 文件流指针,系统调用操作 int 类型文件描述符 fd。
2、操作系统会统一管理进程打开的所有文件,每个进程独立维护一套文件描述符资源,程序打开文件、读写、关闭,本质都是进程和内核之间交互文件管理结构。

二、应用层标准流:stdin /stdout/stderr

程序启动时,操作系统会自动为进程预打开 3 个标准 IO 流,对应固定文件描述符:

1.fd=0:stdin 标准输入,绑定键盘终端;
2.fd=1:stdout 标准输出,默认打印显示器;
3.fd=2:stderr 标准错误,错误信息输出,默认显示器。

1 标准流基础特性

三个流本质都是 FILE* 类型,由 stdio.h 外部全局变量声明:

printf、cout 底层默认操作 stdout;

scanf、cin 操作 stdin;

perror、报错打印默认走 stderr。

stdout 和 stderr 分离的核心价值:标准输出可重定向到日志文件,而错误流单独输出控制台,方便区分正常业务输出和程序异常报错。

2 向显示器输出信息的两类实现方案

1.标准库缓冲方案(FILE 流)

格式化输出:printf/fprintf/sprintf
字符 / 字符串输出:fputs/fputc/puts
块读写:fwrite
自带用户态缓冲区,减少频繁系统调用,IO 效率更高,跨平台可移植性强。

#include <stdio.h>

int main()
{
printf("hello\\n");
fprintf(stdout, "hello\\n");
return 0;
}

2.生系统调用方案(文件描述符)

直接调用 write(int fd, void *buf, size_t count),指定 fd=1 即可向显示器输出,无用户缓冲,每次调用都会触发内核态切换,性能更低,但可控性更强。

#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
const char *msg = "hello\\n";
int count = 5;
while(count–)
{
write(1, msg, strlen(msg));
}
return 0;
}

三、底层系统调用:open /read/write /close

我们前面很大频率用到这几个函数,但对他们知道的很少,下面我们来了解一下。

在了解他们之前,我们来看一下什么是文件标志位

1.文件标志位(File Status Flags)

文件标志位 是一组用于控制文件描述符(File Descriptor)行为和属性的二进制位。

简单来说,它就像文件的“开关”或“模式设定”,决定了你打开文件后,系统该如何处理对这个文件的读写操作。

1.核心特点

1.绑定对象:它属于文件描述符(如 open() 返回的 fd),而不是底层的文件本身。
2.存储位置:这些标志位存储在内核中,与每个打开的文件描述符相关联。
3.作用范围:它只影响当前进程通过这个 fd 进行的 I/O 操作。

2.常见的文件标志位

标志位类别作用说明可动态修改
O_RDONLY 访问模式 以只读方式打开文件 ❌
O_WRONLY 访问模式 以只写方式打开文件 ❌
O_RDWR 访问模式 以读写方式打开文件 ❌
O_CREAT 创建标志 文件不存在时自动创建 ❌
O_EXCL 创建标志 与 O_CREAT 联用,文件存在则报错 ❌
O_TRUNC 截断标志 文件存在且可写时,长度截断为0 ❌
O_APPEND 写入行为 每次写入前自动将偏移量移至文件末尾 ✅
O_NONBLOCK 写入行为 非阻塞模式,操作无法立即完成时不挂起 ✅
O_SYNC 写入行为 同步写入,数据与元数据都刷入磁盘后才返回 ✅
O_DSYNC 写入行为 仅同步数据写入磁盘,不保证元数据同步 ✅
O_NOCTTY 终端控制 若打开的是终端设备,不将其作为控制终端 ❌
O_CLOEXEC 执行安全 等价于 FD_CLOEXEC,exec 时自动关闭该 fd ❌

3.核心 flag 打开模式

1.O_WRONLY | O_TRUNC | O_CREAT:只写、文件存在则清空长度、不存在则创建;
2.O_WRONLY | O_APPEND | O_CREAT:追加写,内容写入文件末尾,不会清空原有数据;
3.O_RDONLY:只读打开;

mode 参数仅新建文件时生效,用于设置文件权限(如 0666)。

4.标志位作用原理

#include <stdio.h>

#define ONE_FLAG (1<<0) // 0000 0000 0000…0000 0001
#define TWO_FLAG (1<<1) // 0000 0000 0000…0000 0010
#define THREE_FLAG (1<<2) // 0000 0000 0000…0000 0100
#define FOUR_FLAG (1<<3) // 0000 0000 0000…0000 1000

void Print(int flags)
{
if(flags & ONE_FLAG)
{
printf("One!\\n");
}
if(flags & TWO_FLAG)
{
printf("Two\\n");
}
if(flags & THREE_FLAG)
{
printf("Three\\n");
}
if(flags & FOUR_FLAG)
{
printf("Four\\n");
}
}

int main()
{
Print(ONE_FLAG);
printf("\\n");
Print(ONE_FLAG | TWO_FLAG);
printf("\\n");
Print(ONE_FLAG | TWO_FLAG | THREE_FLAG);
printf("\\n");
Print(ONE_FLAG | TWO_FLAG | THREE_FLAG | FOUR_FLAG);
printf("\\n");
Print(ONE_FLAG | FOUR_FLAG);
printf("\\n");
return 0;
}

1.open

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

1.第一种形式:用于打开已存在的文件,不需要指定权限。
1.第二种形式:当 flags 参数中包含 O_CREAT(即需要创建新文件)时,必须使用此形式并提供     第三个参数 mode 来指定新文件的访问权限(如 0644)。

注意:虽然 C 语言标准不支持真正的函数重载,但在系统调用文档或某些 C++ 封装中,常以这种方式展示该函数的可变参数特性。

1.int open(const char *pathname, int flags);

#include <stdio.h>
#include <fcntl.h> // 包含 open 函数和 O_RDONLY 等宏定义
#include <unistd.h> // 包含 close 函数
#include <sys/stat.h>

int main() {
// 假设当前目录下有一个名为 "test.txt" 的文件
const char *filename = "test.txt";

// 使用两个参数的形式打开文件
// O_RDONLY: 只读模式
int fd = open(filename, O_RDONLY);

if (fd == -1) {
perror("打开文件失败");
return 1;
}

printf("成功打开文件,文件描述符为: %d\\n", fd);

// … 这里可以进行 read() 操作 …

close(fd); // 记得关闭文件描述符
return 0;
}

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

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h> // 包含 S_IRUSR, S_IWUSR 等权限宏

int main() {
const char *filename = "new_file.txt";

// 使用三个参数的形式创建文件
// O_CREAT: 如果文件不存在则创建
// O_WRONLY: 以只写方式打开
// O_TRUNC: 如果文件已存在,清空内容
// S_IRUSR | S_IWUSR: 设置权限为 所有者可读(4) + 所有者可写(2) = 6 (即 rw——-)
int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR);

if (fd == -1) {
perror("创建文件失败");
return 1;
}

printf("成功创建/打开文件,文件描述符为: %d\\n", fd);

// … 这里可以进行 write() 操作 …

close(fd);
return 0;
}

2 read & write:无缓冲原生读写

1.read

size_t read(int fd, void *buf, size_t count);

1.参数说明

fd:文件描述符,代表一个已打开的文件或 I/O 流(如标准输入 0、网络套接字等)。
buf:指向用户缓冲区的指针,用于接收读取到的数据。
count:希望读取的最大字节数。

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main()
{
int fd = open("myfile", O_RDONLY);
if (fd < 0)
{
perror("open fail");
exit(1);
}

char buffer[1024];
int size = 0;

// 【循环内】只处理读取成功的情况 (size > 0)
while ((size = read(fd, buffer, sizeof(buffer) – 1)) > 0)
{
buffer[size] = '\\0'; // 确保字符串安全截断
printf("%s", buffer); // 注意:通常不需要加 \\n,除非你想每读一段就换行
}

// 【循环外】处理循环结束的原因
if (size == 0)
{
printf("\\n— 文件读取完毕 —\\n");
}
else // size < 0
{
perror("read error");
}

close(fd);
return 0;
}

2.write

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

1.参数说明

fd:文件描述符,必须是通过 open() 成功打开且具备写权限(如 O_WRONLY 或                       O_RDWR)的文件。
buf:指向待写入数据的内存缓冲区,类型为 const void*,表示数据源不可修改。
count:希望写入的字节数,类型为 size_t(无符号整型)

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
// 1. 加上 O_WRONLY 允许写入
// 2. (可选) 加上 O_CREAT,防止文件不存在时报错
int fd = open("myfile", O_WRONLY | O_APPEND | O_CREAT, 0644);

if (fd < 0) {
perror("open fail");
return 1;
}

const char *msg = "hello wyy\\n";

// 3. 检查 write 是否成功
ssize_t bytes_written = write(fd, msg, strlen(msg));
if (bytes_written == -1) {
perror("write fail");
} else {
printf("成功写入 %zd 字节\\n", bytes_written);
}

close(fd); // 记得关闭文件描述符
return 0;
}

3 close:释放文件描述符资源

进程打开文件会占用 fd 资源,进程退出前必须调用 close 释放,否则会出现文件描述符泄漏,耗尽进程最大可打开文件数。

int close(int fd);

四、内核底层架构:进程文件表、file 结构体、inode

1.进程 task_struct:Linux 进程核心结构体,内部包含 files_struct 文件管理结构;

2.files_struct 文件描述符表:维护 fd_array[] 数组,数组下标就是文件描述符 fd,每一项指针指向内核 struct file;
fd 0/1/2 固定指向标准输入、输出、错误流;
新打开文件会分配当前最小未占用 fd 下标,这是 fd 自增分配的底层原理。

3.struct file 内核文件对象:每打开一次文件生成一个独立实例,存储文件读写偏移量、打开模式、内核缓冲区、操作函数集;多个 fd 可以指向同一个 file 实例(重定向场景)。

4.inode 磁盘文件元信息:代表磁盘上真实文件,存储文件大小、权限、磁盘块地址;同一个文件多次打开会生成多个 struct file,但共享同一个 inode。

五、文件重定向原理:dup /dup2 系统调用

重定向是 Linux IO 最常用的能力,核心依赖 dup2:

int dup2(int oldfd, int newfd);

功能:将 newfd 文件描述符指向 oldfd 对应的内核 struct file,若 newfd 原本打开文件,会自动关闭原有资源。

#include <stdio.h> // 提供 printf
#include <fcntl.h> // 提供 open, O_RDONLY
#include <unistd.h> // 提供 read, dup2

int main(int argc, char *argv[])
{
if (argc == 2)
{
int fd = open(argv[1], O_RDONLY);
if (fd > 0)
dup2(fd, 0);

char buffer[1024];
if ((read(fd, buffer, sizeof(buffer) – 1) >= 0))
{
printf("%s\\n", buffer);
}
}
return 0;
}

六、标准库 IO vs 系统调用 IO 核心差异总结

维度标准库 FILE 流 (fopen/printf)系统调用 (fd/open/write)
操作对象 FILE* 流指针 int 文件描述符 fd
缓冲机制 用户态自带缓冲区,减少系统调用 无用户缓冲,直接交互内核
可移植性 跨平台(Windows/Linux 通用) Linux/Unix 专属,不可跨平台
使用场景 上层业务、简单打印、通用程序 嵌入式、底层服务、重定向、高性能 IO
依赖头文件 stdio.h fcntl.h / unistd.h
赞(0)
未经允许不得转载:171主机测评 » Linux篇(六):文件 IO 深度剖析:从标准流、系统调用到内核底层全貌拆解
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址