内存与文件的那些事
|
起来很简单,但还有很多事情会发生。首先,即使这个程序只调用了常规的read函数,此时也会有三个 4KB的页帧存储在页面缓存当中,它们持有scene.dat的一部分数据。尽管有时这令人惊讶,但的确所有的常规文件I/O都是通过页面缓存来进行的。在x86 Linux里,内核将文件看作是4KB大小的数据块的序列。即使你只从文件读取一个字节,包含此字节的整个4KB数据块都会被读取,并放入到页面缓存当中。这样做是有道理的,因为磁盘的持续性数据吞吐量很不错,而且一般说来,程序对于文件中某区域的读取都不止几个字节。页面缓存知道每一个4KB数据块在文件中的对应位置,如上图所示的#0, #1等等。与Linux的页面缓存类似,Windows使用256KB的views。 不幸的是,在一个普通的文件读取操作中,内核必须复制页面缓存的内容到一个用户缓冲区中,这不仅消耗CPU时间,伤害了CPU cache的性能,还因为存储了重复信息而浪费物理内存。如上面每张图所示,scene.dat的内容被保存了两遍,而且程序的每个实例都会保存一份。至此,我们缓和了磁盘延迟的问题,但却在其余的每个问题上惨败。内存映射文件(memory-mapped files)将***我们走出混乱:;《Unix环境高级编程》指出类似的情况也发生在Linux和Solaris系统上。你还可能因此而节省下大量的物理内存,这依赖于你的程序的具体情况。 和以前一样,提到性能,实际测量才是王道,但是内存映射的确值得被程序员们放入工具箱。相关的API也很漂亮,它提供了像访问内存中的字节一样的方式来访问一个文件,不需要你多操心,也不牺牲代码的可读性。回忆一下地址空间、还有那个在Unix类系统上关于mmap的实验,Windows下的CreateFileMapping及其在高级语言中的各种可用封装。当你映射一个文件时,它的内容并不是立刻就被全部放入内存的,而是依赖页故障(page fault)按需读取。在获取了一个包含所需的文件数据的页帧后,对应的故障处理函数会将你的虚拟内存页映射到页面缓存上。如果所需内容不在缓存当中,此过程还将包含磁盘I/O操作。 现在给你出一个流行的测试题。想象一下,在***一个render程序的实例退出之时,那些保存了scene.dat的页面缓存会被立刻清理吗?人们通常会这样认为,但这是个坏主意。如果你仔细想想,我们经常会在一个程序中创建一个文件,退出,紧接着在第二个程序中使用这个文件。页面缓存必须能处理此类情况。如果你再多想想,内核何必总是要舍弃页面缓存中的内容呢?记住,磁盘比RAM慢5个数量级,因此一个页面缓存的***(hit)就意味着巨大的胜利。只要还有足够的空闲物理内存,缓存就应该尽可能保持满状态。所以它与特定的进程并不相关,而是一个系统级的资源。如果你一周前运行过render,而此时scene.dat还在缓存当中,那真令人高兴。这就是为什么内核缓存的大小会稳步增加,直到缓存上限。这并非因为操作系统是破烂货,吞噬你的RAM,事实上这是种好的行为,反而释放物理内存才是一种浪费。缓存要利用得越充分越好。
由于使用了页面缓存体系结构,当一个程序调用write()时,相关的字节被简单的复制到页面缓存中,并且将页面标记为脏的(dirty)。磁盘I/O一般不会立刻发生,因此你的程序的执行不会被打断去等待磁盘设备。这样做的缺点是,如果此时计算机死机,那么你写入的数据将不会被记录下来。因此重要的文件,比如数据库事务记录必须被fsync() (但是还要小心磁盘控制器的缓存)。另一方面,读取操作一般会打断你的程序直到准备好所需的数据。内核通常采用积极加载(eager loading)的方式来缓解这个问题。以提前读取(read ahead)为例,内核会预先加载一些页到页面缓存,并期待你的读取操作。通过提示系统即将对文件进行的是顺序还是随机读取操作(参看madvise(), readahead(), Windows缓存提示),你可以帮助内核调整它的积极加载行为。Linux的确会对内存映射文件进行预取,但我不太确定Windows是否也如此。***需要一提的是,你还可以通过在Linux中使用O_DIREC (编辑:常州站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |


