#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所涉及到的四步操作。
2、标准I/O:属于ISO C实现的标准库函数,调用的是底层的系统调用。
(1) 将逻辑单元中的数据写入文件,根据需求,有三种函数类型可以调用,以fputc、fputs、fwrite为例,这些函数不用人为去控制缓冲区的大小,而是系统自动申请的,当用户定义了相应的I/O函数之后,根据不同的缓存类型(是全缓冲、行缓冲还是无缓冲),系统自动调用malloc等函数申请缓冲区,即标准I/O缓存。
(3)(5) 当用户缓冲区满了之后,如系统I/O操作一般,此时调用write从标准I/O缓存中复制数据到内核缓冲区,再写入磁盘。
(4)(6) 同系统I/O操作,从内核缓冲区调用read读入到用户缓冲区。
(2) 同样有三种函数类型可以调用,以fgetc、fgets、fread为例,读入逻辑单元进行后续的处理。
可见,标准I/O实现的机制就是基于系统I/O,这样看来,标准I/O在效率上肯定不如系统I/O,但事实是标准I/O与系统I/O相比并不慢很多,而且还有很多其他的优点,下面一一述说(本篇文章最重要的就是下一小节)。
五、I/O效率
系统I/O效率受限于read、write系统调用的次数,而系统调用次数则又受限于内核缓冲区的大小,即BUFFSIZE,通过设置不同的BUFFSIZE,系统CPU时间是不同的,其最小值出现在BUFFSIZE=4096处,原因是该测试所采用的是Linux ext2文件系统,其块长为4096字节,也即缓冲区所能申请到的最大缓冲区大小,我们把4096字节看做是本次最佳I/O长度。如果继续扩大缓冲区大小,对此时间几乎没有影响。所以,对于系统I/O操作,一个最大的问题就是:需要人为控制缓存的大小及最佳I/O长度的选择,另外就是系统调用与普通函数调用相比通常需要花费更多的时间,因为系统调用具体内核要执行这样的操作:1)内核捕获调用,2)检查系统调用参数的有效性,3)在用户空间和内核空间之间传输数据。
因此,引入标准I/O的目的就是为了通过标准I/O缓存来避免BUFFSIZE选择不当而带来的频繁的系统调用。根据用户不同的需求,选择不同的I/O函数,然后根据不同的缓存类型,自动调用malloc等缓存分配函数分配合适的缓存,等分配的缓存满之后,再调用系统I/O从标准I/O缓存向内核缓存拷贝数据,这样就进一步减少了系统调用的次数。