标准IO

一、文件I/O和标准I/O

文件I/O:文件I/O也称为不带缓冲的I/O(unbuffered I/O)。不带缓冲指的是每个read,write都调用内核中的一个系统调用。也就是一般所说的低级磁盘I/O,遵循POSIX相关标准,任何兼容POSIX标准的操作系统上都支持文件I/O。——是操作系统提供的基本IO服务,与OS绑定,特定于Linux或Unix平台。

标准I/O:标准I/O是ANSI C建立的一个标准I/O模型,是一个标准函数包和stdio.h头文件中的定义,不依赖系统内核,所以移植性强。又称为高级磁盘I/O,遵循ANSI C相关标准。只要开发环境中有标准I/O库,标准I/O就可以使用。(Linux 中使用的是glibc,它是标准C库的超集。不仅包含ANSI C中定义的函数,还包括POSIX标准中定义的函数。因此,Linux 下既可以使用标准I/O,也可以使用文件I/O)。标准I/O库处理很多细节,例如缓冲分配,以优化长度执行I/O等。

文件I/O:读写文件时,每次操作都会执行相关系统调用。这样处理的好处是直接读写实际文件,坏处是频繁的系统调用会增加系统开销。使用时一般用户需要自己维护一个用户缓冲区,不过在Linux系统中,都使用内核高速缓冲用于提高效率,因此文件I/O的读写系统调用实际上是在内核缓冲区和进程的用户缓冲区之间进行的数据复制

标准I/O:可以看成在文件I/O的基础上由标准I/O库封装并维护了缓冲机制(流缓冲,都在用户空间)。比如调用fopen函数,不仅打开一个文件,而且建立了一个流缓冲(读写模式下将建立两个缓冲区),还创建了一个包含文件和缓冲区相关信息的FILE结构,从而先读写流缓冲区,必要时再访问实际文件,从而减少了系统调用的次数,使用库函数在用户空间的流缓冲上和用户交互的效率高于直接从内核读写数据的效率,因此提高了I/O效率。

带缓存的I/O操作是C标准库实现的,第一次调用带缓存的文件操作函数时标准库会自动分配内存(通常是调用malloc在用户空间上分配堆内存)作为流缓存并且读出一段固定大小的内容存储在缓存中。所以以后每次的读写操作并不是针对磁盘上的文件直接进行的,而是针对标准库的流缓存的。何时从磁盘中读取文件或者向磁盘中写入文件有标准库的机制控制。

文件I/O:所有I/O函数都是围绕文件描述符进行的。当打开一个文件时,即返回一个文件描述符,后续的I/O操作也都使用该文件描述符进行操作。可以访问不同类型的文件如普通文件、设备文件和管道文件等。

标准I/O:所有操作都是围绕流(stream)进行的。当用标准I/O库打开或创建一个文件时,即将一个流和一个文件相关联。通常只用来访问普通文件(???)

不带缓冲I/O:指进程不提供缓冲(但内核还是提供缓冲的),每调用一次write或read函数,直接进行系统调用。系统内核对磁盘的读写都会提供一个块缓冲(也被称为内核高速缓冲),用write函数对其写数据时,直接调用系统调用,将数据写入到块缓冲,当块缓冲满时才会数据写入磁盘。

如果要写数据到文件上(就是写入磁盘),内核先将数据写入到内核中所设的缓冲区,假如这个缓冲储存器的长度是100个字节,调用系统调用 ssize_t write (int fd,const void * buf,size_t count); 写操作时,设每次写入长度count=10个字节,那么需要调用10次系统调用才能把内核缓冲区写满,之前数据还是在缓冲区,并没有写入到磁盘,缓冲区满时才进行实际上的IO操作,即数据写入到磁盘。因此“不带缓冲”不是没有缓冲而是没有直接写进磁盘。

带缓冲I/O:指进程提供一个流缓冲,当用fwrite函数往磁盘写数据时,先把数据写入流缓冲区中,当达到一定条件(比如流缓冲区满,或主动刷新流缓冲)时才会把数据一次送往内核提供的块缓冲,再经块缓冲写入磁盘。(即双重缓冲)

上面的例子,内核缓冲区长度100字节,调用不带缓冲的IO函数write()需要调用10次系统调用,这样系统效率低。而使用标准I/O函数时,用户层建立另一个缓冲区(流缓冲),假设流缓冲的长度是50字节,用标准C库函数fwrite()将数据写入这个流缓冲区,流缓冲区满50字节后再一次调用系统调用write()将数据写入内核缓冲内,如果内核缓冲也被填满,那么内核缓冲区内数据就被写入到文件(实质是磁盘)。由此可以看出: ① 标准IO操作最终还是要调用无缓冲IO系统调用(带缓冲I/O本身就是在不带缓冲I/O基础上提供缓冲实现的),它们并不直接读写磁盘 ; ② 增加用户/流缓冲区可以减少系统调用的次数。

正常情况下,和磁盘交互的读写文件的大致流程

当应用程序尝试读取某块数据的时候, ① 如果这块数据已经存放在页缓存(也就是上面提到的内核高速缓存)中,那么这块数据就可以立即返回给应用程序,而不需要经过实际的物理读盘操作。 ② 如果数据在应用程序读取之前并未被存放在页缓存中,那么就需要先将数据从磁盘读到页缓存中去。

