缓冲区(C语言缓冲区+内核缓冲区)一个例子解释他们的关系和作用!!!

首先提出问题: 为什么以下代码是先sleep三秒后,屏幕才显示"XXXXXXX"。
#include<stdio.h>
#include<unistd.h>int main()
{printf("XXXXXXX");sleep(3);return 0;
}

为什么以下代码是先显示"XXXXXXX",屏幕才显示"XXXXXXX"sleep三秒。

#include<stdio.h>
#include<unistd.h>int main()
{printf("XXXXXXX\n");sleep(3);return 0;
}

在我们学习C语言的时候经常听说“某某字符串,某某变量在printf时是先进入缓冲区啦。。。”类似于这种,那时候没人会去细说缓冲区,那么今天我们就来详细说说缓冲区。

1.什么是缓冲区

举个例子:

假设有两个人,张三和李四。他们两个是网友,有一天呢李四过生日,张三就准备把自己给他准备的礼物给爱他,但是张三和李四距离太远,以前呢张三骑着自己的小单车就出发了,经过两个月的长途跋涉终于送给了李四。张三两个月没上班,一想,亏死了。

后来,有了菜鸟驿站,张三需要给李四礼物就不用骑单车了,张三只需要把快递交给楼下的菜鸟驿站,至于菜鸟驿站什么时候发,快递什么时候到都不需要张三管,张三就可以继续上班了,快递到了先是放在李四楼下的菜鸟驿站,至于李四什么时候在家,菜鸟驿站再送。

至此,我们其实就可以大概猜得出,菜鸟驿站其实就是缓冲区的意思,而张三就是程序员,李四就是硬件。张三家楼下的菜鸟驿站就是C语言FILE结构体里面的一个长数组,他就是C语言下的缓冲区,我们的printf实际上就是先把数据写到它里面去。(下面会详细说FILE)

struct _IO_FILE {// ...其他字段...char* _IO_buf_base;    // 缓冲区起始位置char* _IO_buf_end;     // 缓冲区结束位置// ...其他字段...
};

李四楼下的缓冲区就是内核里面的缓冲区,在struct file里,struct file里不止有文件的属性和内容信息,还有缓冲区。 

struct file {// ...const struct file_operations *f_op;  // 文件操作函数指针struct address_space *f_mapping;     // 指向address_space结构// ...
};

其中,f_mapping字段指向一个struct address_space,这是文件与页缓存之间的桥梁。 

缓冲区是内存空间的⼀部分。也就是说,在内存空间中预留了⼀定的存储空间,这些存储空间⽤来缓冲输⼊或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输⼊设备还是输出设备,分为输⼊缓冲区和输出缓冲区。

 

2 .为什么要引⼊缓冲区机制

读写⽂件时,如果不会开辟对⽂件操作的缓冲区,直接通过系统调⽤对磁盘进⾏操作(读、写等),那么每次对⽂件进⾏⼀次读写操作时,都需要使⽤读写系统调⽤来处理此操作,即需要执⾏⼀次系统调⽤,执⾏⼀次系统调⽤将涉及到CPU状态的切换,即从⽤⼾空间切换到内核空间,实现进程上下⽂的切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执⾏效率造成很⼤的影响。
为了减少使⽤系统调⽤的次数,提⾼效率,我们就可以采⽤缓冲机制。⽐如我们从磁盘⾥取信息,可 以在磁盘⽂件进⾏操作时,可以⼀次从⽂件中读出⼤量的数据到缓冲区中,以后对这部分的访问就不 需要再使⽤系统调⽤了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作⼤ 快于对磁盘的操作,故应⽤缓冲区可⼤ 提⾼计算机的运⾏速度。
⼜⽐如,我们使⽤打印机打印⽂档,由于打印机的打印速度相对较慢,我们先把⽂档输出到打印机相应的缓冲区,打印机再⾃⾏逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是⼀块内存区,它⽤在输⼊输出设备和CPU之间,⽤来缓存数据。它使得低速的输⼊输出设备和⾼速的CPU能够协调⼯作,避免低速的输⼊输出设备占⽤CPU,解放出CPU,使其能够⾼效率⼯作

3.缓冲类型

