用户缓冲区

news2024/9/20 0:20:45

目录

  • 1. 引入问题
  • 2. 用户缓冲区
    • 2.1 解答上述问题
    • 2.2 缓冲区刷新策略
  • 3. 全缓冲案例

1. 引入问题

// 输出信息带换行,调用完后close(1)
int main()    
{    
    const char* s1 = "this is fwrite\n";                                                                         
    const char* s2 = "this is write\n";    
    printf("this is printf\n");    
    fprintf(stdout, "this is fprintf\n");    
    fwrite(s1, strlen(s1), 1, stdout);    
    write(1, s2, strlen(s2));    
    close(1);    
    return 0;    
}    

在这里插入图片描述

// 输出信息不带换行,调用完后close(1)
int main()    
{    
    const char* s1 = "this is fwrite";                                                                         
    const char* s2 = "this is write";    
    printf("this is printf");    
    fprintf(stdout, "this is fprintf");    
    fwrite(s1, strlen(s1), 1, stdout);    
    write(1, s2, strlen(s2));   
    close(1);    
    return 0;    
}    

在这里插入图片描述

// 输出信息不带换行,没有close(1)
int main()    
{    
    const char* s1 = "this is fwrite";                                                                         
    const char* s2 = "this is write";    
    printf("this is printf");    
    fprintf(stdout, "this is fprintf");    
    fwrite(s1, strlen(s1), 1, stdout);    
    write(1, s2, strlen(s2));    
    return 0;    
}    

在这里插入图片描述

现象:printf,fprintf,fwrite 这三个都是 C 库函数,write 是系统调用。当输出信息去掉 \n,调用语言级别的函数后 close(1) 关闭标准输出,三个函数内的输出信息不会被打印在屏幕上(输出信息有 \n 就正常打印),但是作为系统调用的 write 却依旧正常打印。

  • 为什么最后没有调用 close(1) 就正常打印了?
  • 而为什么调用了 close(1) ,printf,fprintf,fwrite 就不打印了呢?close(1) 不是在 printf 之后才关闭的吗?
  • 又为什么 write() 不受影响??

2. 用户缓冲区

2.1 解答上述问题

讲文件描述符的时候,我们认识了系统调用 write(),并且我们知道,C 中的诸如 printf,fprintf,fwrite 这样的打印函数,底层一定是调用的 write,。

当我们不调用 close(1),即便输出信息没有换行符,它是可以正常写入到显示器的,这是毋庸置疑的,所以问题肯定出在 close(1) 这里。而事实也确实是,在 close 之前,输出信息确实输出了,只不过是写入到缓冲区了,并没有写入到显示器上。

需要注意的是,这个缓冲区一定不在操作系统内,它不是系统级别的缓冲区。如果这个缓冲区在内核中的话,那么 close 关闭文件描述符时,就一定会把缓冲区的内容刷新到显示器上。所以可以证明 C 语言各种输出打印函数,它写入的这个缓冲区不是系统级别的缓冲区。

而 write 作为系统调用,自然是往内核中的缓冲区写入,所以当 close(1) 时,内核缓冲区刷新到显示器,信息正常打印。如果这样说明,那就势必会有另一个问题:

你之前不是还说 printf,fprintf,fwrite 这样的函数,最终都是要调用 write 的吗?现在不是刚说调用 write 的话,就往内核的缓冲区中写入,那最终不还是会刷新到显示器上,那我为啥没看到信息

其实不然,语言级别的函数并不会一上来就直接调 write 系统调用去做写入操作,而是先把输出信息写入到自己语言级别的缓冲区上(你没听错,C 也有自己的缓冲区),而这种缓冲区称为用户缓冲区, 根据 C 自己的刷新策略,再决定要不要调用 write 系统调用,将缓冲区的数据通过 write 写入到内核的缓冲区中,再进而刷新到外设上。

上述代码的三个语言库函数,它们先把内容写入到用户缓冲区(不带换行符),然后进程退出时,本应该将缓冲区的内容刷新出去,但却发现调用完几个 C 式接口后,1号文件描述符被关闭了,该进程的文件描述符表中的下标为1的位置处的指针就不再指向显示器文件了,因此也就无法调用 write() 向显示器写入。这就是为什么语言层面上的三个输出函数,最终没能将数据打印到显示器上。简言之,就是库函数中的输出信息,并不会第一时间交给操作系统,而是先仍到自己语言的缓冲区,这样一来,当指向显示器文件的文件描述符被关闭,那么在语言层面上,缓冲区的内容就再也无法刷新到显示器上。

