理解"⽂件"
狭义理解
• ⽂件在磁盘⾥
• 磁盘是永久性存储介质,因此⽂件在磁盘上的存储是永久性的
• 磁盘是外设(即是输出设备也是输⼊设备)
• 磁盘上的⽂件本质是对⽂件的所有操作,都是对外设的输⼊和输出简称IO
⼴义理解
• Linux下⼀切皆⽂件(键盘、显⽰器、⽹卡、磁盘......这些都是抽象化的过程)
⽂件操作的归类认知
• 对于0KB的空⽂件是占⽤磁盘空间的
• ⽂件是⽂件属性(元数据)和⽂件内容的集合(⽂件=属性(元数据)+内容)
• 所有的⽂件操作本质是⽂件内容操作和⽂件属性操作
系统⻆度
• 对⽂件的操作本质是进程对⽂件的操作
• 磁盘的管理者是操作系统
• ⽂件的读写本质不是通过C语⾔/C++的库函数来操作的(这些库函数只是为⽤⼾提供⽅便),⽽ 是通过⽂件相关的系统调⽤接⼝来实现的
回顾C⽂件接⼝
打开⽂件
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;
}

因为我们没有创建这个文件,所以打开失败。
打开的test⽂件在哪个路径下?
• 在程序的当前路径下
那系统怎么知道程序的当前路径在哪⾥呢?
可以使⽤ ls /proc/[ 进程 pid] -l 命令查看当前正在运⾏进程的信息:

其中:
- cwd:指向当前进程运行目录的一个符号链接。
- exe:指向启动当前进程的可执行文件(完整路径)的符号链接。
打开文件,本质是进程打开,所以,进程知道自己在哪里,即便文件不带路径,进程也知道。由此OS就能知道要创建的文件放在哪里。
写入文件
向文件里写入字符串,不要加'\0' ,否则会乱码;'\0'是C语言的规定,与文件系统无关。
"w":默认先清空文件再写入 ;**>**输出重定向的本质。
"a":向文件结尾处写入,文件不存在就创建文件;**>>**追加重定向的本质。
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;
}

读取文件
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;
}

stdin & stdout &stderr
• C默认会打开三个输⼊输出流,分别是stdin,stdout,stderr
• 仔细观察发现,这三个流的类型都是FILE*,fopen返回值类型,⽂件指针
cpp
#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
还有fessk,ftell,rewind的函数,在C语言部分已经有所涉猎,忘记了就回去复习哦。
系统文件I/O
打开⽂件的⽅式不仅仅是fopen、ifstream等流式、语⾔层的⽅案,其实系统才是打开⽂件最底层的⽅ 案。
open函数

pathname:要打开或创建的目标文件
flags:打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行"或"运算,构成flags
参数:
O_RDONLY:只读打开
O_WRONLY:只写打开
O_RDWR:读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT:若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND:追加写
O_TRUNC:清空文件
返回值:
成功:新打开的文件描述符
失败:-1
写文件
int open(const char* pathname,int flags,mode_t mode);
mode_t:权限标记位。
用于打开一个不存在的文件,可以指定权限且必须指定权限,不指定权限就会乱码。
cpp
#include <unistd.h>
#include<string.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<stdio.h>
int main()
{
umask(0);
int fd=open("myfile",O_WRONLY|O_CREAT,0644);
if(fd<0)
{
perror("open");
return 1;
}
int count=5;
const char *msg="hello world!!!\n";//可以不加*,加*主要是骗过vim编辑器
int len=strlen(msg);
while(count--)
{
write(fd,msg,len);
}
close(fd);
return 0;
}
fd: 后⾯讲, msg:缓冲区⾸地址, len: 本次读取,期望写⼊多少个字节的数据。返回值:实际写了多少字节数据

读⽂件
int open(const char* pathname,int flags);
用于打开一个已经存在的,且权限没问题的文件。
cpp
#include <unistd.h>
#include<string.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<stdio.h>
int main()
{
int fd=open("myfile",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));//类⽐write
if(s>0)
{
printf("%s",buf);
}
else
{
break;
}
}
close(fd);
return 0;
}