对于写操作来说,应用程序也会将数据先写到页缓存中去(如果是调用标准库I/O进行写操作,那么首先是写到标准库的流缓冲区,在一定条件之后,再写到页缓冲内;如果是系统调用,那么直接写到页缓冲内),数据是否被立即写到磁盘上取决于应用程序所采用的写操作机制:

  1. 如果用户采用同步写机制,那么数据会立即从页缓存写到磁盘上,应用程序会一直等到数据被写完为止;
  2. 如果用户采用延迟写机制,那么应用程序就完全不需要等到数据全部被写到磁盘,数据只要写到页缓存中就可以了。在延迟写机制的情况下,操作系统会定期地将放在页缓存中的数据刷到磁盘上。
  3. 如果用户采用异步写机制。在数据完全写到磁盘上的时候会通知应用程序。与异步写机制不同,延迟写机制在数据完全写到磁盘上的时候不通知应用程序,因此延迟写机制本身就存在数据丢失的风险,而异步写机制则不会有这方面的担心。

无缓存IO操作的数据流向:数据——内核缓存区——磁盘
标准IO操作的数据流向:数据——流缓存区——内核缓存区——磁盘

标准I/O中,一般由系统选择缓存的长度,并自动分配。标准I/O库在关闭流的时候自动释放缓存。

在标准I / O库中,一个效率不高的不足之处是需要复制的数据量。 当使用每次一行函数fgets和fputs时,通常需要复制两次数据:一次是在内核高速缓存和标准I/O缓存之间(当调用read和write时),第二次是在标准I/O缓存(通常系统分配和管理)和用户程序中的缓存(fgets的参数就需要一个用户行缓存指针)之间。

程序在读写文件时既可以调用C标准I/O库函数,也可以直接调用底层POSIX标准的的Unbuffered I/O函数,那么用哪一组函数好呢?

  1. 用Unbuffered I/O函数每次读写都要进内核,调一个系统调用比调一个用户空间的函数要慢很多,所以使用时在用户空间开辟I/O缓冲区还是必要的,此时用C标准I/O库函数就比较方便,并且省去了自己管理I/O缓冲区的麻烦。
  2. 用C标准I/O库函数要时刻注意I/O缓冲区的存在会使得和实际文件有可能不一致,在必要时需调用fflush() 。
  3. 我们知道Unix的传统是Everything is a file,I/O函数不仅用于读写常规文件,也用于读写设备,比如终端或网络设备。在读写设备时通常是不希望有缓冲的,例如向代表网络设备的文件写数据就是希望数据通过网络设备发送出去,而不希望只写到缓冲区里就算完事儿了,当网络设备接收到数据时应用程序也希望第一时间被通知到,所以设备的编程通常直接调用Unbuffered I/O函数。

fflush将流所有未写的数据送入(刷新)到内核(内核缓冲区),fsync将所有内核缓冲区的数据写到文件(磁盘)。至于究竟写到了文件中还是内核缓冲区中对于进程来说是没有差别的,如果进程A和进程B打开同一文件,进程A写到内核I/O缓冲区中的数据从进程B也能读到,因为内核空间是进程共享的。而C标准库的I/O缓冲区则不具有这一特性,因为进程的用户空间是完全独立的

※※※ 带缓冲I/O 和不带缓冲I/O的区别与联系

※ 标准I/O小结(缓冲区,I/O函数及其他相关问题)

※ 底层I/O(无缓冲)与 C标准I/O

二、标准I/O

2.1 错误报告

1、库函数错误报告

《C和指针》:标准库的许多函数包括I/O函数都会调用操作系统完成任务,若执行失败,则需要反馈给用户错误的原因。因此标准库在一个外部整形变量errno(在errno.h中定义)中保存错误代码之后把这个信息传递给用户程序,提示操作失败的准确原因。而perror函数即向用户报告这些错误。

#include<stdio.h>
//若message指向一个非空字符串,则打印一条错误代码的信息,格式为 message: ...
void perror(char const * message);

2、I/O函数错误判断

《Unix环境编程》:在大多数实现中,为每个流在FILE结构中维护两个标志:

  • 出错标志;
  • 文件结束标志。

例如fgetc函数已到达文件尾端或出错都返回EOF,因此可以调用ferror、feof函数区分。

#include<stdio.h>
int ferror(FILE* fp);   // 判断是否为出错返回:条件为真,返回真(非0);否则返回假(0)
int feof(FILE* fp);     //判断是否为结束返回:条件为真,返回真(非0);否则返回假(0)

总结:在标准库I/O函数的结果判断中,用ferror和feof比较好。

2.2 流和FILE结构

2.2.1 简述

对于流,《C和指针》里有一段解释得很好:

ANSI C进一步对I/O的概念进行了抽象。就C程序而言,所有的I/O操作只是简单地从程序移进或移出字节的事情。因此,毫不惊奇的是,这种字节流便被称为流(stream)。程序只需要关心创建正确的输出字节数据,以及正确地解释从输入读取的字节数据。特定I/O设备的细节对程序员是隐藏的。

简单地说,流是对信息的一种抽象。C在处理文件(文本文件和二进制文件)时,并不区分类型,都看成是字符流,按字节进行处理

C标准库提供两种类型的流:二进制流(binary stream)和文本流(text stream)。二进制流是有未经处理的字节构成的序列;文本流是由文本行(每行有0个或多个字符,并以’\n’结束)组成的序列。注意:在Unix中,并不区别这两种流

