> 技术文档 > 【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)

【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)


小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
linux系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
在这里插入图片描述


目录

    • 前言
    • 一、现象——解释
      • 现象一
        • 补充一下关于fwrite和write的返回值的区别
      • 现象二
      • 现象三
      • 现象四
      • 现象五
      • 现象六
    • 二、用户缓冲区
      • 深刻理解+现象五解释
      • 几个问题
        • 缓冲区什么时候刷新
        • 为什么要有这个缓冲区
        • 那么这个用户缓冲区存在于哪里?
      • 现象六解释
      • 现象四解释
    • 总结

前言

【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)——书接上文 详情请点击<——,本文会在上文的基础上进行讲解,所以对上文不了解的读者友友请点击前方的蓝字链接进行学习
本文由小编为大家介绍——【linux】linux基础IO(三)(用户缓冲区概念与深刻理解,模拟实现c语言库的文件操作接口)


一、现象——解释

现象一

  1. 下面小编分别使用四个接口其中三个是c语言文件接口,printf,fprintf,fwrite对应stdout,一个是系统调用write对应1号描述符,向显示器打印一串hello xxx的信息
#include #include #include int main(){ printf(\"hello printf\\n\"); fprintf(stdout, \"hello fprintf\\n\"); const char* str1 = \"hello fwrite\\n\"; const char* str2 = \"hello write\\n\"; fwrite(str1, strlen(str1), 1, stdout); write(1, str2, strlen(str2)); return 0;}

运行结果如下
【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)
上述现象打印的现象我们很好理解

补充一下关于fwrite和write的返回值的区别
  1. 下面小编补充一下关于fwrite和write的返回值的区别
    【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)
  2. fwrite返回的是实际写入块空间的个数,即nmemb
    【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)
  3. write返回的是实际写入到文件描述符的字节个数
#include #include #include int main(){ const char* str1 = \"hello fwrite\\n\"; const char* str2 = \"hello write\\n\"; size_t ret1 = fwrite(str1, strlen(str1), 1, stdout); ssize_t ret2 = write(1, str2, strlen(str2)); printf(\"fwrite ret: %d\\n\", ret1); printf(\"write ret: %d\\n\", ret2); return 0;}

运行结果如下
【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)

现象二

  1. 和现象一同样的代码,只不过这里小编进行了重定向操作到文件log.txt中
#include #include #include int main(){ printf(\"hello printf\\n\"); fprintf(stdout, \"hello fprintf\\n\"); const char* str1 = \"hello fwrite\\n\"; const char* str2 = \"hello write\\n\"; fwrite(str1, strlen(str1), 1, stdout); write(1, str2, strlen(str2)); return 0;}

运行结果如下
【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)
同样的,重定向操作到log.txt文件中,./myfile中原本是向显示器中打印,但是经过小编的重定向操作到log.txt中之后,就不再向显示器中打印了,相关的内容就被重定向到了log.txt的文件中,我们同样也很好理解(这里小编仅仅是粗略的进行了讲解,更多关于文件重定向的讲解详情请点击<——)

现象三

  1. 那么接下来小编使用fork创建子进程,观察现象
#include #include #include int main(){ printf(\"hello printf\\n\"); fprintf(stdout, \"hello fprintf\\n\"); const char* str1 = \"hello fwrite\\n\"; const char* str2 = \"hello write\\n\"; fwrite(str1, strlen(str1), 1, stdout); write(1, str2, strlen(str2)); fork(); return 0;}

运行结果如下
【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)

  1. 由于fork是在四个接口其中三个是c语言文件接口,printf,fprintf,fwrite对应stdout,一个是系统调用write对应1号描述符,向显示器打印一串hello xxx的信息之后,那么此时fork创建的子进程不会做任何事情,同样的这一点我们也很好理解,因为子进程被创建出来之后是在父进程的执行流的位置继续向后执行,由于父进程已经执行了四个接口函数向显示器打印信息,并且父进程的执行流是fork,所以子进程只会在fork返回值碰到return就结束了
  2. 这时有很多读者友友心里有疑惑,小编小编感觉你演示的这些现象和本文要讲解的用户缓冲区有什么联系呢?况且上面的这些现象都很好理解,又能说明什么呢?稍安勿躁,小编相信下面的现象四会给大家带来一定的小惊奇

