Linux:缓冲区

上篇文章:拒绝踩坑!Windows下MySQL 8.0核心架构原理解析与纯净安装保姆级指南(附"绿色版"Navicat下载)

目录

1.缓冲区概念

2.缓冲区的意义

3.缓冲类型

4.FILE

5.完成一个简单的libc库


1.缓冲区概念

缓冲区是内存空间的一部分。反过来讲,就是在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留空间就叫做缓冲区。缓冲区根据其对应的是输入设备和输出设备,分为输入缓冲区和输出缓冲区。

2.缓冲区的意义

读写文件时,如果不会开辟对文件操作的缓冲区,直接通过系统调用对磁盘进行操作,那么每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调用,执⾏⼀次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执⾏效率造成很⼤的影响。

为了减少使用系统调用的次数,提⾼效率,我们就可以采⽤缓冲机制。比如我们从磁盘中取信息,可 以在磁盘⽂件进⾏操作时,可以⼀次从⽂件中读出⼤量的数据到缓冲区中,以后对这部分的访问就不 需要再使用系统调用了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作快于对磁盘的操作,故应用缓冲区可大大提⾼计算机的运⾏速度。

可以看出,缓冲区就是块内存区,它在输入输出设备和CPU之间,来缓存数据。它使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占CPU,解放出CPU,使其能够高效率工作。

3.缓冲类型

标准I/O提供了3种类型的缓冲区。

  • 全缓冲区:这种缓冲方式要求填满整个缓冲区才能进行I/O系统调用操作。对于磁盘文件的操作通常使用全缓冲的方式访问。
  • 行缓冲区:在缓冲情况下,当在输入和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。当所操作的流涉及一个终端时(例如标准输入和标准输出),使用行缓冲方式。因为标准I/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行1/0系统调用操作,默认行缓冲区的大小为1024。
  • 无缓冲区:无缓冲区是指标准l/O库不对字符进缓存,直接调系统调。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。

除了上述默认刷新方式,下列特殊情况也会引发缓冲区刷新:

  1. 缓冲区满时
  2. 执行flush语句
  3. 进程结束

示例如下:

我们本使用重定向,让打印在显示器的内容写到log.txt中,但实际上并没有。

这是因为我们将1号描述符重定向到磁盘件后,缓冲区的刷新式成为了全缓冲。我们写的内容并没有填满整个缓冲区,导致并不会将缓冲区的内容刷新到磁盘文件中。可以使用fflush强制刷新下缓冲区。

4.FILE

因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问件都是通过fd访问的。

所以C库当中的FILE结构体内部,必定封装了fd。

cpp 复制代码
  1 #include <stdio.h>  
  2 #include <string.h>  
  3 #include <unistd.h>  
  4 #include <sys/stat.h>  
  5 #include <sys/types.h>  
  6 #include <sys/fcntl.h>  
  7   
  8 int main()  
  9 {  
 10     // C库函数
 11     printf("hello printf\n");
 12     fprintf(stdout, "hello fprintf\n");
 13     const char *msg1 = "hello fputs\n";
 14     fputs(msg1, stdout);
 15 
 16     // 系统调用
 17     const char *msg2 = "hello write\n";
 18     write(1, msg2, strlen(msg2));
 19 
 20     fork();
 21                                                                                                                                 
 22     return 0;                                                 
 23 }

其运行结果,和对进程实现输出重定向后的结果:

可以发现printf和fprintf(库函数)都输出了2次,而write只输出了一次(系统调用)。这与fork有关:

  • 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
  • printf和fprintf 库函数会自带缓冲区(以前案例可以说明:Linux:第一个程序--进度条|区分回车与换行|行缓冲区|进度条代码两个版本|代码测试与优化),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变为了全缓冲。
  • 而放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
  • 进程退出后,会统一刷新,写入文件中
  • 在fork时,父子数据会发生写时拷贝,所以当父进程准备刷新时,子进程也有了同样的一份数据,随机产生两份数据。
  • write没有变化,说明没有所谓的缓冲。

综上:printf fprintf 库函数会自带缓冲区,而write系统调用没有带缓冲区。另外,我们这所说的缓冲区,都是用户级缓冲区(也被叫做"语言缓冲区")。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。

那这个缓冲区谁提供呢? printf fprintf是库函数,write是系统调,库函数在系统调的"上层",是对系统调用的"封装",但是write没有缓冲区,而printf fprintf有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。

如果有兴趣,可以看看FILE结构体:typedef struct _Io_FILE FILE;

在/usr/include/stdio.h中

在/usr/include/libio.h中

cpp 复制代码
246 struct _IO_FILE {
247   int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */
248 #define _IO_file_flags _flags
249 
250   /* The following pointers correspond to the C++ streambuf protocol. */
251   /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
252   char* _IO_read_ptr;   /* Current read pointer */
253   char* _IO_read_end;   /* End of get area. */
254   char* _IO_read_base;  /* Start of putback+get area. */
255   char* _IO_write_base; /* Start of put area. */
256   char* _IO_write_ptr;  /* Current put pointer. */
257   char* _IO_write_end;  /* End of put area. */
258   char* _IO_buf_base;   /* Start of reserve area. */
259   char* _IO_buf_end;    /* End of reserve area. */
260   /* The following fields are used to support backing up and undo. */
261   char *_IO_save_base; /* Pointer to start of non-current get area. */
262   char *_IO_backup_base;  /* Pointer to first valid character of backup area */
263   char *_IO_save_end; /* Pointer to end of non-current get area. */
264 
265   struct _IO_marker *_markers;
266 
267   struct _IO_FILE *_chain;
268 
269   int _fileno;                                                                                                                  
270 #if 0
271   int _blksize;
272 #else
273   int _flags2;
274 #endif
275   _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */
276 
277 #define __HAVE_COLUMN /* temporary */
278   /* 1+column number of pbase(); 0 is unknown. */
279   unsigned short _cur_column;
280   signed char _vtable_offset;
281   char _shortbuf[1];
282 
283   /*  char* _save_gptr;  char* _save_egptr; */
284 
285   _IO_lock_t *_lock;
286 #ifdef _IO_USE_OLD_IO_FILE
287 };

5.完成一个简单的libc库

手写一份stdio,包含输出缓冲区,char 数组模拟,fopen,fclose,fwrite,fflush。

请结合下篇文章食用:

用系统调用从零封装一个C语言标准I/O库 | 附源码

本章完。

相关推荐
我真不是小鱼2 小时前
cpp刷题打卡记录29——矩阵置零 & 旋转图像 & 除了自身以外数组的乘积
数据结构·c++·算法·leetcode·矩阵
网域小星球2 小时前
C++ 从 0 入门(二)|引用与指针区别、函数重载、内联函数(面试高频)
开发语言·c++·面试·函数重载·内联函数·引用与指针区别
希望永不加班2 小时前
SpringBoot 中 AOP 实现接口限流
java·spring boot·后端·spring
董董灿是个攻城狮2 小时前
DeepSeek 开始融资,又打了一手新牌
后端
信创DevOps先锋2 小时前
中国企业DevOps工具链选型指南:本土化与安全可控引领技术决策新趋势
运维·安全·devops
亚空间仓鼠2 小时前
Ansible之Playbook(六):实例部署实战
linux·网络·ansible
小梦爱安全2 小时前
ansible基础配置和ansible模块
运维·自动化·ansible
代码中介商2 小时前
C++ 多态与虚函数入门:从概念到规则
开发语言·c++
Sam_Deep_Thinking2 小时前
Spring Bean作用域的设计与使用
java·后端·spring