2.2.2 打开流
FILE* fopen(const char* restrict pathname, const* restrict type);
// 成功返回文件指针,失败返回NULL,errno提示出错的性质———需要判断打开流的执行情况
  • pathname参数指定要打开的文件。标准库将该打开文件用FILE结构进行管理。
  • type参数指定对该I/O流的读写方式。具体如下:(区分r/r+、w/w+、a/a+:多一个读或写;r+、w+、a+:文件是否丢弃、是否尾端写入)

![打开流type参数](D:/Program Files/share/Typora插入图片/打开流type参数.PNG)

打开流的6种方式 ,如下表:

![打开流的方式](D:/Program Files/share/Typora插入图片/打开流的方式.PNG)

fopen函数的用法

FILE* fp;
FILE=fopen("test.txt","w+");
if(FILE==NULL)
{
  perror("test.txt");       //必须处理打开流的错误情况
  exit(EXIT_FAILURE);
}
// ...
2.2.3 进程启动自动打开

当一个进程启动时,会自动打开标准输入、标准输出和标准错误输出三个文件以及三个流对应到默认的物理终端。当一个进程正常终止时(直接调用exit(),或从main返回),所有打开的文件、标准I/O流都会被关闭,所有未写缓冲数据的I/O流都会被冲洗(刷新)。

  • 这三个文件的描述符分别是0、1、2,头文件unistd.h 中有如下的宏定义来表示这三个文件描述符。
  • 同时这三个标准I/O流通过预定义(stdio.h)文件指针stdin,stdout,stderr 加以引用。并且FILE结构中对应包含它们的文件描述符STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
2.2.4 FILE结构

FILE结构包含了管理流所需要的所有信息:实际文件I/O的文件描述符、指向流缓存的指针(标准I/O缓存,由malloc分配,又称为用户态进程空间的缓存,区别于内核所设的缓存)、缓存长度、当前在缓存中的字节数、出错标志等。

// Linux
struct _IO_FILE {
  int _flags;

  char* _IO_read_ptr;
  char* _IO_read_end;
  char* _IO_read_base;      // 读缓冲区起始地址
  char* _IO_write_base;     // 写缓冲区起始地址
  char* _IO_write_ptr;
  char* _IO_write_end;      // write_end - write_base = 当前写缓存的字节数 ???
  char* _IO_buf_base;       // 缓冲区起始地址
  char* _IO_buf_end;        // buf_end - buf_base = 缓存长度

  char *_IO_save_base;
  char *_IO_backup_base;
  char *_IO_save_end;

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;              // 打开文件的文件描述符

  int _flags2;

  int _mode;

};

FILE结构貌似给定3个缓冲区,实际上_IO_read_base, _IO_write_base, _IO_buf_base都指向同一个缓冲区 。它们在第一次buffered I/O操作时由库函数自动申请空间,按照缓冲类型自动填充/刷新,最后由相应库函数负责释放。

2.3 缓冲区

2.3.1 简述

标准I/O库提供缓冲的目的是尽可能地减少使用read和write调用的次数,从而提高I/O效率。标准I/O库也对每个I/O流自动地进行缓冲管理,从而避免了应用程序需要考虑这一点所带来的麻烦。(一个流上执行第一次I/O操作时,相关标准I/O函数通常调用malloc获得需使用的缓冲区,最后由标准I/O函数负责释放)

标准I/O提供了三种类型的缓冲:

  1. 全缓冲。写时,填满流缓冲区才进行实际I/O写操作;读时,读完流缓冲区才进行实际I/O读操作。对于驻留在磁盘上的文件访问通常是由标准I/O库实施全缓冲

    术语“冲洗”说明标准I/O缓冲区的写操作(输出缓冲流)。缓冲区可由标准I/O例程自动冲洗,或者可以调用函数fflush强制冲洗一个流。

  2. 行缓冲。写时,遇到换行符或者填满流缓冲才进行实际I/O写操作;读时,遇到换行符或者读完流缓冲才进行实际I/O读操作。这允许我们一次输出一个字符(使用标准I/O函数fputc),但只有在写了一行之后才进行实际I/O操作。当流涉及一个终端时,通常使用行缓冲

    对于行缓冲有两个限制。第一,因为标准I/O库用来收集每一行的缓冲区的长度是固定的,所以只要填满了缓冲区,那么即使没有写一个换行符,也进行I/O操作。第二,任何时候只要通过标准I/O库要求从 (a) 一个不带缓冲的流,或者 (b) 一个行缓冲的流得到输入数据,那么就会造成冲洗所有行缓冲输出流。在 (b) 中所需的数据可能已在缓冲区中,并不必须在需要数据时才从内核读数据。很明显,从不带缓冲的一个流中进行输入要求立刻从内核得到数据。

  3. 不带缓冲。标准I/O库不对字符进行缓冲存储。相当于直接调用系统调用读写。标准出错流stderr通常是不带缓冲的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个换行符。

注意

  • 上述中的“实际I/O操作”并不是指磁盘的读写,而是指一次读/写的系统调用,即流缓冲区和内核缓冲区的一次数据交换。对于用户而言写入内核等同于写入文件
  • 带缓冲的两种类型 ① 至少都会在缓冲区满执行实际I/O操作;并且都会在 ② 文件正常关闭 和 ③ 主动刷新流 时执行实际I/O操作。

