
💡Yupureki:个人主页
✨个人专栏:《C++》 《算法》《Linux系统编程》《高并发内存池》《MySQL数据库》
🌸Yupureki🌸的简介:

目录
[1. 重谈文件](#1. 重谈文件)
[1.1 狭义理解](#1.1 狭义理解)
[1.2 广义理解](#1.2 广义理解)
[2. 重看C语言文件接口](#2. 重看C语言文件接口)
[2.1 打开文件](#2.1 打开文件)
[2.2 写入文件](#2.2 写入文件)
[2.3 读文件](#2.3 读文件)
[3. 系统文件I/O](#3. 系统文件I/O)
[3.1 接口介绍](#3.1 接口介绍)
[3.2 open和fopen的理解](#3.2 open和fopen的理解)
[3.3 文件描述符](#3.3 文件描述符)
[3.3.1 初识文件描述符](#3.3.1 初识文件描述符)
[3.3.2 文件描述符的本质](#3.3.2 文件描述符的本质)
[3.4 三个默认的文件描述符](#3.4 三个默认的文件描述符)
[3.5 文件描述符分配规则](#3.5 文件描述符分配规则)
[3.6 重定向](#3.6 重定向)
[3.7 理解 "一切皆文件"](#3.7 理解 "一切皆文件")
[3.7.1 具体体现](#3.7.1 具体体现)
[3.7.2 这种设计带来的好处](#3.7.2 这种设计带来的好处)
[4. 缓冲区](#4. 缓冲区)
[4.1 什么是缓冲区](#4.1 什么是缓冲区)
[4.2 为什么需要缓冲区?](#4.2 为什么需要缓冲区?)
[4.3 标准 I/O 的用户态缓冲区](#4.3 标准 I/O 的用户态缓冲区)
[4.4 内核缓冲区](#4.4 内核缓冲区)
[4.5 缓冲区的层级关系](#4.5 缓冲区的层级关系)
1. 重谈文件
1.1 狭义理解
文件在磁盘里
- 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的
- 磁盘是外设(即是输出设备也是输入设备)
- 磁盘上的文件本质是对文件的所有操作,都是对外设的输入和输出简称IO
1.2 广义理解
Linux下一切皆文件(键盘、显示器、网卡、磁盘··这些都是抽象化的过程)
这句话并不是说系统里所有的东西都是普通的文本文件,而是指:系统将所有的资源都通过"文件"这个统一接口来呈现和访问。
在 Linux 中,文件系统是一个巨大的树状结构。无论是:
-
你的普通文本、图片
-
你的键盘、鼠标、显示器、硬盘
-
进程信息、内存
-
网络连接、管道
它们都存在于这个树状结构的某个位置,并且都支持 打开、读写、关闭 这同一套标准的操作。
2. 重看C语言文件接口
2.1 打开文件
cpp
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
int main() {
FILE* fp = fopen("test","w");
if(!fp)
{
printf("fopen error!\n");
return 1;
}
fclose(fp);
return 0;
}

然而fopen指定的只是文件的名字,我怎么知道这个文件在哪个路径下?->一般在当前路径下
但问题又来了,进程怎么知道当前路径是什么?
可以通过ls /proc/进程pid -l查看进程的属性

其中:
- cwd:指向当前进程运行目录的一个符号链接。
- exe:指向启动当前进程的可执行文件(完整路径)的符号链接。
打开文件,本质是进程打开,所以,进程知道自己在哪里,即便文件不带路径,进程也知道。由此OS就能知道要创建的文件放在哪里。
2.2 写入文件
cpp
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
int main() {
FILE* fp = fopen("my.txt","w");
if(!fp)
{
printf("fopen error!\n");
return 1;
}
char buffer[] = "hello world";
fwrite(buffer,sizeof(buffer),1,fp);
fclose(fp);
return 0;
}

2.3 读文件
cpp
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
int main() {
FILE* fp = fopen("my.txt","r");
if(!fp)
{
printf("fopen error!\n");
return 1;
}
char buffer[1024];
fread(buffer,sizeof(buffer),1,fp);
printf("my.txt: %s",buffer);
fclose(fp);
return 0;
}

3. 系统文件I/O
打开文件的方式不仅仅是fopen,ifstream等流式,语言层的方案,其实系统才是打开文件最底层的方案
3.1 接口介绍

pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行"或"运算,构成flags参数:
- O_RDONLY:只读打开
- O_WRONLY:只写打开
- O_RDWR:读,写打开
这三个常量,必须指定一个且只能指定一个
- O_CREAT:若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
- O_APPEND:追加写
返回值:
成功:新打开的文件描述符
失败:-1
先看示例:
cpp
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main()
{
int fd = open("my.txt",O_WRONLY,0644);
if(fd < 0)
{
printf("open error!\n");
return 1;
}
char buffer[] = "hello linux";
write(fd,buffer,sizeof(buffer));
return 0;
}

3.2 open和fopen的理解
本质:库函数 vs. 系统调用
-
open/read/write是 Linux 内核提供的系统调用 (System Call)。它们直接向内核发起请求,由内核完成真正的硬件操作(磁盘、终端、网络等)。
返回值是文件描述符 (
int),一个非负整数,代表内核中打开的文件的索引。 -
fopen/fread/fwrite是 C 标准库(libc)提供的函数。它们内部会调用相应的系统调用,但在这之上做了封装:
-
维护一个
FILE结构体,包含文件描述符、缓冲区指针、当前读写位置等信息。 -
返回值是
FILE *(文件流指针)。
-
可以理解为,C语言库函数fopen,fread这种封装了各种操作系统底层的系统调用,再根据条件编译选择性地裁剪代码,这样就保证了可移植性。而Linux的系统调用直接由操作系统操作底层,也只限于Linux系统可以使用,其他的系统无法使用
3.3 文件描述符
3.3.1 初识文件描述符
fopen返回的是一个FILE*的指针,我们都知道这是一个指向文件的指针
那么open返回的一个整型,叫文件描述符是什么东西?
一个直观类比
想象你去图书馆:
-
图书馆是内核,藏书是各种资源(文件、设备、管道、网络连接......)
-
你走进图书馆,说要借一本书,管理员给你一张借书证号码(文件描述符)
-
你后续要阅读、归还,只需出示这个号码,管理员就知道是哪本书、读到第几页了
这个号码就是 fd。
3.3.2 文件描述符的本质
文件描述符是一个 非负整数 (通常 0、1、2、3...),它是进程用来标识一个已打开文件的唯一索引。
在 Linux 内核中,每个进程都有一个 文件描述符表 (struct files_struct)。这张表就像一个数组,下标就是 fd,每个条目指向内核中一个 打开文件描述 (struct file)。
这个 struct file 包含:
-
当前文件偏移量(
lseek移动的就是它) -
文件状态标志(只读、只写、非阻塞等)
-
指向实际文件元信息的指针(inode / vnode)

而这个fd_array[]是一个数组,其下标就相当于是文件描述符fd。通过文件描述符,可以找到对应的文件
Linux源码:

3.4 三个默认的文件描述符
每个进程启动时,内核会自动打开三个 fd:
| fd | 宏名 | 含义 |
|---|---|---|
| 0 | STDIN_FILENO |
标准输入(默认从键盘读) |
| 1 | STDOUT_FILENO |
标准输出(默认往屏幕写) |
| 2 | STDERR_FILENO |
标准错误(默认往屏幕写) |
因此,如果我们在write函数内指定文件描述符1,就会往显示屏上写
cpp
int main()
{
char buffer[1024] = "hello linux\n";
write(1,buffer,sizeof(buffer));
return 0;
}

同样的,如果我们在read函数内指定文件描述符0,就会从键盘拿数据
3.5 文件描述符分配规则
文件描述符的分配取决于文件描述符的空位情况,他会从0开始,一直找,直到找到一个没被占有的文件描述符

3.6 重定向
文件描述符1默认是与显示屏相关的,然而这也只是默认行为。我们可以修改文件描述符1,将其强行与其他文件关联
dup2函数
int dup2 ( int oldfd, int newfd);
我们可以使用dup2函数来重定向文件描述符
cpp
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main()
{
int fd = open("my.txt",O_WRONLY | O_TRUNC,0644);
if(fd < 0)
{
printf("open error!\n");
return 1;
}
dup2(fd,1);
char buffer[] = "123456";
write(fd,buffer,sizeof(buffer));
return 0;
}

同理,我们也可以重定向其他的文件描述符,如0,我们也可以重定向到其他文件中,从文件中拿取数据
3.7 理解 "一切皆文件"
首先,在windows中是文件的东西,它们在linux中也是文件;其次一些在windows中不是文件的东
西,比如进程、磁盘、显示器、键盘这样硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息;甚至管道,也是文件;将来我们要学习网络编程中的socket(套接字)这样的东西,使用的接口跟文件接口也是一致的。
3.7.1 具体体现
一、硬件设备也是文件
这是最直观的体现。当你插入一个U盘,系统不会给你一个"可移动磁盘"的图标让你双击,而是在 /dev/sdb1 位置生成了一个设备文件。
-
硬盘 :
/dev/sda------ 往这个文件里写数据,就是往硬盘写数据。 -
鼠标 :
/dev/input/mouse0------ 读取这个文件,就能捕获鼠标的移动轨迹。 -
显卡 :
/dev/fb0(帧缓冲)------ 往这个文件写入RGB数据,屏幕就会显示对应的颜色。
例子 :你可以直接用 cat 命令读取鼠标文件:
bash
sudo cat /dev/input/mouse0
二、进程与内核信息也是文件
Linux 的 /proc 目录是驻留在内存中的虚拟文件系统,它把内核和进程的信息"伪装"成了文件。
-
CPU信息 :
cat /proc/cpuinfo------ 虽然它是一个文件,但它其实是内核实时生成的。 -
进程状态 :
/proc/1234/------ 这是一个目录,里面存放着 PID 为 1234 的进程的所有信息。 -
系统配置 :
/proc/sys/net/ipv4/ip_forward------ 你往这个文件里写1,内核就开启了 IP 转发(路由功能)。
三、网络通信也是文件
网络通信通常需要复杂的 Socket 编程接口,但在 Linux 里,它也沿用了文件描述符(FD)的概念。
-
管道 :
|命令的本质,就是将一个进程的输出文件,直接连接到另一个进程的输入文件。 -
Socket :你在
/proc/<pid>/fd/下可以看到,网络连接被抽象成了编号为 0、1、2(标准输入/输出/错误)之后的文件描述符。
3.7.2 这种设计带来的好处
程序员学习 Linux 编程时,只需要掌握五个函数:open, read, write, close, ioctl。
无论操作的是真实的硬盘文件、串口数据、还是屏幕显示,代码逻辑都是一样的。这极大地降低了驱动开发和上层开发的复杂度。
4. 缓冲区
4.1 什么是缓冲区
缓冲区就是一块内存区域,用于临时存放数据,等待合适时机再统一处理。
在 I/O 过程中,缓冲区可以出现在两个层级:
-
用户态缓冲区 :由 C 标准库管理(如
FILE结构体中的缓冲区)。 -
内核态缓冲区:由操作系统内核管理(如页缓存、Socket 缓冲区等)。
4.2 为什么需要缓冲区?
最直接的原因:减少系统调用次数。
-
每次
read/write系统调用,程序都要从用户态陷入内核态,上下文切换开销很大。 -
如果每次只读写 1 字节,做 1000 次,就 1000 次系统调用,效率极低。
-
缓冲区将多次小数据合并成一次大数据块,一次系统调用处理一整块数据,大幅提升性能。
4.3 标准 I/O 的用户态缓冲区
C 标准库(fopen/fread/fwrite/printf 等)在用户态维护了一个缓冲区。
缓冲类型
| 类型 | 行为 | 常见场景 |
|---|---|---|
| 全缓冲 | 填满缓冲区才进行系统调用(write) |
普通磁盘文件 |
| 行缓冲 | 遇到换行符 \n 就刷新(或缓冲区满) |
终端交互(标准输出) |
| 无缓冲 | 立即进行系统调用 | 标准错误(stderr) |
验证


运行后,会看到"Hello"不会立即出现,而是等待2秒后一起输出"Hello World"。因为标准输出是行缓冲,第一个 printf 只是把数据放进缓冲区,直到遇到 \n 或程序结束才真正调用 write。
控制用户态缓冲区
C 标准库提供了函数来改变缓冲模式:
cpp
#include <stdio.h>
// 设置全缓冲,缓冲区大小为 BUFSIZ
setbuf(fp, NULL); // 关闭缓冲(无缓冲)
setvbuf(fp, buffer, _IOLBF, size); // 行缓冲
setvbuf(fp, buffer, _IOFBF, size); // 全缓冲
setvbuf(fp, buffer, _IONBF, 0); // 无缓冲
4.4 内核缓冲区
即使标准 I/O 调用了 write,数据也未必立即写到磁盘。内核也会维护一个页缓存 (Page Cache)或缓冲区缓存。
-
write系统调用将数据从用户态拷贝到内核缓冲区,然后立即返回(除非用O_SYNC标志)。 -
内核随后在合适时机(如缓冲区满、时间到期、显式
fsync)将数据真正写入磁盘。
这样做的好处是:
-
应用程序不必等待慢速的磁盘操作,可以继续执行。
-
内核可以合并多次写入,减少磁盘 I/O。
强制落盘
如果需要确保数据已写入持久存储,可以调用:
cpp
#include <unistd.h>
int fsync(int fd); // 同步文件数据
int fdatasync(int fd); // 只同步数据,不同步元数据
void sync(void); // 全局同步(不常用)
4.5 缓冲区的层级关系
用户程序
│
│ fwrite / printf
▼
┌─────────────────────┐
│ 用户态缓冲区 │ ← 由 stdio 管理,减少系统调用
│ (FILE 结构体) │
└─────────────────────┘
│
│ write 系统调用
▼
┌─────────────────────┐
│ 内核缓冲区 │ ← 页缓存,减少磁盘 I/O
│ (Page Cache) │
└─────────────────────┘
│
│ 磁盘驱动程序
▼
磁盘/硬件
全流程
-
printf("hello")→ 数据进入用户态缓冲区。 -
缓冲区满或遇
\n→ 调用write→ 数据进入内核页缓存。 -
内核异步将页缓存写入磁盘。
-
如果调用
fflush,只会把用户态缓冲区的数据推到内核缓冲区(不保证落盘)。 -
如果调用
fsync,才会强制内核将数据刷到磁盘。