前言
我们日常进行代码编写解决大型项目时,有极大一部分时间都是在与文件进行打交道,尤其是在Linux下开发。所以掌握文件的基本操作是必须的,理解访问文件的本质能够让我们更深刻的理解操作系统看待文件的方式。本文将从三方面来讨论如何对文件进行操作,以及文件的本质是什么。
- 文件的接口;
- 访问文件的本质;
- Linux下一切接文件是什么意思。
在学习文件的知识之前,有几个共识的原理会贯穿文章:
- 文件=文件内容+文件属性;
- 文件分为被打开的文件和没被打开的文件;本文重点谈论被打开的文件,即被进程打开的文件;
C语言的文件接口
C语言中文件接口有很多,此处只介绍几个基本的接口。
FILE* fopen(const char* path,const char* mode)

fopen用来打开文件:
- 返回值FILE*一个指针:指向一个结构体,该结构体专门用来存储打开文件的各个信息;
- 第一个参数path:打开文件所处的路径,如果没有指明路径默认文件在当前路径下查找或创建,即进程当前所在的路径;
- 第二个参数mode,打开文件的方式。
选项 | 效果 | 没有该文件怎么办 | 补充 |
---|---|---|---|
"r": read | 打开文件,只读 | 不会创建,打开失败,返回NULL | 无 |
"r+" | 打开文件,可读可写 | 不会创建,打开失败,返回NULL | 无 |
"w": write | 打开文件,只写 | 会创建新文件 | 会先将文件清空再写入 |
"w+" | 打开文件,可读可写 | 会创建新文件 | 会先将文件清空再写入 |
"a": append | 打开文件,只写 | 会创建新文件 | 在文件末尾追加,不清空 |
"a+" | 打开文件,可读可写 | 会创建新文件 | 在文件末尾追加,不清空 |

与fopen相反的就是int fclose(FILE* pf)用于关闭文件。
size_t fwrite(const void* ptr , size_t size , size_t nmemb , FILE* stream)

fwrite进行写操作,先已经打开的文件中进行写入,关于写入的方式是追加还是覆盖由文件打开的方式决定:
- 第一个参数:const void*类型的指针,指向要写入的数据起始位置;
- 第二个参数:size_t类型的数据,存储写入数据的基本单元的大小;
- 第三个参数:还是一个size_t类型的数据,存储要写入的基本单元的数量;
- 第四个参数:FILE*类型的指针,指向要写入的文件FILE结构体;
- 返回值:成功写入的基本单元的个数。
补充:关于数据的基本单元和基本单元的数量
关于数据的基本单元和基本单元的数量实际上并没有太严格的限制:对于一个字符串来说可以把字符看作基本单元,也可以把整个字符串看作一个基本的单元。只要两者相乘等于要写入的字节总数即可,原因在于其本质上调用的是write系统调用接口,该接口只看字节总数。
补充:在写入字符串的时候,需不需要把\0有写入到文件中???
const char ptr[]="hello world;
fwrite(ptr,strlen(ptr),1,pf);
fwrite(ptr,strlne(ptr)+1,1,pf);
以上两个fwrite哪一个是对的???
答案:不用加\0即第一个是对的,因为\0是C语言中字符串结束的表示,但是这和文件没有任何关系;文件可能被各种语言读取,每个语言都有自己的规则,所以文件不能直接用C语言的规则。
int fprintf(FILE* stream , const char* format...)
fprintf也是对数据进行写入,但是fprintf更好用,其使用起来与printf一样,只不过在第一个参数加上指向文件对应的FILE结构体即可。