ISO C要求下列缓冲特征

  • 当且仅当标准输入和标准输出并不指向交互式设备时,它们才是全缓冲的。
  • 标准错误流绝不会是全缓冲的。

但是,这并没有告诉我们如果标准输入和标准输出涉及交互式设备时,他们是不带缓冲的还是行缓冲的;以及标准出错时不带缓冲的还是行缓冲的。

许多系统默认使用下列类型的缓冲

  • 标准出错是不带缓缓冲的。
  • 若是涉及终端设备的其他流,则是行缓冲的;否则是全缓冲的。
2.3.2 简单总结
  • 强调一下,所谓的带不带缓冲是指不同的流而不是函数。比如驻留在磁盘上的文件流是全缓冲的方式,标准输入/输出流缺省是行缓冲而标准错误缺省不带缓冲。并且在Liunx上的默认情况下,当标准输入输出连接终端时是行缓冲的,缓冲区大小1024字节,重定向到普通文件时,它们变为全缓冲,缓冲区大小4096字节
  • 对于全缓冲来说,读写操作是按照缺省的缓冲区大小(4K)进行的。具体来说:读时,每次读取4K大小的内容到流缓冲区,而程序是从流缓冲区读取数据,当缓冲区里的数据全部处理完后再从内核里读取4K的内容到流缓冲区;写文件的情况类似,当缓冲区写满内容时才会引起实际的I/O操作。
  • 行缓冲是指当遇到换行符’\n’或一行满时,才进行实际的/O操作。Linux缺省情况下一行最多容纳1024个字符,当超出这个范围时,即使没有遇到换行符,也引起实际的I/O操作。同时,当需要从读流缓冲时,也会刷新输出流
  • 又读又写的情况比较特殊。因为读写缓冲区只有一个,所以在读取内容到缓冲区之前会先把缓冲区里要更新的内容(如果有的话)刷新,写到文件
2.3.3 更改缓冲类型

对任何一个给定的流,如果我们并不喜欢这些系统默认的情况,则可调用下列函数中的一个更改缓冲类型:

void setbuf(FILE *restrict fp, char *restrict buf)
int setvbuf(FILE *restrict fp, char *restrict buf,int mode,size_t size) //成功,返回0;出错,非0

更改换成类型的这两个函数一定要在流已经被打开之后和对流执行任何操作之前调用。原因很简单:这些函数都需要一个有效的文件指针作为参数。

setbuf——打开或关闭缓冲区

  • 为了带缓冲,buf设置必须指向长度为BUFSIZ的缓冲区。在此之后缓冲类型由标准库根据操作磁盘文件还是终端设备确定为全缓冲还是行缓冲
  • 关闭缓冲,buf设置为NULL。

setvbuf——精确更改缓冲类型

  • mode参数:_IOFBF 全缓冲、 _IOLBF 行缓冲、 _IONBF 无缓冲:
    • 若指定全缓冲或行缓冲:buf和size参数分别为缓冲区指针和缓冲区长度,并且若buf设置为NULL,则由标准库自动为该流分配BUFSIZ大小的缓冲区。
    • 若指定无缓冲,则忽略buf和size参数。

具体细节如下表:

这里写图片描述

2.3.4 不同的缓冲类型读写验证
例子1
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void)
{
  char buf[30];     //用户自定义缓冲
  FILE *myfile = fopen("bbb.txt","r+");     //"read+"打开流,因磁盘文件,因此缺省全缓冲
  if(FILE==NULL){
    perror("test.txt");     //必须处理打开流的错误情况
    exit(EXIT_FAILURE);
  }
  //setvbuf(myfile,NULL,_IOLBF,BUFSIZ);     //设置为行缓冲

  printf("myfile->_IO_read_base:%p\n", myfile->_IO_read_base);
  printf("myfile->_IO_read_ptr:%p\n", myfile->_IO_read_ptr);
  printf("myfile->_IO_read_end:%p\n", myfile->_IO_read_end);
  printf("myfile->_IO_read_ptr - myfile->_IO_read_base:%ld\n", myfile->_IO_read_ptr - myfile->_IO_read_base);
  printf("myfile->_IO_read_end - myfile->_IO_read_base:%ld\n", myfile->_IO_read_end - myfile->_IO_read_base);
  printf("myfile->_IO_write_base:%p\n", myfile->_IO_write_base);
  printf("myfile->_IO_write_ptr:%p\n", myfile->_IO_write_ptr);
  printf("myfile->_IO_write_end:%p\n", myfile->_IO_write_end);
  printf("myfile->_IO_write_ptr - myfile->_IO_write_base:%ld\n", myfile->_IO_write_ptr - myfile->_IO_write_base);
  printf("myfile->_IO_write_end - myfile->_IO_write_base:%ld\n", myfile->_IO_write_end - myfile->_IO_write_base);
  printf("myfile->_IO_buf_base:%p\n", myfile->_IO_buf_base);
  printf("myfile->_IO_buf_end - myfile->_IO_buf_base:%ld\n", myfile->_IO_buf_end - myfile->_IO_buf_base);
  printf("\n");     //stdout:行缓冲

  //"bbb.txt"文件初始内容:
  //---------------------------
  //1\n234\n56789回车
  //\012回车
  //12345回车
  //123回车
  //12回车
  //回车
  //---------------------------
  int i;
  for(i=0;i!=5;i++){  //执行5次流输入
    fgets(buf, 6, myfile);      //全缓冲——4096字节;fgets函数格式化处理缓冲区字符:读取一行的一部分或资格完整行,并且都强制加一个NUL标识字符串
    printf("%s-",buf);
  }
  printf("\n");
  //fflush(myfile);

  printf("after read:\n");
  printf("myfile->_IO_read_base:%p\n", myfile->_IO_read_base);
  printf("myfile->_IO_read_ptr:%p\n", myfile->_IO_read_ptr);
  printf("myfile->_IO_read_end:%p\n", myfile->_IO_read_end);
  printf("myfile->_IO_read_ptr - myfile->_IO_read_base:%ld\n", myfile->_IO_read_ptr - myfile->_IO_read_base);
  printf("myfile->_IO_read_end - myfile->_IO_read_base:%ld\n", myfile->_IO_read_end - myfile->_IO_read_base);
  printf("myfile->_IO_write_base:%p\n", myfile->_IO_write_base);
  printf("myfile->_IO_write_ptr:%p\n", myfile->_IO_write_ptr);
  printf("myfile->_IO_write_end:%p\n", myfile->_IO_write_end);
  printf("myfile->_IO_write_ptr - myfile->_IO_write_base:%ld\n", myfile->_IO_write_ptr - myfile->_IO_write_base);
  printf("myfile->_IO_write_end - myfile->_IO_write_base:%ld\n", myfile->_IO_write_end - myfile->_IO_write_base);
  printf("myfile->_IO_buf_base:%p\n", myfile->_IO_buf_base);
  printf("myfile->_IO_buf_end - myfile->_IO_buf_base:%ld\n", myfile->_IO_buf_end - myfile->_IO_buf_base);

  //读磁盘文件5次之后写,因此时从5次读之后的ptr处写入
  char* str1="str";                     //一行的一部分
  fputs(str1,myfile);
  char* str2="string\n";                //一个完整行
  fputs(str2,myfile);
  char* str3="string1\nstring2\n";      //两行
  fputs(str3,myfile);

  printf("\n");
  printf("after write:\n");
  printf("myfile->_IO_read_base:%p\n", myfile->_IO_read_base);
  printf("myfile->_IO_read_ptr:%p\n", myfile->_IO_read_ptr);
  printf("myfile->_IO_read_end:%p\n", myfile->_IO_read_end);
  printf("myfile->_IO_read_ptr - myfile->_IO_read_base:%ld\n", myfile->_IO_read_ptr - myfile->_IO_read_base);
  printf("myfile->_IO_read_end - myfile->_IO_read_base:%ld\n", myfile->_IO_read_end - myfile->_IO_read_base);
  printf("myfile->_IO_write_base:%p\n", myfile->_IO_write_base);
  printf("myfile->_IO_write_ptr:%p\n", myfile->_IO_write_ptr);
  printf("myfile->_IO_write_end:%p\n", myfile->_IO_write_end);
  printf("myfile->_IO_write_ptr - myfile->_IO_write_base:%ld\n", myfile->_IO_write_ptr - myfile->_IO_write_base);
  printf("myfile->_IO_write_end - myfile->_IO_write_base:%ld\n", myfile->_IO_write_end - myfile->_IO_write_base);
  printf("myfile->_IO_buf_base:%p\n", myfile->_IO_buf_base);
  printf("myfile->_IO_buf_end - myfile->_IO_buf_base:%ld\n", myfile->_IO_buf_end - myfile->_IO_buf_base);

  int ret=fflush(myfile);
  if(ret != 0)
    printf("fflush error\n");

  fgets(buf, 10, myfile);
  printf("second read:%s-",buf);

  printf("\n");
  printf("second read:\n");
  printf("myfile->_IO_read_base:%p\n", myfile->_IO_read_base);
  printf("myfile->_IO_read_ptr:%p\n", myfile->_IO_read_ptr);
  printf("myfile->_IO_read_end:%p\n", myfile->_IO_read_end);
  printf("myfile->_IO_read_ptr - myfile->_IO_read_base:%ld\n", myfile->_IO_read_ptr - myfile->_IO_read_base);
  printf("myfile->_IO_read_end - myfile->_IO_read_base:%ld\n", myfile->_IO_read_end - myfile->_IO_read_base);
  printf("myfile->_IO_write_base:%p\n", myfile->_IO_write_base);
  printf("myfile->_IO_write_ptr:%p\n", myfile->_IO_write_ptr);
  printf("myfile->_IO_write_end:%p\n", myfile->_IO_write_end);
  printf("myfile->_IO_write_ptr - myfile->_IO_write_base:%ld\n", myfile->_IO_write_ptr - myfile->_IO_write_base);
  printf("myfile->_IO_write_end - myfile->_IO_write_base:%ld\n", myfile->_IO_write_end - myfile->_IO_write_base);
  printf("myfile->_IO_buf_base:%p\n", myfile->_IO_buf_base);
  printf("myfile->_IO_buf_end - myfile->_IO_buf_base:%ld\n", myfile->_IO_buf_end - myfile->_IO_buf_base);

  //while(1);       //全缓冲、行I/O方式下:以 行字符处理方式 读完或写满才执行实际I/O操作。因此此处不执行阻塞操作,使进程正常终止,刷新流写入磁盘文件

  return 0;
}
/* stdout 内容 */
myfile->_IO_read_base:(nil)
myfile->_IO_read_ptr:(nil)
myfile->_IO_read_end:(nil)
myfile->_IO_read_ptr - myfile->_IO_read_base:0
myfile->_IO_read_end - myfile->_IO_read_base:0
myfile->_IO_write_base:(nil)
myfile->_IO_write_ptr:(nil)
myfile->_IO_write_end:(nil)
myfile->_IO_write_ptr - myfile->_IO_write_base:0
myfile->_IO_write_end - myfile->_IO_write_base:0
myfile->_IO_buf_base:(nil)
myfile->_IO_buf_end - myfile->_IO_buf_base:0

