回忆 C 语言文件操作
fopen
# include <stdio.h>
int main()
{
// 第一个参数为文件名, 如果没有指定具体路径,就是在当前工作目录打开
// 第二个参数是指定打开文件方式, w表示写方式打开文件
FILE* fp fopen("whatever.txt", "w");
// 打开文件失败
if (fp == NULL)
{
perror("fopen");
return 1;
}
close(fp);
return 0;
}
当程序运行时,程序变成进程,文件打开操作会由进程执行。因此,这样进程就跟文件产生了关联。
当前工作目录
所谓的当前工作目录是指启动程序的路径,也就是在那个路径启动程序,当前工作目录就是那个路径。为了验证这个事实, 跳转到家目录创建了一个file_test的目录,然后执行上层目录下的code目录下的可执行文件file_test。
同时,启动进程监控, 在/proc目录下得到file_test进程的相关信息,其中cwd所指向的就是当前工作目录。
查看当前工作目录,可知确实存在所创建的文件。
fputs
向文件中写入10行文本。
查看写入结果。
fgets
修改fopen的第二个参数为“r”。
查看运行结果。
由 C 文件操作引出的问题
文件的读写要先打开文件,那么为什么在Linux中一切皆文件,键盘和显示器都可认为是文件,但是我们没有打开文件,就可以使用printf和scanf分别往显示器打印和从键盘输入?
原因就是任何程序在运行时,操作系统都会为其打开三个文件,返回三个文件描述符,因此我们不需要打开,就可以从键盘和显示器分别读和写。
系统级别IO
打开文件
与 C 语言一样,C++ 或是 JAVA 在文件读写时默认都会打开三个文件,因此这三个文件的打开就不是单单一种语言实现,而是操作系统实现的。
执行上面代码,得到的结果是,每当我们创建出一个文件时,系统返回的文件描述符是从3开始,后面每打开一个文件,返回的文件描述在前一个基础上加一。这说明了,标准输入、标准输出和标准错误这三个文件在程序运行时就被指定映射到0 1 2
三个文件描述符。其次,文件描述符类似于数组下标。
系统级别文件管理
从上面的代码可以看出,一个进程可以打开多个文件,系统中存在多个进程,这就意味着系统中可能存在多个打开的文件。因此,就需要对这些文件进行管理。系统为每个打开的文件用一个struct file
描述,然后将多个结构体用双链表组织起来,进而实现对文件管理就是对双链表的增删查改。
文件构成
磁盘文件由文件内容和文件属性(元信息)构成。当打开磁盘文件时,就产生了内存文件,其中内存文件更多包含的是文件属性,当需要读或写数据时,数据采用的是延后式加载数据。
文件与进程关联
在进程描述结构体task_struct
中有一个指向文件描述符指针数组。
当我们创建一个文件时,会在内存中创建一个struct file
结构体描述内存文件。然后将结构体地址,也就是struct file*
类型的地址填入到fd_array
。当形成映射关系后,操作系统就会返回该文件在文件描述符指针数组中对应的下标。
所以文件描述符本质上就是数组下标。默认打开stdin
、stdout
、stderr
就是为这三者分别描述成struct file,然后将其关联到fd_array[0]
、fd_array[1]
、fd_array[2]
。
write底层流水线
read底层流水线
文件描述符的分配规则
从小到大,找到没被占用的即为打开的文件分配。
文件重定向
文件重定向本质就是改变fd_array
中对应下标所指向的struct file
,如下所示,向1号文件描述符写入,默认1号文件描述符指向显示器,写入的时候,会往显示器上写,但是当关闭1号文件描述符,再打开文件后,根据文件描述符分配规则,1号文件描述符就指向描述打开的文件的struct file
。这样一来,指向变了,写入的文件也就变了。