IO常用在外设读写,比如磁盘文件、网络等等,是程序与设备数据交换的唯一方式。文章以文件IO为例,简要介绍IO原理,如何选择合适的IO操作。
IO流是什么
IO流是指面向字节流的IO,个人理解其实就是指带有缓冲的IO,也称标准IO,不带缓冲的IO称为系统IO。当使用不带缓冲的IO操作时,通常需要用户定义byte[]
用于直接读写数据到设备中,每次读写的大小由用户定义;而使用带缓冲的IO操作或方法时,程序定义的byte[]
则会先读写到IO库方法中实现的缓冲区,然后再读写到文件或磁盘中,其中经历了程序byte[] -> 标准库byte[] -> 内核cache(文件)
,看似多了一层数据(从标准缓冲区到程序缓冲)复制,但在实际使用过程中,标准IO有着独到的优势,基本大多数语言IO操作的实现都是通过调用标准IO或带有缓冲的IO库。
为什么要使用缓冲
无论是用户程序自定义byte[]
系统调用读写,还是用户程序直接调用标准IO函数,所有的IO操作最终都需要通过OS内核完成。标准IO库函数中的操作也是通过系统调用把IO操作从用户空间传给内核,然后内核做相应IO操作。
那为什么需要使用byte[]
字节缓冲呢?我们知道OS系统调用时需要陷入到内核态去完成,完成后再返回再继续执行用户态程序,这个操作过程的开销其实是非常昂贵的。用户程序中调用系统函数不像在用户态中那种入栈出栈调用(在同一块堆栈中完成),而是在内核中建立堆栈,这个堆栈与用户态并不是一个堆栈,不能通过传递内存地址相互访问,往往是通过数据复制或地址空间映射来完成访问,另外由于内核态的特权级和安全性,还需要涉及到传参的检验等等,所以在开发过程中尽量减小系统调用,减少开销。
那么使用缓冲的优点就是在系统调用时尽可能一次传递更多的数据,减小系统调用的次数。当然缓冲区的大小也不是越大越好,使用过程中也要考虑到硬件层面比如内存大小等,如果由用户程序自定义缓冲区,则需要综合平衡硬件、效率等各方面因素。而如果采用标准IO库函数,则减小了这方面的开发工作量,调用也方便,移植性较强,也不用关心各平台差异性。
下图可以用来说明标准IO与系统IO间的关系,其中stdio
表示C标准IO库,图片来源于《Linux系统编程手册》。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jKViWXuC-1596017903355)(…/image/cs/io_cache_01.jpg)]
IO性能对比
这里通过一段代码来比较两种IO在读操作上面的性能,有兴趣的同学也可以尝试比较写操作的性能,只是可能相对麻烦点,但在整个性能比较结果与以下demo不会有太多差异。
通过4个函数test_read_standard_io
、test_read_standard_io
、test_read_standard_io_8k
、test_read_standard_io_line
比较系统IO与标准IO、标准IO缓存大小选择、标准IO读取行函数使用等来比较两种IO的性能。代码清单如下:
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<time.h>
// 读操作用的数组大小
#define READ_SIZE 4096
char C_BUF[8 * 1024];
// 系统IO读,用4096数组接收读数据
double test_read_system_io(char *filename);
// 标准IO读,用4096数组接收读数据
double test_read_standard_io(char *filename);
// 标准IO读,设置标准IO的缓冲大小为8K,用4096数组接收读数据
double test_read_standard_io_8k(char *filename);
// 标准IO读,按行读数据,标准IO缓冲大小为8K,用4096数组接收读数据
double test_read_standard_io_line(char *filename);
int main(int argc, char *argv[])