1、理解'文件'
1.1 狭义理解
- 文件在磁盘里。
- 磁盘是永久性存储介质,因此文件在磁盘上永久性存储。
- 磁盘是外设(即是输出设备也是输入设备)。
本文介绍 Linux 基础 I/O 概念,涵盖文件狭义与广义理解(一切皆文件),对比 C 标准库接口与系统调用接口。详细讲解文件描述符 fd 的分配规则、默认流(0/1/2)、重定向机制及 dup2() 系统调用。最后分析缓冲区定义、作用及刷新机制,通过现象说明用户级与内核级缓冲区的差异。

文件分为**'内存级 (被打开)'文件**,'磁盘级 (未打开)'文件。
本节主讲'内存级 (被打开)'文件。
FILE *fopen(const char *path, const char *mode);
| mode | 含义 | 文件不存在时 | 文件存在时 | 写入方式 |
|---|---|---|---|---|
"r" | 只读 | 返回 NULL | 正常打开 | 不可写入 |
"r+" | 读写 | 返回 NULL | 正常打开 | 从当前位置覆盖 |
"w" | 只写(新建) | 新建文件 | 清空原内容 | 从头写入 |
"w+" | 读写(新建) | 新建文件 | 清空原内容 | 从头写入 |
"a" | 追加(只写) | 新建文件 | 保留内容,追加到末尾 | 只能末尾追加 |
"a+" | 追加(读写) | 新建文件 | 保留内容,可读/追加写入 | 可读,但写入仅限末尾 |
int fclose(FILE *fp);
注意:
ls /proc/[ 进程 id] -l 命令,查看当前正在运行进程的信息。
| 函数名 | 功能描述 | 适用流类型 | 参数说明 | 返回值 | 备注 |
|---|---|---|---|---|---|
| fgetc | 从流中读取单个字符 | 所有输入流(如 stdin、文件) | FILE *stream(文件指针) | 读取的字符(int)失败返回 EOF | 通常用于逐字符处理 |
| fputc | 向流写入单个字符 | 所有输出流(如 stdout、文件) | int char(字符)FILE *stream | 写入的字符(int)失败返回 EOF | |
| fgets | 从流中读取一行文本 | 所有输入流 | char *str(缓冲区)int n(最大长度)FILE *stream | 成功返回 str 失败返回 NULL | 保留换行符 ` |
| ` | |||||
| fputs | 向流写入一行文本 | 所有输出流 | const char *str(字符串)FILE *stream | 成功返回非负值 失败返回 EOF | 不自动添加换行符 |
| fscanf | 格式化输入(类似 scanf) | 所有输入流 | FILE *stream const char *format(格式字符串)...(变量地址) | 成功匹配的参数数量 失败返回 EOF | 需注意缓冲区溢出风险 |
| fprintf | 格式化输出(类似 printf) | 所有输出流 | FILE *stream const char *format ...(变量值) | 成功返回写入字符数 失败返回负值 | |
| fread | 二进制输入(块读取) | 文件流 | void *ptr(缓冲区)size_t size(每块大小)size_t nmemb(块数)FILE *stream | 实际读取的块数 | 用于结构体等二进制数据 |
| fwrite | 二进制输出(块写入) | 文件流 | const void *ptr(数据地址)size_t size size_t nmemb FILE *stream | 实际写入的块数 |
注意:
写字符串,不用写 \0,因为这是 C 语言的规定,不是文件的规定,写进去会乱码。
C 程序启动,默认打开三个输入输出流,分别是stdin,stdout,stderr。
#include <stdio.h>
extern FILE *stdin; // 标准输入,键盘文件
extern FILE *stdout; // 标准输出,显示器文件
extern FILE *stderr; // 标准错误,显示器文件
使用位图,用比特位作为标志位。
#include <stdio.h>
#define ONE (1 << 0) // 0000 0001 (二进制)
#define TWO (1 << 1) // 0000 0010 (二进制)
#define THREE (1 << 2) // 0000 0100 (二进制)
void func(int flags) {
if (flags & ONE) printf("flags has ONE! ");
if (flags & TWO) printf("flags has TWO! ");
if (flags & THREE) printf("flags has THREE! ");
printf("\n");
}
int main() {
func(ONE); // 输出:flags has ONE!
func(THREE); // 输出:flags has THREE!
func(ONE | TWO); // 输出:flags has ONE! flags has TWO!
func(ONE | TWO | THREE); // 输出:flags has ONE! flags has TWO! flags has THREE!
return 0;
}
man 2 系统调用,有具体说明。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname:要打开或创建的目标文件路径。
flags:打开文件时的选项标志,可以使用以下常量通过"或"运算 (|) 组合:
必须指定且只能指定一个的标志:
可选标志:
**mode:**创建时,文件的权限。
return value:
注意:
那么 C 语言的 fopen 的 flag 就是:
"r" = O_RDONLY;
"w" = O_CREAT | O_WRONLY | O_TRUNC;
"a" = O_CREAT | O_WRONLY | O_APPEND;
"r+" = O_RDWR;
"w+" = O_CREAT | O_RDWR | O_TRUNC;
"a+" = O_CREAT | O_RDWR | O_APPEND。
类比 C 文件相关接口。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
// fd:文件描述符 buf:存储读取数据的缓冲区 count:请求读取的字节数
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
// fd:文件描述符 buf:包含待写入数据的缓冲区 count:请求写入的字节数
#include <unistd.h>
int close(int fd);
// fd:要关闭的文件描述符
注意:
read() 和 write() 的 buf 都是 void*,不关心数据格式,以二进制流输入输出。
那么为什么语言层,有字符流的输入输出?
字符流的输入输出,是因为,我们输入输出的是字符串。
| 类型 | 示例函数 | 所属层级 | 特点 |
|---|---|---|---|
| 库函数 | fopen, fclose, fread, fwrite | C 标准库 (libc) | 1. 提供更高级的抽象 2. 带缓冲区 3. 可移植性更好 4. 最终会调用系统调用 |
| 系统调用 | open, close, read, write, lseek | 操作系统接口 | 1. 直接与内核交互 2. 无缓冲区 3. 效率更高但更底层 4. 与具体操作系统相关 |

