基础IO
目录
一、进程和文件的关系
二、背景补充
三、打开文件接口
(1) FILE *fopen(const char* filename , const char *mode)
(2)open 系统调用
文件描述符
open和fopen的关系
(3)size_t fwrite(const void * ptr, size_t size, size_t nmemb,FILE*stream)
(4)size_t fread(void *buffer, size_t size,size_t nmemb,FILE *stream)
四、输入输出流
(1)重定向
追加重定向的原理
重定向接口
五、struct file
六、文件继承
七、理解一切皆文件(硬件方向)
八、文件缓冲区
1、缓冲区的理解
2、缓冲区的刷新策略
3、FILE缓冲区
(1)引入
(2)本质
(3)FILE刷新方式(缓冲区到内核缓冲区)
(4)一个现象
九、内核文件区
(1)内核文件缓冲区的刷新方式
(2)接口
一、进程和文件的关系
二、背景补充
- 文件 = 内容 + 属性 所以对文件操作本质 就分为 对内容做操作 或者对属性做操作
- 访问一个文件,都必须先把对应的文件打开 因为根据冯诺依曼我们对文件进行操作的时候必须要要保证文件在内存中,而打开一个一个文件就是把这个文件加载到内存(本质就是把文件的属性和内容加载到内存中)
- 如果一个文件没有被打开,他就在磁盘中,此磁盘的管理者是操作系统
- 进程(用户通过bash,启动进程(进程通过操作系统))打开文件对文件操作本质是进程对文件的操作
- OS内,一定同时存在大量的被打开的文件(通过数据结构管理被打开的文件)
三、打开文件接口
(1) FILE *fopen(const char* filename , const char *mode)
- 我们可以发现以w方式打开文件,如果文件不存在,会在当前工作路径下创建一个文件,为什会在当前路径下本质是因为每一个进程在运行起来都一个cwd,默认会在cwd的后面在加上当前文件名进行创建
- 打开文件,必须要先找到文件,要找到文件就必须要知道文件的路径+文件名,这也是为什么进程要有cwd的原因之一
方式
- w :文件不存在创建,文件存在清空
- w+: 文件不存在就创建,读写打开文件
- r : 读文件
- r+: 读写打开文件
- a : 不清空文件,从当前文件的结尾处追加
- a+ : 读和追加
- 读写文件有读写位置:在我看来文件就是一个"一维数组"所以读写位置就是数组下标
- > : 重定向就是以w的方式打开文件
- >>:追加重定向就是以a+的方式打开文件
(2)open 系统调用
int open(const char * pathname , int flags ,mode_t mode)
pathname:
- 若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
- 若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。(注意当前路径的含义)
flags:
打开方式(标志位): 首先它的类型为int 有32个比特位,一个比特位就有一个标志位(本质是宏)在open函数内部就可以通过使用与运算来判断是否设置了某一选项
例:
mode: 权限
放回值: 失败-1 ,成功文件描述符
文件描述符
- 因为进程和文件的比例关系为 1 : n 且 OS内,一定存在大量被打开的文件,操作系统对这些文件进行管理(struct file),这些对这些文件进行管理变成对struct file链表的增删查改,但是这么多文件,是被多个进程打开的,系统需要表示那个文件是由那个进程打开的由此产生了文件描述符
- 我们知道,当一个程序运行起来时,操作系统会将该程序的代码和数据加载到内存,然后为其创建对应的task_struct、mm_struct、页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系。
- 而task_struct当中有一个指针,该指针指向一个名为files_struct的结构体,在该结构体当中就有一个名为fd_array的指针数组,该数组的下标就是我们所谓的文件描述符。
- 当进程打开文件时,我们需要先将该文件从磁盘当中加载到内存,形成对应的struct file,将该struct file连入文件双链表,并将该结构体的首地址填入到fd_array数组当中下标为3的位置,使得fd_array数组中下标为3的指针指向该struct file,最后返回该文件的文件描述符给调用进程即可。
- 文件描述符本质是数组下标,在OS内部,OS识别被打开的文件,OS只认fd
open和fopen的关系
为什么c语言要封装文件操作接口
系统调用太麻烦了 跨平台性 可移植性
为什么大部分的语言,都对系统调用做封装
需要具有跨平台性(增加语言的竞争性)
如何做到跨平台性
所有版本的都写一遍
(3)size_t fwrite(const void * ptr, size_t size, size_t nmemb,FILE*stream)
ptr: 写入的起始地址
size:写入的基本单元的大小
nmemb: 写几个
stream: 向哪一个文件流中写
注意:我们不需要strlen(str)+1 将字符串的\0带上,等到读取字符串的时候加上就行了
(4)size_t fread(void *buffer, size_t size,size_t nmemb,FILE *stream)
buffer: 读到那
size: 基本单元
nmemb:几个
stream : 从哪里读
写一个cat
- 补充: 今天我们向显示器写入1234就是向显示器中写入了"1" "2" "3" 所以显示器叫做字符设备 。我们向键盘输入1234,我们实际上输入的是"1" "2" "3" "4" 所以键盘叫做字符设备
- 所以我们使用printf()函数的时候必须要使用%d % f,它的本质是将我们输入的数据打散为字符(这就叫格式化输入)
- scanf()同理就叫做格式化输入
- 显示器和键盘是文本文件,而二进制文件不需要做格式化工作(可以使用fwrite)
四、输入输出流
- stdin; stdout ; stderr;(进程在启动的时候,默认会打开三个输入输出流,就是这三个文件)
- 标准输入 标准输出 标准错误
为什么默认打开他们
因为进程大多数都是使用CPU资源进行计算的,都需要有数据的输入,输出结果,输出错误(所以他们默认占据文件描述符的012)
- 文件描述符的分配规则:给新打开的文件分配fd,从文件描述符表数组中寻找:最小的,没有被使用的下标,作为作为改文件的fd
(1)重定向
- 输出重定向就是,将我们本应该输出到一个文件的数据重定向输出到另一个文件中
- 例如:关闭了1 打开文件,默认会将文件描述符1给他,printf默认会向stdout里面打印,但是stdout默认封装了文件描述符1 ,所以会像log.txt里面打印,这就叫输出重定向
- stdout指向是一个struct FILE 结构体,该结构体当中有一个文件描述符变量,stdout的文件描述符变量就是1
追加重定向的原理
输出重定向和追加重定向唯一的区别就是输出重定向是先清空文件,而追加重定向是追加式输出
重定向接口
int dup2 (int oldfd ,int newfd)
注意:如果oldfd不是有效的文件描述符,则dup2调用失败
如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值则直接放回newold
返回值:成功返回文件描述符,失败返回-1
五、struct file
- 在调用open之前已经有了task_struct 和 struct files_struct表
- 当我们在使用write(文件描述符,“要写入的内容”)的时候,我们通过task_struct找到文件描述符表,通过文件描述符找到对应的struct file,然后将要写入的内容写到文件内核的缓冲区,在通过文件内核刷新到磁盘中
- 所以write根本不是写入到文件,本质是拷贝函数,把数据从用户空间拷贝到对应文件的内核缓冲区
- 什么时候刷新到磁盘文件中,由OS决定
- 读数据也只能从文件内核缓冲区中读,如果文件内核缓冲区没有内容,需要等待磁盘刷新到磁盘中
- 我们进行任何文件内容的增删查改都必须把文件的内容提前预加载到该文件的文件内核缓冲区
六、文件继承
- 所以父子printf的时候,会同时向同一个显示器文件,进行打印
- 对于子进程来讲,他继承了父的进程,所以子进程默认打开标准输入,标准输出,标准错误
- file(struct)有一个引用计数,表示改文件由多少struct_file指向我,当引用计数为1的时候才能真正关闭文件
七、理解一切皆文件(硬件方向)
- 我们知道操作系统对硬件会进行管理(描述在组织)形成对应的链表,他们每一个都有对应的实现读写的方法
在打开一个文件的时候,会为我们创建struct_file 包含属性集合,,文件内核缓冲区,还会有一些方法集合指向对应硬件的实现方法,在将stuct_file的地址写入到struct file_struct的表中,所以站在进程视角下,就是一切皆文件
八、文件缓冲区
1、缓冲区的理解
- 缓冲区的本质,其实就是一段内存空间
例
- 如上图张三如果像给李四送一个礼物,如果张三自己去把礼物给李四,那么会花费大量张三的时间,但是如果将礼物给菜鸟驿站,站在张三的视角下礼物已经给了出去,且节省了他的时间,缓存的数据就是礼物,礼物给菜鸟驿站就等于write
- 缓存最大的意义:是提高使用缓存的进程效率,允许进程单位时间内做更多的工作,变相的提高了使用者的效率
- 在菜鸟驿站的角度下来看,他不可能收到一件商品就给他寄出去,所以缓冲区允许数据积压,以一次,就可以刷新多次数据,变相的减少IO的次数)
2、缓冲区的刷新策略
三种形式:
- 无缓冲,立即刷新
- 有缓冲,行刷新 (显示器中使用)
- 有缓冲,写满刷新(普通文件采用者这种方式)
两种情况 :
- 进程退出,主动刷新
- 进程强制刷新fflush
3、FILE缓冲区
(1)引入
- 上图的代码执行的时候先执行的是printf但是当我们运行起来的时候会发现是先暂停的3,然后屏幕上在出现的hello world,,这是因为printf的打印先开始是打印到文件缓冲区中的等到进程结束的时候才刷新出来的,这里的缓冲区是FILE缓冲区,不是内核文件缓冲区
(2)本质
- struct FILE本质是一个结构体,包含文件描述符
- C语言上,输入输出格式化
- C访问文件,都是通过FILE访问的,包括stdin stdout stderr
- FILE结构体内部为我们维护语言级别的缓冲区
过程
- 如上图首先通过fget(),scanf() 将键盘中输入的内容获取,在通过fput() (底层调用write)将获取的内容拷贝到struct FILE文件缓冲区中,通过刷新将内容刷新到文件内核缓冲区中,同过操作方法集刷新到磁盘中
(3)FILE刷新方式(缓冲区到内核缓冲区)
- 无,立即刷新
- 行,行刷新(显示器文件)
- 满,全刷新
缓冲区在哪?
FILE内部
为什么要用语言级别缓冲区
调用系统调用是用成本的
C语言为什么要提供语言级别的缓冲区
加速IO函数的调用效率 (加快使用C语言IO接口的效率,单位时间内执行C代码的行数,就多了)
如何理解printf scanf的格式化过程
1、格式化 2、格式化结果写入到FILE缓冲区中 3、检测是否需要刷新 4、 如果需要刷新调用write
例:
- 如上的代码,我们会发现log.txt中没有内容,因为当我们关闭文件描述符1,在打开log.txt他的文件描述符默认就是为1,printf()是向stdout中进程打印,stdout 中默认封装了文件描述符1,所以printf()回向log.txt中打,但是他有文件缓冲区不会立即刷新到log.txt,默认文件关闭的时候刷新到内核缓冲区,但是这时候我们关闭了文件。 所以他会刷新到log.txt
(4)一个现象
- 我们能发现上面的接口我们打印到显示器上,只会打印一次,但是我们重定向到文件中write,打印一次,其他的都会打印二次
- 首先打印到显示器中有\n他按行刷新, 所以他每一个都只打印一个
- 当他打印到文件中他的刷新规则变成了写满刷新,所以只有退出的时候才会刷新,write是直接写到内核中的,所以他是第一个打印的,其他的都在内核缓冲区中,当退出的时候fork()父子进程都有自己的stdout,指向同一个缓冲区,当要进行清空缓冲区的时候,就是要进行改数据,会发生写时拷贝,从而打印两次
九、内核文件区
(1)内核文件缓冲区的刷新方式
- 一般而言:全刷新
- 显示器:行刷新
- 单独的执行流,根据内存的使用方式来动态刷新,即使刷新条件不满足
(2)接口
int fsync(int fd)
强制从内核 ——》外设