Linux操作系统之文件(三):缓冲区

前言:

上节课我们讲授重定向的概念时,曾提到了一点缓冲区的概念。本文将会为大家更详细的带来缓冲区的有关内容:用户级缓冲区是什么,以及其与内核级缓冲区的关系,最后,我会为大家模拟实现一下stdio.h的关于FILE结构体的有关内容,当然,这个模拟只是从原理上实现。真正的stdio.h肯定会更加复杂。

一、再谈缓冲区

我们上篇文章链接曾做了一个实验:

这里我们加了一个fflush之后,就可以在文件中看到我们的输出结果了。当时我们解释的是“ printf 的输出被缓冲在内存中,尚未写入文件,而程序结束时没有触发缓冲区的自动刷新。” 

这句话还是太抽象了,那么更加具体点的解释是什么呢?

我们曾经说过,内核级缓冲区的出现,是为了提高IO的效率,让数据积累后再一次性保存在文件中,而不是一次一次的连续IO。

那么我们的C语言,同样也是为了提高效率,也实现了一个缓冲区,而这个缓冲区被我们称为用户级缓冲区。我们调用的printf,fprintf,fputs实际上都是C语言封装后的函数。这个缓冲区也恰好对应了这些函数。

在stdio.h文件中,FILE实际上是struct _IO_FILE 结构体的重命名。源码部分如下:
 

 

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 putback+get 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; // 封装的⽂件描述符
  # if 0
  int _blksize;
  # else
  int _flags2;
  # endif
  _IO_off_t _old_offset; /* This used to be _offset but it's too small. */
  # define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[ 1 ];
/* char* _save_gptr; char* _save_egptr; */
  _IO_lock_t *_lock;
  # ifdef _IO_USE_OLD_IO_FILE
  };

大家可以看到,在 _IO_FILE结构体中,有一段关于缓冲区的代码,而这个缓冲区,就是我们说得用户级的缓冲区。

它存在的意义与内核级缓冲区是一模一样的,调用printf等C语言封装后的函数,把数据拷贝到用户级缓冲区里,随后根据一定的条件,把堆积的数据拷贝到内核级缓冲区。

所以printf等一系列的C语言函数本质上,也是一个拷贝函数。

如果我们的目标文件是显示器文件,那么这个拷贝条件就是行刷新,当检测到\n时就会拷贝到内核级缓冲区。如果是普通文件,对应的就是全缓冲,即等待缓冲区写满后再刷新。

只要把数据从用户级缓冲区拷贝到了内核级缓冲区,我们就认为把数据交给了操作系统,这个数据就和用户无关了。


二、重定向与缓冲区

仍然是这个代码:

#include <stdio.h>

#include <unistd.h>

#include <fcntl.h>

int main()

{

    close(1);

    int fd=open("log1.txt",O_WRONLY | O_CREAT | O_TRUNC, 0666);

    printf("%d\n",fd);

    return 0;

}

如果我们把close(fd)删除掉,那又是什么情况呢?
 

我们可以看见,重定向后,虽然不能打印到显示屏,但是文件中是存在打印的的结果的。

为什么把后面的close(fd)去掉后,文件中就存在打印结果了呢?

 先不着急回答,我们再把后面的close换成fclose:

可以看见,fclose也可以达成目标。

所以我们可以告诉大家原因了,当一个进程退出时, 它会自动刷新自己的用户级缓冲区,但当你调用系统的close把fd关掉后,它想要把数据刷新到操作系统内部都没有机会了。

而C语言的fflush与close也都只是把用户级缓冲区的数据刷新到内核级缓冲区,最多也就是fclose会释放 FILE 结构体,并调用 close(fd) 关闭底层文件描述符。

由于我们重定向了,导致原本打印1到显示器文件的行缓冲模式变成了全缓冲,导致不会因为‘\n’而刷新数据。

把用户级缓冲区的数据刷新到内核级我们可以调用fflush,那有没有什么方式把内核级缓冲区数据刷新到文件里?