文件系统接口
库函数是对系统调用接口的封装使得程序员开发更加方便,在C语言中printf/fprintf/fgets...都是对系统调用接口的封装,下面介绍一些常见的文件系统接口。
不同语言的文件操作接口千变万化,但是对于操作系统来说都是同一个接口调用的 。
int open(const char* pathname,int flags ,mode_t mode)
open是一个操作系统提供的用来打开文件的接口。
在C语言中的fopen就是对open进行的封装,不论是任何语言只要是在Linux下打开文件的接口其底层必定是调用的open函数。
open的各个参数:
- 第一个参数pathname:打开文件所处的位置,绝对路径/先对路径,如果没有知名具体路径就默认在当前路径中打开;
- 第二个参数flags:打开文件的模式/选项,具体选项可将下标:
选项 | 作用 |
---|---|
O_RDONLY | 只读 |
O_WRONLY | 只写 |
O_RDWR | 即读又写 |
O_CREAT | 没有文件进行创建 |
O_TRUNC | 有文件将文件内容清空 |
O_APPEND | 在文件末尾追加 |
关于以上选项实际上就是个宏定义,实际上就是八进制的数字:![]() |
使用比特位对应的不同的数值来表示不同的含义,因为是在不同的比特位上,所以可以通过按位或|将选项进行叠加使用,比如O_WRONLY|O_CREAT表示以写的方式打开文件,如果没有文件就创建新文件。
-
第三个参数是用来调整新创建的文件权限的,一般普通文件权限就是0666,目录文件就是0777,注意还要取出权限掩码对应的权限。
-
返回值,open返回值是一个int的整形,操作系统通过这一个整形来确定后续要操作的文件,该整形被称为文件标识符,后面会具体讲解。
通过open的各个参数的解释,我们大概可以感觉到C语言库函数中的fopen是如何进行封装的了,下面写一个简单的demo代码,演示下库中fopen的实现方式:
int close(int fd)
close关闭文件,参数是文件描述符。这个就不多说了。
ssize_t write(int fd , const void* buf , size_t count)
write操作系统提供的用来先文件中写入的接口。
- 参数一fd:文件描述符,决定了对哪一个文件进行操作;
- 参数二buf:指向要写入数据的起始位置;
- 参数三count:决定写入数据的字节总数。
ssize_t read(int fd , const void* buf , size_t count)
与write的操作方式一摸一样。
操作系统如何管理文件
操作系统管理的底层逻辑一律是:先描述,再组织。
Linux下又struct files_struct来描述一个进程打开的文件信息:
其中有两个数组是需要特别关注的:struct file ** fd和struct file * fd_array[NR_OPEN_DEFAULT],他们两个的作用是一样的,类似于动态顺序表和静态顺序表,其中一级指针类似于静态顺序表常用于打开文件少的时候来减少空间的开辟,二级指针类似于动态顺序表常用于打开文件多。该数组每个位置的下标就是对应着已经打开文件的文件描述符,open系统调用接口的返回值就是数组对应的下标。
这两个结构体指针都指向struct file *的数组,struct flie是专门用来记录每个文件的各种属性的:
该结构体又包含其他很多的结构体,其中struct list_head f_list是用来记录所以系统打开的文件,
就是一个双向指针,通过该双向指针可以实现所有文件属性的查找 ;f_count是文件的引用引用计数,记录有多少个进程打开这个文件,f_flags用来记录文件被打开的方式/选项,就是open中的flag,f_mode记录文件的权限,f_error记录操作文件的错误码,还有一个比较重要的就是struct address_space *f_mapping在后面的内存管理中会终点介绍。总而言之struct file中存储着打开的文件中的各种信息。
示意图如下所示:
文件描述符的填写规则:从0开始寻找最小的没有被使用的小标来使用。
补充
在学习C语言的时候我们知道,C语言会默认打开三个流分别是stdin,stdout,stderr,因为Linux下一切接文件,这三个流也是文件,所以其一定也在struct files *的数组中。确实是这样的,并且这三个流分别占用数组的前三个下标。
C语言为什么要打开这三个流,是C语言的特性吗???其他语言有没有???
所有语言都有这一特性,这三个流的使用太频繁了,所以进程加载后都会默认打开这三个文件,其不是C语言的特性,而是操作系统的特性。实际上这三个文件默认就是打开的状态,,操作系统只是在启动进程以后,将这三个文件填写到进程的文件描述符表对应的下标位置而已。
*close()关闭文件需要进行那些操作???
- 将文件struct file中的count引用计数-1,当引用计数为0时,操作系统对文件进行回收;
- 将进程的文件描述符表对应的位置悬空。