标准I/O提供了3种类型的缓冲区:
缓冲区:这种缓冲⽅式要求填满整个缓冲区后才进⾏I/O系统调⽤操作。对于磁盘⽂件的操作通常使⽤全缓冲的⽅式访问。
⾏缓冲区:在⾏缓冲情况下,当在输⼊和输出中遇到换⾏符时,标准I/O库函数将会执⾏系统调⽤操作。当所操作的流涉及⼀个终端时(例如标准输⼊和标准输出),使⽤⾏缓冲⽅式。因为标准I/O库每⾏的缓冲区⻓度是固定的,所以只要填满了缓冲区,即使还没有遇到换⾏符,也会执⾏I/O系统调⽤操作,默认⾏缓冲区的⼤⼩为1024。
⽆缓冲区:⽆缓冲区是指标准I/O库不对字符进⾏缓存,直接调⽤系统调⽤。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显⽰出来。
除了上述列举的默认刷新⽅式,下列特殊情况也会引发缓冲区的刷新:
1.  缓冲区满时;
2.  执⾏flush语句;
⽰例如下:
include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {close(1);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0) {perror("open");return 0;}printf("hello world: %d\n", fd);close(fd);return 0;
}

这里相当于把stdout用log.txt替换

我们本来想使⽤重定向思维,让本应该打印在显⽰器上的内容写到“log.txt”⽂件中,但我们发现,
程序运⾏结束后,⽂件中并没有被写⼊内容:
[ljh@VM-8-12-centos buffer]$ ./myfile
[ljh@VM-8-12-centos buffer]$ ls
log.txt makefile myfile myfile.c
[ljh@VM-8-12-centos buffer]$ cat log.txt
[ljh@VM-8-12-centos buffer]$
这是由于我们将1号描述符重定向到磁盘⽂件后, 缓冲区的刷新⽅式成为了全缓冲 。⽽我们写⼊的内容并 没有填满整个缓冲区 ,导致并不会将缓冲区的内容刷新到磁盘⽂件中。怎么办呢?
可以使⽤fflush强制刷新下缓冲区。

当调用printf()时,数据被写入stdout对应的FILE结构体管理的缓冲区,这个缓冲区完全在用户空间,由 C 标准库 (libc) 维护默认情况下,重定向到文件的流是全缓冲的,缓冲区大小通常为 4KB 或 8KB。

只有当 C 库缓冲区刷新时 (通过fflush()或缓冲区满),才会调用write()系统调用,write()将数据从用户空间复制到内核空间的页缓存 (Page Cache),页缓存由内核管理,位于物理内存中

include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {close(1);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0) {perror("open");return 0;}printf("hello world: %d\n", fd);fflush(stdout);close(fd);return 0;
}
有⼀种解决⽅法,刚好可以验证⼀下stderr是不带缓冲区的,代码如下:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {close(2);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0) {perror("open");return 0;}perror("hello world");close(fd);return 0;
}
这种⽅式便可以将2号⽂件描述符重定向⾄⽂件,由于stderr没有缓冲区,“hello world”不⽤fflash
就可以写⼊⽂件。

4.FILE

上面我们提到了FILE,这里我们详细来聊聊。

4.1 FILE结构体的本质与作用

FILE是 C 标准库中定义的结构体类型,用于封装文件操作的相关信息,是用户空间与系统调用之间的桥梁。它本质上是对文件描述符(fd)的一层封装,同时管理 I/O 缓冲区,以提升 I/O 操作效率。

4.2 FILE结构体的核心字段

 在 GNU C 库(glibc)中,FILE结构体的关键字段如下:

struct _IO_FILE {int _fileno;              // 封装的文件描述符(fd)char* _IO_buf_base;       // 缓冲区起始地址char* _IO_buf_end;        // 缓冲区结束地址char* _IO_read_ptr;       // 读缓冲区当前位置char* _IO_write_ptr;      // 写缓冲区当前位置int _flags;               // 文件状态标志(如读写模式、是否关闭等)const struct _IO_jump_t* _vtable;  // 函数指针表,指向I/O操作函数// 其他字段(省略)
};
  1. _fileno
    直接关联系统调用的文件描述符,是FILE与内核交互的桥梁。例如,printf最终会通过_fileno对应的 fd 调用write系统调用。

  2. 缓冲区相关指针

    • _IO_buf_base_IO_buf_end:标记缓冲区的物理范围。
    • _IO_read_ptr_IO_write_ptr:记录当前读写位置,用于控制数据在缓冲区中的流动。
  3. _vtable
    指向函数表,包含一系列 I/O 操作的实现(如读、写、刷新缓冲区等),体现了面向对象的设计思想。

4.3 FILE与系统调用的关系

用户空间视角:通过FILE*指针调用printffscanf等库函数,数据先存入FILE的缓冲区。

