【Linux探索学习】第二十二弹——用户缓冲区:深入解析操作系统中数据交互时的缓冲区机制

我们讲解的重点会放在讲解什么是缓冲区上,对于缓冲区存在的作用和种类等方面上了解一下就行

一、什么是缓冲区?
1.1 问题提出
我们通过几个场景来揭露这个问题,首先我们先来看下面这串代码及其输出结果:

#include<stdio.h>
#include<string.h>
#include<unistd.h>
int main()
{
const char *fstr="hello fwrite\n";
const char *str="hello write\n";

//C语言接口
printf("hello printf\n"); //stdout -> 1
fprintf(stdout,"hello fprintf\n"); //stdout -> 1
fwrite(fstr,strlen(fstr),1,stdout); //fread, stdout -> 1
//操作系统提供的系统接口
write(1,str,strlen(str));
return 0;
}

运行结果:

我们可以把这个结果输出重定向到指定文件中去

./myfile>log.txt
cat log.txt

但是如果我们在代码段的最后一行加入fork函数来创建子进程,我们就会得到一个不一样的输出结果:

#include<stdio.h>
#include<string.h>
#include<unistd.h>
int main()
{
const char *fstr="hello fwrite\n";
const char *str="hello write\n";

//C语言接口
printf("hello printf\n"); //stdout -> 1
fprintf(stdout,"hello fprintf\n"); //stdout -> 1
fwrite(fstr,strlen(fstr),1,stdout); //fread, stdout -> 1
//操作系统提供的系统接口
write(1,str,strlen(str));
fork();
return 0;
}

运行结果:

我们发现再次输出重定向时结果发生了很大的改变,调用C语言接口的语句被打印了两遍,而系统调用接口则只被打印一遍,而且顺序也发生了变化,调用系统接口的先被打印

1.2 缓冲区概念的引出
为什么会出现这种情况呢?在解释之前我们先来看下面这种情况:

#include<stdio.h>
#include<string.h>
#include<unistd.h>
int main()
{
const char *fstr="hello fwrite\n";
//const char *str="hello write\n";

//C语言接口
printf("hello printf\n"); //stdout -> 1
fprintf(stdout,"hello fprintf\n"); //stdout -> 1
fwrite(fstr,strlen(fstr),1,stdout); //fread, stdout -> 1
close(1);
//操作系统提供的系统接口
//write(1,str,strlen(str));
//fork();
return 0;
}

我们只留下C语言的几个打印方式,同时在打印执行完后把1号文件关闭了

运行结果:

上面的每一条打印语句我们都通过\n来刷新缓冲区的,如果我们把\n去掉再执行一遍:

#include<stdio.h>
#include<string.h>
#include<unistd.h>
int main()
{
const char *fstr="hello fwrite";
//const char *str="hello write\n";

//C语言接口
printf("hello printf"); //stdout -> 1
fprintf(stdout,"hello fprintf"); //stdout -> 1
fwrite(fstr,strlen(fstr),1,stdout); //fread, stdout -> 1
close(1);
//操作系统提供的系统接口
//write(1,str,strlen(str));
//fork();
return 0;
}

运行结果:

我们发现没有任何输出结果,这又是什么原因呢?
注意:是在close(1)关闭和\n去掉同时存在的情况下才有了这样的结果

但是如果我们是对系统接口执行这个操作:

执行结果:

我们发现此时有打印结果

其实原因如下:

我们对上面的内容做一个叙述:我们都知道我们写入的内容是要先存在缓冲区的,但实际上我们通过C语言的接口所写的内容,比如print、fprint等接口所写内容所存在的缓冲区并不是在内核中的,它是在内核以外的,是语言的这些接口随后调用write系统接口才将其写入内核中去的,在我们关闭1号显示器文件时,内核中写入的内容会被我们输出到显示其中,但是缓冲区中的内容我们需要先调用write接口写入内核中去,但是1号文件都被关闭了,所以自然 是不能再写入内核中去的,所以最后就不能被显示在显示器上

这其实就解释了缓冲区的概念:缓冲区是计算机存储器中的一块内存区域,用于临时存放在不同设备或进程间传输的数据

二、缓冲区刷新方案
2.1 刷新方案
在解决最一开始的问题前,我们先要知道一个知识点,那就是缓冲区的刷新方案是什么,因为缓冲区就是内核外一个特定的空间,所以它的大小是有限的,需要定时进行刷新,而且我们读取或存入的文件的大小也是有限的,所以就需要不同的刷新方案来帮助我们解决不同的场景

缓冲区刷新方式主要有以下三种:
a.无缓冲 --- 直接刷新
b.行缓冲 ---不刷新,直到碰到\n

c.全缓冲 --- 缓冲区满了,才刷新

除此之外当进程结束时也会进行刷新

一般向显示器打印时是行缓冲,向文件中打印时是全缓冲

2.2 问题解决
下面我们继续解决一下最一开始提出的问题,为什么fork()之后再向文件中打印出现那种情况

向文件中输出时实际上我们的缓冲方案是发生改变了,由往显示器上显示时的行刷新变为了全刷新,只有在缓冲区被写满或进程要结束时时我们的内容才会被刷新到内核中,而write是系统接口,它所打印的内容是直接被写在内核中的,所以先被打印了出去,而我们缓冲区的内容需要等待进程终止时被强制刷新,而我们的fork()创建了新的子进程,子进程会复制父进程的缓冲区,子进程在结束时也会将缓冲区中的内容进行刷新,所以我们C接口所写的内容就被打印了两次

