本文主要讨论文件和文件描述符。
文件包括内容和属性,文件也包括被打开的和没有打开的文件;打开文件本质上还是进程去操作;一个进程可以打开多个文件,而操作系统也需要对这些文件进行管理。(先描述再组织)
如果一个文件被打开,那么它会被加载到内存中;而操作系统中移动存在大量被打开的文件。在内核中,一个被打开的文件都必须有自己的文件打开对象,其中包含很多的属性。和进程类似,这个对象就和PCB结构一样,内部存储着文件的信息;而这些对象通过链表等数据结构串在一起,转化为链表的增删改查。
C语言接口
在c语言中提供了打开文件的接口fopen:

我们在程序中使用,尝试创建一个名为 log.txt 的文件,如果创建成功就立即关闭它:
cpp
#include <stdio.h>
int main()
{
FILE *fp = fopen("log.txt","w");
if(fp == NULL)//open file failed
{
perror("fopen failed");
return 1;//给一个错误码
}
fclose(fp);
return 0;
}
fopen有两个参数,第一个表示操作的文件名,第二个w表示打开模式;它的作用为:
- 文件存在,清空其所有内容重新开始;
- 文件不存在,创建一个新文件。
最后使用fclose关闭文件,释放系统资源并确保数据写入磁盘。
在创建log.txt文件时,会自动在进程的当前路径下创建;即当前进程的cwd(工作路径)那么如果对当前进程的工作路径进行更改,是否就可以将文件新建到其他目录呢?
修改进程的工作路径我们需要使用chdir函数:

参数就是想要修改到的路径。
打开文件的方式除了w外还有其他的类型,总结如下:
|----|---|---|--------------------------------------------------------------|
| 模式 | 读 | 写 | 作用 |
| r | √ | × | 以只读方式打开文本文件。如果文件不存在,则会打开失败。 |
| w | × | √ | 以只写方式打开文件;如果文件已存在,其长度会被截断为零(即清空原内容);如果文件不存在,则会创建一个新文件。 |
| a | × | √ | 以追加(写入)方式打开文件;其文件流位于文件末尾,写入的新内容会接在原文件的后面而不会覆盖。且如果文件不存在,则创建它。 |
| r+ | √ | √ | 以读写方式打开文件。作用同r。 |
| w+ | √ | √ | 以读写方式打开文件,作用同w。 |
| a+ | √ | √ | 以读取和追加的方式打开文件。作用同a。 |
所以,w方法在写入之前都会将文件清空。
而w和a方法都是写入,w会清空文件,a值在原有的内容上追加。
c程序默认在启动的时候,会默认打开三个标准输入输出流:stdin、stdout、stderr;因为在Linux中一切皆文件,这三个输入输出流分别对应了不同的对象:
- stdin:键盘文件
- stdout:显示器文件
- stderr:显示器文件

我们的printf、scanf等函数就是对这些流文件进行读和写来实现的。
系统文件I/O
文件是存储在磁盘上的,而磁盘是外部设备,访问文件实际上就是访问硬件。
几乎所有的库,只要是访问文件或硬件设备,必须要被封装。如printf,scanf等。,我们上面使用的函数全是封装好的可以直接使用的函数。不只是c语言,其他语言都会有这样的输入输出函数;而它们的底层实现都是基于下面这写函数来实现的:

和进程类似,文件的各种信息会存储于一个专门的结构体,叫做struct file;该结构体包含了文件的所有属性,而各个文件通过链表的形式链接在一起,通过操作系统来进行管理。
不过,进程和文件是 1:n 的关系,即一个进程可能会打开多个文件,所以文件的结构体中还需要一个信息来标注该文件的位置,这样才能知道哪个文件是哪个进程打开的。
在struct file中,会存在一个struct file* next指针,该指针会指向一个包含有数组的大结构体,这个数组就为文件描述符表。这个数组是一个指针数组,存储了文件的地址,并存放于一个数组下标对应的空间中。
所以本质上来说,open的返回值就是该数组的下标,从而能够找到对应的文件。
键盘、显示器默认打开,所以任何语言中都会自动打开自己封装好的流,就是对应的键盘显示器。
open
open函数的返回值:
成功:新打开的文件描述符
失败:-1
open 函数的使用和具体应用场景相关;如目标文件不存在,需要open创建,则第三个参数表示创建文件 的默认权限,否则,使用两个参数的open。
文件fd
文件描述符就是一个小整数,即open函数的返回值。
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2。
0,1,2对应的物理设备一般是:键盘,显示器,显示器。
文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体来表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进 程和文件关联起来。每个进程都有一个指针*files,;指向一张表files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针。
所以,也就是上面提到的,在本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
运行可以看到fd = 3。这是因为fd会取第一个没有使用的下标作为返回值。如果我们关闭了3,那么3就会被fd获取到。
同样的,如果我们关闭0或者2:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
//close(2);
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
输出后发现结果为fd = 0或fd = 2。原理和上面相同。
不过,如果我们关闭了1,却发现显示器上不会显示任何结果。
这是因为我们使用的是printf函数,该函数的底层逻辑使用的就是1号描述符,所以我们关掉了1号之后当然就不会打印出结果了。