1\n23-4\n56-789
-\012
-12345-
after read:
myfile->_IO_read_base:0x55dbc3736650
myfile->_IO_read_ptr:0x55dbc3736668
myfile->_IO_read_end:0x55dbc3736670
myfile->_IO_read_ptr - myfile->_IO_read_base:24
myfile->_IO_read_end - myfile->_IO_read_base:32
myfile->_IO_write_base:0x55dbc3736650
myfile->_IO_write_ptr:0x55dbc3736650
myfile->_IO_write_end:0x55dbc3736650
myfile->_IO_write_ptr - myfile->_IO_write_base:0
myfile->_IO_write_end - myfile->_IO_write_base:0
myfile->_IO_buf_base:0x55dbc3736650
myfile->_IO_buf_end - myfile->_IO_buf_base:4096

after write:
myfile->_IO_read_base:0x55dbc3736670
myfile->_IO_read_ptr:0x55dbc3736670
myfile->_IO_read_end:0x55dbc3736670
myfile->_IO_read_ptr - myfile->_IO_read_base:0
myfile->_IO_read_end - myfile->_IO_read_base:0
myfile->_IO_write_base:0x55dbc3736668
myfile->_IO_write_ptr:0x55dbc3736682
myfile->_IO_write_end:0x55dbc3737650
myfile->_IO_write_ptr - myfile->_IO_write_base:26
myfile->_IO_write_end - myfile->_IO_write_base:4072
myfile->_IO_buf_base:0x55dbc3736650
myfile->_IO_buf_end - myfile->_IO_buf_base:4096
second read:12345-
second read:
myfile->_IO_read_base:0x55dbc3736650
myfile->_IO_read_ptr:0x55dbc3736650
myfile->_IO_read_end:0x55dbc3736650
myfile->_IO_read_ptr - myfile->_IO_read_base:0
myfile->_IO_read_end - myfile->_IO_read_base:0
myfile->_IO_write_base:0x55dbc3736650
myfile->_IO_write_ptr:0x55dbc3736650
myfile->_IO_write_end:0x55dbc3736650
myfile->_IO_write_ptr - myfile->_IO_write_base:0
myfile->_IO_write_end - myfile->_IO_write_base:0
myfile->_IO_buf_base:0x55dbc3736650
myfile->_IO_buf_end - myfile->_IO_buf_base:4096
/* bbb.txt 最终内容 */
1\n234\n56789回车
\012回车
12345strstring回车
string1回车
string2回车
回车