内核空间视角:当缓冲区刷新时(如调用fflush、缓冲区满或程序结束),库函数通过_fileno调用writeread等系统调用,将数据传入内核缓冲区(页缓存)

4.4 为什么需要FILE结构体?

  1. 跨平台兼容性:封装不同系统的文件操作细节(如 Windows 和 Linux 的文件描述符机制差异)。
  2. 缓冲区优化:通过用户空间缓冲区减少系统调用次数,提升 I/O 效率。
  3. 高层抽象:提供更易用的接口(如printf的格式化输出),隐藏底层系统调用的复杂性

FILE结构体是 C 语言 I/O 体系的核心,它通过封装文件描述符和管理用户空间缓冲区,在高效性和易用性之间取得平衡。理解FILE的内部结构,有助于深入掌握 C 语言 I/O 操作的本质,以及缓冲区刷新、重定向等关键机制。

5.一个关键问题

为了让我们更好的理解上面的内容,我们来看以下代码:

#include <stdio.h>
#include <string.h>
#include <unistd.h> 
int main()
{const char *msg0="hello printf\n";const char *msg1="hello fwrite\n";const char *msg2="hello write\n";printf("%s", msg0);fwrite(msg1, strlen(msg1), 1, stdout);write(1, msg2, strlen(msg2));fork();return 0;
}

这个代码看起来简单,却是检测是否理解缓冲区的关键!!!

问题:

为什么运行出结果:

而重定向时:

为什么结果不一样呢???

其实关键的原因就在于,显示器文件和该文本文件的缓冲区刷新方式不同。

显示器是行刷新,而文本是满刷新。

所以在显示器上,一行一行刷新,到子进程时缓冲区也没东西了。

在重定向时,开始数据进入缓冲区,但是一直没刷新,write是系统调用,直接输入到内核缓冲区了所以先打印,到了fork时,子进程继承了父进程缓冲区的东西,文件退出时,自动ffulsh,所以就得到了我们看见的现象。

printf fwrite (库函数)都输出了2次,⽽ write 只输出了⼀次(系统调⽤)。为
什么呢?肯定和fork有关!
  • 一般 C 库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
  • printf fwrite 库函数 + 会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
  • 而我们放在缓冲区中的数据,就不会被立即刷新,甚至 fork 之后
  • 但是进程退出之后,会统一刷新,写入文件当中。
  • 但是 fork 的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
  • write 没有变化,说明没有所谓的缓冲区。
综上: printf fwrite 库函数会⾃带缓冲区,⽽ write 系统调⽤没有带缓冲区。另外,我们这⾥所说的缓冲区,都是⽤⼾级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调⽤,库函数在系统调⽤的
“上层”, 是对系统调⽤的“封装”,但是 write 没有缓冲区,⽽ printf fwrite 有,⾜以说
明,该缓冲区是⼆次加上的,⼜因为是C,所以由C标准库提供。

6.简单设计一下libc库

结合上面的内容我们可以试着写出libc的mini版:
my_stdio.h
#pragma once#define SIZE 1024#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2struct IO_FILE
{int flag; // 刷新方式int fileno; // 文件描述符char outbuffer[SIZE];int cap;int size;// TODO
};typedef struct IO_FILE mFILE;mFILE *mfopen(const char *filename, const char *mode);
int mwrite(const void *ptr, int num, mFILE *stream);
void mfflush(mFILE *stream);
void mfclose(mFILE *stream);

my_stdio.c

