目录
为什么close(fd1)之后还能向log.txt写入数据?
前言
在【Linux】进程IO|系统调用|文件描述符fd|封装|理解一切皆文件-优快云博客
中讲述了文件描述符,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函数