现象四

  1. 和现象三同样的代码,只不过这里小编进行了重定向操作到文件log.txt中
#include #include #include int main(){ printf(\"hello printf\\n\"); fprintf(stdout, \"hello fprintf\\n\"); const char* str1 = \"hello fwrite\\n\"; const char* str2 = \"hello write\\n\"; fwrite(str1, strlen(str1), 1, stdout); write(1, str2, strlen(str2)); fork(); return 0;}

运行结果如下
【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)

  1. ???什么,为什么会出现这样的现象,我仅仅是在fork的基础上将其重定向到文件中,文件中就会出现7行信息,观察一下现象
  2. 其中c语言的接口的函数输出的信息有六条,分别是printf,fprintf,fwrite各打印两条,但是离奇的是系统调用的接口函数输出的信息只有一条,为什么会这样
  3. 明明是write最后进行打印到显示器上,可以为什么经过重定向之后,在文件中是write对应的信息却是最先被放到了文件中
  4. 并且我们对比一下现象二和现象四的代码与重定向结果,现象二的代码中是没有fork的,现象四的代码中是有fork的,现象四和现象二的重定向结果不同,那么此时我们可以推测一定与fork有莫大的关系
  5. 下面小编将会对文件缓冲区进行详细的讲解,但是在正式讲解前,小编还是要介绍两种现象

现象五

  1. 仅仅对c语言的三个接口printf,fprintf,fwrite进行信息打印到显示器上的操作,小编使用文件系统调用close将显示器文件对应的文件描述符1进行关闭,此时所有的信息后面均有换行\\n,观察现象
#include #include #include int main(){ printf(\"hello printf\\n\"); fprintf(stdout, \"hello fprintf\\n\"); const char* str1 = \"hello fwrite\\n\"; fwrite(str1, strlen(str1), 1, stdout); close(1); return 0;}

运行结果如下
【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)
此时尽管我们添加了close(1),此时信息可以打印,很好理解

  1. 但是如果小编将信息中的换行\\n去掉呢?此时信息还可以打印吗?
#include #include #include int main(){ printf(\"hello printf\"); fprintf(stdout, \"hello fprintf\"); const char* str1 = \"hello fwrite\"; fwrite(str1, strlen(str1), 1, stdout); close(1); return 0;}

运行结果如下
【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)
此时信息一个却都没有打印到屏幕上,什么原因呢?

  1. 下面小编给一个粗浅的解释,在进度条小程序中详情请点击<——,其中的知识铺垫中,小编讲到了\\n可以刷新缓冲区,此时c语言的c语言的三个接口printf,fprintf,fwrite打印信息会将信息放到缓冲区中,等待合适的时机将信息刷新出来,这个合适的时机的其中之一就是\\n,所以c语言的三个接口printf,fprintf,fwrite会将信息打印出来,而此时这里没有\\n,并且在进程结束前小编还调用的close将显示器文件对应的文件描述符1进行了关闭,所以信息自然也不会被刷新出来,所以自然也就不会进行打印,这里仅仅是停留在粗浅的解释,后面小编还会进行详细的讲解

现象六

  1. 对文件系统调用write将进行信息打印到显示器上,同时小编使用文件系统调用close将显示器文件对应的文件描述符1进行关闭,此时所有的信息后面均有换行\\n,观察现象
#include #include #include int main(){ const char* str2 = \"hello write\\n\"; write(1, str2, strlen(str2)); close(1); return 0;}

