当前位置: 首页 > news >正文

缓冲区(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);
}

相关文章:

  • TF-IDF算法的代码实践应用——关键词提取、文本分类、信息检索
  • AI时代的弯道超车之第二十五章:《生命3.0》未来AI有生命了怎么办?
  • Vuex 中Mutation 和Action介绍
  • Python环境搭建竞赛技术
  • wordpress搬家 数据库备份迁移
  • 大模型Transformer触顶带来的“热潮退去”,稀疏注意力架构创新或是未来
  • STM32外设学习之ADC
  • HNCTF2025 - Misc、Osint、Crypto WriteUp
  • 日语学习-日语知识点小记-进阶-JLPT-真题训练-N2阶段(1):单词部分练习
  • Linux操作系统基线检查与安全加固概述
  • 《HarmonyOSNext终极UIAbility手册:从启动模式到页面跳转,一网打尽!》
  • C++之前向声明
  • [学习] Costas环详解:从原理到实战
  • 2025GEO供应商排名深度解析:源易信息构建AI生态优势
  • 一数一源一标准的补充
  • 【C】 USB CDC、Bulk-OUT 端点
  • PostgresSQL日常维护
  • 网页组件强制设置右对齐
  • python下载与开发环境配置
  • 从“字对字“到“意对意“:AI翻译正在重塑人类的语言认知模式
  • 上市公司中 哪家网站做的好/站长论坛
  • 网站建设推广总结/搭建网站的五大步骤
  • 辽阳网站建设多少钱/微信朋友圈广告推广
  • 企业网站管理的含义/sem专业培训公司
  • 门户网站静态页面/网站推广优化的原因
  • wordpress文章评论功能/湘潭关键词优化服务