Linux 进程默认情况下会有 3 个缺省打开的文件描述符,分别是
标准输入 0,标准输出 1,标准错误 2
0,1,2 对应的物理设备一般是:键盘,显示器,显示器
所以输入输出还可以采用如下方式:
0,1,2 是自动打开的
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h> // 添加 read/write 所需的头文件
int main() {
char buf[1024];
// 从标准输入 (文件描述符 0) 读取数据
ssize_t s = read(0, buf, sizeof(buf) - 1); // 保留 1 字节给结尾的\0
if(s > 0) {
buf[s] = '\0'; // 添加字符串结束符
// 将输入内容同时输出到标准输出 (1) 和标准错误 (2)
write(1, buf, s);
write(2, buf, s);
}
return 0;
}

而现在知道,文件描述符就是从 0 开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了 file 结构体,表示一个已经打开的文件对象。而进程执行 open 系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针 * files,指向一张表 files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
注意:
C 语言的 stdin(fd = 0),stdout(fd = 1),stderr(fd = 2),是一个 FILE 结构体的指针,FILE 结构体里面封装了文件描述符 fd,其他语言也一样。
直接看代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h> // 添加 close 函数所需的头文件
int main() {
int fd = open("myfile", O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd); // 正确的关闭位置
return 0;
}
输出:fd: 3
关闭 fd = 0 或者 fd = 2,再看
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h> // 添加 close() 所需的头文件
int main() {
close(0); // 关闭标准输入(文件描述符 0)
// close(2); // 注释掉的关闭标准错误(文件描述符 2)
int fd = open("myfile", O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd); // 关闭文件描述符
return 0;
}
输出:fd: 0 或 fd: 2
结论:
在 Linux 系统中,文件描述符的分配原则:最小的,没有被使用的下标,作为fd,给新打开的文件。
那如果关闭 fd = 1 呢?看代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main() {
close(1);
int fd = open("myfile", O_CREAT | O_WRONLY | O_TRUNC, 0644);
if(fd < 0) {
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
}
因为语言层只认 stdout 中的 fd = 1,此时下标为 1 的指针指向 myfile,所以
本来应该输出到显示器上的内容,输出到了 myfile 文件中。
这种现象叫做输出重定向。
常见的重定向有:>,>>,<。
输出重定向的本质:

注意:
#include <unistd.h>
int dup2(int oldfd, int newfd);
oldfd的指针 覆盖 newfd的指针。
如:dup2(fd,0),实现输入重定向,dup2(fd,1),实现输出重定向。
所以,重定向 = 文件打开方式 + dup2()。
首先,在 Windows 中是文件的东西,它们在 Linux 中也是文件;其次一些在 Windows 中不是文件的东西,比如进程、磁盘、显示器、键盘这样的硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息;甚至管道,也是文件;将来我们要学习网络编程中的 socket(套接字)这样的东西,使用的接口跟文件接口也是一致的。
这样做最明显的好处是,开发者仅需要使用一套 API ,即可调取 Linux 系统中绝大部分的资源。举个简单的例子,Linux 中几乎所有读(读文件,读系统状态,读 PIPE)的操作都可以用 read 函数来进行;几乎所有更改(更改文件,更改系统参数,写 PIPE)的操作都可以用 write 函数来进行。

上图中的外设,每个设备都可以有自己的 read、write,但一定是对应着不同的操作方法!!但通过 struct file 下的 struct file_operations 中的各种函数回调,让我们开发者只用 file 便可调取 Linux 系统中绝大部分的资源!!这便是 "Linux 下一切皆文件" 的核心理解。
多态的体现。
临时存储数据的内存区域。
提高使用者的效率。

基于上面的机制,可以理解下面的现象:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
// 关闭标准输出(文件描述符 1)
close(1);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0664);
if (fd < 0) {
perror("open");
return 1;
}
printf("hello world: %d\n", fd); // 注意:这里打印的 fd 值应该是 1
close(fd);
return 0;
}
这个时候,对于普通文件,应该是满了刷新,可是没满,也没有强制刷新,然后关闭了 fd,在程序退出时,刷新,但 fd 已经关闭了,刷新不了,所以 log.txt 中不会有数据。
可以使用 fflush() 强制刷新下缓冲区。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
// 关闭标准输出(文件描述符 1)
close(1);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0664);
if (fd < 0) {
perror("open");
return 1;
}
printf("hello world: %d\n", fd); // 注意:这里打印的 fd 值应该是 1
fflush(stdout); // 强制刷新
close(fd);
return 0;
}
注意:stderr是不带缓冲区,即立即刷新。
#include <stdio.h>
#include <string.h>
#include <unistd.h> // 添加 write() 和 fork() 所需的头文件
int main() {
const char *msg0 = "hello printf\n";
const char *msg1 = "hello fwrite\n";
const char *msg2 = "hello write\n";
printf("%s", msg0);
fwrite(msg1, 1, strlen(msg1), stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
结果:
hello printf hello fwrite hello write
显示器,行刷新;
系统调用 write(),直接写入内核。
但是重定向一下 ./hello > file,结果:
hello write hello printf hello fwrite hello printf hello fwrite
系统调用 write(),直接写入内核;
重定向,改变了刷新方式,普通文件,满了刷新,可是没满,也没有强制刷新,程序退出时,刷新,父子进程各刷新一份。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 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