运行结果如下
【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)
此时信息可以正常被打印,这很好理解

  1. 但是如果小编将信息中的换行\\n去掉呢?此时信息还可以打印吗?
#include #include #include int main(){ const char* str2 = \"hello write\"; write(1, str2, strlen(str2)); close(1); return 0;}

运行结果如下
【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)
此时尽管小编去掉了换行\\n,使用了close将显示器文件对应的文件描述符1关闭,但是此时的文件系统调用仍然可以打印出信息,又该如何理解,又该怎么解释?

下面小编跳出讲解的这六个现象,讲解一下用户缓冲区与内核缓冲区的原理之后,读者友友就可以很好的将上述的六种现象逐个很好的理解了

二、用户缓冲区

深刻理解+现象五解释

【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)

  1. 关于在现象五中,使用c语言的接口函数printf/fprintf/fwrite,输出的数据被首先放到了缓冲区中,当不加换行\\n的时候,数据没有从缓冲区中刷新出来,小编提到了缓冲区,那么这个缓冲区究竟在哪里?首先我们可以肯定一点,上文提到的缓冲区一定不在内核中,因为现代操作系统不会做任何一件浪费时间和空间的事情,如果现象五中提到的缓冲区在内核中,那么当进程结束的时候,此时如果内核的缓冲区中有数据,那么将会占用空间,此时内核一定会将缓冲区中的数据刷新出来释放空间,但是现象五的结果演示中数据并没有刷新出来,所以缓冲区一定不在内核中
  2. 所以这个缓冲区一定不在操作系统内部,这个缓冲区不是系统级别的缓冲区,c语言会为我们提供一个缓冲区,我们将这个缓冲区称为用户缓冲区,同时内核也会有缓冲区叫做内核缓冲区,目前我们认为只要将数据刷新到内核,数据就可以到硬件了
  3. 对于现象五中,所以当我们调用c语言的接口函数printf/fprintf/fwrite,输出的数据被首先放到了用户缓冲区,当遇到合适的时机数据就会被刷新,合适的时机包括换行\\n,此时现象五中没有使用换行\\n,所以此时数据就会被放到用户缓冲区不会被刷新,同时当我们调用c语言的接口函数printf/fprintf/fwrite将数据写入到缓冲区之后,紧接着我们close关闭文件描述符1,由于文件描述符1对应的是显示器文件,此时显示器文件对应的文件打开对象就会被close释放掉,并且将文件描述符1对应到文件描述符表中的下标位置上的内容置空,所以当进程结束的时候,我们知道进程结束的时候也要刷新用户缓冲区,但是这时候由于printf/fprintf/fwrite是要向文件描述符1中进行写入,但是此时文件描述符1已经被我们close关闭了,找不到文件描述符1对应的文件打开对象,所以此时用户缓冲区的内容也就不会向内核缓冲区中刷新,所以此时自然而然也就不会向显示器输出数据信息了,所以此时针对现象五中没有换行\\n不向显示器打印数据,我们就理解了
  4. 但是对于现象五中有换行\\0向显示器打印数据,其实显示器的文件的书刷新方案是行刷新,所以在调用c语言的接口函数printf/fprintf/fwrite的时候,当遇到换行\\n,此时就会立即将数据刷新到内核缓冲区中,此时数据就可以到硬件上了,但是如何刷新?用户刷新的本质其实就是将数据通过未关闭的文件描述符+write将数据写入内核中

几个问题

缓冲区什么时候刷新

【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)

  1. 无缓冲——直接刷新,例如c语言的fflush,同时我们也可以推测,这个fflush中必定封装了系统调用write才可以直接将用户缓冲区的数据刷新到内核缓冲区中
  2. 行缓冲——不刷新,直到碰到\\n,例如显示器,因为显示器是给用户看的,用户在阅读的时候通常是以行为单位,所以为了满足用户的视觉阅读需求不得不这样做
  3. 全缓冲——用户缓冲区满了才刷新,例如向文件中写入,这类文件通常是指普通文件
  4. 同时进程退出的时候——用户缓冲区默认也会被刷新到内核缓冲区中
