ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Re:Linux 系统篇(二十四):IO 缓冲区深度解析 —— 三种缓冲类型、FILE 底层结构、fork 写时拷贝实验与手搓简易 stdio 库

Re:Linux 系统篇(二十四):IO 缓冲区深度解析 —— 三种缓冲类型、FILE 底层结构、fork 写时拷贝实验与手搓简易 stdio 库 观众老爷们大家好 这里是邪修KING的独家频道本文属于系列Linux系统篇 ——操作指令一起学Linux的小伙伴可订阅专栏 Linux系统篇 前面我们讲了文件描述符、open/write 系统调用也一直在用 printf 这类标准 IO 库函数。 但很多人一直有个疑问同样是写文件write 直接就能写到磁盘printf 为什么有时候不立刻输出重定向到文件后行为为什么不一样fork 之后为什么会打印两次 答案全部指向一个核心机制**标准 IO 缓冲区**。 本篇我们从缓冲区的本质讲起拆解三种缓冲类型、FILE 结构体底层、经典 fork 缓冲实验最后手搓一个简易版 stdio 库彻底把缓冲区讲透。一、什么是缓冲区缓冲区是内存空间的一部分操作系统 / C 标准库会预留一块存储空间用来临时存放输入或输出的数据这块预留空间就叫缓冲区。按对应设备方向分为输入缓冲区暂存从外设读入的数据输出缓冲区暂存要写到外设的数据我们常说的标准 IO 缓冲区属于用户态缓冲区由 C 标准库提供并管理在系统调用的上层。二、为什么要引入缓冲区机制缓冲区的核心价值是减少系统调用、提升整体效率我们从两个维度理解。2.1 减少用户态 / 内核态切换开销如果没有缓冲区每读写一个字节都调用一次write/read系统调用每调用一次系统调用CPU 从用户态切换到内核态内核执行磁盘 IO 操作再切回用户态状态切换本身有 CPU 开销频繁切换会让大部分时间浪费在切换上真正干活的时间少。有了缓冲区之后写数据先写到内存缓冲区里攒够一批再调用一次系统调用批量写入磁盘读数据一次从磁盘读一大块进缓冲区后面读数据直接从内存拿系统调用次数大幅减少性能显著提升。2.2 匹配高速 CPU 与低速外设的速度差CPU 运算速度是磁盘、打印机这类外设的成千上万倍。如果 CPU 每输出一个字节都等着外设处理完CPU 大部分时间都在闲置等待。缓冲区就是中间的 “快递集散中心”CPU 快速把数据丢进缓冲区不用等外设直接去干别的事外设慢慢从缓冲区里取数据处理完美解耦高速 CPU 和低速 IO 设备解放 CPU 算力。 生活类比缓冲区就像快递驿站没有驿站你每买一件东西快递员都单独跑一趟送上门效率极低。有驿站快递先攒到驿站攒够一车统一派送一趟送几十件效率大幅提升。三、标准 IO 的三种缓冲类型C 标准 IO 库提供了三种缓冲策略对应不同的使用场景。3.1 全缓冲Full Buffering规则缓冲区填满之后才会触发真正的 IO 系统调用把数据刷到磁盘。默认场景普通磁盘文件的读写默认全缓冲。特点系统调用次数最少效率最高但数据不会立刻落盘。3.2 行缓冲Line Buffering规则输入输出遇到换行符\n时触发刷新系统调用如果缓冲区被填满即使没遇到换行也会刷新。默认场景终端标准输入stdin、标准输出stdout默认行缓冲。默认行缓冲区大小一般是 1024 字节。特点兼顾效率和交互体验换行就输出符合终端逐行交互的习惯。3.3 无缓冲No Buffering规则不做缓存每次调用 IO 库函数直接触发系统调用。默认场景标准错误stderr默认无缓冲。特点出错信息立刻输出不会因为缓冲延迟方便定位问题。3.4 缓冲区刷新的触发条件除了缓冲类型自身的规则以下情况也会强制刷新缓冲区缓冲区被填满手动调用fflush()函数进程正常退出时会自动刷新所有打开的标准 IO 流四、经典踩坑重定向后的缓冲区变化4.1 现象printf 重定向到文件内容丢失我们看这段代码把 1 号文件描述符标准输出关闭重定向到磁盘文件然后 printf 输出。#includestdio.h#includestring.h#includesys/types.h#includesys/stat.h#includefcntl.h#includeunistd.hintmain(){close(1);intfdopen(log.txt,O_WRONLY|O_CREAT|O_TRUNC,0666);if(fd0){perror(open);return0;}printf(hello world: %d\n,fd);close(fd);return0;}运行后查看log.txt发现文件是空的什么都没写进去。原因分析标准输出原本是终端默认行缓冲遇到\n就刷新重定向到磁盘文件后缓冲模式自动变成全缓冲printf的内容只写到了用户态缓冲区缓冲区没满不会触发系统调用写磁盘还没等刷新我们就直接close(fd)了数据丢在缓冲区里没写出去解决方法在 close 之前手动fflush(stdout)强制刷新printf(hello world: %d\n,fd);fflush(stdout);// 强制把缓冲区数据刷到磁盘close(fd);此时再运行内容就正常写入文件了。4.2 验证stderr 天生无缓冲#includestdio.h#includefcntl.h#includeunistd.hintmain(){close(2);intfdopen(log.txt,O_WRONLY|O_CREAT|O_TRUNC,0666);if(fd0){perror(open);return0;}perror(hello world);close(fd);return0;}运行后log.txt里立刻有内容。因为stderr默认无缓冲perror直接调用系统调用写入不需要等缓冲区满。五、FILE 结构体封装 fd 缓冲区5.1 核心结论系统调用open/write用文件描述符 fd操作文件标准库printf/fwrite用FILE* 操作文件FILE 结构体内部一定封装了 fd再加上缓冲区的读写指针、容量、状态等信息缓冲区是 C 标准库在用户态加的一层内核层面的系统调用write是没有这个用户态缓冲区的。5.2 FILE 结构体底层glibcglibc 的_IO_FILE结构体核心字段节选struct_IO_FILE{int_flags;// 状态标志// 读缓冲区指针char*_IO_read_ptr;// 当前读指针char*_IO_read_end;// 读缓冲区末尾char*_IO_read_base;// 读缓冲区起始// 写缓冲区指针char*_IO_write_base;// 写缓冲区起始char*_IO_write_ptr;// 当前写指针char*_IO_write_end;// 写缓冲区末尾int_fileno;// 封装的文件描述符 fd// ... 其他字段};所有标准 IO 函数printf、fputc、fwrite 等本质都是先操作内存里的缓冲区满足刷新条件时调用write系统调用把缓冲区数据写入内核缓冲区完全由 C 标准库管理和内核无关。六、经典实验fork 与缓冲区写时拷贝这是最能证明「缓冲区在用户态」的实验。6.1 实验代码#includestdio.h#includestring.h#includeunistd.hintmain(){constchar*msg0hello printf\n;constchar*msg1hello fwrite\n;constchar*msg2hello write\n;printf(%s,msg0);fwrite(msg1,strlen(msg1),1,stdout);write(1,msg2,strlen(msg2));fork();return0;}6.2 两种运行场景对比场景 1终端直接运行./a.out输出helloprintfhello fwrite hellowrite三句话各出现一次符合预期。场景 2重定向到文件./a.outfilecatfile输出hellowritehelloprintfhello fwrite helloprintfhello fwriteprintf和fwrite各出现2 次write只出现1 次6.3 原因深度解析终端运行stdout 是行缓冲遇到\n立刻刷新。fork 之前三句话都已经刷到终端了缓冲区是空的。fork 之后父子进程都没东西可刷所以各一次。重定向文件stdout 变成全缓冲数据只写到用户态缓冲区没触发系统调用。fork 发生时缓冲区里还有数据进程写时拷贝父子进程各有一份独立的缓冲区数据父子进程退出时各自刷新缓冲区各写一次到文件所以 printf/fwrite 出现两次write 永远一次write 是系统调用直接写进内核没有用户态缓冲区。fork 之前就已经写完了所以永远只有一次。6.4 实验结论✅printf /fwrite 等库函数自带用户态缓冲区✅write 系统调用没有用户态缓冲区✅缓冲区由 C 标准库提供位于用户态fork 写时拷贝会复制缓冲区数据七、动手实现简易版 my_stdio 库理解了原理我们自己手写一个简化版的标准 IO 库实现行缓冲输出彻底吃透缓冲区。7.1 头文件 my_stdio.h#pragmaonce#defineSIZE1024// 缓冲区大小#defineFLUSH_NONE0// 无缓冲#defineFLUSH_LINE1// 行缓冲#defineFLUSH_FULL2// 全缓冲// 自定义FILE结构体structIO_FILE{intflag;// 缓冲模式intfileno;// 封装的文件描述符charoutbuffer[SIZE];// 输出缓冲区intcap;// 缓冲区总容量intsize;// 当前已用大小};typedefstructIO_FILEmFILE;// 打开文件mFILE*mfopen(constchar*filename,constchar*mode);// 写数据intmfwrite(constvoid*ptr,intnum,mFILE*stream);// 强制刷新voidmfflush(mFILE*stream);// 关闭文件voidmfclose(mFILE*stream);7.2 实现 my_stdio.c#includemy_stdio.h#includestring.h#includestdlib.h#includesys/stat.h#includesys/types.h#includefcntl.h#includeunistd.h// 打开文件创建mFILE对象mFILE*mfopen(constchar*filename,constchar*mode){intfd-1;if(strcmp(mode,r)0){fdopen(filename,O_RDONLY);}elseif(strcmp(mode,w)0){fdopen(filename,O_CREAT|O_WRONLY|O_TRUNC,0666);}elseif(strcmp(mode,a)0){fdopen(filename,O_CREAT|O_WRONLY|O_APPEND,0666);}if(fd0)returnNULL;// 申请自定义FILE结构体mFILE*mf(mFILE*)malloc(sizeof(mFILE));if(!mf){close(fd);returnNULL;}// 初始化mf-filenofd;mf-flagFLUSH_LINE;// 默认行缓冲mf-size0;mf-capSIZE;returnmf;}// 强制刷新缓冲区把缓冲区数据写入磁盘voidmfflush(mFILE*stream){if(stream-size0){// 调用系统调用write写入内核write(stream-fileno,stream-outbuffer,stream-size);// 强制同步到磁盘fsync(stream-fileno);// 重置缓冲区已用大小stream-size0;}}// 写数据到缓冲区intmfwrite(constvoid*ptr,intnum,mFILE*stream){// 1. 拷贝数据到缓冲区memcpy(stream-outbufferstream-size,ptr,num);stream-sizenum;// 2. 行缓冲模式遇到换行符就刷新if(stream-flagFLUSH_LINEstream-size0stream-outbuffer[stream-size-1]\n){mfflush(stream);}// 3. 全缓冲模式缓冲区满就刷新if(stream-flagFLUSH_FULLstream-sizestream-cap){mfflush(stream);}returnnum;}// 关闭文件voidmfclose(mFILE*stream){// 关闭前先刷新缓冲区剩余数据if(stream-size0){mfflush(stream);}close(stream-fileno);free(stream);}7.3 测试主函数 main.c#includemy_stdio.h#includestdio.h#includestring.h#includeunistd.hintmain(){mFILE*fpmfopen(./log.txt,a);if(fpNULL){return1;}intcnt5;while(cnt){charbuffer[64];snprintf(buffer,sizeof(buffer),hello message, number is : %d\n,cnt);cnt--;mfwrite(buffer,strlen(buffer),fp);sleep(1);}mfclose(fp);return0;}7.4 运行效果编译运行后每秒写入一行日志遇到\n立刻刷新到文件和标准库的行缓冲行为完全一致。全文总结缓冲区本质用户态内存空间用来缓存 IO 数据减少系统调用匹配 CPU 与外设速度差。三种缓冲全缓冲磁盘文件、行缓冲终端、无缓冲stderr。FILE 结构体C 标准库封装内部包含文件描述符 缓冲区读写指针 状态信息。fork 实验库函数带用户态缓冲fork 写时拷贝会复制重定向后输出两次系统调用无缓冲永远一次。分层理解write是内核级系统调用printf/fwrite是用户态库函数在系统调用之上加了一层缓冲区。下篇预告缓冲区讲完我们正式进入进程间通信 IPC 专题第一站匿名管道与命名管道看看进程之间怎么通过缓冲区传递数据。
返回列表