一.理解文件
1.狭义理解
①.文件=内容+属性。
②.文件储存在磁盘上。
③.磁盘是永久性存储介质,因此⽂件在磁盘上的存储是永久性的。
④.磁盘是外设(即是输出设备也是输⼊设备)。
⑤. 磁盘上的⽂件本质是对⽂件的所有操作,都是对外设的输⼊和输出简称IO。
2.广义理解文件
Linux下**⼀切皆⽂件**(键盘、显⽰器、⽹卡、磁盘......这些都是抽象化的过程)。
3.文件 = 内容 + 属性
①.对于0KB的空⽂件是占⽤磁盘空间的,空文件也具有属性,也要占空间!
②.⽂件是⽂件属性(元数据)和⽂件内容的集合(⽂件=属性(元数据)+内容)。
③. 所有的⽂件操作本质是**⽂件内容操作和⽂件属性操作。**
4.系统角度
任何文件都必须先打开,然后才能访问!! 那么是谁打开了文件呢?答案是进程 。所以文件操作的本质是进程对文件进行操作。
既然涉及到进程,就事关OS,也就是说想要访问文件(文件储存在磁盘),就是访问磁盘,而管理磁盘的正是OS,所以访问文件,其实是去调用OS的接口。

二.回顾
1.如何将信息输出到显示器


向显示器打印数据,本质上就是对显示器这个文件进行写入(linux下一切皆文件!)。
2.stdin & stdout &stderr
这是三个文件流,是FILE类型的指针,分别代表键盘文件 ,显示器文件。c语言程序在启动的时候,默认会打开这三个输入输出流(打开指向的文件)。

那么为何要默认打开输入输出流呢?
程序运行几乎一定有输入输出 ,如果每次写程序都要用fopen+打开方式去打开文件,代码会十分繁琐和冗余。就比如使用基础IO函数时,需要手动打开文件,那么每写一个helloworld或者用scanf获取一段数据,都需要去手动打开屏幕文件和显示器文件,特别麻烦。
3.文件打开的方式
①.w
以w(写)的方式打开文件时,默认会先将文件的内容清空。
这里就能解释,为什么用">"(重定向操作符)向文件写入内容时,每写一次进去,就把原来的东西覆盖掉一次。使用">"时,默认会先以写(w)的方式打开文件,就会清空原来文件里的内容。

②.a
用这种方式打开的文件,在写入数据时,玩的是追加(向文件结尾处追加),也就是说不会想上面那样每打开一次文件,就清空一次文件内容。

在使用">>"(追加重定向操作符)写入数据时,会默认先以"a"的方式打开文件,因此写入的数据是追加的:

三.系统文件IO
打开⽂件的⽅式不仅仅是fopen,ifstream等流式,语⾔层的⽅案,其实系统才是打开⽂件最底层的⽅ 案。
1.open

就比如fopen是语言层面的接口,当调用fopen时,fopen其实还会去调用系统调用接口open。open的参数列表里有一个flags标记位,我们先来明晰何谓标志位。
①.标志位
flags是一个整型的数据,是宏。
flags的几个宏

把这些宏传入open,可以让文件以不同方式被打开,这也是语言层面fopen里"w","r"等打开方式的实现原理。
来看一下各个宏的作用:
·O_RDONLY:只读打开·OWRONLY:只写打开
·O_RDWR:读写打开
·O_CREAT:若文件不存在则创建
·O_TRUNC:打开文件的同时清空文件内容
(Truncate)
·O_APPEND:追加写模式
这些宏是可以组合使用的,在传参的时候用"|"连接就行了。
fopen的"w"和"a"打开方式的底层:

2.文件描述符fd到底是个什么鬼?
①.类型角度
通 过对open函数的学习,我们知道了⽂件描述符就是**⼀个⼩整数,是open的返回值。**
这里有个奇怪的现象:当我们用open打开多个文件,并打印它们对应的fd时,怎么从3开始,那么0,1,2去哪了呢?