为什么要有这个缓冲区
  1. 解决效率问题——具体指的是解决用户效率问题,如果我们使用c语言的文件接口函数一有数据就调用底层封装的write,并且找到未关闭的文件描述符对应的文件打开对象,将数据刷新到内核缓冲区,这个过程中要产生消耗,有内核缓冲区在刷新到文件打开对象对应的硬件上,但是如果调用了100次函数,有100份数据呢?那么我们就要使用100次未打开的文件描述符+write,1000份呢?效率更低下了,可是如果我们将这么多的数据堆积在用户缓冲区中,仅需要调用一次未打开的文件描述符+write就可以一次性将数据全部写入到内核缓冲区中,并且操作系统会将数据从内核缓冲区将数据写入到对应到对应的硬件中,这样效率就被大大的提高,所以这也就是我们常说的使用语言效率更快的原因之一
  2. 配合格式化——思考一下,我们电脑的显示器上能显示的是否是整形,指针等的中类型,其实不能,显示器能显示的只有一个一个的字符,例如int a = 10; printf(“%d\\n”, a); 这个语句printf要先向用户缓冲区写入,写入到缓冲区后由缓冲区将其转换字符,即字节流,并且int a = 0,; scanf(“%d”, &a); 我们使用键盘输入100的时候,这个100起初并不是整数,而是一个一个的字符,缓冲区要承担将这些字符转换成整形,将整形输入到a中的任务,因此诸如printf,scanf也叫做格式化输入输出函数,所以缓冲区的存在可以配合格式化
那么这个用户缓冲区存在于哪里?
  1. c语言的文件操作离不开FILE,这个FILE的本质其实就是结构体,这个结构体中有封装的文件描述符fd,同时这个FILE中还有对应打开文件的缓冲区字段和维护信息
  2. 这个FILE对象是属于用户呢?还是属于操作系统呢?这个缓冲区是否属于用户级的缓冲区呢?
  3. 只要是语言级别的都属于用户层,FILE对象是在c语言层次,所以FILE对象属于用户,这个缓冲区属于用户级的缓冲区
  4. FILE所以既然属于用户,并且FILE中还有对应的缓冲区字段,而c语言的文件操作离不开FILE,其实这个用户缓冲区就存在于FILE中
    【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)
  5. 那么此时我们看一下c语言打开文件使用的文件接口函数fopen,返回的FILE其实就是语言层给我们malloc(FILE),所以这个用户缓冲区存在的位置更详细点讲是存在于堆上,FILE中会存储有对应的指针,存储文件缓冲区对应的堆空间的地址,所以通过FILE自然可以进行文件操作,c语言文件操作函数就可以借助FILE找到指向文件缓冲区对应堆空间的地址,此时就可以向缓冲区中写入数据了
  6. 此时我们再看一下fprintf(stdout, “hello fprintf\\n”); 此时小编再来解释一下,其实将hello fprintf\\n 写入到文件流FILE* stdout中对应的用户缓冲区对应的堆空间上,此时stdout对应的文件描述符1是显示器文件,显示器文件的刷新策略是行刷新,此时hello fprintf\\n 的结尾有换行\\n,所以遇到了换行\\n,此时就会调用底层的write以及找到文件描述符1,向内核缓冲区中写入数据,当写入到内核缓冲区中之后,此时数据就会被写入到硬件中了,但是并不是即刻刷新,内核关于数据有自己的刷新策略,这里为了便于理解,我们目前认为只要将数据写入到内核缓冲区后,数据就会被刷新到硬件上

