学习 Linux 系统编程,基础 IO是绕不开的核心关卡。它连接了用户态与内核态,串联起文件操作、文件描述符、重定向、缓冲区等关键知识点。本文结合 C 语言文件 IO 与 Linux 系统调用,从零到一吃透 Linux 基础 IO,帮你建立完整的知识体系。
一、重新理解「文件」
1. 狭义与广义文件
- 狭义文件 :存储在磁盘上的永久数据,磁盘属于外设,文件操作本质是I/O(输入输出)。
- 广义文件:Linux 一切皆文件!键盘、显示器、网卡、管道、套接字等硬件与抽象资源,都被抽象为文件,统一用一套 IO 接口操作。
2. 文件的本质构成
文件 = 内容数据 + 属性元数据
- 空文件(0KB)也占用磁盘空间,因为需要存储属性。
- 所有文件操作,最终都是对内容 或属性的操作。
3. 系统视角下的文件
- 磁盘由操作系统管理,文件操作本质是进程对文件的操作。
- C 语言
fopen/fread/fwrite等库函数,底层都是封装 Linux 系统调用,真正操作硬件的是内核提供的系统接口。
二、C 语言文件 IO 回顾(用户层接口)
C 语言标准库提供了完整的文件操作函数,默认打开三个标准流:
stdin(标准输入,0):键盘stdout(标准输出,1):显示器stderr(标准错误,2):显示器
1. 核心操作函数
cpp
// 打开文件
FILE* fopen(const char* path, const char* mode);
// 写入文件
size_t fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream);
// 读取文件
size_t fread(void* ptr, size_t size, size_t nmemb, FILE* stream);
// 关闭文件
int fclose(FILE* fp);
2. 文件打开模式
r:只读,文件必须存在w:只写,文件不存在则创建,存在则清空a:追加写,文件不存在则创建r+/w+/a+:读写模式,逻辑对应基础模式
3. 输出到显示器的多种方式
cpp
printf("hello printf\n");
fputs("hello fputs\n", stdout);
fwrite("hello fwrite\n", 1, 12, stdout);
fprintf(stdout, "hello fprintf\n");
三、Linux 系统文件 IO(内核层接口)
库函数是封装,系统调用才是底层真相 。Linux 提供open/read/write/close等接口,直接与内核交互。
1. 核心系统调用
(1)open:打开 / 创建文件
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char* pathname, int flags, mode_t mode);
- flags :打开标志,用按位或 组合
O_RDONLY:只读O_WRONLY:只写O_RDWR:读写O_CREAT:文件不存在则创建O_TRUNC:清空文件O_APPEND:追加写
- mode :创建文件时指定权限(如
0644),需配合umask(0)使用。
(2)read/write/close
cpp
ssize_t read(int fd, void* buf, size_t count); // 读
ssize_t write(int fd, const void* buf, size_t count); // 写
int close(int fd); // 关闭文件描述符
2. 系统调用示例(写入文件)
cpp
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
umask(0);
// 只写|创建|清空,权限0644
int fd = open("myfile.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open failed");
return 1;
}
const char* msg = "hello system IO\n";
write(fd, msg, strlen(msg));
close(fd);
return 0;
}
四、文件描述符(fd):内核访问文件的唯一标识
1. fd 是什么?
- 进程打开文件时,内核会创建
struct file描述文件,并在进程的files_struct中用指针数组管理打开的文件。 - 文件描述符 fd :就是这个指针数组的下标(从 0 开始的小整数)。
2. 默认 fd 分配
进程启动时,默认打开 0、1、2 三个 fd:
- 0:标准输入(stdin)
- 1:标准输出(stdout)
- 2:标准错误(stderr)
3. fd 分配规则
找当前未使用的最小下标作为新 fd。
- 关闭 0,新打开文件 fd=0;
- 关闭 1,新打开文件 fd=1;
- 关闭 2,新打开文件 fd=2。
五、重定向:改变数据流向
1. 重定向本质
修改 fd_array 数组中,下标对应的文件指针指向。
- 原本
fd=1指向显示器,关闭 1 后打开新文件,fd=1指向新文件,所有输出都会写入文件。
2. 常见重定向类型
>:输出重定向(覆盖)>>:追加重定向<:输入重定向
3. dup2:标准重定向接口
cpp
int dup2(int oldfd, int newfd);
- 作用:把
newfd指向oldfd对应的文件,自动关闭newfd原指向。 - 示例:输出重定向到文件
cpp
int fd = open("log.txt", O_WRONLY | O_CREAT, 0644);
dup2(fd, 1); // 把stdout重定向到log.txt
printf("hello redirect\n");
fflush(stdout);
4. 自定义 Shell 实现重定向
核心逻辑:
- 解析命令中的
>/>>/<符号 - 子进程中调用
open+dup2完成重定向 - 执行程序替换,不影响父 Shell
六、Linux「一切皆文件」的底层原理
1. 虚拟文件系统(VFS)
内核用struct file统一描述所有打开的文件,其中f_op指向struct file_operations(函数指针集合)。
2. 统一接口,不同实现
- 磁盘、键盘、显示器等硬件,
read/write实现不同。 - 对用户层暴露统一的
open/read/write接口,上层无需关心底层硬件差异。
这就是 Linux「一切皆文件」的核心:用统一抽象屏蔽硬件差异,一套 IO 接口操作所有资源。
七、缓冲区:提升 IO 效率的关键
1. 缓冲区是什么?
内存中预留的一段空间,用于缓存 IO 数据,减少系统调用与 CPU 切换开销。
2. 三种缓冲类型
- 全缓冲:缓冲区满才刷新(磁盘文件默认)
- 行缓冲 :遇到
\n或缓冲区满刷新(stdout 默认) - 无缓冲:立即写入(stderr 默认)
3. 刷新缓冲区的时机
- 缓冲区满
- 调用
fflush强制刷新 - 进程退出
- 关闭文件
4. 库函数缓冲区 vs 系统调用
printf/fwrite(C 库函数):带用户级缓冲区write(系统调用):无用户缓冲区,直接进入内核- 重定向到文件时,stdout 从行缓冲变为全缓冲,容易出现数据不刷新问题。
八、FILE 结构体:库函数与系统调用的桥梁
C 标准库的FILE结构体,封装了文件描述符 fd + 用户缓冲区:
cpp
struct _IO_FILE {
int _flags;
char* _IO_write_ptr; // 缓冲区指针
char* _IO_write_end;
int _fileno; // 封装的文件描述符fd
// ... 缓冲区与状态字段
};
FILE*是用户层操作句柄,底层通过_fileno找到 fd,调用系统调用完成 IO。
九、核心总结(一张图记住所有重点)
- 文件 = 内容 + 属性,Linux 一切皆文件
- 库函数封装系统调用,
FILE封装fd - fd 是进程文件数组的下标,最小未使用分配
- 重定向 = 修改 fd 指向,
dup2是标准实现 - 缓冲区分用户 / 内核层,目的是减少系统调用
- 行缓冲→重定向→全缓冲,记得
fflush刷新
Linux 基础 IO 是进程、文件、内核交互的核心,理解本文内容,后续学习管道、套接字、高级 IO 都会事半功倍。动手写一遍open/read/write和重定向代码,能更快吃透底层逻辑