open和fopen
本质:系统调用vs库函数
| 对比项 | open(系统调用) |
fopen(C 库函数) |
|---|---|---|
| 本质 | 内核提供的原生接口,直接和内核交互 | 用户态的封装函数,底层依赖open |
| 头文件 | <fcntl.h>、<unistd.h> |
<stdio.h> |
| 返回值 | int 类型的文件描述符(fd) |
FILE* 类型的文件流指针(封装了 fd + 用户态缓冲区) |
| 缓冲机制 | 仅内核态缓冲,无用户态缓冲 | 带用户态缓冲,写操作需fflush()/fclose()才会刷到磁盘 |
| 打开方式 | 用标志位组合(如O_RDONLY/O_WRONLY/O_CREAT) |
用字符串模式(如"r"/"w"/"a"),更易记 |
| 创建文件权限 | 可直接指定(如0644) |
由系统默认 umask 决定,无法直接控制 |
| 关闭方式 | close(fd) |
fclose(fp) |
| 可移植性 | POSIX 标准,仅 Linux/Unix 支持 | C 标准,跨平台(Windows/Linux 通用) |
文件描述符
初识文件描述符
open的返回值是一个整形,叫做文件描述符。文件描述符是内核为每个进程打开的文件分配的一个非负整数 ID,进程用这个 ID 来让内核操作文件(读写、关闭等),相当于进程和内核之间操作文件的 "钥匙"。
0&1&2
Linux进程默认情况下会有3个缺省打开的⽂件描述符,分别是标准输⼊0,标准输出1,标准错误2
| fd 值 | 宏定义 | 含义 | 对应操作 |
|---|---|---|---|
0 |
STDIN_FILENO |
标准输入(stdin) | 比如 scanf、键盘输入 |
1 |
STDOUT_FILENO |
标准输出(stdout) | 比如 printf、终端输出 |
2 |
STDERR_FILENO |
标准错误(stderr) | 比如 perror、错误输出 |
示例:如果我们在write函数内指定文件描述符1,就会往显示屏上写
cpp
#include<stdio.h>
#include<unistd.h>
int main()
{
char buffer[1024] = "hello world!!!\n";
write(1,buffer,sizeof(buffer));
return 0;
}

类似的,如果我们在read函数内指定文件描述符0,就会从键盘读取数据。
文件描述符的本质
⽂件描述符是一个非负整数。当我们打开⽂件时,操作系统在内存中要创建相应的 数据结构来描述⽬标⽂件。于是就有了files结构体。表⽰⼀个已经打开的⽂件对象。⽽进程执⾏open系 统调⽤,所以必须让进程和⽂件关联起来。每个进程都有⼀个指针*files,指向⼀张表files_struct,该表 最重要的部分就是包含⼀个指针数组,每个元素都是⼀个指向打开⽂件的指针!所以,**⽂件 描述符本质就是该数组的下标。**所以,只要拿着⽂件描述符,就可以找到对应的⽂件。

文件描述符分配规则
在files_struct数组当中,找到当前没有被使⽤的最⼩的⼀个下标,作为新的⽂件描述符分配给用户。

总结
文件描述符就是进程打开文件的 "整数 ID",是用户态和内核态之间操作文件的唯一桥梁,所有文件操作最终都要通过它和内核交互。
重定向
如果我们关闭标准输出(1)呢?会怎么样?
cpp
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
int main()
{
close(1);
int fd=open("myfile",O_WRONLY|O_CREAT,0644);
if(fd<0)
{
perror("open");
return 1;
}
printf("fd:%d\n",fd);
fflush(stdout);
close(fd);
exit(0);
}
结果:

发现屏幕上什么都没有,再看看myfile:

结果竟然写在了myfile文件里。这是怎么回事呢?
因为我们提前关闭了标准输出;我们上层用户printf只认stdout->1,但是被关了,且当前fd被分配到了原来标准输出(1)的位置。可是现在的1位置底层的指针指向已经发生了变化,现在的1指向myfile,所以往myfile里写入了数据。这样修改文件描述符对应数组下标里面的指针指向的操作叫作重定向。