现象六解释

  1. 在现象六中为什么系统调用write打印到显示器上,虽然显示器文件是行刷新,并且打印信息中没有换行\\n,并且使用close在执行完write之后将显示器文件对应的文件描述符1关闭,但是write仍然可以将信息打印到显示器上?
  2. 因为write是系统调用,系统调用属于操作系统为用户提供的接口,系统调用不属于用户层,它的操作范围就在内核里面,所以系统调用write在得知信息,以及文件描述符1之后,可以直接将信息写入内核缓冲区中,当进程结束或者遇到close显示器文件对应的文件描述符1的时候,由于操作系统不会做任何浪费时间和空间的事情,所以不论是进程结束或者是close对应的文件描述符,操作系统都会将内核缓冲区的信息通过文件描述符1找到对应的文件打开对象struct file,将信息写入到文件打开对象对应的硬件上,所以尽管打印信息中没有换行\\n,并且使用close在执行完write之后将显示器文件对应的文件描述符1关闭,但是write仍然可以将信息打印到显示器上

现象四解释

【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)

  1. 首先明确一点,对于用户缓冲区的刷新策略,其中一条的刷新策略是对于文件,即向普通文件中输入的时候,采用的方式不是行缓冲,而是全缓冲,即尽管信息中有换行\\n,但是碰到了换行\\n仍然不刷新,而是等待用户缓冲区满了才刷新,值得注意的是,当用户缓冲区不满,但是此时进程结束了,此时也会刷新用户缓冲区
  2. 那么虽然在代码中我们是向显示器文件中写入,但是我们进行了重定向操作,所以就是要想向普通文件中进行写入,此时的刷新策略是全缓冲,所以我们就可以编写下面代码进行测试了,同时使用脚本命令进行检测我们重定向到的文件
//脚本命令while :; do cat log.txt; sleep 1; echo \"-------------------\"; done
#include #include #include int main(){ printf(\"hello printf\\n\"); sleep(1); fprintf(stdout, \"hello fprintf\\n\"); sleep(1); const char* str1 = \"hello fwrite\\n\"; const char* str2 = \"hello write\\n\"; fwrite(str1, strlen(str1), 1, stdout); sleep(1); write(1, str2, strlen(str2)); sleep(2); fork(); return 0;}