而为什么输出信息带上 \n 的时候,即便调用完后 close(1),这些内容也依旧可以正常写入到显示器,是因为显示器文件的刷新策略就是行刷新,所以在诸如 printf 将内容写入到用户缓冲区,发现内容中有 \n,就立即调用 write() 系统调用将数据进行刷新,而用户缓冲区刷新的本质,就是将数据通过1号文件描述符 + write() 写入到内核缓冲区中。

曾经在讲 进程的创建、终止 的时候,我们做过一个实验,exit 退出进程时,信息正常打印,但是系统调用 _exit 退出时,一个信息也不打,现在我们就应该要清楚,并不是 _exit 不想做这件事,而是它做不到!调用 printf 打印输出,内容是写入到 C 自己的缓冲区, _exit 是系统函数,它可无法看到上层的用户缓冲区,更别提对上层用户缓冲区的内容做刷新了。那 exit 为什么能刷新,那不就是因为它也是 C 式接口吗,进程退出时,调一下 write + fd=1 的方式,将自己的缓冲区内容刷新到内核中不就行了吗?!包括 C 中的强制刷新 fflush 这样的函数,底层都是通过调 write 系统调用实现的。

2.2 缓冲区刷新策略

关于缓冲区是根据什么策略刷新的,我们只需要关注上层,即用户缓冲区即可。内核缓冲区是操作系统内部的事情,不需要我们关心,怎么刷新,用户无法控制,因为用户无法访问系统内核。

用户缓冲区刷新策略:

  1. 无缓冲:直接刷新,即写即刷
  2. 行缓冲:直到碰到 \n 才刷新,即不换行不刷新(显示器默认刷新策略,根据用户的阅读习惯决定的)
  3. 全缓冲:累计到缓冲区空间满了才进行刷新(文件写入一般都采用全缓冲,因为文件写入一般不会立马查看)
  4. 不管什么情况,进程退出时,缓冲区都强制刷新

缓冲区的作用:

  1. 解决用户的效率问题。
    就好比快递行业,如果没有快递公司帮助用户运输快递,假设今天我有一个很重要的文件需要给远在天边的客户,那我就得自己坐飞机、坐高铁跑过去,有了快递运输,我可以直接上一个某丰寄过去就行了。这就是解决了我(用户)办事的效率。但是这并不解决原来该有的时间问题,你交给快递公司运输,快递公司还是需要时间,只不过是快递公司帮你做了这件事,因此提高的只是用户的效率问题,底层该怎么做还是怎么做。

    快递公司也总不能我寄一个快递,就给我送一个快递吧。那一个小区、一个学校一天多少人寄快递,快递公司不得跑成百上千趟,亏到裤衩子都得卖了。所以快递行业也是一个现实版缓冲区的写照。

    所以缓冲区是如何提高用户效率的呢?? ---- 当我在 C 中调用诸如 printf,fprint,fwrite 等接口时,都是先将信息丢到缓冲区,然后函数就直接返回了,返回之后直接执行后续代码,这样在用户层,调用 C 语言接口的速度变快,用户效率就得到了提高;如果没有缓冲区,那么用户每调一次 printf,fprint,fwrite 等接口时,C 语言就调一次 write 将数据刷新出去,这样效率就被拉低了。

    上述的行缓冲,就相当于当天的寄件当天发(默认的);全缓冲则是等到我的驿站装不下了,再一次性发出去;无缓冲可以理解为 “八百里加急”。

  2. 配合格式化。
    write 可是一个只能接收字符串输入的系统调用,在我们键盘输入的 123 或者显示器打印的 123,这些在操作系统看来都是字符串!而诸如 printf 格式化输出,就是在整合输出信息,将输出信息转换为字符串,再写入自己的缓冲区中,进而写入内核。调用 a = 1; printf("this is %d", a) 写入用户缓冲区的信息,最终都会被解释为: this is 1

  • 那这个 C 语言提供的缓冲区具体在哪里呢??
    谈论缓冲区就离不开文件操作,在 C 中,任何文件操作都离不开 FILE,FILE 就是封装了一个 struct,其结构体内又封装了 fd 文件描述符这样的字段,还有打开文件的缓冲区字段和相关维护信息。简言之,缓冲区就在 FILE 这个结构体内。

所以当执行 fprintf(stdout, "this is write ope\n"); 无非就是将输出信息拷贝到 stdout,stdout 是一个 FILE 结构体,里面有缓冲区,再检查是否有 \n 或者 缓冲区是否满了,然后调用 write 刷新到外设。这也是为什么我们在 C 中 fopen 得到的返回值 FILE*,后续任何文件操作都不离开这个 FILE*。

而假设在 C 内一次性打开10个文件,那就会有10个缓冲区,每个文件都会有自己的缓冲区,因此每个文件也都才能有自己的文件描述符。每一个文件都配置一个语言层缓冲区,再通过文件描述符把缓冲区内容刷新到外设。


3. 全缓冲案例

int main()
{
    const char* s1 = "this is fwrite\n";
    const char* s2 = "this is write\n";
    printf("this is printf\n");
    fprintf(stdout, "this is fprintf\n");
    fwrite(s1, strlen(s1), 1, stdout);
    write(1, s2, strlen(s2));
    sleep(10);
    return 0;
}

在这里插入图片描述

现象:重定向到文件输出,除了 write 系统调用会立即写入文件,C 式接口都不没有将信息写入到文件中。

这是因为当我们运行的同时,做重定向把信息写入到文件中,缓冲区的刷新策略就变为了全缓冲(那么即便输出信息带有换行符,也不刷新)。作为用户,第一观察的是显示器,所以显示器才以行缓冲作为刷新策略,而用户第一时间并不会查看文件,文件所承担的更多职责是存储数据,而非拿来看的。因此涉及到文件写入操作,缓冲区的刷新策略都是全缓冲(为了提高效率)。

而代码中那几条输出信息,不足以写满缓冲区,所以只有当进程退出了,缓冲区的数据才会被刷新写入到文件中。我们也确实看到了,一开始只有 write 输出的信息,并没有 C 式接口的信息,这也是因为 write 是系统调用,它直接就可以写入到内核级别的缓冲区,而文件写入,改变缓冲区的刷新策略,变的是用户级别的缓冲区,跟内核没有关系,因此 write 该写入还是正常写入。而几条 C 式接口的信息虽然也写入了,但是停留在缓冲区中不被刷新。

// 最后 frok() 创建子进程
int main()
{
    const char* s1 = "this is fwrite\n";
    const char* s2 = "this is write\n";
    printf("this is printf\n");
    fprintf(stdout, "this is fprintf\n");
    fwrite(s1, strlen(s1), 1, stdout);
    write(1, s2, strlen(s2));
    fork();
    return 0;
}

在这里插入图片描述

现象:正常运行的话,信息正常写入到显示器,但是当运行时做了重定向,运行结果就显得非常奇怪,只有 write 这个系统调用输出了一次,其它 C 式接口都输出了两次,这是为什么呢??

这其实是 fork 导致的现象,执行 fork(),操作系统创建子进程,当父子进程一方有写入数据的操作时,那么会发生写时拷贝。而语言是属于用户层的,所以语言中提供的缓冲区,自然也是用户级别的缓冲区,其本质就是在进程地址空间中堆区 malloc 出来的一段内存空间而已。

而进程退出之前,就已经 fork 了,代表着进程退出之前就已经有子进程了。此时进程退出,需要刷新缓冲区,刷新的本质就是将缓冲区的内容全部写入到内核中,是一种清空的行为,而清空也算是写入操作的一种!既然是写入,那就必须要发生写时拷贝问题,因此父子进程各自有一份缓冲区数据。进程退出时,父子进程都要对自己的缓冲区做刷新操作,所以 C 式接口的信息输出了两次。

