目录
文件描述符
基础知识
文件描述符
对“Linux一切皆文件”的理解
文件描述符分配规则
缓冲区
刷新策略
存放位置
解释一个"奇怪的现象"
格式化输入输出
文件描述符
基础知识
在系统层面上,文件操作都是通过文件描述符来操作的。
程序在启动的时候,会默认打开三个文件流stdin(标准输入流)、stdout(标准输出流)、stderr(标准错误流)。这三个也是文件,在程序启动时由操作系统默认打开,程序退出时由操作系统默认关闭。这三个文件也有其对应的文件描述符,分别为0、1、2。所以,我们在程序中打开的文件的文件描述符是从3开始的。
C语言的文件接口,本质就是封装了系统调用。
C语言封装系统调用本质上是为了解决跨平台性、可移植性。在任何平台都使用的同一套C语言对文件的接口,底层会去调用不同平台的系统调用接口。在语言上访问文件的接口是不一样的,但只要是在一个系统中,底层调用的系统调用接口都是相同的。
在系统的角度来看,操作文件依赖的是文件描述符。
而在C语言中,操作文件却用的是文件指针。
这是因为C语言中的FILE是C语言标准库自己封装的一个结构体,在FILE结构体中,必定封装了文件描述符、文件属性、文件内容指向等字段。
进程创建出来后,操作系统将stdin、stdout、stderr文件加载到内存,由FILE结构体对象将其文件的信息记录下来,并将对象节点以某种数据结构进行连接管理。由于这三个文件已经占用了0、1、2,所以我们在进程中打开的文件的文件描述符就从3开始。
文件描述符
文件描述符本质是数组下标。
操作系统要管理被进程打开的文件,就要"先描述,再组织",利用特定的数据结构节点来管理被打开的文件,此后,对被打开的文件做管理就变成了对特定的数据结构进行管理了。
CPU执行到处理文件时,内部通过文件描述符调用相关的系统调用接口,操作系统通过访问进程PCB中的struct files_struct结构体指针,通过这个指针找到文件描述符表,再以文件描述符作为下标索引对应管理文件的struct file结构体的指针,通过对这个结构体进行处理,等到文件退出时再将修改的信息对磁盘文件中做更新。
在缓冲区上访问文件内容,缓冲区通过与磁盘的刷新来对文件做修改操作。
文件描述符本质上就是数组下标。
对“Linux一切皆文件”的理解
1、每个硬件都有与之匹配的特有的读写方法。
2、操作系统要管理每个硬件("先描述,再组织")
3、利用类似多态调用的思想,实现调用时调用指针所指向的对象的方法。
操作系统用上层统一的接口来屏蔽硬件读写差异。
文件描述符分配规则
文件描述符分配规则:最小的没有被使用的文件描述符会优先分配给最新打开的文件。
系统打印时如果没有特殊要求,系统只在stdout文件中去打印,本质上是系统只在文件描述符为1的文件中去打印。
当文件描述符为1的文件是stdout时,则输出到显示器文件上。
当文件描述符为1的文件是其他文件时,则输出到其他文件上去。
所以,修改1号文件描述符的文件,即可发生类似于重定向的操作。
重定向的本质就是修改文件描述符表中指针指向的内容,将这个内容修改为其他文件的地址(覆盖掉原来的地址即可),写入操作就会写入到其他文件中去。
直接覆盖文件描述符表对应下标的内容接口
这种处理会导致一个文件被多个指针指向,操作系统会对此情况进行处理。
printf/fprintf/sprintf函数在输出的时候都去stdout文件中输出,
本质是都输出到文件描述符为1的文件中去。
scanf/fscanf/sscanf函数在输入的时候都去stdin文件中去读取,
本质是都从文件描述符为0的文件中去读取。
重定向操作也是通过dup2系统调用接口,将对应的文件描述符表的内容做修改,从而实现的。
缓冲区
缓冲区本质是一块内存区域。
缓冲区的设计是用空间换时间,存在本质是为了提高用户的操作效率。
此缓冲区为语言层面的缓冲区,与操作系统内核中的缓冲区无关。
C语言本身自带缓冲区。
缓冲区是将数据聚集起来,做少次数的数据传输,来提高效率。
以用户写入操作举例,用户通过调用用户级接口向指定文件写入数据,C语言会将用户传的数据写入到自己的缓冲区中,等到缓冲区达到某种规则时,C语言会将自己缓冲区内的内容通过系统调用接口写入到操作系统内核中该进程对应的缓冲区中(C语言会通过进程PCB中的文件描述符表,以文件描述符为下标索引到管理文件的节点,通过节点的字段找到该进程对应的内核缓冲区)。由操作系统决定何时将缓冲区中的内容写入刷新到磁盘文件中。
每个进程都有自己的内核缓冲区。
系统调用本身是有成本的。
单次调用一次系统调用的效率高。比如:申请相同大小的空间,第一种方法:多次调用系统调用且每次申请较小的空间资源。第二种方法:少次调用系统调用接口且每次申请较多的空间资源。这两种方法,第二种是比第一种的效率高的,所以我们应该尽可能的减少调用系统调用接口的次数。
操作系统深知应尽可能少调用系统调用,所以在内存空间足够的情况下,对于系统调用接口,会在内存中预先申请大量的空间资源来提供使用。比如:我们调用fork系统调用的时候,看似我们调用了系统调用,其实是操作系统提前在内存中申请的空间资源,提前创建空壳进程PCB、页表、mm_struct等,等到我们调用该系统调用时,操作系统就不用给我们重新申请资源、创建PCB等操作了,就直接使用内存中提前准备好的空壳进程的一套资源,只需要写入对应的字段即可,这种做法提高了操作系统的效率。当然,当内存中资源不足的时候,操作系统会做各种挂起、终止不必要进程等操作,来保证操作系统的正常运行。
缓冲区就是提前申请好的内存空间。比如:我们在使用数据结构插入接口时,会直接插入到提前申请好的内存空间上去,不用再调用扩容机制接口,来提高效率。
执行多次写入操作都是将数据写入到语言级缓冲区中,此缓冲区按照自己的机制/策略,在合适的时机调用系统调用接口,将数据一次性全部写入到内核缓冲区中,再由操作系统决定,将内核缓冲区中的数据刷新到磁盘上,此操作有效减少了调用系统调用接口的次数,提高效率。
用户级别接口只需要将数据写入到语言级的缓冲区中,语言层就会给用户返回调用结果。从语言级缓冲区到内核缓冲区再到刷新到磁盘文件中这个过程,不用用户关心处理,这操作由操作系统整体把控,此操作还有效提高了语言级接口的使用效率。
刷新策略
语言级缓冲区通过系统调用接口将数据刷新(拷贝)到内核缓冲区的策略为:
1、无刷新---无缓冲:没有设计缓冲区,每次调用语言级接口都会调用系统调用。
2、行刷新:以'\n'字符为标志,每次刷新'\n'字符之前的数据,显示器文件就是行刷新。
3、全缓冲:缓冲区被写满后,才会被刷新。普通文件就是全缓冲。
如果上述情况都没有满足时,比如:行刷新但没有检测到'\n'、全刷新但缓冲区没有被写满
1、强制刷新接口
2、进程退出时,会自动刷新。
存放位置
语言级缓冲区本质是在管理文件的FILE结构体中,由该FILE结构体维护。
每一个文件都有对应的FILE结构体,都有对应的语言级缓冲区。
语言级缓冲区被管理该文件的FILE结构体维护,内存缓冲区被进程PCB维护。
C语言封装的意义:
1、将数据写入到语言级缓冲区中
2、调用系统调用
解释一个"奇怪的现象"
C语言的缓冲区,如果是向显示器文件中输出,刷新方案就是行刷新,如果是向普通文件中输出,刷新方案就是全刷新。
系统调用接口会直接将用户输入的数据写入到内核级缓冲区中,C语言级接口会先写到语言级缓冲区中再写到内核缓冲区中。
格式化输入输出
FILE类型是C语言提供的,就存在C语言级缓冲区。
stdin一般都有输入缓冲区,stdout一般都有输出缓冲区。
我们在使用printf函数时,是向显示器文件中做输出,但显示器文件属于字符设备文件,只认识字符,不能识别我们输入的非字符数据类型,此种情况,就是格式化输出的工作了。
格式化输出本质是将非字符类型转化为一个个的字符,然后拷贝至发送缓冲区中,然后由printf函数获取输出。
printf函数输出,实际上输出的都是字符。在printf函数内部,都是以字符来对待数据的,格式化输出会将全部的非字符类型转化为字符类型,拷贝到发送缓冲区中,格式化输出时,再将其拷贝至语言级缓冲区中即可。
格式化输入,实际上将用户输入的数据视作字符,保存在stdin输入缓冲区中,由格式化输入将一个个的字符转化为各种数据类型,再通过变量地址写入到变量中。
数据流动本质上就是拷贝数据,所有的文件接口本质上也是拷贝接口。
我们所学的C++中的cin、cout本质都是类,都存在缓冲区,我们将这种语言级别的缓冲区称作字节流,读写都是从字节流中获取。