运行结果如下
【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)

  1. 为什么write对应的信息出现在第一位?因为write是系统调用,系统调用可以直接向内核缓冲区写入,系统调用并不是向用户缓冲区进行写入,所以write并不和c语言对用的接口printf/fprintf/fwrite一样采用的策略是全缓冲,内核缓冲区采用自己的刷新策略,当向内核缓冲区写入之后我们认为写入到内核缓冲区的信息就可以到文件描述符1对应的文件打开对象对应的硬件上了,所以此时的log.txt文件中的信息第一条出现的是write对应的信息
  2. 并且c语言的文件接口函数printf/fprintf/fwrite输出的信息中尽管有换行,但是由于是向普通文件中写入,用户缓冲区采用的策略是全缓冲,尽管碰到换行不刷新用户缓冲区,而是用户缓冲区满了才刷新,值得注意的是当进程结束也会刷新用户缓冲区,所以此时所以c系列函数输出的信息中尽管有换行\\n也不会刷新到内核缓冲区中,而是被写入到了用户层,即用户缓冲区中,当遇到合适的时机再将用户缓冲区刷新出来,我们fork了子进程,子进程的代码和数据和父进程共用一份,此时子进程的执行流和父进程一样,处于fork返回
  3. fork返回之后,父进程和子进程会直接遇到return 0语句,关于究竟是父进程还是子进程哪一个先遇到return 0语句,这一点我们不知道,谁知道?调度器知道,因为父进程和子进程的优先级相同,谁先被调用器放到运行队列中,谁就先执行return 0语句,父进程和子进程最终都是要被调度执行的
  4. 所以这里我们完全可以假设父进程先被调度,先执行return 0语句,那么执行完return 0语句之后,进程结束,此时要刷新用户缓冲区的数据,用户缓冲区的是存在于FILE对象中,子进程和父进程共用同一份代码和数据,当父进程或子进程对数据修改的时候,此时先修改的发生写时拷贝,在写时拷贝对应的区域完成数据的修改,原空间则属于另一个进程,这样关于数据,父进程和子进程就各自私有了一份,所以所以所以,我父进程要对用户缓冲区进行刷新,那么就是要将用户缓冲区的数据写入到内核缓冲区中,并且写入完成之后要对用户缓冲区的数据做清空处理,这个清空是不是就是要对数据做修改,清空也是修改呀,用户缓冲区对应的数据存在于FILE对象中的指针指向的堆空间的上,所以就会将FILE对象给子进程也拷贝一份,但是对于指针指向的文件缓冲区对应的堆空间发生了变换
  5. 所以此时父进程和子进程各自有一份FILE对象,其中的指针指向的文件缓冲区的数据是相同的,但是在堆空间的区域不同,这样就可以保证进程的独立性,所以此时父进程就可以对FILE对象中的指针指向的用户缓冲区的数据做刷新,清除任务了,当刷新完成之后,子进程也会遇到return 0语句,也会对属于它自己的FILE对象中的指针指向的用户缓冲区的数据做刷新,清除任务,所以父进程刷新自己的用户缓冲区一遍,子进程刷新自己的用户缓冲区一遍,所以此时关于c语言文件接口printf/fprintf/fwrite对应的数据就会在log.txt中有两份
  6. 虽然父进程和子进程先后的对属于自己的FILE对象中的指针指向的用户缓冲区的数据做刷新,清除任务,但是由于CPU的运行速度很快,达到纳秒级别,而我们观察检测的频率是一秒观察检测一次,所以我们观察检测的出来的是一瞬间c语言文件接口printf/fprintf/fwrite对应的数据会有两份同时写入到log.txt文件中(过程是c语言文件接口printf/fprintf/fwrite对应的数据先被放入用户缓冲区,之后通过文件描述符1(此时文件描述符1上对应文件描述符表存储的文件打开对象已经被替换为了log.txt的文件打开对象的地址)+write的方式刷新到内核缓冲区中,操作系统再通过文件描述符1找到对应的文件打开对象struct file,通过文件打开对象写入到对应的硬件中,这里指的是磁盘中的文件log.txt)
  7. 我们可以看出关于现象四的解释是包含了众多知识的铺垫,仅仅是一个现象,我们需要使用语言级别+操作系统的知识一起进行解释,单单的语言或者操作系统的知识是解释不通的,只有将两个对应上一起进行解释才可以解释的通现象四,可以说小编前面做的所有的知识铺垫以及讲解都是为了解释这个重头戏——现象四
    【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)
  8. 此时还有一个疑问,那么就是关于现象三中同样有fork,但是我们不进行重定向,那么为什么c语言文件接口printf/fprintf/fwrite对应的数据只打印一份,而不是两份呢?
  9. 因为当小编不进行重定向的时候,此时是向显示器文件中进行输出,此时用户缓冲区的刷新策略是行刷新,此时信息中同样有换行\\n,那么c语言文件接口printf/fprintf/fwrite就会直接通过底层的封装的write+文件描述符1将数据直接写入内核缓冲区中,由操作系统将通过文件描述符1找到对应文件描述符表对应的文件打开对象的地址,通过文件打开对象找到对应的硬件文件(显示器文件)进行写入,所以此时数据就可以到对应的硬件上了,每一个数据中都有换行\\n,都会进行刷新,同样的write是直接向内核缓冲区数据中进行写入,数据同样也会到对应的硬件上(显示器文件),那么等到fork子进程之后,用户缓冲区的数据都已经全部被刷新了,用户缓冲区已经没有数据了,所以父进程或子进程也就不会对用户缓冲区进行修改,所以也就会不发生写时拷贝,所以也c语言文件接口printf/fprintf/fwrite对应的数据也就只会打印一份了

总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!