
C/C缓冲区的概念及应用缓冲区概念观察现象缓冲区的概念缓冲区刷新策略回车\r和回车换行\nC语言的缓冲区文件缓冲区缓冲区特性的应用进度条的实现简易进度条的实现工程实践版本\r被解释成回车加换行的情况缓冲区概念观察现象首先是执行以下 3 个 C 程序带来的现象带sleep函数的程序 Linux 的需要展开unistd.hC 程序的printf输出的字符串带\n消息会立马刷新且命令提示符[用户名主机名 目录名]$会出现在下一行。例如这个测试程序#includestdio.h#includeunistd.hintmain(){printf(Can you see me?\n);sleep(2);return0;}交互界面[BjarneVM-8-8-centos cppTest]$makegcc a.c-oa.exe[BjarneVM-8-8-centos cppTest]$ ./a.exe Can you see me?# 这里无法体现现象需要自行测试[BjarneVM-8-8-centos cppTest]$这里会先输出语句 “Can you see me?” 再暂停 2 秒之后再输出下一条命令提示符[BjarneVM-8-8-centos cppTest]文本和图片均无法展示过程还可以使用 gif但这里没做可自行测试。而不带\n则会先暂停一下再刷新消息且命令提示符会紧随其后。这里再次修改程序intmain(){printf(Can you see me?);sleep(2);return0;}交互界面[BjarneVM-8-8-centos cppTest]$makegcc a.c-oa.exe[BjarneVM-8-8-centos cppTest]$ ./a.exe Can you see me?[BjarneVM-8-8-centos cppTest]$# 这里会先暂停2秒再输出下一条命令提示符文本无法展示过程可以看到不带\n和带\n有明显差别。若带fflush函数对stdout进行强制刷新则会先显示printf的字符串再睡眠。参考man 3 fflush#includestdio.hintfflush(FILE*stream);对于输出流stdiofflush通过该流的底层写函数强制将给定输出或更新流的所有用户空间缓冲数据执行一次写入即刷新流。对于输入流stdinfflush丢弃所有已从底层文件获取但尚未被应用程序消费的缓冲数据。流的打开状态不受影响。如果参数stream为NULLfflush将刷新所有已打开的输出流。\n也是一种刷新流的策略行刷新。这里再次修改程序#includestdio.h#includeunistd.hintmain(){printf(Can you see me?);fflush(stdout);sleep(2);return0;}交互界面[BjarneVM-8-8-centos cppTest]$makegcc a.c-oa.exe[BjarneVM-8-8-centos cppTest]$ ./a.exe Can you see me?[BjarneVM-8-8-centos cppTest]$# 这里会先输出语句再暂停2秒再输出下一条命令提示符文本无法展示过程对 3 个现象进行分析C 语言的函数内的语句是从上往下执行。所以不存在先执行sleep再执行printf的情况。但不带\n时会看到sleep先运行的假象这种情况只能是printf早已经运行但字符串没有被显示出来。所以到这里只会 C 语言已经没法解释需要站在操作系统的视角理解。缓冲区的概念根据之前的学习可知缓冲区的本质就是进程管理的一块内存空间用于临时存储数据。之前的现象就是printf的数据先拷贝到某个缓冲区里然后根据是否有\n来决定算是先清空缓冲区还是先sleep或者使用fflush提前刷新缓冲区。理解为什么要有缓冲区这里举一个生活中常见的案例。假设甲在江苏读大学乙在浙江读大学甲和乙是一对好哥们。某天甲在自己城市的电脑商城买了一个仅在自己城市的电脑城出售的键盘数据觉得很好用于是想让好友乙也使用于是带着键盘骑自行车从江苏一路跑到浙江将键盘送给乙。这种甲亲自行动将键盘数据带给乙对甲来说效率是非常低下的。所以甲可以讲键盘委托菜鸟驿站让菜鸟驿站的工作人员将键盘给乙送去。尽管可能这个菜鸟驿站不靠谱同样是要骑自行车送键盘甚至很可能还要等仓库堆满了再送快递刷新策略但对甲来说甲只耗费了委托菜鸟驿站的时间极大提高了甲的效率。除此之外甲可能会觉得菜鸟驿站送的太慢于是催促菜鸟驿站的工作人员fflush让他们送快点。于是工作人员也不骑自行车摸鱼了直接改乘高铁跑过去。所以缓冲区扮演的就是菜鸟驿站的角色。进程想要通过printf将字符串数据输出到显示屏可以先将字符串数据保存到缓冲区缓冲区满了之后这里没有填满而是sleep加进程阻塞再统一将数据给显示屏传输过去。对 C/C 语言会针对标准输出stdoutLinux 会给程序提供默认的缓冲区。在sleep期间字符串在输出缓冲区中。C/C 程序在运行时会默认打开三个文件流stdin、stdout、stderr。其中stdout就是输出缓冲区标准输出流。同理stdin也有输入缓冲区标准输入流输入的数据中也包括\n在算法竞赛中需要对这个\n进行处理才能正确读取数据。缓冲区刷新策略通俗地讲数据在缓冲区堆积了多少进程才将数据拷贝出去并清理缓冲区这个模式叫刷新策略。C/C 程序常见的刷新策略有 3 种全缓冲。即等缓冲区满了再输出菜鸟驿站货物堆满了再送快递或检测到fflush的使用用户催发货或程序正常结束例如菜鸟驿站倒闭了为防止用户上门找茬将仅剩的快递送完再散伙。行缓冲。检测到\n时再输出。其他情况例如缓冲区满、强制刷新、程序结束时可能也会触发行缓冲。无缓冲。立即输出无需缓冲区。因为内存和外部设备显示屏的交流速度很慢指几毫秒和 CPU 的纳秒级别的数据处理无法比较所以全缓冲、行缓冲一定程度上提高了整体的效率否则每输出一个字符都要访问显示屏传输效率会特别低下。回车\r和回车换行\nC 语言的转义运算符\n即回车换行其实是做了 2 件事换到下一行和将光标放在下一行的最开头。老式回车键是想通过形式来告诉用户回车的原理。C 语言的这 2 个转义字符\r是只回车\n是 ”回车 换行“。验证只回车\r而设计的倒计时程序#includestdio.h#includeunistd.hintmain(){intcnt9;while(cnt){printf(%d\r,cnt);//输出完成后光标会回到开头fflush(stdout);//没有fflush不会输出cnt--;sleep(1);}return0;}第7行的printf数据暂时被缓冲起来直到最后一直没显示且光标回到开头将数据清理掉或干脆就不输出。解决方法是加fflush刷新stdout。如此便完成了一个倒计时程序。这些 C 程序向显示器打印的并不是数字而是字符。将各种数据转换成字符并输出是printf的工作所以printf是格式化控制函数。这就意味着若之前的程序的cnt初始化为10则会输出{90,80,...,10}。这是倒计时程序不希望出现的 bug所以可以用%-2d进行格式控制-会让输出的字符进行左对齐不加-则是右对齐。能显示 2 位数倒计时的程序如下#includestdio.h#includeunistd.hintmain(){intcnt10;while(cnt){printf(%-2d\r,cnt);//输出倒计时fflush(stdout);cnt--;sleep(1);}return0;}所以将显示器称为字符设备因为它显示的只是字符不是整型也不是浮点数。从键盘中得到的数据同理也是字符哪怕是数字这种读取方式是格式化输入键盘也被称之为字符设备。c程序在进行打印时是要将数据打印到显示器上直接刷新到显示器上时每次刷新本质都是访问外设的过程效率太低所以c语言先暂时将字符串存储在缓冲区。类比的话是快递小哥一次将所有的快递给小哥小哥一次将所有快递递交给用户。强制刷新输出流相当于是给小哥压力让小哥及时反馈信息。C语言的缓冲区这里运行一个程序来进行一个测试。#includestdio.h#includestring.h#includeunistd.hintmain(){printf(C: hello printf\n);fprintf(stdout,C: hello fprintf\n);fputs(C: hello fputs\n,stdout);fwrite(C: hello fwrite\n,16,1,stdout);constchar*strsystem call: hello write\n;write(1,str,strlen(str));fork();return0;}交互界面[BjarneVM-8-8-centos cTest]$makegcc a.c-oa.exe-stdc99[BjarneVM-8-8-centos cTest]$ ./a.exe C: helloprintfC: hello fprintf C: hello fputs C: hello fwrite system call: hellowrite[BjarneVM-8-8-centos cTest]$ ./a.exet.txt[BjarneVM-8-8-centos cTest]$catt.txt system call: hellowrite# 系统调用最先输出C: helloprintfC: hello fprintf C: hello fputs C: hello fwrite C: helloprintf# 从这里开始凡是C语言封装的函数都输出了第二次C: hello fprintf C: hello fputs C: hello fwrite[BjarneVM-8-8-centos cTest]$现象解释一般C库函数写入文件时是全缓冲的而使用系统调用write写入显示器是行缓冲。printf、fprintf、fputs、fwrite这些库函数会自带缓冲区当发生重定向到普通文件时数据的缓冲方式由行缓冲变成了全缓冲因为对磁盘上的文件写入时默认是全缓冲。此时放在缓冲区中的数据就不会被立即刷新指将缓冲区数据立马送走并将缓冲区清空。fork创造进程待进程退出之后会统一刷新写入文件当中。但是fork调用时父子进程的数据会发生写时拷贝指两个进程指向同一缓冲区哪怕是拷贝一份完全一样的缓冲区也不影响测试结果所以当两个进程结束时两个进程各自将自己的缓冲区刷新父、子进程谁先结束没有影响。write没有变化说明没有所谓的缓冲而是直接输出到文件在Linux显示屏也被看成文件。综上printf、fwrite等 C 语言的库函数会自带缓冲区而write系统调用没有带缓冲区。这里所说的缓冲区都是用户级缓冲区即提供给用户使用的缓冲区。为了提升整机性能OS也会提供相关内核级缓冲区这里暂时不讨论。printf、fwrite是库函数write是系统调用库函数在系统调用的 “上层” 是对系统调用的“封装”。但是write没有缓冲区而printf、fwrite有所以缓冲区是二次加上的又因为是 C 语言所以这个 “用户级缓冲区” 由 C 标准库提供。例如printf的大致内容为了处理可变参数、错误处理和线程安全肯定会更复杂intprintf(constchar*str,...){//声明一段缓冲区一般是 char buffer[SIZE]; SIZE为某个整数的标识符//因为缓冲区本质是暂时存放一段数据然后根据刷新策略将数据弄走//所以只要能在进程地址空间上表示的连续空间都可以作为缓冲区charbuffer[SIZE];//这里处理各种%c、%d等等进行格式化while(*str!\0){if(*str%){//格式化处理例如将int型数据转换成char型//将识别出来的数据拷贝到buffer}}//将处理好的buffer通过系统调用打印到显示屏Linux是writeWindows和其他操作系统可能是别的intlenstrlen(buffer);write(1,buffer,len);returnlen;//打印输出成功的字符数}日常用的最多啊的是 C/C 提供的用户级别的缓冲区也可以叫做语言级别的缓冲区后者是前者的具体实现方式。文件缓冲区FILE结构体typedef struct _IO_FILE FILE;在/usr/include/stdio.h可看到。[BjarneVM-8-8-centos cTest]$grep-ntypedef struct _IO_FILE FILE/usr/include/stdio.h48:typedef struct _IO_FILE FILE;# -n指定出现在第几行[BjarneVM-8-8-centos cTest]$grep-Rnstruct _IO_FILE {/usr/include/*# -R表示递归查找/usr/include/libio.h:246:struct _IO_FILE{[BjarneVM-8-8-centos cTest]$head-287/usr/include/libio.h|tail-42# 通过hean、tail和管道查看指定部分通过head -287 /usr/include/libio.h | tail -42在/usr/include/libio.h可以看到的信息大概是这样struct_IO_FILE{int_flags;/* High-order word is _IO_MAGIC; rest is flags. */#define_IO_file_flags_flags//缓冲区相关的管理指针/* The following pointers correspond to the C streambuf protocol. *//* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */char*_IO_read_ptr;/* Current read pointer */char*_IO_read_end;/* End of get area. */char*_IO_read_base;/* Start of putbackget area. */char*_IO_write_base;/* Start of put area. */char*_IO_write_ptr;/* Current put pointer. */char*_IO_write_end;/* End of put area. */char*_IO_buf_base;/* Start of reserve area. */char*_IO_buf_end;/* End of reserve area. *//* The following fields are used to support backing up and undo. */char*_IO_save_base;/* Pointer to start of non-current get area. */char*_IO_backup_base;/* Pointer to first valid character of backup area */char*_IO_save_end;/* Pointer to end of non-current get area. */struct_IO_marker*_markers;struct_IO_FILE*_chain;//管理方式还是通过链表进行管理int_fileno;//封装的文件描述符// 省略部分信息};因此进程打开的文件除了具体的文件 inode、文件的打开方法集还有文件指定的缓冲区。缓冲区特性的应用进度条的实现进度条的应用通常是在下载、登录时使用。进度条样式效果示例[### ][xx%][\][][xx%][\]这里通过在第一个[]内填充符号的方式实现进度条通过修改第 2 个[]内的数字和第 3 个[]内的字符让用户 ”感受到进度条确实是在运行“ 。所以进度条理论上只会降低程序运行的效率一般只有在特别耗时的情况才会通过进度条随时监视进度。但很多情况为了 “安抚”用户的情绪不得不设计。简易进度条的实现这里的简易进度条要求限制一段范围一般是100 个字符可自定义。在这段范围中推进自定义的符号可以是#也可以是还可以个性化例如^v^。在末尾携带进度百分比。给出旋转光标给用户反馈进度条一直在推进。进度条样式效果[###### ][xx%][\][][xx%][\][^v^^v^][xx%][\]实现这个样式可以用printf([%s][%d\%][%c]\r,...);输出因为是\r所以printf的下一句需要是fflush(stdout)否则进程不会进行行刷新输出的数据一直在缓冲区不会打印到显示屏。之后就是对进度条的进度操作一般是下载的数据占整体的比例若是图形化界面会有更直观的表示。sleep在 Linux 中是以秒为单位这对进度条来说太慢需要使用函数usleep。参考man 3 usleep#includeunistd.hintusleep(useconds_tusec);usleep是以微秒为单位让程序挂起或者说睡眠一段时间1000微秒等于1毫秒。这里提供三个版本的进度条简单原理版本。实际工程实践版本。C 语言扩展–带颜色。简单原理版本不进行封装和声明、定义分离仅供参考#includestdio.h#includeunistd.h#includestring.hconstchar*rotat|/-\\;//进度条旋转表示程序正常推进intmain(){intrate0;//进度百分比charbar[101]{\0};//进度条本体intnumstrlen(rotat);while(rate100){//%-100s 是为了打印长度为100的进度条-是左对齐。printf([%-100s][%d%%][%c]\r,bar,rate,rotat[rate%num]);fflush(stdout);usleep(1000*50);//50毫秒为单位可修改bar[rate]#;}printf(\n);return0;}工程实践版本任何进度条一定是和某种任务关联。例如下载、压缩、解压、删除文件等。所以整理进度条函数的逻辑给进度rate进度条函数会根据进度rate刷新进度条之后在调用进度条函数的地方进行任务记录、任务进行。先执行任务时再打印进度条时循环可设置进度为100 % 100\%100%时停止。可能会出现任务暂停在某个进度的情况例如网络卡顿这种情况可以通过操控进度的值让最后的光标进行旋转“迷惑”用户。这里采用 101 个长度的char数组模拟进度条floor(rate*100)作为进度条的填充则有效进度条范围是[0,99]当进度达到99 % 99\%99%时理应填满整个进度条。进度有可能从 0.99 跳到 1或者 1.1 等所以当进度大于100 % 100\%100%时直接输出100 % 100\%100%。因此需要特殊处理。可选择自定义的进度条例如这里的模拟实现采用。工程实践版本的大致参考程序如下#includestdio.h#includestring.h#includeunistd.htypedefvoid(*callshow_v)(double);// 进度条展示版本voidprocess(doublerate){constchar*rotat|/-\\;// 进度条旋转表示程序正常推进staticcharbar[101]{\0};// 进度条本体staticsize_t cnt0;// 控制进度条的旋转无符号数无需担心溢出intrlenstrlen(rotat);if(rate100.0){// 决定采用浮点数时要考虑不同下载速度的情况if(rate99.0){bar[(int)rate];bar[(int)rate1];inttmp(int)rate-1;while(tmp0bar[tmp]!){// 一次完成几个百分比时向前填充bar[tmp];--tmp;}}else{bar[99];bar[100]\0;// 第101处填充\0}printf([%-100s][%5.1lf%%][%c]\r,bar,rate,rotat[cnt%rlen]);cnt;fflush(stdout);}else{// 任务完成的情况rate100.0;bar[99];bar[100]\0;inttmp98;// 假设用的是更先进的通信技术例如4G网1秒钟流量7MBwhile(tmp0bar[tmp]!){// 向前填充bar[tmp];--tmp;}printf([%-100s][%5.1lf%%][%c]\n,bar,rate,rotat[0]);memset(bar,\0,sizeof(bar));// 手动清空迎接下个任务}}voiddownload(callshow_v cs){intaim1024*1024*10;// 10MB 待下载数据intv1024;// 下载速度为1KB原始人的网络intret1024*1024*9;// 下载进度记录初始90%可设为0// int cnt 500;while(retaim){// 达到目标时应该停止因为这里是先下载再记录retv;// 正在下载usleep(1000*10);doublerateret*100.0/aim;// while(cntrate66.0){//模拟网络卡顿的情况// ret-v;// rate ret*100.0/aim;// --cnt;// }cs(rate);// 刷新进度条}}intmain(){download(process);//上传函数名作为参数// download(process); //模拟多次下载// download(process);return0;}C语言可以使用格式化语句输出简单的彩色代码片段。例如蓝底红字printf([\033[31;44m%-100s\033[0m][%5.1lf%%][%c]\r,bar,rate,rotat[cnt%rlen]);具体参考【C语言】C语言基础——printf带颜格式化输出Linux_printf格式化输出文本颜色-CSDN博客。\r被解释成回车加换行的情况部分拓展屏的终端驱动程序或 xshell 在不同的拓展屏可能会将\r解释成回车加换行这时可通过循环输出\b即一直输出退格直到回退到行首。这个方案适用所有终端显示屏。voidprocess(doublerate){//保留原代码不变if(rate100.0){//决定采用浮点数时要考虑不同下载速度的情况//保留原代码不变intoutlenprintf([%-100s][%5.1lf%%][%c],bar,rate,rotat[cnt%rlen]);for(inti0;ioutlen;i)//循环输出\b退格printf(\b);cnt;fflush(stdout);}else{//任务完成的情况//保留原代码不变}}也可以使用 ANSI 专业序列\033[2K\r。这个序列的功能是检测到这个序列时终端设备会将光标移到开头。这个方案相对于输出等量的\b能显著提升性能但要求目标终端支持 ANSI 。voidprocess(doublerate){//保留原代码不变if(rate100.0){//保留原代码不变//检测到\033[2K\r将光标移动到开头再输出后续字符printf(\033[2K\r[%-100s][%5.1lf%%][%c],bar,rate,rotat[cnt%rlen]);cnt;fflush(stdout);}else{//保留原代码不变printf(\033[2K\r[%-100s][%5.1lf%%][%c]\n,bar,rate,rotat[0]);memset(bar,\0,sizeof(bar));//手动清空迎接下个任务}}除了进度条标准库stdio.h的部分库函数例如printf、fputs、fopen等可以自己模拟实现。其中结构体FILE内部存在缓冲区这个缓冲区可以是栈上开辟也可以用malloc库函数申请根据自己的需求还可以添加文件描述符fd接收系统调用open的返回值、flag接收文件的打开模式。例如简易版#pragmaonce#defineSIZE4096#defineFLUSH_NONE1#defineFLUSH_LINE(11)#defineFLUSH_ALL(12)typedefstruct_myFILE{intfileno;intflag;charbuffer[SIZE];intend;}myFILE;其他的fopen、fwrite、fflush和fclose等都可以模拟实现。这里暂时省略。