1、fread和read有何不同?

先看两段代码:
fread.c

 

read.c

 

 

 

两个文件的功能完全一样,打开同一个名为test.file的文件,并逐字节地读取整个文件。

将它们编译后得到的可执行程序fread和read分别在同一台PC(linux系统)上执行,得到的如果如下:

 

 

发现没有?fread与read的耗时相差数十倍之多!可见啊~ read一个字节这种写法是相当不可取的!

2、是什么引起的差异?

但是,事情为什么会是这样的呢?让我们用strace来看看:

 

 

 

 

看到了吧~fread库函数在内部做了缓存,每次读取4096个字节;而read就老老实实一个字节一个字节地读……

那么再想想,我们读的是什么?是磁盘。难道上面提到的差异,就是因为这4096倍的读磁盘次数差而引起的吗?并不是这样。
磁盘是块设备,每次读取的最小单位是块。而当我们通过系统调用读一个字节时,linux会怎么做呢?它会是读取一个块、然后返回一个字节、再把其余字节都丢掉吗?当然不会,这样的操作系统也太拙劣了……

实际上linux的文件系统层(fs层)不仅会将每次读的一整块数据缓存下来,还有预读机制(一次预读多个块,以减少磁盘寻道时间),并且缓存的内容是放在文件对应的inode里面,是可以在进程间共享的。(省略细节若干……)

那 么,fread与read执行的耗时差别来自于哪里呢?从代码看,它们都做了相同次数的函数调用;从内核看,它们都造成了基本上相同的磁盘IO……但是注 意到,第一段代码中一共进行了N(N=约24M)次fread函数调用,产生约N/4096次系统调用;第二段代码中一共进行了N次read函数调用,产 生N次系统调用。实际上这里的耗时差就来自于4096倍的系统调用次数差!fread()库函数中缓存的作用并不是减少读磁盘的次数,而是减少系统调用的 次数。

由此可见,系统调用比起普通函数调用有很大的开销,编写代码时应当注意避免滥用系统调用。

3、进一步提高效率?

 

为了进一步减少系统调用的次数,关于读文件的这个问题,我们还可以这样做:

mmap.c

 

 

 

同 样是遍历整个文件,但是读文件的过程中不需要使用系统调用,直接把文件当成内存buffer来读就行了。其原理是:mmap的执行,仅仅是在内核中建立了 文件与虚拟内存空间的映射关系。用户访问这些虚拟内存空间时,页表里面并没有这些空间的表项,于是CPU产生缺页异常。内核捕捉这些异常,逐渐将文件读入 内存,并建立相关的页表项。(省略细节若干……)

 

将其编译后得到的可执行程序mmap和之前的fread、read分别在同一台PC上执行,得到的如果如下:

 

 

mmap方式与fread方式相比,耗时又减少了好几倍。

4、为什么?

看到这里,我们不禁要问,系统调用为什么就这么耗时呢?系统调用与普通函数调用到底有什么不同?

1、两者都是在调用处进行跳转,转到被调用的代码中去执行;
系 统调用使用的"跳转"指令相对复杂。因为跳转到内核空间去执行时,CPU特权级别需要改变(否则没有权限访问到内核空间)。于是,CPU必须封装一条指 令,既实现跳转、又实现特权级别的改变,并且还要保证跳转到的地方就是内核代码(否则用户程序用这个指令假跳一下,自己就拥有特权了)。而软中断指令恰好 能满足这三点要求,所以,X86下实现系统调用的经典方法就是"INT 0x80"(现在好像换sysenter了吧~ 但是指令要做的事情应该不会变);

2、两者都是执行到返回点,然后跳转回到原先的调用点;
系统调用的返回过程还伴随着很多的工作,比如检查是否需要调度、是否有异步信号需要处理、等等。然后,既然来的时候改变了CPU特权级别,返回的时候还得改回去;

3、两种调用中,调用前后的代码都在相同的虚拟地址空间中(内核空间也属于用户进程所能看到的虚拟地址空间范围内,尽管进程一般情况下没有权限去访问),地址空间并没有切换;
运行内核代码时使用的栈是内核栈,系统调用时需要进行栈的切换;

4、两者的参数传递看似相同;
普通函数调用是通过栈来传递参数的;而系统调用是通过寄存器来传递参数,寄存器不够用时才逼不得已使用栈。因为栈要切换,参数传递起来不那么简单;(但是在这一点上,系统调用与普通函数调用的耗时并无太大差异。)

5、CPU执行内核代码和执行用户程序代码没什么区别;
但 是注意到,内核代码对用户参数是充分的不信任。以read/fread的buffer参数为例,fread库函数一般不会检查buffer参数是否合法。 就算想要检查,也没这个能力。他不知道buffer是不是个野指针,不知道buffer的大小是否与len不符,不知道buffer指向的这块内存是否可 写……他唯一能做的检查只是buffer是否为NULL,可惜这没什么意义。但是通过系统调用进入内核以后,情况就不同了。前面说到的那些检查,统统都要 做,并且每次调用都要不厌其烦地做;

以上几点区别,仅是我目前能够想到的。但是管中窥豹,可见一斑。进入内核以后,要做的事情的确是很多很多。

Posted on 2012-11-05 14:45  鬼寿  阅读(1317)  评论(0编辑  收藏  举报