三、缓冲区的作用
缓冲区的主要作用是缓解速度差异和提高系统的效率。例如,硬盘的读写速度远低于内存,网络传输速度也比本地内存慢,因此需要缓冲区来暂时存储数据,避免频繁的硬件访问造成性能瓶颈。

四、用户缓冲区的管理
在 Linux 系统中,用户缓冲区的分配通常是由程序员或操作系统自动管理的。在许多高级语言中,如 Python、Java,缓冲区的管理是透明的,程序员只需要调用相应的 I/O 操作函数即可。对于 C 语言或其他低级语言,程序员需要手动管理缓冲区。

4.1 缓冲区的分配
在 C 语言中,用户缓冲区可以通过 malloc() 或 calloc() 等函数动态分配内存。例如:

#include <stdio.h>
#include <stdlib.h>

int main() {
size_t bufferSize = 1024;
char *buffer = (char *)malloc(bufferSize);
if (!buffer) {
perror("malloc");
return -1;
}

// 使用缓冲区
snprintf(buffer, bufferSize, "Hello, buffer!");
printf("%s\n", buffer);

free(buffer); // 释放内存
return 0;
}

上述代码动态分配了一个大小为 1024 字节的缓冲区,并使用 snprintf() 函数将数据写入缓冲区。

4.2 缓冲区的对齐
在某些情况下,缓冲区需要满足特定的对齐要求,以便提高访问效率或满足硬件要求。例如,某些系统要求缓冲区的起始地址必须是 16 字节的倍数。为此,可以使用 posix_memalign() 或 aligned_alloc() 等函数来分配对齐的内存。

#include <stdio.h>
#include <stdlib.h>

int main() {
void *buffer;
size_t alignment = 16;
size_t bufferSize = 1024;

if (posix_memalign(&buffer, alignment, bufferSize) != 0) {
perror("posix_memalign");
return -1;
}

// 使用缓冲区
snprintf(buffer, bufferSize, "Aligned buffer!");
printf("%s\n", (char *)buffer);

free(buffer);
return 0;
}

五、性能优化与缓冲区管理
5.1 使用合适的缓冲区大小
选择合适的缓冲区大小对于 I/O 性能至关重要。过小的缓冲区会导致频繁的 I/O 操作,降低性能;过大的缓冲区会占用过多的内存资源。因此,合理地选择缓冲区的大小是性能优化的关键。

5.2 异步 I/O 操作
在处理大量 I/O 请求时,使用异步 I/O 操作可以显著提高系统的吞吐量。Linux 提供了 aio(Asynchronous I/O)接口,允许程序在进行 I/O 操作时不阻塞主线程。

六、 总结
用户缓冲区是 Linux 系统中处理 I/O 操作的重要机制。它能够有效地减少硬件访问次数,提高数据传输效率。本文主要就是对缓冲区的概念进行详细讲解,帮助大家理解缓冲区究竟是什么,在实际开发中,合理使用用户缓冲区能够显著提升程序的性能和资源利用率。
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/2301_80220607/article/details/144565752

版权声明:
作者:SE_Wang
链接:https://www.cnesa.cn/2954.html
来源:CNESA
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
【Linux探索学习】第二十二弹——用户缓冲区:深入解析操作系统中数据交互时的缓冲区机制
我们讲解的重点会放在讲解什么是缓冲区上,对于缓冲区存在的作用和种类等方面上了解一下就行 一、什么是缓冲区? 1.1 问题提出 我们通过几个场景来揭露这个问题,首先我们先来看下面这串代码及其输出结果: #include<stdio.h> #include<string.h> #include<unistd.h> int main() { const char *fstr="hello fwrite\n"; const char *str="hello write\n"; //C语言接口 printf("hello printf\n"); //stdout -> 1 fprintf(stdout,"hello fprintf\n"); //stdout -> 1 fwrite(fstr,strlen(fstr),1,stdout); //fread, stdout -> 1 //操作系统提供的系统接口 write(1,str,strlen(str)); return 0; } 运行结果: 我们可以把这个结果输出重定向到指定文件中去 ./myfile>log.txt cat log.txt 但是如果我们在代码段的最后一行加入fork函数来创建子进程,我们就会得到一个不一样的输出结果: #include<stdio.h> #include<string.h> #include<unistd.h> int main() { const char *fstr="hello fwrite\n"; const char *str="hello write\n"; //C语言接口 printf("hello printf\n"); //stdout -> 1 fprintf(stdout,"hello fprintf\n"); //stdout -> 1 fwrite(fstr,strlen(fstr),1,stdout); //fread, stdout -> 1 //操作系统提供的系统接口 write(1,str,strlen(str)); fork(); return 0; } 运行结果: 我们发现再次输出重定向时结果发生了很大的改变,调用C语言接口的语句被打印了两遍,而系统调用接口则只被打印一遍,而且顺序也发生了变化,调用系统接口的先被打印 1.2 缓冲区概念的引出 为什么会出现这种情况呢?在解释之前我们先来看下面这种情况: #include<stdio.h> #include<string.h> #include<unistd.h> int main() { const char *fstr="h……
<<上一篇
下一篇>>