有的:

我们可以调用fsync系统调用接口。


总的来说,C语言之所以存在缓冲区,就是为了提高效率,也就是说,C语言从设计上,就十分注重效率。

那C++有没有自己的用户级缓冲区呢?

肯定是有的。但是C++的缓冲区效率没有C语言高,所以才会出现在一些十分注重时间复杂度的算法题上面,出现同样结构的思维的代码,出现使用printf可以通过,但是使用cout无法通过的现象。


 

三、子进程与缓冲区

请看下面的代码:

int main()

{

    //C库函数

    pinrtf("hello printf\n");

    fprintf(stdout,"hello fprintf\n");

    const char*message="hello fwrite\n";

    fwrite(message,strlen(message),1,stdout);

    //系统调用

    const char*w="hello write";

    write(1,w,strlen(w));

   

   

    fork();

    return 0;

}

它的运行结果是: 

很好,看起来没有问题,那我们试着重定向一下呢?

 

诶,为什么重定向后,C语言的打印就都执行了两次,系统调用的打印只执行了一次呢?

这是因为重定向后,用户级缓冲区的缓冲方式变成全缓冲,父子进程结束后各自fflush了一次到内核级缓冲区 ,导致C语言中,拷贝到用户级缓冲区的数据,各自被刷新到了内核级缓冲区一次,于是当内核级缓冲区的数据刷新到文件中时,就出现了这个现状。

当我们把\n取消时:

int main()

{

    //C库函数

    printf(" hello printf ");

    fprintf(stdout," hello fprintf ");

    const char*message=" hello fwrite ";

    fwrite(message,strlen(message),1,stdout);

    //系统调用

    const char*w="hello write\n";

    write(1,w,strlen(w));

   

   

    fork();

    return 0;

}

打印结果如下:
 

这就是因为,在打印到显示器文件时,缓冲方式为行缓冲,我们没有遇见\n,同样导致了重复的刷新。

如果想要达成正确的打印效果,就需要子进程之前使用fflush刷新用户级缓冲区。


四、模拟实现stdio.h 

相信前面的用例,已经能够让大家较为清楚的理解到缓冲区与重定向之间的精密联系了。

那么现在我就来带大家简答的模拟实现一下stdio中的FILE结构体,与fwrite,fclose,fflush,fopen函数吧!!

(注意,本次模拟只是为了让大家更能理解原理,真正的实现肯定有所不同)

 

我们先不管C标准库中是什么样子的,我们只知道,有.h文件,就自然要有.c文件来实现;

首先,我们先根据所知道的知识,把基础的FILE结构体与这四个函数的声明写到.h文件里:

#pragma once#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdlib.h>#define SIZE 1024//定义刷新方式
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2struct _OI_FILE
{int flag;//刷新方式int fileno;//文件描述符char outbuffer[SIZE];//缓冲区int size;int cap;//TODO
};typedef struct _OI_FILE mFILE;mFILE *mfopen(const char *pathname, const char *mode);
int mfflush(mFILE *_stream);
size_t mfwrite(const void *ptr,int size, mFILE *stream);
int mfclose(mFILE *stream);

随后,我们要在.c文件里实现:
第一个就是fopen函数了,首先,我们要先根据传入的打开方式,通过open把文件描述符获取到。而打开方式的判断,我们选择用if语句与strcmp相结合。

随后,我们要为这个文件创建一个FILE结构体对象,并对里面的参数进行初始化:
设置缓冲区的大小,文件标识符,刷新方式,以及容量,这里的容量我们选择在.h中定义一个SIZE为1024常数,当然,要记得判断一下是否失败。

mFILE *mfopen(const char *pathname, const char *mode)
{int fd=-1;// 根据打开模式选择不同的文件打开方式 if(strcmp(mode,"w")==0)  // 写入模式:创建/截断文件 {fd=open(pathname,O_WRONLY | O_CREAT | O_TRUNC, 0666);}else if(strcmp(mode,"r")==0)  // 只读模式 {fd=open(pathname,O_RDONLY);}else if(strcmp(mode,"a")==0)  // 追加模式 {fd=open(pathname,O_WRONLY | O_CREAT | O_APPEND, 0666);}// 检查文件是否成功打开 if(fd<0){return NULL;}// 分配mFILE结构体内存 mFILE* mf=malloc(sizeof(mFILE));// 内存分配失败处理 if(!mf){close(fd);return NULL;}// 初始化结构体字段 mf->flag=FLUSH_LINE;     // 默认行缓冲模式 mf->fileno=fd;           // 设置文件描述符 mf->size=0;              // 初始化缓冲区大小为0 mf->cap=SIZE;            // 设置缓冲区容量 return mf;
}

随后就是fwrite拷贝函数,他最主要的作用就是通过memcpy函数将数据拷贝到用户级缓冲区:

我们要注意的是,size参数始终代码我们的大小,所以outbuffer[size-1]就代表当前数据的最后一个字符。所以我们可以通过这个特性找到memcpy的初始地址,以及判断是否最后一个字符为\n。根据我们的刷新方式参数以及size的大小是否不为0,来调用我们的mfflush刷新。

而fflush的刷新就更加简单,我们只需要判断size大小是否大于0,随后调用系统接口write,将用户级缓冲区的内容拷贝到内核级缓冲区就行了。

int mfflush(mFILE *_stream)
{// 检查缓冲区是否有数据需要刷新 if(_stream->size>0){// 将缓冲区数据写入文件 write(_stream->fileno,_stream->outbuffer,_stream->size);_stream->size=0;  // 重置缓冲区大小 }return 0;  // 总是返回成功 
}size_t mfwrite(const void *ptr,int size, mFILE *stream)
{// 将数据拷贝到缓冲区 memcpy(stream->outbuffer+stream->size,ptr,size);stream->size+=size; // 更新缓冲区当前大小 // 根据刷新模式决定是否立即刷新 if(stream->flag==FLUSH_LINE && stream->size>0&&stream->outbuffer[stream->size-1]=='\n')  // 行缓冲且遇到换行符 {mfflush(stream);}else if(stream->flag==FLUSH_FULL&&stream->size>=stream->cap)  // 全缓冲且缓冲区满 {mfflush(stream);}return size;  // 返回成功写入的字节数
}

最后只剩下一个mfclose。这个函数又该怎么实现呢?

我们需要先明确一下该函数应该完成的任务:

  1. 刷新缓冲区:如果缓冲区还有未写入的数据(size > 0),调用mfflush写入内核缓冲区。

  2. 关闭文件描述符:使用close() 关闭 fileno文件描述符的文件。

  3. 释放内存:释放 mFILE 结构体占用的内存。

所以具体实现如下:

int mfclose(mFILE *stream) 
{if (stream == NULL) {return -1;  // 错误:传入空指针}// 1. 刷新缓冲区(如果还有未写入的数据)if (stream->size > 0) {mfflush(stream);}// 2. 关闭文件描述符int ret = close(stream->fileno);if (ret < 0) {// 关闭失败,但仍然需要释放内存free(stream);return -1;}// 3. 释放 mFILE 结构体free(stream);return 0;  // 成功
}

 实现还是比较简单的,最重要的就是理解这些函数干了什么事情,达成了什么效果,在内部一一通过系统调用或者函数的复用来实现。

另外,请注意我们应该把在.c文件中用到的各种函数、系统调用的相关头文件,在.h中进行声明。

最后,我们可以添加一个测试用例:

 

int main()
{mFILE *mf = mfopen("test.txt", "w");if (!mf) {perror("mfopen failed");  // 打印错误信息return 1;}size_t written = mfwrite("Hello", 5, mf);  // written = 5mfclose(mf);return 0;
}

 


 

总结:
 

 本文我们继续详细谈了缓冲区有关的概念,并为大家模拟实现了FILE结构体的有关内容,希望通过这些知识点,能够帮助你更加了解操作系统中,文件的缓冲区的相关知识。

如果有任何疑问与指正欢迎私信或者评论区留言

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

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

相关文章

Linux云计算基础篇(7)

一、< 输入重定向 wc -l < filelist .txt 统计数据&#xff0c;从file这个文件拿结果。 二、tr 转换字符命令 $ tr A-Za-z<.bash_profile 将bash_profile文件中的大写字符全部转成小写字符 三、管道符&#xff08;|&#xff09; com…

【学习笔记】Lean4基础 ing

文章目录 概述参考文档运行程序elan 命令行工具lean 命令行工具lake 命令行工具运行单文件程序Hello, world!验证 Lean4 证明 运行多文件项目 Lean4 基础语法注释表达式求值变量和定义定义类型变量 定义函数命名规则命名空间数据类型结构体构造子模式匹配多态List 列表Option 可…

FPGA实现40G网卡NIC,基于PCIE4C+40G/50G Ethernet subsystem架构,提供工程源码和技术支持

目录 1、前言工程概述免责声明 3、相关方案推荐我已有的所有工程源码总目录----方便你快速找到自己喜欢的项目我这里已有的以太网方案 4、工程详细设计方案工程设计原理框图测试用电脑PClE4CDMA40G/50G Ethernet subsystem工程源码架构驱动和测试文件 5、Vivado工程详解1详解&a…

SAP从入门到放弃系列之流程管理概述

文章目录前言1.Process Management&#xff08;过程管理&#xff09;2.关键术语2.1Control recipe destination2.2 Process instruction characteristic2.3 Process message characteristic2.4 Process instruction category2.5 Process message category2.6 PI sheet3.关键配置…

RCLAMP0554S.TCT升特Semtech 5通道TVS二极管,0.5pF+20kV防护,超高速接口!

RCLAMP0554S.TCT&#xff08;Semtech&#xff09;产品解析与推广文案 一、产品定位 RCLAMP0554S.TCT是Semtech&#xff08;升特半导体&#xff09;推出的5通道超低电容TVS二极管阵列&#xff0c;专为超高速数据接口&#xff08;USB4/雷电4/HDMI 2.1&#xff09;提供静电放电&a…

【人工智能】DeepSeek的AI实验室:解锁大语言模型的未来

《Python OpenCV从菜鸟到高手》带你进入图像处理与计算机视觉的大门! 解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界 DeepSeek作为中国AI领域的先锋,以其开源大语言模型(LLM)DeepSeek-V3和DeepSeek-R1在全球AI研究中掀起波澜。本文深入探讨DeepSeek AI实验…

nacos+nginx动态配置大文件上传限制

前言 今天还要跟大家分享的一个点就是微服务网关gateway用webflux响应式不用servlet后&#xff0c;引发的一个忽略点差点在演示的时候炸锅&#xff0c;也不多讲废话&#xff0c;说说现象&#xff0c;说说处理就了事。 一、上传超过20MB的视频报错 配置在nacos里&#xff0c;读…

mr 任务运行及jar

mainclass如下&#xff1a;LoggingDriver

Python 数据分析:numpy,抽提,整数数组索引与基本索引扩展(元组传参)。听故事学知识点怎么这么容易?

目录1 代码示例2 欢迎纠错3 论文写作/Python 学习智能体------以下关于 Markdown 编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右Sm…

ECU开发工具链1.10版:更强大的测量、校准与数据分析体验.

汽车电子开发与测试领域&#xff0c;高效、精准且安全的工具是成功的基石。DiagRA X 作为一款广受认可的 Windows 平台综合解决方案&#xff0c;持续引领行业标准。其最新发布的 1.10 版本带来了显著的功能增强与用户体验优化&#xff0c;进一步巩固了其在 ECU 测量、校准、刷写…

Qt C++串口SerialPort通讯发送指令读写NFC M1卡

本示例使用的发卡器&#xff1a;https://item.taobao.com/item.htm?spma21dvs.23580594.0.0.52de2c1bVIuGpf&ftt&id18645495882 一、确定已安装Qt Serial Port组件 二、在.pro项目文件声明引用Serialport组件 三、在.h头文件内引用Serialport组件 四、在.cpp程序中实…

Go 语言开发中用户密码加密存储的最佳实践

在现代 Web 应用开发中&#xff0c;用户密码的安全存储是系统安全的重要环节。本文将结合 Go 语言和 GORM 框架&#xff0c;详细介绍用户密码加密存储的完整解决方案&#xff0c;包括数据库模型设计、加密算法选择、盐值加密实现等关键技术点。 一、数据库模型设计与 GORM 实践…

优化Facebook广告投放的五大关键策略

一、精确筛选目标国家用户在Audience的locations设置目标国家时&#xff0c;务必勾选"People living in this location"选项。系统默认会选择"People living in this location or recently in this location"&#xff0c;这会扩大受众范围&#xff0c;包含…

Debian-10-standard用`networking`服务的`/etc/network/interfaces`配置文件设置多网卡多IPv6

Debian-10-buster-standard用networking服务的/etc/network/interfaces配置文件设置多网卡多IPv6 Debian-10-buster-standard用networking服务的/etc/network/interfaces配置文件设置多网卡多IPv6 250703_123456 三块网卡 : enp0s3 , enp0s8 , enp0s9 /etc/network/interfac…

对话式 AI workshop:Voice Agent 全球五城开发实录

过去几个月&#xff0c;TEN Framework 团队与 Agora 和声网围绕 “对话式AI”题&#xff0c;踏上了横跨全球五大城市的精彩旅程——东京、旧金山、巴黎、北京、京都。 五场精心筹备的Workshop 场场爆满&#xff0c; 汇聚了来自当地及全球的开发者、创业者、产品经理与语音技术爱…

算法学习笔记:6.深度优先搜索算法——从原理到实战,涵盖 LeetCode 与考研 408 例题

在计算机科学领域&#xff0c;搜索算法是解决问题的重要工具&#xff0c;其中深度优先搜索&#xff08;Depth-First Search&#xff0c;简称 DFS&#xff09;凭借其简洁高效的特性&#xff0c;在图论、回溯、拓扑排序等众多场景中发挥着关键作用。无论是 LeetCode 算法题&#…

vue create 和npm init 创建项目对比

以下是关于 vue create 和 npm init 的对比分析&#xff1a; 1. 定位与功能 vue create 定位&#xff1a;Vue 官方提供的脚手架工具&#xff0c;基于 Vue CLI&#xff0c;用于快速创建标准化的 Vue 项目&#xff0c;支持 Vue 2 和 Vue 3。功能&#xff1a;提供交互式配置&…

C++ bitset 模板类

bitset<256> 数据类型详解 bitset<256> 是 C 标准库中的一个模板类&#xff0c;用于处理固定大小的位集合&#xff08;Bit Set&#xff09;。它可以高效地操作和存储二进制位&#xff0c;特别适合需要处理大量布尔标志或简单计数的场景。 基本定义与特性 1. 模板参…

通信握手言和:PROFINET转EtherCAT网关让汽轮机振动数据“破壁”传输

某大型电厂的关键汽轮机设备采用EtherCAT振动传感器进行实时监测&#xff0c;但由于工厂PLC振动分析系统基于PROFINET协议&#xff0c;数据无法直接接入&#xff0c;导致振动数据延迟、预警滞后&#xff0c;严重影响设备健康管理。传统的人工巡检和定期维护难以捕捉早期机械故障…

golang 中当 JSON 数据缺少结构体(struct)中定义的某些字段,会有异常吗

目录关键影响示例演示潜在问题与解决方案问题 1&#xff1a;逻辑错误&#xff08;零值干扰&#xff09;问题 2&#xff1a;忽略可选字段问题 3&#xff1a;第三方库验证最佳实践总结在 Go 语言中&#xff0c;当 JSON 数据缺少结构体&#xff08;struct&#xff09;中定义的某些…