所以造成这个现象的原因为以下几点:

  1. 重定向写入文件,用户缓冲区刷新策略变为全缓冲
  2. 父进程要刷新缓冲区,本质是一种写入数据的行为。而缓冲区是父子进程共享的,要对共享数据做写入,就要写时拷贝,因此子进程拷贝父进程的缓冲区作为自己的缓冲区
  3. 父子进程退出,都要刷新缓冲区,因此同样的数据,父子进程都调用了 write,把缓冲区的数据写入到内核,进而刷新到文件中。

如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!

感谢各位观看!

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

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

相关文章

数据手套横向对比:4款手套哪款适合您?

数据手套是与虚拟物体交互、记录手部动作以及制作手部动画的重要工具。数据手套根据类别可分为只传输动作数据的数据手套,拥有触觉震动反馈的触觉手套,带有外骨骼的力反馈手套等。这些手套根据功能性的不同可分别应用于不同行业之中,在本文中…

【项目功能扩展】在线网站 -用户管理功能(用户注册登录修改等、利用cookie存储用户会话状态)

文章目录 0. 前言开发环境 & 涉及技术 1. 宏观结构2. 后端部分① sqlite 管理类② user 管理类 3. 前端部分(与后端交互)① 登录② 注册③ 查看登录用户的信息④ 更新用户信息⑤ 登出用户 & 注销用户注意 效果演示 0. 前言 源码链接&#xff1a…

【Unity】简易而又实用的概率算法

1.两个数中任选一个&#xff08;抛硬币&#xff09; 基础版本&#xff1a; public int RandomBetweenTwoNumber(int a,int b) {float random Random.Range(0,1f);return radom<0.5f ? a : b ; } 升级版本&#xff08;支持概率调整&#xff09;&#xff1a; /*pa表示“…

并发编程:读写锁

一、ReentrantReadWriteLock 1.ReentrantReadWriteLock 是什么&#xff1f; ReentrantReadWriteLock 实现了 ReadWriteLock &#xff0c;是一个可重入的读写锁&#xff0c;既可以保证多个线程同时读的效率&#xff0c;同时又可以保证有写入操作时的线程安全。 public class …

CDGA|做好数据治理的几个策略,不看后悔

在当今这个数据驱动的时代&#xff0c;数据已成为企业最宝贵的资产之一。然而&#xff0c;随着数据量的爆炸性增长和来源的多样化&#xff0c;如何有效地管理和利用这些数据&#xff0c;即数据治理&#xff0c;成为了企业面临的重要挑战。 良好的数据治理不仅能够提升数据质量&…

中医世家龚洪海博士:用医术和真诚赢得患者的心

医生&#xff0c;可以说是世界上最伟大的人&#xff0c;他们以高超的医疗技术和崇高的职业道德&#xff0c;以患者为先&#xff0c;为患者带来生的奇迹&#xff0c;抚平患者的病痛&#xff0c;是生命忠诚的的捍卫者。明代御医龚廷贤龚氏传人龚洪海博士就是这样一个&#xff0c;…

英国数字化战略下的人工智能时代:挑战与发展机遇

文章目录 前言一、英国数字化转型初探二、数字化转型重点举措1、 供应链2、金融市场3、数字基础设施4、科学研究5、数字技术赋能绿色转型6、数字包容性7、国际合作:重视与发展中国家合作8、完善数字民主建设三、战略启示前言 后疫情时代,英国正面临包括首相更迭频繁导致的内…

AnyGPT:多模态语言模型,任意处理语音、图像和音乐

人工智能咨询培训老师叶梓 转载标明出处 大模型的能力大多局限于文本处理&#xff0c;而现实世界环境本质上是多模态的&#xff0c;涉及视觉、语言、声音和触觉等多种感知渠道。为了使LLM能够更好地模拟人类的多模态感知能力&#xff0c;复旦大学的研究团队提出了AnyGPT&#x…

巴西美客多广告打法,这样开广告有泼天的流量!

听说做巴西美客多本土店不需要开广告就有流量&#xff1f;这是真的吗&#xff1f;相信这对于一直在做欧美市场的卖家来说&#xff0c;简直是不敢相信&#xff0c;What? 有运营巴西美客多本土店铺多年的卖家说&#xff0c;确实是不开广告也能获得不错的流量&#xff0c;过去几…

汽车EDI:montaplast EDI对接

