前言
在 Linux 世界里,'一切皆文件'是最核心的设计哲学之一。小到终端输入输出,大到网络通信、设备交互,底层都依赖基础 IO完成数据流转。无论你是后端开发、嵌入式工程师,还是内核爱好者,掌握基础 IO 的底层逻辑,都是打通 Linux 开发任督二脉的关键一步。
Linux 基础 IO 核心在于理解“一切皆文件”哲学,涵盖 C 标准库与内核系统调用两层机制。C 库函数(如 fopen)提供带缓冲区的跨平台接口,而系统调用(如 open)直接操作内核资源。文章通过代码对比了两者在文件读写、权限设置及模式上的差异,指出掌握底层逻辑对后续学习网络 IO 和进程通信至关重要。

在 Linux 世界里,'一切皆文件'是最核心的设计哲学之一。小到终端输入输出,大到网络通信、设备交互,底层都依赖基础 IO完成数据流转。无论你是后端开发、嵌入式工程师,还是内核爱好者,掌握基础 IO 的底层逻辑,都是打通 Linux 开发任督二脉的关键一步。
'文件'的概念远比我们想象的宽泛,这是理解 IO 的前提:
属性(元数据)+ 内容**组成,即使是 0KB 的空文件,也会占用磁盘空间存储属性;Linux 下'一切皆文件',键盘、显示器、网卡、进程等都被抽象为文件,统一通过 IO 接口操作,在之后的学习中会深入理解这一概念;进程对文件的操作**,磁盘由操作系统管理,任何文件读写最终都要通过系统调用接口实现,C 库函数只是封装层。在学习 Linux 系统 IO 之前,我们大多已经接触过 C 标准库的文件操作——这是跨平台的'上层工具',也是理解底层原理的起点。
C 标准库提供了**fopen、fclose、fread、fwrite、fprintf等封装好的函数,它们最大的特点是自带用户态缓冲区**,并通过标准化接口实现了跨平台兼容。
举个简单的例子,用 C 库写入文件:
#include <stdio.h>
int main() {
FILE* fp = fopen("test.txt", "w");
if (fp == NULL) return -1;
fputs("Hello Linux IO\n", fp);
fclose(fp);
return 0;
}
这类函数封装了底层系统调用,自带用户态缓冲区,可在 Windows、Linux、macOS 等平台使用。
| 功能分类 | 函数名 | 函数原型(核心简化版) | 功能说明 | 关键备注 |
|---|---|---|---|---|
| 打开 / 关闭 | fopen | FILE *fopen(const char *path, const char *mode); | 打开指定路径的文件,返回 FILE 结构体指针(文件句柄) | mode 常用值:"r"(只读)、"w"(只写,覆盖创建)、"a"(追加)、"r+"(读写);失败返回 NULL |
fclose | int fclose(FILE *stream); | 关闭已打开的 FILE 句柄,刷新缓冲区数据到文件 | 成功返回 0,失败返回 EOF;关闭后 FILE 指针不可再使用 | |
| 字节 / 块读写 | fread | size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); | 从文件读取数据到内存缓冲区 | 返回实际读取的'数据块个数'(nmemb),而非字节数;到达文件末尾可能返回小于传入的 nmemb |
fwrite | size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); | 把内存缓冲区数据写入文件 | 返回实际写入的'数据块个数';数据先存入用户态缓冲区,不立即落盘 | |
| 字符读写 | fgetc/getc | int fgetc(FILE *stream); | 从文件读取单个字符 | 返回读取的字符(强转为 int),到达末尾或失败返回 EOF |
fputc/putc | int fputc(int c, FILE *stream); | 向文件写入单个字符 | 成功返回写入的字符,失败返回 EOF | |
| 字符串读写 | fgets | char *fgets(char *str, int n, FILE *stream); | 从文件读取一行字符串(最多 n-1 个字符),自动添加 `' | |
| '` 终止符 | 遇到换行符 `' | |||
'或文件末尾停止;返回str指针,失败 / 末尾返回NULL` | ||||
fputs | int fputs(const char *str, FILE *stream); | 向文件写入字符串(不自动添加换行符) | 成功返回非负整数,失败返回 EOF | |
| 格式化读写 | fprintf | int fprintf(FILE *stream, const char *format, ...); | 按指定格式向文件写入数据(类似 printf,输出目标为文件) | 成功返回写入的字符总数,失败返回负数 |
fscanf | int fscanf(FILE *stream, const char *format, ...); | 按指定格式从文件读取数据到变量(类似 scanf,输入来源为文件) | 成功返回匹配并赋值的变量个数,失败 / 末尾返回 EOF | |
| 文件定位 | fseek | int fseek(FILE *stream, long offset, int whence); | 移动文件读写指针到指定位置 | whence:SEEK_SET(文件开头)、SEEK_CUR(当前位置)、SEEK_END(文件末尾);成功返回 0 |
ftell | long ftell(FILE *stream); | 获取当前文件读写指针相对于文件开头的偏移量(字节数) | 成功返回偏移量,失败返回 -1L | |
rewind | void rewind(FILE *stream); | 把文件读写指针重置到文件开头(等价于 fseek(stream, 0, SEEK_SET)) | 无返回值,会清除文件的错误标记 | |
| 缓冲区操作 | fflush | int fflush(FILE *stream); | 强制刷新用户态缓冲区,把数据写入底层文件(内核缓冲区) | 成功返回 0,失败返回 EOF;fflush(NULL) 刷新所有打开的文件句柄 |
setvbuf | int setvbuf(FILE *stream, char *buf, int mode, size_t size); | 设置文件句柄的缓冲区类型和大小 | mode:_IOFBF(全缓冲)、_IOLBF(行缓冲)、_IONBF(无缓冲);成功返回 0 | |
| 错误处理 | ferror | int ferror(FILE *stream); | 检查文件操作是否发生错误 | 有错误返回非 0,无错误返回 0 |
feof | int feof(FILE *stream); | 检查是否到达文件末尾 |
r(只读)、w(只写,清空创建)、a(追加)、r+(读写)、w+(读写,清空创建)、a+(读写,追加);**fwrite**用于向文件写入数据,适用于二进制文件和文本文件
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main() {
FILE *fp = fopen("log.txt", "w");
if (!fp) {
perror("fopen");
return -1;
}
const char *message = "hello fwrite\n";
int count = 10;
// 循环写入
while (count--) {
fwrite(message, strlen(message), 1, fp); // 不用 + 1
// fputs(message, fp);
// fprintf(fp, "hello fwrite: %d\n", cnt);
}
fclose(fp);
return 0;
}
**fread**用于从文件读取数据,需通过返回值判断读取结果
#include <stdio.h>
#include <string.h>
int main() {
FILE *fp = fopen("load.txt", "r");
if (!fp) {
perror("fopen");
return -1;
}
char outbuf[1024];
const char *msg = "hello Lotso!\n";
while (1) {
size_t s = fread(outbuf, 1, strlen(msg), fp);
if (s > 0) {
outbuf[s] = '\0';
printf("%s", outbuf);
}
// 判断是否到达文件末尾
if (feof(fp)) {
break;
}
}
fclose(fp);
return 0;
}
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main() {
FILE *fp = fopen("log.txt", "r");
if (NULL == fp) {
perror("fopen");
return 0;
}
char inbuffer[1024];
while (1) {
// ftell 的使用
// long pos = ftell(fp);
// printf("pos: %ld\n", pos);
// int ch = fgetc(fp);
// if(ch == EOF)
// {
// break;
// }
printf("%c\n", ch);
if (!fgets(inbuffer, sizeof(inbuffer), fp)) {
break;
}
printf("file : %s", inbuffer);
}
fclose(fp);
return 0;
}
C 语言默认打开 3 个标准流,类型均为FILE*,对应系统的 3 个默认文件描述符:
stdin:标准输入(键盘),对应文件描述符 0;stdout:标准输出(显示器),对应文件描述符 1;stderr:标准错误(显示器),对应文件描述符 2。如果说 C 库函数是'快捷方式',那么系统调用就是直接和内核对话的'原生接口'。Linux 提供了**open、close、read、write**等系统调用,它们跳过了用户态缓冲区,直接操作内核态资源。
头文件:
<sys/types.h>、<sys/stat.h>、<fcntl.h>、<unistd.h>
| 系统调用 | 对应 C 库函数 | 功能描述 |
| open | fopen | 打开 / 创建文件 |
| read | fread | 从文件读取数据 |
| write | fwrite | 向文件写入数据 |
| close | fclose | 关闭文件 |
参数说明:
O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写);O_CREAT(文件不存在则创建)、O_APPEND(追加模式)、O_TRUNC(清空文件);0644、0755),仅当 flags 包含 O_CREAT 时有效;-1权限说明:
mode参数指指定的是文件的'默认权限',最终权限会被**umask(权限掩码,以前就学习过了)修正,公式为:最终权限 = mode & ~umask**。举例:默认umask为0002,因此mode=0666时,最终权限为0644。在下面我们还会再涉及到这个的,并且提到了一个就近原则
一个小 demo 理解位图:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define ONE (1<<0) // 1
#define TWO (1<<1) // 1
#define THREE (1<<2) // 4
#define FOUR (1<<3) // 8
#define FIVE (1<<4) // 16
void Print(int flags) {
if(flags & ONE) printf("ONE\n");
if(flags & TWO) printf("TWO\n");
if(flags & THREE) printf("THREE\n");
if(flags & FOUR) printf("FOUR\n");
if(flags & FIVE) printf("FIVE\n");
}
int main() {
Print(ONE); printf("\n");
Print(TWO); printf("\n");
Print(ONE | TWO); printf("\n");
Print(ONE | TWO | THREE); printf("\n");
Print(ONE | TWO | THREE | FOUR); printf("\n");
Print(TWO | THREE | FOUR | FIVE);
}
用**open、write、close实现与 C 库fopen**相同的功能:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
umask(0); // 清除默认权限掩码,确保创建文件权限正确
// 打开文件:只写模式,不存在则创建,权限 0666
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
if (fd < 0) {
// 打开失败,fd 为 -1
perror("open"); // 打印错误信息
return 1;
}
const char *msg = "hello open\n";
int len = strlen(msg);
int count = 5;
while (count--) {
// 写入数据:参数(文件描述符、数据地址、写入字节数)
write(fd, msg, len);
}
close(fd); // 关闭文件,释放文件描述符
return 0;
}
追加模式:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main() {
umask(0);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
if(fd < 0) {
perror("open");
return 1;
}
const char *msg = "hello world\n";
int cnt = 10;
while(cnt--) {
write(fd, msg, strlen(msg));
}
close(fd);
return 0;
}
用**open、read、close**实现文件读取:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
// 只读模式打开文件
int fd = open("load.txt", O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
const char *msg = "hello world\n";
char buf[1024];
while (1) {
// 读取数据:参数(文件描述符、缓冲区地址、读取字节数)
ssize_t s = read(fd, buf, strlen(msg));
if (s > 0) {
// 成功读取到 s 个字节
printf("%s", buf);
} else {
// s=0 表示文件末尾,s<0 表示错误
break;
}
}
close(fd);
return 0;
}
补充:
// cat file.txt
int main(int argc, char *argv[]) {
if(argc != 2) {
printf("Usage: %s filename\n", argv[0]); // ./myfile filename
return 1;
}
int fd = open(argv[1], O_RDONLY);
if(fd < 0) {
perror("open");
return 2;
}
char inbuffer[128];
while(1) {
ssize_t n = read(fd, inbuffer, sizeof(inbuffer)-1);
if(n > 0) {
inbuffer[n] = 0;
printf("%s", inbuffer);
} else if(n == 0) {
printf("end of file!\n");
break;
} else {
perror("read");
break;
}
}
close(fd);
return 0;
}
Linux 基础 IO 的核心,是理解'用户态 - 内核态 - 硬件'三层数据流转的逻辑:
掌握这些知识,不仅能帮你写出更高效的 IO 代码,也为后续学习网络 IO、进程间通信、内核驱动打下了坚实基础。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online