
🎬 个人主页:HABuo
📖 个人专栏:《C++系列》 《Linux系列》《数据结构》《C语言系列》《Python系列》《YOLO系列》
⛰️ 如果再也不能见到你,祝你早安,午安,晚安

目录
[📖3.1 open](#📖3.1 open)
[📖3.2 close](#📖3.2 close)
[📖3.3 write](#📖3.3 write)
[📖3.4 read](#📖3.4 read)
[📖4.1 文件描述符的本质](#📖4.1 文件描述符的本质)
前言 :
前几篇博客我们认识了进程控制的相关知识,如何创建进程、退出进程有哪些信息需要给父进程、进程等待是什么、进程程序替换做了那些事?如果记忆模糊请返回继续阅读。本篇博客我们将进入文件部分相关知识的了解和学习当中!无论是C语言或者C++亦或是Java都拥有文件操作,但是它们的使用还都不一样,让人挺恼火,有没有什么根本的东西来应对呢?当然有,在冯诺依曼体系结构那篇博客中我们知道,用户级接口是在系统调用接口基础上实现的,那么是不是就意味着我们把文件操作的系统调用接口熟悉了,那一切妖魔鬼怪也就都消散了,没错!所以让我们一探究竟吧!
本章重点:
本篇文章着重讲解I/O的四个系统调用接口 ,以及文件描述符fd的认识与fd的本质,在此之前,会先复习一下C语言的文件相关的库函数
📚一、C语言的文件接口
我们将C语言的文件操作接口在这里进行大总结:
- C打开文件: fopen
- C的读取: fread, fscanf, fgets
- C的写入: fwrite, fprintf, fputs
- C关闭文件: fclose
整理成表格如下:
| 函数 | 主要功能 | 参数说明 | 返回值 | 头文件 |
|---|---|---|---|---|
| fopen | 打开文件 | filename:文件名, mode:模式 | 成功:FILE*, 失败:NULL | stdio.h |
| fclose | 关闭文件 | stream:文件指针 | 成功:0, 失败:EOF | stdio.h |
| fgetc | 读取字符 | stream:文件指针 | 成功:字符, 失败/EOF:EOF | stdio.h |
| fputc | 写入字符 | character:字符, stream:文件指针 | 成功:字符, 失败:EOF | stdio.h |
| fgets | 读取字符串 | str:缓冲区, n:最大长度, stream:文件指针 | 成功:str, 失败/EOF:NULL | stdio.h |
| fputs | 写入字符串 | str:字符串, stream:文件指针 | 成功:非负值, 失败:EOF | stdio.h |
| fprintf | 格式化写入 | stream:文件指针, format:格式串, ...:参数 | 成功:写入字符数, 失败:负值 | stdio.h |
| fscanf | 格式化读取 | stream:文件指针, format:格式串, ...:参数 | 成功:匹配项数, 失败/EOF:EOF | stdio.h |
| fread | 二进制读取 | ptr:缓冲区, size:项大小, count:项数, stream:文件指针 | 成功读取的项数 | stdio.h |
| fwrite | 二进制写入 | ptr:数据区, size:项大小, count:项数, stream:文件指针 | 成功写入的项数 | stdio.h |
| fflush | 刷新缓冲区 | stream:文件指针 | 成功:0, 失败:EOF | stdio.h |
📚二、文件相关共识问题
经过之前知识的学习,我们先要有以下共识:
- 空文件,也要在磁盘中占据空间
- 文件 = 内容 + 属性
- 文件操作 = 对内容 + 对属性 or 对内容和属性
- 标定一个文件,必须使用:文件路径+文件名(唯一性)
- 如果没有指明对应的文件路径,默认是在当前路径进行文件访问
- 当我们把fopen、fclose、fread、fwrite等接口写完之后,代码编译之后,形成了二进制可执行程序之后,但是没运行,文件对应操作是没有被执行的,因此对文件的操作本质是进程对文件的操作
- 一个文件如果没有被打开,不可以直接进行文件访问,即一个文件要被访问,就必须先被打开
所以,文件操作的本质:进程和被打开文件的关系
什么叫做当前路径:
我们在命令行解释器中:
这一串路径我们很清楚它就是当前路径,但是我们仅仅知道它就是路径,我们并不知道它真正的是什么,请看下图:
可以看到实际上当前路径就是该进程文件下cwd文件,它指明了当前进程执行的是磁盘路径下的哪一个程序!因此对于上篇博客我们实现的cd命令,在父进程不执行的情况下,cd命令是无效的,这正是因为,我们修改的是子进程文件下的cwd路径,父进程文件下的并没有修改,因此我们pwd查看时依然查看的是父进程的路径!
没有被打开的文件怎么办?
我们得到文件操作本质是:进程和被打开文件的关系,没有被打开的文件属于文件系统,我们将在接下来的博客中进行介绍!
文件操作:
C语言有文件操作接口,C++有文件操作接口,Java有,Python,php,go,shell都有,而且它们的操作接口都不一样!但是就如我们在前言当中所说的那样,我们怎么以不变应万变?
cpp
文件在哪里
---> 磁盘
---> 磁盘属于硬件
---> 所有人想访问磁盘(硬件)不能绕过OS
---> 使用OS提供文件级别的系统调用接口
---> 语言级别在系统调用接口基础上封装供人们使用
所以我们清楚了,学习了OS提供的文件级别的系统调用接口,语言级别的就是小儿科,你不就是在我的基础上做的封装嘛!
📚三、操作文件的系统调用接口
一共四个函数:
open:打开文件close:关闭文件write:向文件写入read:从文件中读取
我们一个一个来进行介绍:
📖3.1 open
cpp
#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);

这个flag比较特殊,虽然它是整型,但是内部却当作了位图在使用,即传递过来的选项,会被当作位图中的不同位,通过判断某位是否为1来查看是否有这个选项,怎么理解这个宏,请看下面这个巧妙的代码:
cpp#define ONE (1<<0) #define TWO (1<<1) #define THREE (1<<2) #define FOUR (1<<3) void show(int flags) { if(flags & ONE) printf("ONE\n"); if(flags & TWO) printf("TWO\n"); if(flags & THREE) printf("THREE\n"); if(flags & FOUR) printf("FOUR\n"); } int main() { show(ONE); show(TWO); show(ONE | TWO); show(ONE | TWO | THREE); show(ONE | TWO | THREE | FOUR); }通过位的操作实现了不同选项执行不同功能的一段代码!那么你对open里面的那个宏选项相信你会更加理解,事实就是通过这样的一种方式实现的!
cpp
flags:打开方式标志(必须包含以下之一):
O_RDONLY:只读
O_WRONLY:只写
O_RDWR:读写
可选标志(通过按位或|组合):
O_CREAT:文件不存在则创建
O_TRUNC:先清零再进行写入
O_APPEND:追加模式
mode:创建文件时的权限(八进制数,如0644)
返回值:
成功:返回文件描述符(非负整数)
失败:返回-1,设置errno
代码示例:
cpp
// 只读打开现有文件
int fd = open("file.txt", O_RDONLY);
// 创建新文件(读写权限),权限为rw-r--r--
fd = open("new.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
// 以追加模式打开文件
fd = open("log.txt", O_WRONLY | O_APPEND | O_CREAT, 0644);
📖3.2 close
cpp
#include <unistd.h>
int close(int fd);
参数:
fd:要关闭的文件描述符
返回值:
-
成功:返回0
-
失败:返回-1,设置errno
代码示例:
cpp
int fd = open("test.txt", O_RDONLY);
if (fd >= 0) {
// 使用文件...
close(fd); // 关闭文件
}
📖3.3 write
cpp
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数:
-
fd:文件描述符 -
buf:要写入的数据缓冲区 -
count:要写入的字节数
返回值:
-
成功:返回实际写入的字节数
-
失败:返回-1,设置errn
代码示例:
cpp
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
const char *text = "Hello, World!\n";
ssize_t bytes_written = write(fd, text, strlen(text));
if (bytes_written == -1) {
perror("写入失败");
}
close(fd);
📖3.4 read
cpp
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数:
-
fd:文件描述符 -
buf:存放读取数据的缓冲区 -
count:要读取的字节数
返回值:
-
成功:返回实际读取的字节数
-
到达文件尾:返回0
-
失败:返回-1,设置errno
cpp
int fd = open("data.txt", O_RDONLY);
char buffer[1024];
ssize_t bytes_read;
while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0) {
// 处理读取的数据
write(STDOUT_FILENO, buffer, bytes_read);
}
if (bytes_read == -1) {
perror("读取失败");
}
📚四、文件描述符
上面我们fd,fd的使用的快起兴,但是它是什么呢?让我们来认识一下:
首先先看下述推导链:
cpp
进程可以打开多个文件吗?当然可以
---> 系统中一定会存在大量的被打开的文件
---> 被打开的文件,要不要被OS管理起来呢?
---> 如何管理?-先描还,在组织
---> 操作系统为了管理对应的打开文件,必定要为文件创建对应的内核数据结构表示文件
---> struct_file与C语言当中的FILE没有关系
---> 包含了文件的大部分属性
上述内容想说明的就是OS为每个文件都抽象出了一个struct_file结构体来描述文件(被打开的),管理这些被打开的文件也就变成了对这些结构体的管理!
我们看下述代码的现象:
cpp
int fd1 = open("/mnt/workspace/test/test5/text1.txt",O_WRONLY | O_CREAT);
int fd2 = open("/mnt/workspace/test/test5/text2.txt",O_WRONLY | O_CREAT);
int fd3 = open("/mnt/workspace/test/test5/text3.txt",O_WRONLY | O_CREAT);
int fd4 = open("/mnt/workspace/test/test5/text4.txt",O_WRONLY | O_CREAT);
printf("%d, %d, %d, %d\n",fd1,fd2,fd3,fd4);

带来下述问题:
- 0、1、2哪去了?
- 为什么是3、4、5、6连续的数字?
📖4.1 文件描述符的本质
通过上面的推导与演示,我们来认识文件描述符的本质,请看下图:

为了证明stdin、stdout、stderr占用了012三个文件描述符,请看下述代码:
cpp
printf("stdin->%d\n", fileno(stdin));
printf("stdout->%d\n", fileno(stdout));
printf("stderr->%d\n", fileno(stderr));
int fd1 = open("/mnt/workspace/test/test5/text1.txt",O_WRONLY | O_CREAT);
int fd2 = open("/mnt/workspace/test/test5/text2.txt",O_WRONLY | O_CREAT);
int fd3 = open("/mnt/workspace/test/test5/text3.txt",O_WRONLY | O_CREAT);
int fd4 = open("/mnt/workspace/test/test5/text4.txt",O_WRONLY | O_CREAT);
printf("%d, %d, %d, %d\n",fd1,fd2,fd3,fd4);

那按照这个意思,我们关闭其中一个再建立一个新文件就能用012了?对的,请看下述代码:
cpp
printf("stdin->%d\n", fileno(stdin));
close(0);
printf("stdout->%d\n", fileno(stdout));
printf("stderr->%d\n", fileno(stderr));
int fd1 = open("/mnt/workspace/test/test5/text1.txt", O_WRONLY | O_CREAT);
int fd2 = open("/mnt/workspace/test/test5/text2.txt", O_WRONLY | O_CREAT);
int fd3 = open("/mnt/workspace/test/test5/text3.txt", O_WRONLY | O_CREAT);
int fd4 = open("/mnt/workspace/test/test5/text4.txt", O_WRONLY | O_CREAT);
printf("%d, %d, %d, %d\n", fd1, fd2, fd3, fd4);

所以通过上述的演示,我们能得到一下结论:
- 文件描述符就是files_struct结构体里的一个指针数组的下标,这个数组中的指针指向struct_file,而files_struct是被PCB所维护的
- 文件描述符的分配就是按照从小到大,按照顺序寻找最小的且没有被占用的fd
📚五、总结
本篇博客我们介绍学习了系统调用接口、以及文件描述符相关知识,并对C语言的文件操作接口进行了回顾。
小结一下:
系统调用接口:
open:
- 参数:"文件名", 执行选项(O_RDONLY、O_WRONLY、O_RDWR、O_CREAT、O_TRUNC、O_APPEND), 权限
- 返回值:成功:文件描述符,失败:-1
close:
- 参数:文件描述符
- 返回值:成功:0,失败:-1
write:
- 参数:write(要写入文件的文件描述符, 写入的内容(void *buf), 要写入的字节数)
- 返回值:成功:实际写入的字节数,失败:-1
read:
- 参数:read(要读取文件的文件描述符, 存放读取数据的缓冲区(void *buf), 要读取的字节数)
- 返回值:成功:实际读取的字节数。读到结尾返回0。失败:-1
文件描述符:
是什么:就是files_struct中fd_array指针数组的下标,整体的关系:PCB通过指向files_struct,files_struct结构体通过fd_array指针数组指向对应的struct_file即文件!
文件描述符的分配规则:按照从小到大,按照顺序寻找最小的且没有被占用的fd。系统运行时天然打开了stdin、stdout、stderr文件,因此文件描述符我们自己操作时会从3开始

这一串路径我们很清楚它就是当前路径,但是我们仅仅知道它就是路径,我们并不知道它真正的是什么,请看下图: