理解"一切皆文件"
哪些东西都算作文件
- Windows 里认定的文件,在 Linux 同样是文件:文本、程序、图片、压缩包等磁盘存储文件,常规增删改查逻辑完全一致。
- Windows 中独立分类、不属于文件的对象,Linux 全部抽象成文件形态,可按文件方式访问:
- 硬件设备:键盘、显示器、硬盘、网卡、鼠标
- 进程资源:运行中的程序、进程状态信息
- 通信载体:进程间管道、网络编程套接字 Socket
核心设计思想
- 形态统一
抹平硬件、软件、通信、进程的外形差异,不分设备类型、存储位置,全部归类为逻辑文件。 - 接口统一
对外只暴露open/read/write/close四个基础系统调用,读取、写入、打开、关闭行为通用,无需为不同资源编写专属函数。 - 寻址统一
所有打开的资源,都用文件描述符 fd作为唯一访问标识,通过进程文件表定位资源。 - 规则统一
权限校验、资源占用、生命周期管理、用户归属,全部沿用文件管理规则。
底层内核实现


架构层级自上而下
用户态统一接口 → VFS虚拟文件系统(核心抽象层) → 进程文件描述符体系 → 底层资源适配层
1. 层级 1:用户态统一调用入口
应用程序无论操作什么资源,代码写法完全一致
- 打开资源:
open() - 读取数据:
read() - 写入数据:
write() - 释放资源:
close()
应用层完全感知不到底层是屏幕、磁盘还是网络,只传递文件描述符fd即可。
2. 层级 2:VFS 虚拟文件系统
VFS 本身不存储数据,是内核的中间抽象调度层
- 定义一套标准文件操作规范,强制所有底层资源都遵循这套规范开发接口
关键核心:struct file_operations
当打开一个文件时,操作系统为了管理所打开的文件,都会为这个文件创建一个file结构体,而struct file里面有一个重要成员:
c
struct file {
// ... 其他成员(偏移量、权限、引用计数)
const struct file_operations *f_op; // 核心!
};
struct file 的 f_op 指针,直接指向 struct file_operations 结构体 。struct file_operations 是标准文件操作函数指针集合 ,这个结构体中的成员除了struct module* owner 其余都是函数指针 。通过这个结构体的指针,就能自动执行不同的硬件 / 资源逻辑。
精简源码:
c
struct file_operations {
struct module *owner; /* 所属模块指针 */
loff_t (*llseek) (struct file *, loff_t, int); /* 修改文件偏移 */
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); /* 同步读取数据 */
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); /* 异步读初始化 */
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); /* 同步写入数据 */
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); /* 异步写初始化 */
int (*readdir) (struct file *, void *, filldir_t); /* 读取目录项 */
unsigned int (*poll) (struct file *, struct poll_table_struct *); /* 轮询可读写状态 */
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); /* 传统ioctl */
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); /* 无大内核锁ioctl */
long (*compat_ioctl) (struct file *, unsigned int, unsigned long); /* 兼容32位ioctl */
int (*mmap) (struct file *, struct vm_area_struct *); /* 内存映射设备 */
int (*open) (struct inode *, struct file *); /* 打开设备文件 */
int (*flush) (struct file *, fl_owner_t id); /* 刷新缓冲区 */
int (*release) (struct inode *, struct file *); /* 释放文件结构 */
int (*fsync) (struct file *, struct dentry *, int datasync); /* 同步文件数据 */
int (*aio_fsync) (struct kiocb *, int datasync); /* 异步同步操作 */
int (*fasync) (int, struct file *, int); /* 异步通知设置 */
int (*lock) (struct file *, int, struct file_lock *); /* 文件加锁 */
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); /* 零拷贝发送页 */
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); /* 获取未映射区域 */
int (*check_flags)(int); /* 检查文件标志 */
int (*dir_notify)(struct file *filp, unsigned long arg); /* 目录通知 */
int (*flock) (struct file *, int, struct file_lock *); /* 建议性文件锁 */
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); /* splice写入 */
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); /* splice读取 */
int (*setlease)(struct file *, long, struct file_lock **); /* 设置文件租约 */
};
struct file_operations的作用:规定了「文件 / 设备 / 管道 / Socket」能做什么操作(读、写、打开、关闭)
3. 层级 3:进程私有文件寻址体系
每个进程独立维护一套文件管理结构,形成固定访问链路
bash
task_struct 进程控制块
↓
files_struct 文件描述符总表
↓
fd_array[] 指针数组 (数组下标 = 文件描述符fd)
↓
struct file 打开实例
↓
file_operations 函数指针集
↓
底层硬件/文件/管道/Socket 真实逻辑
4. 层级 4:底层资源适配层
各类差异化资源,各自实现 VFS 标准接口,完成封装
如:磁盘文件:ext4、xfs 等文件系统,实现读写磁盘逻辑
缓冲区
什么是缓冲区?
缓冲区就是内存里一块临时开辟的存储空间,专门用来临时存放待读写的数据。
缓冲区的作用:攒够一批数据,再一次性批量读写外设,大幅提升程序效率。
简单理解:
缓冲区 = 快递驿站
数据 = 快递
外设 = 收件人
为什么必须存在缓冲区
- 高速 CPU 与低速外设天生不匹配,缓冲区充当速度缓冲带
- 减少系统调用,降低用户态与内核态切换开销
printf/scanf最终都会触发read/write系统调用。每一次系统调用,都会发生用户态 ↔ 内核态切换
Linux IO 两层缓冲区
1. 用户态标准库缓冲区
- 名字:C 标准库缓冲区 / 用户态缓冲
- 位置:进程的用户空间内存(每个进程独立一份,互不干扰)
- 管理者:C 语言标准库
stdio.h - 绑定对象 :
printf/fprintf/cout/scanf/fopen等库函数
核心作用:
解决 用户态 ↔ 内核态切换开销太大 的问题
每一次系统调用(write)都要切换状态,成本极高;缓冲区先把数据攒在用户空间,攒够一批再调用一次系统调用;大幅减少切换次数,提升程序效率。
缓冲模式
1. 无缓冲:数据产生后立刻、直接写入内核缓冲区,不做任何暂存
默认绑定对象
标准错误 stderr(fd=2)
2. 行缓冲 :缓冲区遇到换行符 \n 才会刷新数据
默认绑定对象
连接「终端 / 显示器」的标准输出 stdout(fd=1)
3. 全缓冲 :缓冲区写满固定大小(默认 4KB) 才刷新数据
默认绑定对象
- 所有普通磁盘文件
- 重定向到文件的标准输出
stdout(fd=1)
其他刷新规则
- 程序正常退出
- 手动调用
fflush(stdout) - 关闭文件描述符
close(fd)
注意:close(fd) 只刷新内核缓冲区,完全不刷新用户态缓冲区
2. 内核文件缓冲区
- 位置:内核空间内存
- 管理者:Linux 内核(应用程序完全无法直接操作)
- 绑定对象 :
write/read/open/close等系统调用
刷新规则
- 内核自动定时刷新到硬件
- 应用程序无需干预
看一个例子:
c
#include <cstdio>
#include <iostream>
#include <cstring>
#include <unistd.h>
int main()
{
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
const char *s = "hello fwrite\n";
fwrite(s, strlen(s), 1, stdout);
// 系统调用
const char *ss = "hello write\n";
write(1, ss, strlen(ss));
fork();
return 0;
}

