IO概述
IO模型
scanf和printf函数看起来一个是从键盘接收数据输入,一个是将数据输出打印到显示器,但计算机内部处理的过程却没有那么简单。
我们通过输入/输出模型来简单了解一下这两个函数大体上的执行原理。
冯诺依曼体系计算机
冯诺依曼体系计算机(也叫存储程序控制型计算机),主要包含三大核心组件:CPU、IO设备以及存储器,而存储器当中最重要的则是内存储器,也就是内存。
直到今天,硬件设备的发展日新月异,但现代计算机仍没有脱离此体系。
冯诺依曼体系计算机,一个核心问题是CPU、内存以及IO设备三者之间的速度差异从而导致的性能瓶颈,这就是常说的"冯·诺依曼瓶颈”。这些年,我们的硬件设备:CPU,内存,I/O 设备都在不断迭代,不断朝着更快的方向努力。但是,在快速发展的过程中,有一个核心矛盾一直存在,那就是这三者之间的速度差异。我们可以形象地描述为:CPU 一天,内存一年;内存一天,IO 设备十年。(假设 CPU 执行一条普通指令需要一天,那么 CPU 读取内存就需要一年;假设内存之间传递单位数据需要一天,那么内存与 IO 设备之间传递单位数据就需要十年)。
具体的说,这个瓶颈指的是:
CPU 的处理速度远远快于内存和 I/O 设备,导致在等待数据处理和传输时,CPU 大部分时间处于空闲等待的状态。这种速度差异造成了显著的性能瓶颈,限制了整个系统的效率。
为了平衡这三者之间的速度鸿沟,一个简单有效的手段是引入缓冲区技术(也叫做缓存),下面我们简单介绍一下缓冲区技术。
缓冲区
缓冲区本质上是一块临时存储数据的内存区域(一般是在内存中分配的),它在速度较慢的内存和 I/O 设备与速度较快的 CPU 之间起到桥梁的作用。
为了更深入地解释缓冲的工作原理,以printf
和scanf
函数为例,可以更直观地解释其运作机制:
- 当你使用
printf
输出数据时,数据并不是立刻写入到输出设备(如屏幕)。它首先被放置在一个stdout缓冲区中,然后在满足特定条件时,数据会被刷新到输出设备。 - 当你使用
scanf
输入数据时,数据也不是直接从输入设备(如键盘)读取的。它首先被加载到一个stdin缓冲区中,然后scanf
从这个缓冲区中获取数据。
这个过程可以用下图来描述:
那么添加这样的一个缓冲区?有什么好处呢?
缓冲区的优点
使用缓冲区的好处显而易见——提高IO性能。
缓冲区是如何提高IO性能的?
printf函数和scanf函数:
- printf函数将程序(内存)中的数据打印到外部设备(显示器)上
- scanf函数代表从外部设备(键盘)中读取数据到程序(内存)中
这些都是非常典型的I/O操作过程。
我们都知道,I/O的过程效率很低。除了硬件性能本身的差异外,I/O操作的复杂性也是非常重要因素。每次进行I/O操作都会带来一些固定的开销,比如:
- 每次 I/O 操作都需要设备初始化和响应等待等。
- 操作系统管理 I/O 请求,涉及中断处理和上下文切换,这些都消耗了大量时间。
- 应用从用户态切换到内核态的系统调用也会带来额外的时间开销。(I/O操作普遍涉及系统调用)
- ....
总之,如果每输入或输出一个字符都要进行一次完整的I/O操作,那么这些固定的开销就会迅速积累,导致性能显著下降。
硬件层面的效率低下,我们没有办法通过软件层面的优化去解决。但对于这些大量的固定开销,我们可以通过缓冲区来进行效率优化。
缓冲区的主要目的是暂时存储数据,然后在适当的时机一次性进行大量的I/O操作。这样,多个小的I/O请求可以被组合成一个大的请求,有效地分摊了固定开销,并显著提高了总体性能。
拿上述两个函数,具体来说:
- scanf函数。当从键盘输入时,输入的字符首先被保存在
stdin
的缓冲区中,而且是一次性将较多的数据读取到这个缓冲区,这样就减少了总的IO次数,提高了效率。而程序只需要从缓冲区读取并处理数据,这个过程是纯粹的内存交互,效率很高。 - printf函数。输出到屏幕的内容会先被暂存到
stdout
的缓冲区。当满足某个触发条件后,这些内容会一次性写入并显示到屏幕,降低了与显示设备的交互频率。
如果你还不理解,就想象一次I/O操作就是搬运工搬运货物的过程,货物总量是一定的,搬一趟的时间也是差不多的。那么当然是一次性搬得尽量多,搬运的次数尽量少,总效率越高。
不使用缓冲区的I/O操作就像搬运工每次只能手提一个货物,频繁往返。而使用缓冲区,则好比搬运工使用了一个小推车,可以一次性搬运多个货物,大大提高了效率。
缓冲区的分类
从上述内容中,我们可以明确地看到缓冲区的一个显著特点:
这种“满足条件即触发数据传输”的行为,被我们称为机制。
基于这种自动刷新的触发条件的不同,我们可以将常见缓冲区划分为以下三种类型:
- 顾名思义,仅当缓冲区达到容量上限时,缓冲区才会自动刷新,并开始处理数据。否则,数据会持续积累在缓冲区中直到缓冲区满触发自动刷新。
- 缓冲区一旦遇到换行符,缓冲区就会自动刷新,所有数据都会被传输。
- 在此模式下,数据不经过中间的缓冲步骤,每次的输入或输出操作都会直接执行。这种方法适用于需要快速、实时响应的场合。例如,stderr(标准错误输出)就是这种方式,它经常被用来即时上报错误信息。
注意事项
关于缓冲区,有以下几点需要特别注意:
- 全缓冲区:唯一的自动刷新条件是缓冲区满。
- 行缓冲区:除了缓冲区满导致的自动刷新,还有遇到换行符的自动刷新机制。
- 手动刷新。大多数缓冲区提供了手动刷新的机制,比如使用fflush函数来刷新stdout缓冲区。 3.
- 当程序执行完毕(如main函数返回)时,缓冲区通常会自动刷新,除此之外,还有一些独特的机制也可以刷新缓冲区。但这些机制可能因不同的编译器或平台而异,不能作为常规手段。
- 不同的编译器和开发环境可能会对输出缓冲进行特殊设置,尤其是在调试模式下,以便提供更好的调试体验。比如在VS的Debug模式下,即使没有换行符,
printf
函数的输出通常也会立即显示在控制台上。这种行为是为了帮助程序员更有效地调试程序,即时看到他们的输出,而不需要固定等待缓冲区刷新条件。
至此,我们已经对输入输出的基本概念有了全面的了解。接下来,我们将深入探讨具体的函数如何使用。
当涉及到函数调用时,虽然查阅文档是重要的学习步骤,但真正的关键在于
流的概念
流是一个动态的概念,可以将一个字节形象地比喻成一滴⽔,字节在设备、文件和程序之间的传输就是流,类似于⽔在管道中的传输,可以看出,流是对输⼊输出源的一种抽象,也是对传输信息的一种抽象。
C语⾔中,I/O操作可以简单地看作是从程序移进或移出字节,这种搬运的过程便称为流 (stream)。程序只需要关⼼是否正确地输出了字节数据,以及是否正确地输⼊了要读取字节数据,特定I/O设备的细节对程序员是隐藏的。
。数据如流水一般从一处流向另一处,C++形象的将此过程称为流。程序的输入指的是从输入文件将数据传送给程序,程序的输出指的是从程序将数据传送给输出文件。C++输入输出包含以下三个方面的内容:
- 对系统指定的标准设备的输入和输出。即从键盘输入数据,输出到显示器屏幕。这种输入输出称为标准的输入输出,简称标准I/O。
- 以外存磁盘文件为对象进行输入和输出,即从磁盘文件输入数据,数据输出到磁盘文件。以外存文件为对象的输入输出称为文件的输入输出,简称文件I/O。
- 对内存中指定的空间进行输入和输出。通常指定一个字符数组作为存储空间(实际上可以利用该空间存储任何信息)。这种输入和输出称为字符串输入输出,简称串I/O。
文本流
文本流,也就是我们常说的以文本模式读取文件。文本流的有些特性在不同的系统中可能不同。其中之一就是文本行的最⼤长度。标准规定⾄少允许254个字符。另一个可能不同的特性是文本行的结束⽅式。例如在Windows系统中,文本文件约定以一个回车符和一个换行符结尾。但是在Linux下只使用一个换行符结尾。
标准C把文本定义为零个或者多个字符,后⾯跟一个表⽰结束的换行符(\n).对于那些文本行的外在表现形式与这个定义不同的系统上,库函数负责外部形式和内部形式之间的翻译。例如,在Windows系统中,在输出时,文本的换行符被写成一对回车/换行符。在输⼊时,文本中的回车符被丢弃。这种不必考虑文本的外部形势而操纵文本的能力简化了可移植程序的创建。
二进制流
二进制流中的字节将完全根据程序编写它们的形式写⼊到文件中,而且完全根据它们从文件或设备读取的形式读⼊到程序中。它们并未做任何改变。这种类型的流适用于非文本数据,但是如果你不希望I/O函数修改文本文件的行末字符,也可以把它们用于文本文件。
c语⾔在处理这两种文件的时候并不区分,都看成是字符流,按字节进行处理。
我们程序中,经常看到的文本方式打开文件和二进制方式打开文件仅仅体现在换行符的处理上。
比如说,在widows下,文件的换行符是 \r\n ,而在Linux下换行符则是 \n .
当对文件使用文本⽅式打开的时候,读写的windows文件中的换行符 \r\n 会被替换成\n读到内存中,当在windows下写⼊文件的时候,\n 被替换成 \r\n 再写⼊文件。如果使用二进制⽅式打开文件,则不进行 \r\n 和 \n 之间的转换。 那么由于Linux下的换行符就是 \n ,所以文本文件⽅式和二进制⽅式⽆区别。
文件流
标准库函数是的我们在C程序中执行与文件相关的I/O任务非常⽅便。下⾯是关于文件I/O的一般概况。
- 程序为同时处于活动状态的每个文件声明一个指针变量,其类型为FILE*。这个指针指向这个FILE结构,当它处于活动状态时由流使用。
- 流通过fopen函数打开。为了打开一个流,我们必须指定需要访问的文件或设备以及他们的访问⽅式(读、写、或者读写)。Fopen和操作系统验证文件或者设备是否存在并初始化 FILE。
- 根据需要对文件进行读写操作。
- 最后调用fclose函数关闭流。关闭一个流可以防⽌与它相关的文件被再次访问,保证任何存储于缓冲区中的数据被正确写⼊到文件中,并且释放FILE结构。
标准I/O更为简单,因为它们并不需要打开或者关闭。
I/O函数以三种基本的形式处理数据:单个字符、文本行和二进制数据。对于每种形式都有一组特定的函数对它们进行处理。