一、文章来由
  近看了《UNIX环境高级编程》,对以前比较模糊的一些知识结构又做了进一步的加强,特别是前两章讲到不带缓冲的文件I/O和带缓冲的标准I/O,对read、write、fread、fwrite、printf等等这些函数又有了新的认识。一个很大的感受是我们很多时候编程开发都只注重上层逻辑,虽然一个项目接一下项目,看上去做了不少事,但是夜深人静时仔细一想,究竟我们是否真正掌握了这些知识点,对于每一个知识点实现的机制我们是否能完整地说出来。这些东西能体现一个人的基础知识是否扎实,我发现互联网公司的面试中喜欢问这些基础知识,由一个很基本的函数都会层层递进引申出很多的问题。很多时候我们内心可能会很排斥,甚至不屑于这些基础知识,想着等用到的时候,我再来查,我专注上层逻辑好了,这样有助于提升我的开发效率。这样的想法貌似也没什么错,但是往往这是瓶颈的来源,程序员可怕的是遇到瓶颈了。因为瓶颈这个东西是很难意识到的,一味追求实践而放弃理论学习,很容易遇到瓶颈。(个人见解,不喜勿喷)
  本文算是自己看完《UNIX环境高级编程》文件I/O和标准I/O两章的读书笔记,文件I/O一章说不带缓冲,但后面又出现可带缓冲,搞得我有点晕,特意记下自己对此的理解。如果有什么不对的,欢迎指出,如果你觉得本文对你有帮助,动动手指推荐下,或者是粉我下,你的关注是我写作的大动力。^_^
  二、缓冲机制
  众所周知,CPU和内存的数据交换要远大于磁盘操作,通过缓存机制,可以减少磁盘读写的次数,提高并发处理程序的效率,因此,缓存是一种提高任务存储和处理效率的有效方法。我们很多时候可以看到,缓存不单单在操作系统方面被采用,更是在Web技术、服务器端、分布式系统等领域发挥着及其重要的作用。
  从宏观上看,Linux操作系统分为用户态和内核态,在处理I/O操作的时候,两者都提供了缓存。用户态的称为标准I/O缓存,也称为用户空间缓存,而内核态的称为缓冲区高速缓存,也叫页面高速缓存。既然都提供了缓存,那为什么这本书上却分不带I/O的缓存和带I/O的缓存,原因其实是“不带I/O缓存”指的是用户空间中不为这些I/O操作设有缓冲,而内核是带缓冲的,这样来看,不会糊涂了。
  三、系统I/O和标准I/O
  系统I/O,又称文件I/O,或是内核态I/O,引用文件的方式是通过文件描述符,一个文件对应一个文件描述符。一个文件描述符用一个非负整数表示,0、1、2系统默认表示标准输入、标准输出、标准错误,某些UNIX系统规定了描述符的上限值OPEN_MAX,这些常量都定义在头文件<unistd.h>中。当读或写一个文件时,使用open或create系统调用返回的文件描述符标识该文件,并将其作为参数传递给read或write系统调用。
  #include <unistd.h>
  ssize_t read(int filedes, void *buf, size_t nbytes);
  ssize_t write(int filedes, const void *buf, size_t nbytes);
  标准I/O,又叫用户态I/O,引用文件的方式则是通过文件流(stream),一般用fopen和freopen函数打开一个流,返回一个指向FILE对象的指针,其他函数如果要引用这个流,则将FILE指针作为参数传递。一个进程预定义了三个流,并且这三个流自动被进程使用,它们是标准输入流、标准输出流和标准出错流,这三个流和系统I/O所规定的三个文件描述符所引用的文件相同。当读或写一个文件时,不像系统I/O,仅定义了read和write两个系统调用函数,标准I/O定义了多个函数,程序员可以根据自己的需求灵活使用。这些函数可以分为每次一个字符的I/O,每次一行的I/O和直接I/O(或者二进制I/O、一次一个对象I/O、面向记录的I/O、面向结构的I/O)。
  1)每次一个字符的I/O
  #include<sdio.h>
  /* 输入函数 */
  int getc(FILE *fp) -> 宏
  int fgetc(FILE *fp) -> 函数
  int getchar(void) 等价于getc(stdin)
  /* 输出函数 */
  int putc(int c, FILE *fp)
  int fputc(int c, FILE *fp)
  int putchar(int c) 等效于putc(c, stdout)
  2)每次一行I/O
  #include <stdio.h>
  /* 输入函数 */
  char *fgets(char *restrict buf, int n, FILE *restrict fp)
  char *gets(char *buf)
  /* 输出函数 */
  int fputs(cont char *restrict str, FILE *restrict fp)
  int puts(const char *str)
  3)直接I/O
  #include <stdio.h>
  size_t fread(void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp)
  size_t fwrite(const void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp)
  到此,我们大概了解了系统I/O和标准I/O引用文件的方法,以及一些常用的I/O函数。下面通过一个图来详细看下当用户调用一个I/O函数时,用户态和内核态的一个执行流程是什么样的,进一步了解缓存在I/O操作中的作用,以及用户态I/O和内核态I/O在执行效率上的区别。
  四、I/O操作的流程

  如上图所示,用户进程空间和内核进程空间读写磁盘的操作都要经过缓冲区缓存,缓存的作用前面也提到过,是为了减少磁盘读写的次数,提高I/O的效率。当读写一个文件时,首先看系统I/O的操作流程。
  1、系统I/O: 属于内核系统调用,没有涉及用户态的参与。以图中标号为例:
  (3) 调用write函数向文件中写数据,buf中存放的是要写入的数据,如write(fd, 'abc', 3)。调用前需要先设置BUFFSIZE。不同的BUFFSIZE会影响I/O效率,下面再来说这个问题。
  (5) 延迟写:当缓存区高速缓存满或者内核要重写缓冲区的时候,才将数据写入输出队列,等数据到队列首部的时候,才真正触发磁盘的写操作。
  (6) 预读:当检测到正进行顺序读取时,内核试图读入比应用程序所要求更多的数据,并假想应用程序很快会读到这些数据。这样,当缓冲区没有数据时,能够快速填充下次要读取的数据。
  (4) 调用read从缓冲区高速缓存读取所需数据到逻辑单元中进行处理。
  以上,是系统I/O所涉及到的四步操作。