Montaplast 是一家总部位于德国的全球知名汽车零部件供应商&#xff0c;专注于高精度塑料部件的设计、开发和生产。公司成立于1958年&#xff0c;主要为汽车行业提供轻量化、高性能的塑料解决方案。Montaplast 以其在注塑成型技术、表面处理和装配技术方面的专业能力而著称&…

vue3 项目中使用git

一.vue项目创建 二.创建本地仓库并和远程仓库进行绑定 在vue3-project-git 项目文件夹下 初始化一个新的Git仓库&#xff0c;可以看到初始化成功之后就会出现一个.git文件&#xff0c;该文件包含所有必要的 Git 配置和版本控制信息。 创建远程仓库: 打开gitee ,点击右上角 ‘…

电源模块检测方法之功率因数的测量

在设计和维护电源系统时&#xff0c;功率因数是一个不可忽视的参数。那么功率因数是什么呢&#xff1f;怎么测试电源模块的功率因数呢&#xff1f;又该如何提高功率因数呢&#xff1f;让我们一起来探讨吧。 一、功率因数概述 功率因数是指交流电路中有功功率和视在功率的比值&a…

安全产品概述

防火墙 防火墙的核心功能是过滤掉有害的流量&#xff0c;在专用网络和公共网络之间建立保护屏障。防火墙过滤通常基于一系列规则&#xff0c;如 IP 地址、域名、协议、端口号、关键字等&#xff0c;对入站和出站的流量进行过滤。这些规则也称为访问控制列表&#xff08;ACCESS…

假装勤奋,无效努力!看了上热搜的北京十一学校,我更加理解了衡水家长们的无奈——早读(逆天打工人爬取热门微信文章解读)

怀念伟大 引言Python 代码第一篇 看了上热搜的北京十一学校&#xff0c;我更加理解了衡水家长们的无奈第二篇 股市路注定是孤独的结尾 引言 不断尝试新的改变 发现最近把很多时间用在股票上 但是总结复盘却没有多 所以得改 一般是看一个视频讲解 然后一不小心就睡着了 日复一日…

超轻量级、支持插件的 .NET 网络通信框架

目录 前言 项目介绍 项目环境 项目功能 1、功能导图 2、项目文档 项目特点 1、传统 IOCP 与 TouchSocket 的 IOCP 模式 2、数据处理适配器 3、兼容性与适配 项目使用 1、Nuget安装 2、TcpService 3、TcpClient 4、TcpClient 断线重连 项目案例 1、工程师软件工…

elementUI之不会用

form表单注意事项 <template><div class"container"><el-form :model"form" label-width"80px" :rules"RulesName" ref"loginForm"><el-form-item label"姓名" prop"username">…

【阿雄不会写代码】全国职业院校技能大赛GZ036第九套

也不说那么多了&#xff0c;要用到这篇博客&#xff0c;肯定也知道他是干嘛的&#xff0c;给博主点点关注点点赞&#xff01;&#xff01;&#xff01;这样博主才能更新更多免费的教程&#xff0c;不然就直接丢付费专栏里了&#xff0c;需要相关文件请私聊

软件测试外包公司分享:软件产品鉴定测试内容和作用

软件产品鉴定测试是指对软件在不同阶段进行系统性、全面性的检测与评估&#xff0c;确保产品在功能、安全性、性能等方面达到既定标准。这对企业选择、验证及应用软件产品至关重要。 软件产品鉴定测试的内容主要包括以下几项&#xff1a;   1、功能测试&#xff1a;验证软件…

Sumsub 获评 Gartner® “新兴技术:针对在线欺诈预防的 GenAI 安全服务”代表性供应商

AI 为业务增长提供了巨大机遇。但也带来了重大威胁&#xff0c;比如可能会导致企业蒙受数百万美元损失的深度伪造攻击。 近日&#xff0c;全球领先的验证平台 Sumsub 被权威机构 Gartner 列入《新兴技术&#xff1a;针对在线欺诈预防的 GenAI 安全服务》报告的代表性供应商名单…

十二、C语言:内存函数

一、memcpy 1.1 使用 void * memcpy ( void * destination, const void * source, size_t num ); 1.前两个参数类型都是void*&#xff0c;因此可以拷贝任何数据类型&#xff1b; 2.num参数为要拷贝的字节数&#xff1b; int main() {char arr[10] "abcdef";char b…