一般情况下,操作文件既可以使用标准I/O,也可直接使用系统调用。两者有何区别呢?
在输入输出中,直接使用底层的系统调用效率是非常低的,为什么?
(1) 系统调用会影响系统性能。执行系统调用时,Linux必须从用户态代码
切换到内核态,然后再返回用户代码。
(2) 硬件会对底层系统调用一次所能读写的数据块做出一定的限制。
带缓存的文件操作是标准C 库的实现,第一次调用带缓存的文件操作函数时标准库会自动分配内存并且读出一段固定大小的内容存储在缓存中。所以以后每次的读写操作并不是针对硬盘上的文件直接进行的,而是针对内存中的缓存的。何时从硬盘中读取文件或者向硬盘中写入文件有标准库的机制控制。不带缓存的文件操作通常都是系统提供的系统调用,更加低级,直接从硬盘中读取和写入文件,由于IO瓶颈的原因,速度并不如意,而且原子操作需要程序员自己保证,但使用得当的话效率并不差。另外标准库中的带缓存文件IO 是调用系统提供的不带缓存IO实现的。
库函数与系统调用的层次关系
open、read、write、close等系统函数称为无缓冲I/O(Unbuffered I/O )函数,因为它
们位于C 标准库的I/O 缓冲区的底层 。用户程序在读写文件时既可以调用C 标准I/O 库函
数,也可以直接调用底层的Unbuffered I/O 函数,那么用哪一组函数好呢?
C 标准库函数是C 标准的一部分,而Unbuffered I/O 函数是UNIX 标准的一部分,在所 支
持C 语言的平台上应该都可以用C 标准库函数(除了
标准之外),而只
在头文件 stdio.h中声明,而read、write等函数在头文件unistd.h中声明。在支持
C 语言的非UNIX 操作系统上,标准I/O 库的底层可能由另外一组系统函数支持,例如
Windows 系统的底层是Win32 API,其中读写文件的系统函数是ReadFile、WriteFile。
先看两段代码:
//使用fopen库函数,每次读取1个字节
// 标准库函数是带缓冲的
fread.c
- #include <stdio.h>
- #include <stdlib.h>
- void main()
- {
- FILE *pf = fopen("test.file", "r");
- char buf[2] = {0};
- int ret = 0;
- do {
- ret = fread(buf, 1, 1, pf);
- }while(ret);
- }
read.c
- #include <stdio.h>
- #include <stdlib.h>
- void main()
- {
- int fd = open("test.file", 0);
- char buf[2] = {0};
- int ret = 0;
- do {
- ret = read(fd, buf, 1);
- }while(ret);
- }
将它们编译后得到的可执行程序fread和read分别在同一台PC(linux系统)上执行,得到的如果如下:
- [xiangy@compiling-server test_read]$ time ./fread
- real 0m0.603s
- user 0m0.597s
- sys 0m0.006s
- [xiangy@compiling-server test_read]$ time ./read
- real 0m15.240s
- user 0m3.847s
- sys 0m11.392s
- [xiangy@compiling-server test_read]$ ll test.file
- -rw-r--r-- 1 xiangy svx8004 23955531 Sep 24 17:17 test.file
发现没有?fread与read的效率差有数十倍之多!可见啊~ read一个字节这种写法是相当不可取的!
但是,事情为什么会是这样的呢?让我们用strace来看看:
- [xiangy@compiling-server test_read]$ strace ./fread
- execve("./fread", ["./fread"], [/* 34 vars */]) = 0
- ……
- read(3, "BZh91AY&SY/20v/322/25/4/320/240/177/377/377/377/377/376"..., 4096) = 4096
- ……
- [xiangy@compiling-server test_read]$ strace ./read
- execve("./read", ["./read"], [/* 34 vars */]) = 0
- ……
- read(3, "B", 1) = 1
- ……
看到了吧~fread库函数在内部做了缓存,每次读取4096个字节;而read就老老实实一个字节一个字节地读……
那么再想想,我们读的是什么?是磁盘。难道上面提到的差异,就是因为这4096倍的读磁盘次数差而引起的吗?并不是这样。
磁 盘是块设备,每次读取的最小单位是块。而当我们通过系统调用读一个字节时,linux会怎么做呢?它会是读取一个块、然后返回一个字节、再把其余字节都丢 掉吗?当然不会,这样的操作系统也太拙劣了……换个角度想一想,如果真是每read一个字节就操作一次磁盘去读一个块,那么上面的test.file有 24M之大,近两千四百万次的磁盘读操作也不大可能会在15秒钟完成吧~
实际上linux的文件系统层(fs层)不但不是这样拙劣,反而很高明。不仅会将每次读的一整块数据缓存下来,还有预读机制(一次预读多个块,以减少磁盘寻道时间)。
那么,fread与read执行的效率差来自于哪里呢?实际上就是来自于4096倍的系统调用次数差!fread库函数中缓存的作用并不是减少读磁盘的次数,而是减少系统调用的次数。
由此可见,系统调用比起普通函数调用有很大的开销,编写代码时应当注意尽量减少系统调用的使用。
为了进一步减少系统调用的次数,关于读文件的这个问题,我们还可以这样做:
mmap.c
- #include <stdio.h>
- #include <stdlib.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <unistd.h>
- #include <sys/mman.h>
- void main()
- {
- int fd = open("test.file", 0);
- struct stat statbuf;
- char *start;
- char buf[2] = {0};
- int ret = 0;
- fstat(fd, &statbuf);
- start = mmap(NULL, statbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
- do {
- *buf = start[ret++];
- }while(ret < statbuf.st_size);
- }
同样是遍历整个文件,但是读文件的过程中不需要使用系统调用。(原理是:mmap的执行,仅仅是在内核中建立了文件与虚拟内存空间的对应关系。用户访问这些虚拟内存空间时,页面表里面是没有这些空间的表项的,于是产生缺页异常。内核捕捉这些异常,逐渐将文件读入。)
将其编译后得到的可执行程序mmap和之前的fread、read分别在同一台PC上执行,得到的如果如下:
- [xiangy@compiling-server test_read]$ time ./fread
- real 0m0.901s
- user 0m0.892s
- sys 0m0.010s
- [xiangy@compiling-server test_read]$ time ./mmap
- real 0m0.112s
- user 0m0.106s
- sys 0m0.006s
- [xiangy@compiling-server test_read]$ time ./read
- real 0m15.549s
- user 0m3.933s
- sys 0m11.566s
- [xiangy@compiling-server test_read]$ ll test.file
- -rw-r--r-- 1 xiangy svx8004 23955531 Sep 24 17:17 test.file
mmap方式与fread方式相比,效率还要高出几倍