在场景2(对进程实现输出重定向)中,我们可以发现 printf、fprintf、fwrite(库函数)都调用了两次,而 write(系统调用)只调用了一次。为什么呢?这肯定与fork有关。
场景 1:直接运行(终端)
原理:
stdout是行缓冲 ,遇到\n立即刷新用户缓冲区write系统调用直接输出到屏幕- fork 时用户缓冲区是空的
场景 2:重定向(文件)
原理:
stdout自动切换为 全缓冲 ,规则:\n完全失效,缓冲区不刷新,直到程序退出才刷printf/fprintf/fwrite→ 数据全部留在用户态缓冲区(不写入文件)write系统调用 → 直接写入文件- 执行
fork()创建子进程 → 子进程会完整复制父进程的用户态缓冲区 - 父子进程先后退出时,各自刷新自己的缓冲区,各自写入3条数据
解决方法 :在 fork() 之前,手动刷新用户态缓冲区,清空数据!
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定封装了fd。
FILE结构体
- 归属:C 语言标准库 <stdio.h> 定义,运行在用户空间
- 本质 :一个 封装了「文件描述符 fd + 用户态缓冲区 + 缓冲模式 + 文件状态」 的用户态结构体
- 作用 :给
printf/fopen/fread/fflush等库函数提供上下文管理,屏蔽底层内核细节
简化版FILE结构体:
c
// C标准库 stdio.h 中的 FILE 结构体(简化版)
typedef struct _IO_FILE FILE;
struct _IO_FILE {
// 1. 核心:文件描述符 fd(对应内核的 int fd)
int _fileno;
// 2. 核心:用户态缓冲区(printf 数据暂存的地方!)
char* _buf;
// 缓冲区总大小(默认 4KB 全缓冲,行缓冲更小)
int _buf_size;
// 缓冲区当前读写位置
int _ptr;
// 3. 核心:缓冲模式(无缓冲/行缓冲/全缓冲)
int _flags;
// 4. 文件状态(读写权限、是否结束、错误标记)
int _mode;
};
简单模拟一个glibc库
补充:fsync 系统调用
函数原型
c
#include <unistd.h>
// 成功返回0,失败返回-1
int fsync(int fd);
作用:强制将指定文件描述符对应的「内核缓冲区(页缓存)」中的所有数据,同步、阻塞地写入物理磁盘,直到数据完全落盘才返回。
总结
fsync= 内核系统调用,刷内核缓冲区 → 物理磁盘fflush= 库函数,刷用户缓冲区 → 内核
头文件
c
#pragma once
#include <stdio.h>
#define MAX 1024
#define NONE_FLUSH (1<<0)
#define LINE_FLUSH (1<<1)
#define FULL_FLUSH (1<<2)
typedef struct IO_FILE
{
int fileno;
int flag;
char outbuffer[MAX];
int bufferlen;
int flush_method;
}MyFile;
MyFile *MyFopen(const char *path, const char *mode);
void MyFclose(MyFile *);
int MyFwrite(MyFile *, void *str, int len);
void MyFFlush(MyFile *);
实现文件
c
#include "mystdio.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
static MyFile *BuyFile(int fd, int flag)
{
MyFile *f = (MyFile*)malloc(sizeof(MyFile));
if(f == NULL) return NULL;
f->bufferlen = 0;
f->fileno = fd;
f->flag = flag;
f->flush_method = LINE_FLUSH;
memset(f->outbuffer, 0, sizeof(f->outbuffer));
return f;
}
MyFile *MyFopen(const char *path, const char *mode)
{
int fd = -1;
int flag = 0;
if(strcmp(mode, "w") == 0)
{
flag = O_CREAT | O_WRONLY | O_TRUNC;
fd = open(path, flag, 0666);
}
else if(strcmp(mode, "a") == 0)
{
flag = O_CREAT | O_WRONLY | O_APPEND;
fd = open(path, flag, 0666);
}
else if(strcmp(mode, "r") == 0)
{
flag = O_RDWR;
fd = open(path, flag);
}
else
{
// TODO
}
if(fd < 0) return NULL;
return BuyFile(fd, flag);
}
void MyFclose(MyFile *file)
{
if(file->fileno < 0) return;
MyFFlush(file);
close(file->fileno);
free(file);
}
int MyFwrite(MyFile *file, void *str, int len)
{
// 1. 拷贝
memcpy(file->outbuffer+file->bufferlen, str, len);
file->bufferlen += len;
// 2. 尝试判断是否满足刷新条件
if((file->flush_method & LINE_FLUSH) && file->outbuffer[file->bufferlen-1] == '\n')
{
MyFFlush(file);
}
return 0;
}
void MyFFlush(MyFile *file)
{
if(file->bufferlen <= 0) return;
// 把数据从用户拷贝到内核文件缓冲区中
int n = write(file->fileno, file->outbuffer, file->bufferlen);
(void)n;
fsync(file->fileno);
file->bufferlen = 0;
}
测试代码
c
#include "mystdio.h"
#include <string.h>
#include <unistd.h>
int main()
{
MyFile *filep = MyFopen("./log.txt", "a");
if(!filep)
{
printf("fopen error\n");
return 1;
}
int cnt = 10;
while(cnt--)
{
char *msg = (char*)"hello myfile!!!";
MyFwrite(filep, msg, strlen(msg));
MyFFlush(filep);
printf("buffer: %s\n", filep->outbuffer);
sleep(1);
}
MyFclose(filep); // FILE *fp
return 0;
}