常见的重定向有:>,>>,<。
使⽤dup2系统调⽤
函数原型如下:
cpp
int dup2(int oldfd, int newfd);
- 如果
newfd已经被打开了,系统会自动先关闭它(不用手动 close) - 调用成功后,
oldfd和newfd指向同一个文件 - 调用失败返回
-1
通俗一点:本来要打印到newfd的内容,打印到oldfd里。
我们平时用的 ./test > file.txt,底层就是用 dup2 实现的:
- 打开目标文件,得到一个 fd(比如 3)
- 用
dup2(3, 1)把 标准输出 fd=1 强行指向文件 - 之后所有
printf/write(1)都会写入文件,而不是屏幕
代码示例:
cpp
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
// 1. 打开文件,获得 fd=3
int fd = open("test.txt", O_WRONLY|O_CREAT, 0644);
// 2. 核心:把标准输出(1) 重定向到 fd 对应的文件
dup2(fd, 1);
// 3. 这句话不会打印到屏幕,会写入 test.txt!
printf("我被重定向到文件里了!\n");
close(fd);
return 0;
}
总结
dup2(oldfd, newfd) = 强行让 newfd 变成 oldfd 的副本
是 Linux 实现输入 / 输出重定向的唯一底层函数
操作对象是文件描述符 ,和 open/close/write 是一套系统调用
重定向:打开方式+dup2
理解 "一切皆文件"
⾸先,在windows中是⽂件的东西,它们在linux中也是⽂件;其次⼀些在windows中不是⽂件的东 西,⽐如进程、磁盘、显⽰器、键盘这样硬件设备也被抽象成了⽂件,你可以使⽤访问⽂件的⽅法访 问它们获得信息;甚⾄管道,也是⽂件;将来我们要学习⽹络编程中的socket(套接字)这样的东西, 使⽤的接⼝跟⽂件接⼝也是⼀致的。
这样做最明显的好处是,开发者仅需要使⽤⼀套API和开发⼯具,即可调取Linux系统中绝⼤部分的 资源。
缓冲区
什么是缓冲区?
缓冲区就是一块内存区域,用于临时存放数据,解决读写双方的速度差异,等攒够数据或时机合适时,再统一处理,避免频繁的低速操作,从而减少系统调用来提高效率。
在 I/O 过程中,缓冲区可以出现在两个层级:
- 用户级(用户态)缓冲区 :由应用程序或标准库管理(比如 C 语言
FILE结构体自带的缓冲区,就是程序层面自己维护的临时内存)。 - 系统级(内核态)缓冲区:由操作系统内核管理(比如文件系统的页缓存、Socket 网络收发缓冲区,都是操作系统统一维护的公共缓存)。
为什么要引⼊缓冲区机制?
最直接的原因:减少系统调用次数,降低 I/O 操作的开销。
- 每次 read/write 系统调用,程序都要从用户态陷入内核态,上下文切换开销很大。
- 如果每次只读写 1 字节,做 1000 次操作,就需要 1000 次系统调用,效率极低。
- 缓冲区会将多次小数据合并成一次大数据块,通过一次系统调用处理一整块数据,大幅提升性能。
缓冲类型
标准I/O提供了3种类型的缓冲区。
| 类型 | 行为 | 常见场景 |
|---|---|---|
| 全缓冲 | 填满缓冲区才进行系统调用(write) | 普通磁盘文件 |
| 行缓冲 | 遇到换行符 \n 就刷新(或缓冲区满) |
终端交互(标准输出) |
| 无缓冲 | 立即进行系统调用 | 标准错误(stderr) |
除了上述列举的默认刷新⽅式,下列特殊情况也会引发缓冲区的刷新:
-
缓冲区满时;
-
执⾏flush语句;
-
进程结束。
内核缓冲区
内核缓冲区(也常被称为页缓存 / Page Cache),是操作系统内核为优化磁盘 I/O 性能而维护的一块内存区域。哪怕程序调用了 write 系统调用,数据也不会直接写入磁盘设备。
- 当程序发起 write 调用时,数据会先从用户进程的内存拷贝到内核缓冲区中,随后调用就会直接返回给程序,无需等待磁盘写入完成(除非开启了 O_SYNC 强制同步标志)。
- 内核会在后续合适的时机,再将缓冲区中的数据真正写入磁盘,比如缓冲区被写满、达到预设的超时时间,或是程序主动调用 fsync 强制刷盘时。
这样的设计带来了两个核心优势:
- 应用程序无需等待耗时的磁盘写入操作,就能继续执行后续逻辑,不会被慢速 I/O 阻塞。
- 内核可以把多次零散的写入请求合并成一次批量操作,减少磁盘 I/O 的次数,大幅提升整体性能。
语言层缓冲区与内核层缓冲区的关系

全流程
1.printf("hello") → 数据进入用户态缓冲区。
2.缓冲区满或遇 \n → 调用 write → 数据进入内核页缓存。
3.内核异步将页缓存写入磁盘。
4.如果调用 fflush,只会把用户态缓冲区的数据推到内核缓冲区(不保证落盘)。
5.如果调用 fsync,才会强制内核将数据刷到磁盘。