先说结论:
一切皆文件不是口号,而是内核把每个设备都装进struct file,再用file_operations里的函数指针做回调,于是开发者拿着一个 fd 就能读文件、读系统状态、读管道缓冲区是一块内存,让低速外设和高速 CPU 协调工作------它由 C 标准库提供,所以库函数有缓冲、write没有,后面两个怪现象都源于此。
一、一切皆文件:Linux 的终极抽象
在 Windows 里是文件的,在 Linux 里也是文件;Windows 里不是文件的进程、磁盘、显示器、键盘,在 Linux 里同样被抽象成文件,可以用访问文件的方式取到信息。管道是文件,将来网络编程里的 socket,接口也和文件一致。
好处很直接:一套 API 通吃。
- 几乎所有"读"都能用
read:读文件、读系统状态、读管道; - 几乎所有"改"都能用
write:改文件、改系统参数、写管道。
二、内核凭什么做到:struct file + file_operations
打开文件时,操作系统会为它创建一个 file 结构体(定义在 include/linux/fs.h),它同时属于所有设备:
c
struct file {
struct inode *f_inode;
const struct file_operations *f_op; // 函数指针表
atomic_long_t f_count; // 引用计数
unsigned int f_flags; // 打开权限
fmode_t f_mode; // 访问模式
loff_t f_pos; // 当前读写位置
};
关键在于 f_op,它指向的 file_operations 里除了 owner 几乎全是函数指针:
c
struct file_operations {
loff_t (*llseek)(struct file *, loff_t, int); // 改读写位置
ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
int (*open)(struct inode *, struct file *);
int (*release)(struct inode *, struct file *);
};
每个成员都对应一个系统调用。用户调 read,内核读出相应的函数指针并把控制权交出去,设备驱动的工作就完成了。每个设备都可以有自己的 read、write 实现,但通过这层回调,开发者只用 file 就能调取系统绝大部分资源------这就是"Linux 下一切皆文件"的核心理解。
本小节复盘和图示:

三、缓冲区:给高速 CPU 配一条缓冲带
缓冲区是内存空间的一部分:在内存中预留一块存储空间,用来缓冲输入或输出的数据。按对应设备方向,分为输入缓冲区和输出缓冲区。
为什么要引入它?不设缓冲区时,每次读写都要对磁盘发起系统调用,而一次系统调用意味着 CPU 从用户空间切到内核空间、发生上下文切换,CPU 时间是白花的,频繁的磁盘访问会严重拖慢程序。有了缓冲机制:从磁盘取信息时一次读入大量数据进缓冲区,后续访问不再走系统调用,取完再读下一批,磁盘读写次数大幅减少;打印机这类慢设备则先把文档送进缓冲区,自己慢慢打,CPU 转身去干别的事。
所以缓冲区的本质,就是一块用在设备和 CPU 之间、缓存数据的内存区,让低速外设与高速 CPU 协调工作。
3.1 三种缓冲类型
- 全缓冲:填满整个缓冲区才做 I/O 系统调用,磁盘文件默认如此;
- 行缓冲:遇换行符就刷新,终端设备默认如此,默认大小 1024;
- 无缓冲:不缓存,直接调系统调用,
stderr就是如此,保证出错信息尽快显示。
除默认规则外,缓冲区满、执行 flush、进程结束也会触发刷新。
复盘和图示:

四、两个实验看穿缓冲行为
4.1 重定向后 printf 的内容去哪了
c
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("hello world: %d\n", fd);
close(fd);
// cat log.txt ------ 空的
明明把 1 号描述符重定向到了文件,为什么文件里什么都没有?因为 1 号指向磁盘文件后,刷新方式从行缓冲变成了全缓冲,内容没填满缓冲区自然不会落盘,加一句 fflush(stdout) 强制刷新即可。
另一个验证:把 2 号描述符重定向到文件,用 perror 输出:
c
close(2);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
perror("hello world"); // 不用 fflush,直接写进文件
因为 stderr 没有缓冲区,内容立刻落盘,反向验证了"无缓冲"这一类型。
4.2 fork 之后为什么输出两遍
c
printf("%s", "hello printf\n");
fwrite("hello fwrite\n", 13, 1, stdout);
write(1, "hello write\n", 12);
fork();
直接运行输出三行,但执行 ./hello > file 重定向到文件后出现五行------printf 和 fwrite 各输出两次,write 只有一次。
原因链条:
- 重定向到普通文件后缓冲方式变为全缓冲,数据先留在库函数的用户级缓冲区
fork时父子写时拷贝,各自持有一份未刷新的数据- 进程退出时统一刷新,两份都写进了文件
write是系统调用,没有缓冲区,不受影响
顺带回答一个疑问:write 没有缓冲而 printf 有,说明该缓冲区是二次加上的,由 C 标准库提供;这里讨论的都是用户级缓冲区,OS 另有内核级缓冲区。
五、FILE 结构:fd 与缓冲区的载体
访问文件最终都要走 fd,所以 FILE 内部必然封装了 fd:
c
typedef struct _IO_FILE FILE; // stdio.h
struct _IO_FILE {
int _flags;
char *_IO_read_ptr, *_IO_read_end, *_IO_read_base; // 读缓冲区指针
char *_IO_write_base, *_IO_write_ptr, *_IO_write_end; // 写缓冲区指针
char *_IO_buf_base, *_IO_buf_end; // 缓冲区首尾
int _fileno; // 封装的文件描述符
};
一组指针描述读写缓冲区的起点与当前位置,_fileno 则是通往内核的钥匙。结论闭环:用户调 printf,数据先进 FILE 的缓冲区,满足刷新条件后再由 write 交给 fd,最后落到设备上。
六、总结
- 一切皆文件:进程、磁盘、键盘、管道、socket 都被抽象成文件,
read/write一套 API 通吃; - 内核依据:
f_op指向file_operations函数指针表,把系统调用与设备驱动挂钩; - 缓冲区:内存中预留的一块空间,用空间换系统调用次数,让慢外设与快 CPU 协调;
- 三种类型:全缓冲(磁盘文件)、行缓冲(终端,1024)、无缓冲(
stderr),缓冲区满、flush、进程退出也会刷新; - 两张验方:重定向后 printf 不输出(全缓冲,需
fflush)、fork后库函数输出两遍(写时拷贝带走未刷新数据),write始终只一次。