答案是Linux进程默认情况下会有3个缺省打开的⽂件描述符,分别是标准输⼊0,标准输出1,标准错 误2。前面说过,标准输入,输出,错误,这三位是系统默认打开的!!!!因此它们在系统默认打开时,已经有fd了,就已经把0,1,2占据了!!!!
那系统调用open的上一层语言层面的接口fopen的返回值怎么又是FILE*类型的呢?这里的FILE又是个什么玩意?
答案是在语言层面的FILE是一个typedef出来的结构体 !!但在OS层面,只认fd,不认FILE。既然不认,我语言层面又只有FILE,但语言层面的接口的执行又要去调用系统调用接口,那这该咋办呢?答案是FILE里一定封装了fd!!!!

②.内容角度
那上面这些个fd数字(0,1,2,3,4)到底有啥含义呢?答案是它们是数组下标 !那这个数组是用来干啥的呢?上面有个结论,打开文件的本质是进程打开文件,属于操作系统的范畴。操作系统需要把打开的文件管理起来,管理方式是先描述,再组织。
当进程打开文件时,内核会创建一个struct file,上面的成员(包括内容+属性)与磁盘上的文件是相对应的。打开多个文件,就会创建多个对应的struct file。创建struct file,就是先描述。所以再组织,就是将各个文件对应的struct file链接起来!所以OS对被打开文件的管理,就是对链表的增删查改!
每个struct file都有一段缓冲区,磁盘上对应文件的内容就会被加载到这个缓冲区上。
所以打开(open)这个过程,其实是创建struct file,然后把文件的内容+属性加载到对应的struct file上,而执行读/写一类的操作,就是从缓冲区里进行读写。
那怎么分清哪些struct file是某一个对应进程创建的呢?
当一个进程被创建,在进程的PCB里还会创建一个叫struct file_struct 的文件描述符表。而这个表里会包含一个struct file*类型指针数组 :fd_arry\[\]。每个元素指向一个file_struct,那么这些元素的下标就是上面的fd!!

而属于系统调用的read函数的底层是,去文件描述符表里索引,找到对应的struct file,然后将其文件缓冲区的内容拷贝一份,拷贝到用户层的缓冲区里,所以read的本质是内核到用户空间的拷贝函数!
**对文件的任何操作,都必须先把文件加载到内核的struct file的缓冲区里!**假如要修改文件内容,就先将文件内容加载到缓冲区内,然后对缓冲区上的内容进行修改,修改完成后,将缓冲区内容写回磁盘。
那加载的本质又是什么呢?答案是从磁盘到内存的拷贝!
4.重定向的原理
①.fd分配原则
首先我们要明晰fd的分配原则:当新的文件被加载进内存(被进程打开),OS就会去到文件描述符表里去查询,将查到的最小的,且没有被使用的空间的下标,作为新的fd给用户。
②.说回重定向
下面代码演示了很有意思的一幕:
先将fd = 1的输出流stdout关闭了,然后同时打开一个log.txt文件,并打印log,txt的fd到屏幕上。这时我们发现fd并没有打印到屏幕上,而是写入了log,txt里,这是什么鬼?
哈哈,这就是出现了重定向!
我们来说说原理:

补充一点:printf的底层是向stdout指向的屏幕文件写入,而std只认fd = 1,也就是说下标fd = 1的元素是屏幕文件,才能向屏幕文件写入东西!
所以重定向的本质是是去修改files_struct里指定下标所对应元素(指针)的指向。也就是用新文件的struct file的地址去覆盖旧的地址。
这里还有个系统调用------dup2,可以用来重定向:
用newfd对应的地址去覆盖oldfd对应的地址。使得oldfd的地址指向新文件的 struct file。也就是说oldfd对应的指针,最终是newfd对应指针的一份拷贝!
所以形参要传newfd和oldfd:

重定向 = 打开文件的方式 + dup2。
最后再来说一个点,上面fd = 1的指针经重定向指向新的 struct file,那么原来的 struct file该怎么解决呢?会不会造成空间无法释放,进而造成浪费呢?
答案是struct file上有个计数器ref_cnt。同时由于一个文件支持多个进程打开,也就是说这些进程的files_struct上都会有指针指向这个struct file,每多一个指针指向,ref_cnt就++一次,相反每少一个指针指向,ref_cnt就--一次,当ref_cnt = 0的时候,OS会将这个struct file释放掉。