为了理解文件IO和标准IO的区别,可能要先理解下用户态与内核态,系统调用与库函数的概念。
什么是用户态和内核态:
区分用户态和内核态的作用:
之所以区分用户态和内核态,是为了限制不同程序的访问能力,防止它们获取随意获取其他程序或外围设备的数据。
(限制用户态的访问能力,还可以防止外围设备的访问冲突。)
我们知道,线程
用户态和内核态的切换:
程序都是工作在用户态,但是有时候程序需要访问一些受限的数据(如从硬盘读数据,从键盘获取输入等),这是就需要切换到内核态。切换的方式一般时调用系统调用。用户态通过系统调用通知内核态,需要访问哪些受限的数据,或操作哪些受限的设备。内核态就帮助用户态完成。
小结:
简单的说,用户程序只能访问内存,而操作系统能访问所有数据。用户程序工作的模式即是用户态,当用户程序想要访问受限的数据时,就需要向操作系统发请求,让操作系统帮忙完成。 操作系统工作的模式即内核态。
库函数工作在用户态,系统调用工作在内核态。
所有的操作系统都会提供一些服务用以访问/操作设备等,操作系统会为这些服务提供接口。这些接口就是系统调用 。用户态程序通过调用系统调用可以切换到内核态,访问内存意外的数据,操作外围设备等。
不同的操作系统提供的系统调用会有所差异。
Unix为每个系统调用在标准C库中设置一个具有同样名字的函数 。 一般我们是称这些函数为系统调用。(如标准C库中有函数write(), write()函数直接使用write系统调用相应的内核服务)。 所以一般说write(),指的不是write()这个函数,而是系统调用write。
标准C库,或是其他库会定义一些函数,这些函数称之为库函数。很多库函数是跨平台的。虽然有些库函数最终会调用系统调用,但不少库函数会根据具体的操作系统选用响应的系统调用接口。
有些库函数仅仅是操作内存,不访问硬盘等外围设备的数据,因此最终并不会调用系统带哦用。即:并不是所有的库函数最终都要使用系统调用的, 如:strcpy、atoi等。
I/O:输入输出。
既然是输入输出,就要有输入输出设备(一般内存不算输入输出设备)。如硬盘、键盘、终端等都可以作为输入输出设备。
用户态是不能访问硬盘、键盘、终端这些外围(虚拟)设备的,因此需要切换到内核态。用户态和内核态的切换是会产生开销的。
举个例子,程序要从硬盘上的文件读取数据,其过程如下:
文件IO相关的函数open、close、read、write等等
这里我们主要以write()函数为例,介绍一些文件IO的特性。
文件IO是不带缓冲的,所谓不带缓冲,即每调用一次文件IO(如write),就进行一次用户态与内核态的切换。
调用一次write,就需要立即把数据从用户态拷贝到内核态。
标准IO相关的函数主要有:fopen、fclose、fread、fwrite、fgetc等等
标准IO是带缓冲的。
标准IO提供缓冲的目的是尽可能减少使用read和write调用的次数(也就是用户态和内核态切换的次数)。
所谓带缓冲,其实就是在调用fwrite的时候,先把数据放在缓冲区。不直接使用系统调用,切换到内核态。而是等到缓冲区满或是调用fflush等条件满足时,再一次性调用write,把数据拷贝到内核空间。
缓冲是标准IO库提供的,缓冲区存在于用户空间(不管是fread还是fwrite)。 对于fwrite,根据上面的例子很好理解。对于fread而言,就是在调用fread的时候,切换到内核态,内核态读取缓冲去能够接受的大小的数据(一般会多余此次fread期望的数据), 然后返回给fread函数。下次再调用fread的时候,可能只需要从缓冲区读取,而不需要再到内核空间拷贝了。
标准IO有三种缓冲类型:
全缓冲:
行缓冲
不带缓冲
文件IO不带缓冲,因此每调用write,就需要做用户态和内核态的切换,把数据从用户态切换到内核态。
标准IO带缓冲,调用fwrite的时候,先把数据保存在缓冲区,等待缓冲区满或调用fflush等条件满足时,再调用write,一次性把多次fwrite的数据
因此,从效率上看,带缓冲的IO作用户态与内核态切换的次数较少,效率比较高。没有特殊要求的话,一般应该选用fopen、fwrite系列带缓充的IO。
在多线程日志系统中,每个线程打开一个日志文件的描述符。
如果使用带缓冲的IO,最终可能导致日志的顺序发生错乱,可能影响阅读。因此如果对多线程的日志顺序有要求的话,可能需要使用不带缓冲的IO。
另一方面,在测试中发现,使用标准IO时,每次调用write的数据并不一定都是多次完整的fwrite的数据。可能会有一些fwrite的数据被分为两次write。举个例子,每次调用fwrite写100个字节的数据,而缓冲区为550个字节,那么第6次fwrite的数据可能就被截断分成两次write。(这是我在centOS7.2上的测试结论,目前不清楚是否可以配置在fwrite写缓冲之前先判断缓冲区剩余空间是否充足,不足时先调用write,再把完整的fwrite数据写入缓冲)。(就刚刚那个例子来说,就是不知道是否可以配置,在第6次fwrite写缓冲之前先write前面的500字节)。