目录
前言
重定向
实验一
为什么log.txt文件的文件描述符是1
为什么向stdout打印的信息也出现在文件中
实验二
用户级缓冲区
为什么要有用户级缓冲区
系统调用
dup
为什么close(fd1)之后还能向log.txt写入数据?
dup2
缓冲区
观察现象
测试1
测试2
测试3
测试4
现象解释
现象1 解释
现象2解释
现象3解释
现象4解释
缓冲区的刷新策略
用户级缓冲区与OS的关系
实现一个缓存区
myStdio.h
myStdio.c
testmyStdio.c
前言
在【Linux】进程IO|系统调用|文件描述符fd|封装|理解一切皆文件-CSDN博客
中讲述了文件描述符,0代表着键盘,1和2代表着显示器,只不过一个是标准输出,一个是标准错误;
重定向
重定向的本质是上层使用的文件描述符不变(即数组下标不变),数组里面的内容发生变化,即在内核中更改文件描述符指向的文件对象,使另一个文件对象指向原有的文件对象,从而使原有的文件对象被另一个文件对象覆盖
实验一
观察下面代码
原本应该向屏幕输出的信息,却没有显示任何信息,但是打印生成的文件发现文件描述符fd是1;
为什么log.txt文件的文件描述符是1
这跟上篇讲述的文件描述符分配有关;调用 open 打开 log.txt 之前关闭了标准输出,其对应的文件描述符1就闲置了出来,而 fd 的分配规则是从小到大依次寻找未被使用的最小值,所以 log.txt 对应的 fd 就为1;
为什么向stdout打印的信息也出现在文件中
stdout是个FILE*结构体类型,其封装的文件描述符默认为1,当分配1给文件后,printf()和fprintf(stdout)实际上是在向文件描述符为1的文件写入数据(语言的文件操作是对系统调用的封装),底层的系统调用write只认文件描述符。此时文件描述符为1的文件是log.txt,所以数据会写入到该文件中。
fprintf(stdout,…)等价于write(1,…)
在文件创建之前,我们关闭了标准输出流,即 close(1) ,原指向标准输出的文件描述符不再指向标准输出,对应的 1号文件描述符就闲置了
调用 open 打开 log.txt 根据分配规则,所以log.txt的文件描述符就为 1, fd_array[1] 指向的是新的文件对象, fd_array[1] 不再指向标准输出
实验二
关闭用户级刷新
现象: 注释掉fflush(...)后,文件没有被写入数据,
出现原因:数据在close(fd)之前还在缓冲区内。
fflush的作用是将用户级缓冲区的数据刷新到内核级的缓冲区。
用户级缓冲区
该缓冲区是应用程序直接管理和控制的内存区域。这些缓冲区位于应用程序的地址空间中,应用程序可以直接访问和操作它们,而无需进行系统调用。
在语言层面上也有一个的缓冲区,使用库函数printf()或者fprintf()向文件写入数据时,会先加载到这个用户级缓冲区里面,并不会直接加载到内核级缓冲区中。
当用户缓冲区刷新到内核缓冲区后,后面就是内核操作了,操作系统会适时来刷新,该数据流被刷新到磁盘的文件中了。
我们用printf()和fpritnf()函数向文件写入数据时,这些数据会先进入用户级缓冲区里面。
当我们注释掉fflush(),实际上就是没有主动的刷新用户级缓冲区里面的数据。
紧接着关闭文件,即使程序结束时会自动刷新用户级缓冲区,但由于在此之前已经关闭了文件log.txt,那些数据也就丢失了。这就是重定向
为什么要有用户级缓冲区
- 用户级缓冲区通常用于提高数据传输的效率,减少应用程序与操作系统之间的频繁交互。不必每次数据交互都访问内核缓冲区,而是可以先存在一个区域中,达到一定量了之后再刷新到内核缓冲区(相当于寄快递,统一时间段拉走)。
- 对于操作系统来说,如果没有用户级缓冲区,我们每次向文件读写数据都要访问内核,间接加剧了内核访问磁盘的次数。
- 对于用户来说,每次读写操作都要等待操作系统响应,这样无疑会降低用户的体验。所以用户级缓冲区可以提高用户的体验,也可以提高数据传输的效率。
综上所述:当改变原fd指向的内容时,就会发生重定向,因为非内核操作只认文件描述符
系统调用
操作系统提供了一个系统调用,可以直接实现重定向。
dup
- 复制当前文件描述符的内容,返回一个新的文件描述符(当前可用的最小的描述符)。
实际上就是复制在文件数组(fd_array[N])中下标为fd的内容到文件数组(fd_array[N])中的另一个地址空间中。新旧描述符指向的文件是同一个。
- 打开文件log.txt,得到描述符fd1;
- 拷贝一份fd1文件表项内容,得到fd2,此时fd1和fd2描述同一个的文件
- 向fd1写入数据msg1之后关闭fd1;
- 向fd2写入数据msg2之后关闭fd2;
为什么close(fd1)之后还能向log.txt写入数据?
close()会不会直接关闭文件,取决于是否还有其它文件描述符指向该文件 。
这里采用了引用计数的原理:
- 每个被打开的文件都会有一个计数器记录该文件被引用的次数。
- 每多一个描述符指向该文件,该文件的引用计数器就会加一。反之,就会减一。一旦引用计数器为0,表示没有可用的描述符指向该文件,该文件也就才能真正地关闭。
- 所以close(fd)的本质,是清空fd再使文件fd的引用计数器减一。
- fd1和fd2指向的文件是同一个,关闭close(fd1)等于关闭了文件log.txt
虽然dup可以复制文件描述符,但是得到的新的描述符是不可控制的。
比如:不能拷贝一个文件描述符到另一个已存在的文件描述符(dup函数得到的描述符是新的)
dup2
int dup2(int oldfd, int newfd);
dup2会将 fd_array[oldfd] 的内容拷贝到 fd_array[newfd] 当中,即把 fd_array[newfd] 的内容覆盖 ;
头文件:<unistd.h>oldfd:旧的文件描述符
newfd:新的文件描述符函数返回值,成功返回 newfd,失败返回-1
dup2函数可以将一个已存在的文件描述符复制到另一个文件描述符上,并允许自定义新文件描述符的编号
- 打开文件log.txt并获得其描述符fd
- 复制fd到1中。此时fd和1都是指向log.txt。此刻完成输出重定向,原本向屏幕文件输出的变成了向log.txt文件输出。
缓冲区
- 操作系统级别的缓冲区是内核的一部分,用于在硬件设备和应用程序之间提供一个临时存储区域。这些缓冲区用于提高I/O操作的效率,比如文件读写、网络通信等。
- 用户级缓冲区是指在用户程序中显式创建和管理的缓冲区。需要自己负责缓冲区的分配、初始化、使用和释放。
观察现象
测试代码
测试1
测试2
将运行程序的结果重定向到文件中;
测试3
在原有的代码末尾使用 fork 创建子进程,子进程不做任何事情;
测试4
输出重定向到 log.txt 文件中
现象解释
现象1 解释
显示器采用的时行缓冲区;
printf()、fprintf():这两种语言封装的接口向标准输出(显示器)打印数据,程序中,每条打印语句后面都有\n(换行符),所以在这两个语句执行后会立即刷新缓冲区;
write():是系统调用,没有缓冲区,数据会立即被发送给操作系统,然后由操作系统负责进一步处理。它直接与底层文件描述符交互,将数据从用户空间复制到内核空间,并最终写入到相应的文件或设备上。
现象2解释
- 通过输出重定向(>)将原本输出在显示器中的数据写在文件中,磁盘文件是采用的全缓冲刷新策略,所以printf(),fprintf()语句执行完毕后并不会立即刷新,一般会等进程退出这种特殊情况才会将所有数据刷新;
- 标准输出被重定向到一个文件时,标准输出流被设置为全缓冲模式。这意味着标准输出流会在缓冲区填满或者显式调用fflush()时才刷新输出。
现象3解释
显示器采用行缓冲;
所以在 fork 之前 printf(),fprintf()语句的数据均已刷新到显示器上了
- 对于进程来说,如果数据位于缓冲区内,那么该数据属于进程,此时 fork 子进程也会指向该数据
- 但如果该数据已经写入到磁盘文件了,那么数据就不属于进程了,此时 fork 子进程也不在指向该数据了
所以,这里 fork 子进程不会做任何事情,结果和现象1一样
现象4解释
使用重定向指令将本该写入显示器文件的数据写入到磁盘文件中,而磁盘文件采用全缓冲,所以 fork 子进程时 printf(),fprintf()的数据还存在于缓冲区中 (缓冲区没满,同时父进程还没有退出,所以缓冲区没有刷新!);
此时数据还属于父进程,那么 fork 之后子进程也会指向该数据,而 fork 之后紧接着就是进程退出,父子进程某一方先退出时会刷新缓冲区,由于刷新缓冲区会清空缓冲区中的数据,为了保持进程独立性,先退出的一方会发生 写时拷贝,然后向磁盘文件中写入 printf(),fprintf()数据;然后,后退出的一方也会进行缓冲区的刷新;
所以,最终 printf(),fprintf()的数据会写入两份 (父子进程各写入一份),且 write 由于属于系统调用没有缓冲区,所以只写入一份数据且最先写入.
缓冲区的刷新策略
缓冲区在达到一定数量后才会刷新,采取一定的刷新策略;
缓存区刷新策略有三种:
- 立即刷新,无缓冲:缓冲区中一出现数据就立马刷新,IO 效率低,很少使用
- 行刷新,行缓冲:每拷贝一行数据就刷新一次,显示器采用的就是这种刷新策略,因为显示器是给人看了,而按行刷新符合人的阅读习惯,同时IO效率也不会太低
- 缓冲区满,全缓冲:待数据把缓冲区填满后再刷新,这种刷新方式 IO 效率最高
两种特殊情况:
- 用户强制刷新缓冲区;
- 进程退出,进程退出都要进行缓冲区刷新;
用户级缓冲区与OS的关系
C语言文件操作向磁盘文件写入数据的过程是:程序运行,进程通过 fwrite 等函数将数据拷贝到缓冲区中,然后再由缓冲区以某种刷新方式刷新 (写入) 到磁盘文件中;
并不是直接将数据写入到磁盘文件中的(用户级语言级缓冲区在用户部分),这个过程还要经过操作系统处理,然后数据才写入磁盘的文件中;
总结:
- 数据先通过 fwrite 等文件操作接口将进程数据拷贝到语言级的缓冲区里面
- 然后在语言级缓冲区再根据自己的的刷新策略通过 write 系统调用将数据拷贝到 file 结构体中的内核缓冲区中,
- 最后再由操作系统自主将数据(内核级缓冲区也有自己刷新数据的策略)真正的写入到磁盘的文件中,到这一步数据才真正意义上写入磁盘的文件中
实现一个缓存区
用C语言实现一个缓冲区
myStdio.h
myStdio.c
testmyStdio.c
就是main函数