可以看出:

  • FILE结构的指针细节
    • IO_buf_base和IO_buf_end标识全缓冲流及大小(读写时保持不变),并且可以看出: ① 第一次读/写时建立全缓冲流; ② 并且读写使用一个缓冲区
    • IO_read_base和IO_read_end标识读缓冲区及大小,IO_read_ptr标识指向I/O下一个要读的字符。
    • IO_write_base和IO_write_end标识写缓冲区及大小,IO_write_ptr标识I/O下一个要写的位置
  • 标准I/O的字符处理
    • 一般:stdin、stdout的回车和磁盘文件的换行对应,并被标准I/O的缓冲类型、操作I/O作为行标识,其他字符(包括\n、\0)都作为一般字符处理。
    • 特殊:字符串中\n表示换行,但NUL在行I/O中只作为字符串标识(√) ;格式化输出\n也表示换行。
  • 标准I/O操作过程解读
    • 总括:先确定读写的原始数据,再由缓冲类型和 I/O类型判断读写的具体情况:缓冲类型决定内核和流缓冲何时进行实际 I/O读写,I/O类型决定如何解析流的数据 。读原始数据:“ 1\n234\n56789回车\012回车12345回车123回车12回车回车”;写原始数据:“strNULstring\nNUL string1\nstring2\nNUL”。
    • 全缓冲、行I/O读文件时:第一次行I/O引起第一次读磁盘文件,将文件的字符(含回车,计一个字符)尽可能写满缓冲区,此处 一次 就读入磁盘文件的全部32个字符(即写缓冲区为IO_read_base ~ IO_read_end);最终程序执行5次行I/O操作,只能处理24个字符,下个要读取字符由IO_read_ptr标识。
    • 行缓冲、行I/O读文件时:第一次行I/O引起第一次实际I/O操作读入“ 1\n234\n56789回车NUL”,此时程序需要3次行I/O处理行缓冲,处理完即进行第二次实际I/O操作读入“\012回车NUL”,一次行I/O处理,之后同理,最终也会读入32个字符到流缓冲。
    • 行I/O写入流:strNULstring\nNULstring1\nstring2\nNUL。去掉用于标识字符串的NUL,尽可能写满流缓冲区,因此写入流缓冲的数据为:strstring\nstring1\nstring2\n。文件可以正确解析换行符\n。(
    • 行I/O写入:读缓冲区大小变成0,IO_write_base从刚才的IO_read_end处开始至IO_buf_end。即从上次读完的位置(IO_read_ptr)开始写入,因此写入26个字符(含\n为一个字节)。因此覆盖了文件的一部分字符。不太懂为什么下次读出12345???
    • 再次写时,指针位置不懂。应该需要建立辅助的指针记录上次的读、写位置吧。
    • 无论是全缓冲或行缓冲的读/写磁盘文件,最终都会正确读/写。主要区别在于缓冲区大小不同造成的实际执行次数不同,而对于用户而言,可以认为所有原始数据都存储在一个大缓冲区等待被读即可。写磁盘文件时区别在于刷新流的时机不同导致需要刷新流的时机也不同,最终写入磁盘文件的时机不同。而对读磁盘并无区别
2.3.5 多进程与缓冲
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int globa = 4;

int main (void )
{
        pid_t pid;
        int vari = 5;

        printf ("before fork\n" );

        if ((pid = fork()) < 0){
                printf ("fork error\n");
                exit (0);
        }
        else if (pid == 0){
                globa++ ;
                vari--;
                printf("Child changed\n");
        }
        else
                printf("Parent did not changed\n");

        printf("globa = %d vari = %d\n",globa,vari);
        exit(0);
}

执行结果:
输出到标准输出
[root@happy bin]# ./simplefork
before fork
Child changed
globa = 5 vari = 4
Parent did not changed
globa = 4 vari = 5

重定向到文件时before fork输出两边
[root@happy bin]# ./simplefork>temp
[root@happy bin]# cat temp
before fork
Child changed
globa = 5 vari = 4
before fork
Parent did not changed
globa = 4 vari = 5

分析直接运行程序时标准输出是行缓冲的,很快被新的一行冲掉。而重定向后,标准输出是全缓冲的。当调用fork时before fork这行仍保存在缓冲中,并随着数据段复制到子进程缓冲中。这样,这一行就分别进入父子进程的输出缓冲中,余下的输出就接在了这一行的后面。(子进程复制了父进程的进程空间,缓冲区也复制。)

标准I/O缓冲区详解

2.4 标准I/O函数

2.4.1 简述

标准I/O函数定义了三种不同类型的非格式化I/O操作对打开的流进行读写操作。若带缓冲,三种不同类型本质上是对缓冲区的字符以不同的方式进行解析/处理。

  1. 字符I/O:一次读或写一个字符。
  2. 行I/O:一次读一行或一行的一部分;一次写一行、一行一部分或多行。
  3. 块I/O(结构I/O):一次读或写多个指定大小的块或块的一部分(结构对象)。
// 字符I/O
//读字符:成功,返回读取到的字符;已到达文件尾端或出错,返回EOF
int fgetc(FILE* fp);    //从指定流读
int getchar(void);      //从stdin读:行缓冲
//写字符:成功,返回c;出错,返回EOF
int fputc(int c, FILE* fp);     //往指定流写字符
int putchar(int c);             //往stdout写字符:行缓冲

//行I/O
//读一行或一行的一部分:成功,返回buf指针;已到达文件尾端或出错,返回NULL
char* fgets(char* buf, int n, FILE* fp);
//写字符串(对写磁盘文件时,可能是一行或一行的一部分或多行):成功,返回非负值;出错返回EOF
char* fputs(const char* str, FILE* fp);

//块I/O
//两个函数都返回:读或写的对象数,若到达尾端或出错,返回数字比请求的元素个数少
size_t fread(void* ptr, size_t size, size_t nobj, FILE* fp);
size_t fwrite(const void* ptr, size_t size, size_t nobj, FILE* fp);

注:块I/O必须处理规定字符个数的字符作为一个块

2.4.2 行I/O具体细节

fgets:缓冲区类型不同时,从流缓冲区(全缓冲或行缓冲)或内核缓冲区读入。 ① 读取到一个换行符并存储到用户缓冲区即停止;(读取一整行) ② 未读到换行符,但达到n-1时也停止读取。(读取一行的一部分

  • 都会将换行符读入用户缓冲区。
  • 都会强制在用户缓冲区所存数据末尾加一个NUL字符,使其标识为字符串存储。
  • 第二种情况下,下一次读取从流的下一个字符开始,因此也不存在数据丢失风险。
  • 已到达文件尾端或出错返回NULL指针,用来检查是否到达了文件尾。

fputs:str缓冲区必须包含一个字符串,这个字符串默认以NUL结尾,并且其中可以包含多个换行符。因此fgets只能读取一行的一部分,一个完整行。而fputs时可以根据换行符的多少写入一行的一部分,一个完整行,多行。(分别为char* str=”str”; char* str=”string\n”; char* str=”string1\nstring2\n”);。

  • 三个str都以NUL标识字符串结尾,但NUL不写入流缓冲。
  • 换行符\n可以被磁盘文件或标准输出正确处理为回车。
2.4.3 字符串处理

字符串总是要被强制处理标识,由用户或标准库函数自己处理字符串的字符

  • 读取的字符串“string”或“string\n”或“string\nstr”都被强制加上NUL,NUL只是标识出字符串,由标准库或用户自己处理字符串中的字符(包括换行符)。strlen=6、strlen=7和strlen=10表明NUL之前的字符被当做字符串。
  • 写入的字符串char* str=”string”或“string\n”或“string1\nstr”末尾也会被加上NUL标识为一个完整字符串。

由例子2可以看出读写磁盘文件时,字符串处理在不同缓冲类型和I/O方式时的差别。

判断文件结尾:如果尝试读取达到文件结尾,标准IO的getc会返回特殊值EOF。而fgets碰到EOF会返回NULL,而对于Linux的read函数,情况有所不同:read读取指定的字节数,最终读取的数据可能没有所要求的那么多,而当读到结尾再要读的话,read函数将返回0。

随机存取:fseek()、ftell()和lseek():标准I/O使用fseek和ftell用于文件的随机存取,先看看fseek函数原型

2.4.4 C语言编写Linux系统的cp指令

先看看使用标准I/O版本的:

#include<stdio.h>
#include<stdlib.h>
void oops(char *,char *);
int main(int ac,char *av[])
{
 FILE *in,*out;
  intch;

 if(ac!=3){
  fprintf(stderr,"Useage:%s source-file target-file.\n",av[0]);
  exit(1);
  }
 if((in=fopen(av[1],"r"))==NULL)
  oops("can not open ",av[1]);
 if((out=fopen(av[2],"w"))==NULL)
  oops("can not open ",av[2]);
 while((ch=getc(in))!=EOF)
   putc(ch,out);
 if(fclose(in)!=0||fclose(out)!=0)
   oops("can not close files.\n"," ");
 return 0;
}
void oops(char *s1,char* s2)
{
 fprintf(stderr,"Error:%s %s\n",s1,s2);
 exit(1);
}

再看一个使用Linux io的版本:

#include<unistd.h>
#include<stdio.h>
#include<fcntl.h>

#define BUFFERSIZE 4096
#define COPYMODE 0644
void oops(char *,char *);

int main(int ac,char *av[])
{
  intin_fd,out_fd,n_chars;
 char buf[BUFFERSIZE];
 if(ac!=3){
   fprintf(stderr,"useage:%s source-file target-file.\n",av[0]);
   exit(1);
  }

 if((in_fd=open(av[1],O_RDONLY))==-1)
    oops("Can't open ",av[1]);
 if((out_fd=creat(av[2],COPYMODE))==-1)
   oops("Can't open ",av[2]);
 while((n_chars=read(in_fd,buf,BUFFERSIZE))>0)
     if(write(out_fd,buf,n_chars)!=n_chars)
          oops("Write error to ",av[2]);
 if(n_chars==-1)
     oops("Read error from ",av[1]);
 if(close(in_fd)==-1||close(out_fd)==-1)
   oops("Error closing files","");
 return 0;
}
void oops(char *s1,char *s2)
{
 fprintf(stderr,"Error:%s",s1);
 perror(s2);
 exit(1);
}

显然,在使用Linux i/o的时候,你要更多地关注缓冲问题以提高效率,而stdio则不需要考虑。

参考自:Linux系统编程(1)——文件与I/O之C标准I/O函数与系统调用I/O

三、文件I/O

为什么总是需要将数据由内核缓冲区换到用户缓冲区或者相反呢?

答:用户进程是运行在用户空间的,不能直接操作内核缓冲区的数据。 用户进程进行系统调用的时候,会由用户态切换到内核态,待内核处理完之后再返回用户态

应用缓冲技术能很明显的提高系统效率。内核与外围设备的数据交换,内核与用户空间的数据交换都是比较费时的,使用缓冲区就是为了优化这些费时的操作。其实内核到用户空间的操作本身是无缓冲的,是由I/O库用buffer来优化了这个操作。比如read本来从内核读取数据时是比较费时的,所以一次取出一块,以避免多次陷入内核。

应用内核缓冲区的 主要思想就是一次读入大量的数据放在缓冲区,需要的时候从缓冲区取得数据。

管理员模式和用户模式之间的切换需要消耗时间,但相比之下,磁盘的I/O操作消耗的时间更多,为了提高效率,内核也使用缓冲区技术来提高对磁盘的访问速度。磁盘是数据块 的集合,内核会对磁盘上的数据块做块缓冲。内核将磁盘上的数据块复制到内核缓冲区中,当一个用户空间中的进程要从磁盘上读数据时,内核一般不直接读磁盘,而 是将内核缓冲区中的数据复制到进程的缓冲区中。当进程所要求的数据块不在内核缓冲区时,内核会把相应的数据块加入到请求队列,然后把该进程挂起,接着为其他进程服务。一段时间之后(其实很短的时间),内核把相应的数据块从磁盘读到内核缓冲区,然后再把数据复制到进程的缓冲区中,最后唤醒被挂起的进程。

注:理解内核缓冲区技术的原理有助于更好的掌握系统调用read&write,read把数据从内核缓冲区复制到进程缓冲区,write把数据从进程缓冲区复制到内核缓冲区,它们不等价于数据在内核缓冲区和磁盘之间的交换。

从理论上讲,内核可以在任何时候写磁盘,但并不是所有的write操作都会导致内核的写动作。内核会把要写的数据暂时存在缓冲区中,积累到一定数量后再一 次写入。有时会导致意外情况,比如断电,内核还来不及把内核缓冲区中的数据写道磁盘上,这些更新的数据就会丢失。

应用内核缓冲技术导致的结果是:提高了磁盘的I/O效率;优化了磁盘的写操作;需要及时的将缓冲数据写到磁盘。

![输入输出](D:/Program Files/share/输入输出.jpg)

  • 9
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值