#include "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>mFILE *mfopen(const char *filename, const char *mode)
{int fd = -1;if(strcmp(mode, "r") == 0){fd = open(filename, O_RDONLY);}else if(strcmp(mode, "w")== 0){fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);}else if(strcmp(mode, "a") == 0){fd = open(filename, O_CREAT|O_WRONLY|O_APPEND, 0666);}if(fd < 0) return NULL;mFILE *mf = (mFILE*)malloc(sizeof(mFILE));if(!mf){close(fd);return NULL;}mf->fileno = fd;mf->flag = FLUSH_LINE;mf->size = 0;mf->cap = SIZE;return mf;
}void mfflush(mFILE *stream)
{if(stream->size > 0){// 写到内核文件的文件缓冲区中write(stream->fileno, stream->outbuffer, stream->size);// 刷新到外设fsync(stream->fileno);stream->size = 0;}
}int mfwrite(const void *ptr, int num, mFILE *stream)
{// 1. 拷贝memcpy(stream->outbuffer + stream->size, ptr, num);stream->size += num;// 2. 检测是否要刷新if(stream->flag == FLUSH_LINE && stream->size > 0 && stream->outbuffer[stream->size - 1] == '\n'){mfflush(stream);}return num;
}void mfclose(mFILE *stream)
{if(stream->size > 0){mfflush(stream);}close(stream->fileno);
}

main.c

#include "my_stdio.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>int main()
{mFILE *fp = mfopen("./log.txt", "a");if(fp == NULL){return 1;}int cnt = 10;while(cnt){printf("write %d\n", cnt);char buffer[64];snprintf(buffer, sizeof(buffer),"hello message, number is : %d", cnt);cnt--;mfwrite(buffer, strlen(buffer), fp);mfflush(fp);sleep(1);}mfclose(fp);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/web/83355.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【2025版】Java 工程师学习路线图 —— 掌握程度描述版

✅【2025版】Java 工程师学习路线图 &#x1f4a1; 目标&#xff1a;成为合格的 Java 工程师&#xff08;前后端都要会&#xff09; &#x1f4dd; 结构清晰 | 阶段明确 | 掌握程度分级 | 适合自学或转行 &#x1f539; 阶段一&#xff1a;编程基础 计算机通识 模块内容推荐掌…

从零实现一个红队智能体

从零实现一个红队智能体(持续更新) 2025-06-09 背景&#xff1a;最近学了基础些东西和工具基础使用&#xff0c;发现一套流程下来太多需要手工要做的&#xff0c;就像自己能不能结合自己的技术栈实现小工具 &#x1f947; 第一步&#xff1a;从实用性开始分析 目标场景 希望…

Uniapp实现多选下拉框

文章目录 前言一、效果展示1.1 下拉效果图1.2 下拉选择效果图1.3 选择显示效果图 二、组件源码2.1.CustomCheckbox.vue源码2.2.niceui-popup-select.vue源码 三、demo.vue代码演示 前言 之前在使用Uniapp时&#xff0c;一直都是下拉框单选。今天某个项目需求需要使用Uniapp实现…

JavaScript-Array.from

Array.from() 是 JavaScript 中用于将类数组对象&#xff08;array-like&#xff09;或可迭代对象&#xff08;iterable&#xff09;转换为真实数组的一个非常有用的方法。 &#x1f4cc; 一、基本语法 Array.from(arrayLike, mapFn?, thisArg?)参数说明&#xff1a; 参数类…

二刷苍穹外卖 day02

新增员工 DTO 将前端传递的参数列表通过对应的实体类接收 当前端提交的数据和实体类中对应的属性差别较大时&#xff0c;使用DTO来封装数据 Data public class EmployeeDTO implements Serializable {private Long id;private String username;private String name;private…

通过Heron Handoff 插件我们在figma设计中可以像sketch导出离线标注

一、设计交付的历史困境与破局契机 在数字产品开发的全流程中&#xff0c;设计标注的高效传递始终是连接创意与实现的关键纽带。传统设计工具如 Sketch 凭借 Bluebeam、Sketch Measure 等插件构建了成熟的离线标注体系&#xff0c;设计师可将标注文件打包交付&#xff0c;开发…

SSE 数据的传输无法流式获取

问题 调试过程中发现SSE数据返回的时间都是一样的&#xff0c;怀疑是接口问题。 参考 EventSource数据一次性出来&#xff0c;并未流式输出的原因_sourceevent为什么结果一下全部返回了-CSDN博客 处理 EventStream 不能流式返回的问题&#xff1a;Nginx 配置优化 解决方案 …

markdown文本转换时序图

好久没更新了~这篇是markdown文本转换时序图的常用方法 文章目录 前言一、Mermaid语法示例二、PlantUML语法示例三、在线工具快速转换总结 前言 使用专业工具如Mermaid或PlantUML可以直接在Markdown中绘制时序图。这些工具支持简洁的语法&#xff0c;生成可嵌入文档的图表&…

谷粒商城-分布式微服务 -集群部署篇[一]

十九、k8s 集群部署 19.1 k8s 快速入门 19.1.1 简介 Kubernetes 简称 k8s。是用于自动部署&#xff0c;扩展和管理容器化应用程序的开源系统。 中文官网 中文社区 官方文档 社区文档 概述 | Kubernetes 传统部署时代&#xff1a; 早期&#xff0c;各个组织是在物理服务器上…

微信小程序- 用canvas生成排行榜

设计功能不是很复杂&#xff0c;也不想用插件&#xff0c;最终出现现在版本&#xff0c;主要用到微信小程序 wx.canvasToTempFilePath方法 // 直接调用改方法 createQRCode() {const qrCodeCanvasId "qrcodeCanvas";drawQrcode({width: 200,height: 200,canvasId: …

深度剖析:UI 设计怎样为小程序构建极致轻量体验

内容摘要 在小程序的世界里&#xff0c;用户都追求快速、便捷的轻量体验。但你是否好奇&#xff0c;为啥有些小程序能让人轻松上手&#xff0c;快速达成目标&#xff0c;而有些却让人感觉繁琐、卡顿&#xff1f;这里的关键差异&#xff0c;往往就藏在 UI 设计中。UI 设计到底施…

【网络安全】Qt免杀样本分析

初步研判 SHA256&#xff1a;9090807bfc569bc8dd42941841e296745e8eb18b208942b3c826b42b97ea67ff 我们可以看到引擎0检出&#xff0c;是个免杀样本&#xff0c;不过通过微步云沙箱的行为分析&#xff0c;已经被判为恶意 行为分析 进程行为 可以看到demo显示调用了winver获…

window 显示驱动开发-如何查询视频处理功能(六)

D3DDDICAPS_FILTERPROPERTYRANGE请求类型 UMD 返回指向 DXVADDI_VALUERANGE 结构的指针&#xff0c;该结构包含传递D3DDDICAPS_FILTERPROPERTYRANGE请求类型时特定视频流上特定筛选器设置允许的值范围。 Direct3D 运行时在D3DDDIARG_GETCAPS的 pInfo 成员指向的变量中为特定视…

Oracle线上故障问题解决

----重启电脑找不到sid Listener refused the connection with the following error: ORA-12505, TNS:listener does not currently know of SID given in connect descriptor Could not open connection sqlplus "/as sysdba" SQL> shutdown immediate 数据库…

语音信号处理三十——高效多相抽取器(Polyphase+Noble)

文章目录 前言一、Polyphase 多项分解1.定义2.拆分公式3.推导过程1&#xff09;按模 M M M拆分求和项2&#xff09;提取因子 4.总结 二、Noble恒等式1. 定义2.Noble恒等式表达方式1&#xff09;抽取系统的 Noble 恒等式2&#xff09;插值系统的 Noble 恒等式 2.Nodble恒等式推导…

广告推荐系统中模型训练中模型的结构信息、Dense数据、Sparse数据

下面结合广告推荐系统常见的深度学习模型(比如 Wide & Deep、DeepFM、Two-Tower 等),介绍一下“模型的结构信息”、Dense 数据和 Sparse 数据在训练过程中的角色及处理方式。 模型结构信息 输入层(Input Layer) • Sparse 输入:各类离散高维特征(用户 ID、广告 ID、…

安全生产管理是什么?安全生产管理主要管什么?

安全生产管理是什么&#xff1f;安全生产管理主要管什么&#xff1f; 不管是制造业、建筑业&#xff0c;还是仓储、物流、化工等等&#xff0c;一聊到“安全事故”&#xff0c;大家脑子里最先冒出来的两个词&#xff0c;肯定就是&#xff1a; 人的不安全行为物的不安全状态 …

SecureRandom.getInstanceStrong() 与虚拟机的爱恨情仇

问题描述 使用Ruoyi-cloud 二开&#xff0c;将服务部署到虚拟机上后&#xff0c;准备登录&#xff0c;发现验证码一致加载不出来&#xff0c;接口请求超时! 解决步骤 telnet 虚拟机ipport 发现可以通.curl 接口&#xff0c;发现一致不返回&#xff0c;超时了./code 接口超时&am…

DEM 地形分析与水文建模:基于 ArcGIS 的流域特征提取

技术点目录 一、 GIS理论及ArcGIS认识二、ArcGIS数据管理与转换三、ArcGIS地图制作与发布四、ArcGIS数据制备与编辑五、ArcGIS矢量空间分析及应用六、ArcGIS栅格空间分析及应用七、ArcGIS空间插值及应用八、DEM数据与GIS三维分析九、ArcGIS高级建模及应用十、综合讲解了解更多 …

芯伯乐XBLW GT712选型及应用设计指南

前言 在电子工程领域&#xff0c;精准的电流测量对于众多电路设计与系统监控至关重要。芯伯乐推出的XBLW GT712电流传感器以其独特的优势&#xff0c;成为工程师在诸多应用中的首选工具。本文将深入剖析XBLW GT712的工作原理、性能特点以及应用要点&#xff0c;为工程师提供详…