缓冲区1、内核文件缓冲区2、为什么要存在缓冲区3、语言缓冲区1、内核文件缓冲区通过目前对文件的学习我们知道进程打开文件intfdopen(log.txt,O_CREAT|O_WRONLY|O_TRUNC,0666);做的工作是将文件的属性和内容从磁盘中加载到内存中创建file结构体然后文件的内容和属性保存到file中进程task_struct的files_struct中的fd_array找最小的未被使用的下标存储file*指针将这个下标作为open的返回值也就是文件的文件描述符fd。实际上文件内容存入的位置就是内核文件缓冲区。以系统调用write为例说明内核文件缓冲区的功能。用户自己定义了一个缓冲区想要使用系统调用write对文件做写入constchar*msgHello world.\n;write(fd,msg,strlen(msg));write并没有直接写入到磁盘的文件中而是写入到了内核文件缓冲区中。CPU与内存的交互是很快的所以write写入内核文件缓冲区后立即返回。而几乎所有文本内容的传递本质上都是拷贝。所以write本质上是一个拷贝函数write的功能是将用户自己的缓冲区内容拷贝到内核文件缓冲区中。当内核文件缓冲区中的内容达到规定的阈值时(就是快满了)一般操作系统就会访问file中的文件操作表调用写入操作函数将内核文件缓冲区的内容写入到磁盘中。这个动作也叫刷新。2、为什么要存在缓冲区如果没有快递站我们的生活会是什么样子今天张三想把买好的键盘当作生日礼物送给李四而李四住在张三的1000km开外。于是张三必须带着键盘亲自赶路到李四的家里去。路上费时费力效率太低了。过了几天张三楼下开了一个快递站。张三想送键盘的话只需将键盘打包好然后到楼下快递站填写相关信息递交键盘最后就直接上楼干自己的活了。在这个故事中键盘就是输入的内容张三就是write等向文件做写入的系统调用李四就是磁盘文件快递点就是(内核文件)缓冲区如果没有缓冲区系统调用就需要直接访问磁盘而访问磁盘是一件很慢的事有了缓冲区系统调用就可以直接将内容写入到本来就在内存中的内核文件缓冲区交互速度就提高了许多。缓冲区的存在提高了用户使用系统调用的效率避免了用户直接进行IO访问。让我们把视角切换到快递点上面。现在快递点有了一个张三寄给李四的键盘包裹。那么快递点是直接将这一个快递发出吗这样做效率也太低了。快递点每到一个快递就只发这一个快递交通运费可是成倍增长的。快递点采取的策略是累积到一定数量的快递后再统一发出。类似的内核文件缓冲区并不是每存下一个字符就调用驱动程序向磁盘做写入的当内核文件缓冲区的内容积攒到一定限度的时候才会向磁盘做写入这就是缓冲区刷新的总体思路。缓冲区的存在降低了操作系统对磁盘的访问频率真正提高了整机的IO效率。说到缓冲区刷新这一点我们就不得不展示不同情况下缓冲区刷新的具体策略策略名称常规 / 特殊具体策略举例立即刷新常规--行缓冲常规写入一行就写入磁盘显示器文件全缓冲常规写满缓冲区就写入磁盘普通文件强制刷新特殊调用系统调用作强制的刷新-进程退出特殊进程退出OS就会强制刷新缓冲区-小贴士文件的读取read也是系统调用读取内核文件缓冲区的内容。但是内核文件缓冲区加载文件内容并不是全部加载而是按需加载为了提高内存利用率。那么肯定存在“用户使用系统调用读取100字节的内容而内核文件缓冲区只有80字节的内容”的情况发生。那么问题来了操作系统怎么知道用户要读取的内容还没有加载到内存中呢答案是根据读写位置 数据量。loff_t f_pos标记了当前读取到内核文件缓冲区的读写位置。用户想要接着读取的数据大小为数据量如果 数据量 读写位置 超出了当前缓冲区有效内容的长度操作系统就会继续加载文件内容到缓冲区如果没有超出直接读取即可3、语言缓冲区实际上调用系统调用是有性能消耗的。例如C中的vector扩容扩容的本质是申请内存申请内存就一定要调用系统调用void*mmap(void*addr,size_tlength,intprot,intflags,intfd,off_toffset);intbrk(void*addr);调用了系统调用操作系统就只干了“给你内存”一件事吗比如内存严重不足时操作系统就会挂起进程将进程的数据和代码拷贝到swap分区销毁进程在内存中的数据和代码最后将腾出的空间返回给你。所以调用系统调用是有代价的。我们再来看一个有意思的实验。#includestdio.h#includesys/types.h#includeunistd.h#includestring.hintmain(){constchar*msgHello write.\n;write(1,msg,strlen(msg));printf(Hello printf.\n);fprintf(stdout,Hello fprintf.\n);constchar*msg1Hello fputs.\n;fputs(msg1,stdout);fork();return0;}直接运行将结果打印到显示器上看起来一切正常。可是如果将结果重定向输出到log.txt文件中现象很奇怪写入log.txt的内容中Hello write.只写了一次其余的内容却写了两次怎么回事上面实验代码中write与printf fprintf fputs的区别就是系统调用与库函数的关系。系统调用write直接将msg的内容拷贝到内核文件缓冲区而库函数printf fprintf fputs还需要将内容写入FILE* stdout中。那么FILE里肯定有猫腻。事实上FILE结构体里除了有_fileno文件描述符还有一个东西语言缓冲区(输入输出缓冲区)库函数printf fprintf fputs向stdout做写入的时候并不是像write一样直接写入内核文件缓冲区而是先写入语言缓冲区。对于stdout连接的显示器文件(语言)缓冲区刷新的方式是行缓冲读到一行内容就刷新到显示器上。而对于log.txt这样的普通文件刷新方式是全缓冲只有在(语言)缓冲区满了的情况下才会刷新到显示器上。当我们重定向到普通文件log.txt由于普通文件的刷新方式是全缓冲fork创建子进程的时候(语言)缓冲区还没有填满创建了子进程后父子共享同样的代码和数据其中就包括(语言)缓冲区意味着**(语言)缓冲区的内容向内核文件缓冲区写入了两遍**。所以直接写入内核文件缓冲区的write内容只写一遍而其它库函数写入了两遍当我们直接向stdout做写入即向显示器文件做写入由于显示器文件的刷新方式是行缓冲fork创建子进程之前(语言)缓冲区上的内容就已经刷新到了内核文件缓冲区就算fork后父子共享(语言)缓冲区(语言)缓冲区里面也是空的。所以最后看到的结果是正常的四个打印内容值得一提的是当用户想对显示器文件做写入的时候语言层就会判断用户输入是否带\n。只有带上\n才会进行语言缓冲区的刷新这才是行缓冲。语言缓冲区的存在使得用户调用库函数直接向语言缓冲区做写入与读取然后返回无需直接访问操作系统内核当语言缓冲区内容达到阈值系统调用再将语言缓冲区内容写入内核文件缓冲区减少了系统调用的使用。所以语言缓冲区的存在提高了用户使用库函数的效率和整机的IO效率。当然无论是使用库函数还是使用系统调用只要是做向磁盘文件写入的工作只要最终把数据写入到内核中就算最终完成了任务。之后就是操作系统自己的工作了。像这样我们要写入的内容先输入语言缓冲区缓存再输入内核文件缓冲区缓存最后刷新到磁盘文件中。这个过程就像流水一样所以我们常说“文件流”。
阅读完成 · 觉得有帮助?