Linux操作系统在读文件中的某页的时候(pagesize=4k),它会首先去查找缓存,看下缓存中是否有要读的那个页面。没有的话,就去后备设备(大多数是块设备)去读上来。读上来对应页面的内容也会缓存在内存中,下一次再读同一个页面的时候,因为缓存中已经有个这个文件,不需要磁盘操作,所以会提升速度。talk is cheap,我实验证明之:
- root@manu:~# time cp NVIDIA-Linux-x86-310.44.run bean_1
- real 0m0.593s
- user 0m0.004s
- sys 0m0.128s
- root@manu:~# time cp NVIDIA-Linux-x86-310.44.run bean_2
- real 0m0.055s
- user 0m0.004s
- sys 0m0.052s
- root@manu:~# time cp NVIDIA-Linux-x86-310.44.run bean_3
- real 0m0.056s
- user 0m0.000s
- sys 0m0.052s
我们看到第一操作这个38M文件的用了0.6second,但是第二次操作的时候的时候,只用了0.055和0.056秒,差距是10倍。原因无他,第一次牵扯到了磁盘操作,需要将NVIDIA-Linux-x86-310.44.run的内容读入缓存之中。第二次的时候,就不需要了操作磁盘了,因为缓存中已经有了这个文件的页面。
到了此时,我们有必要讲述下一个很有名气很常用的命令了,那就是free。
上面一副图源自Linux Performance and Tuning Guideline,很好的一本书。这附图基本讲明确了内存的构成。
如何计算,内存的使用呢,霸爷今年有篇Linux used 内存到哪里去了?讲的特别细致,我就不多说了,我们关心的是cache。
首先,我们写一个新文件,用dd灌一个文件,我们知道,写的时候,文件的内容会在缓存中,有后台进程定期刷写进磁盘。这部分内容就会计入cache之中:
dd写文件之前:
然后用dd灌一个新文件:
我们发现cache的内容显著增加从185232升到了361156,大小接近与我们的新文件bean_1. 我说这就是这个文件缓存在内存的大小。空口无凭,如何证明:
这时候,我们需要提到的另一个主角就要登场了,mincore系统调用。Linux提供了这个系统调用用来判断文件的某个页面是否驻留在内存中。内核代码在mm/mincore.c下面,代码比较简单。这个系统调用实现没啥好说的,可是这个系统调用的作用实在是太大了,有了它,你给我一个文件名,我就能告诉你这个文件某个页面是否驻留在内存中。事实上已经有不少工具这么做了。
首先有个工具是linux-ftools,在Ubuntu下下载方式如下::
- apt-get install install mercurial
- hg clone https://code.google.com/p/linux-ftools/
linux-fincore这个工具清楚的告诉我们:我们一个共缓存了4096个页面,缓存率是100%,即所有的页面都在内存中可以找到。很神奇吧,其实代码非常简单,就是用了open,mmap,stat,mincore等有限的系统调用。我们这只可以把这个工具做的更强大,不止是统计,把每个页面是否出现在内存都展现出来。我们一起看下代码实现:
- fd = open( path, O_RDONLY );
- ....
- if ( fstat( fd, &file_stat ) < 0 )
- ....
- file_mmap = mmap((void *)0, file_stat.st_size, PROT_NONE, MAP_SHARED, fd, 0 );
- ...
- size_t calloc_size = (file_stat.st_size+page_size-1)/page_size;
- mincore_vec = calloc(1, calloc_size);
- ....
- if ( mincore(file_mmap, file_stat.st_size, mincore_vec) != 0 )
- ....
- if (mincore_vec[page_index]&1) {
- ++cached;
vmtouch也是一个相关的工具,也提供类似的功能,代码路径在:http://hoytech.com/vmtouch/vmtouch.c,只有一个文件,直接编译即可:
- root@manu:~/code/c/classical/pearl/vmtouch# gcc -Wall -O3 -o vmtouch vmtouch.c
- root@manu:~/code/c/classical/pearl/vmtouch# ll
- 总用量 48
- drwxr-xr-x 2 manu root 4096 4月 26 23:50 ./
- drwxrwxr-x 8 manu manu 4096 4月 26 23:47 ../
- -rwxr-xr-x 1 root root 22220 4月 26 23:50 vmtouch*
- -rw-rw-r-- 1 manu manu 16106 4月 26 23:49 vmtouch.c
- root@manu:~/code/c/classical/pearl/vmtouch# cp vmtouch /usr/bin/
使用vmtouch工具同样可以看出,bean_1文件,100%的页面都在缓存之中。这部分的实现同linux-fincore大同小异。
看到这里,我们发现文件一直驻留在内存中,实际上Linux采用的策略是没超过门限之前,操作系统不作为。至于Linux操作系统的策略,本身又可以写一篇文章,本文不多说,本文只说下用户有什么办法改变现状。假如说,我刚才写的文件bean_1,其实我不会在再操作它了,内存完全没有必要缓存160M的内容在内存里面,怎么告知内存?
最粗暴的方法是最容易想到的,
- echo 3 >/proc/sys/vm/drop_caches
除了这条粗暴的方法外,Linux提供了posix_fadvise系统调用,可以允许用户给linux 提建议。
- #include <fcntl.h>
- int posix_fadvise(int fd, off_t offset, off_t len, int advice);
-
- posix_fadvise():
- _XOPEN_SOURCE >= 600 || _POSIX_C_SOURCE >= 200112L
- POSIX_FADV_WILLNEED
- The specified data will be accessed in the near future.
- POSIX_FADV_DONTNEED
- The specified data will not be accessed in the near future.
第二个POSIX_FADV_DONTNEED 相当与告知linux ,这个文件哥不用了,请你不要在把它放在内存里面浪费内存了,把内存留给能需要兄弟吧。本质相当与sync。
请看PostgreSQL中的相关应用:
- int
- FilePrefetch(File file, off_t offset, int amount)
- {
- #if defined(USE_POSIX_FADVISE) && defined(POSIX_FADV_WILLNEED)
- int returnCode;
- Assert(FileIsValid(file));
- DO_DB(elog(LOG, "FilePrefetch: %d (%s) " INT64_FORMAT " %d",
- file, VfdCache[file].fileName,
- (int64) offset, amount));
- returnCode = FileAccess(file);
- if (returnCode < 0)
- return returnCode;
- returnCode = posix_fadvise(VfdCache[file].fd, offset, amount,
- POSIX_FADV_WILLNEED); //预读
- return returnCode;
- #else
- Assert(FileIsValid(file));
- return 0;
- #endif
- }
- int
- pg_flush_data(int fd, off_t offset, off_t amount)
- {
- #if defined(USE_POSIX_FADVISE) && defined(POSIX_FADV_DONTNEED)
- return posix_fadvise(fd, offset, amount, POSIX_FADV_DONTNEED);
- #else
- return 0;
- #endif
- }
现在有个这个fadvise宝贝,我们就能把某文件彻底赶出缓存,代码非常简单(代码采用的利用posix_fadvise清理系统中的文件缓存)
- int clear_file_cache(const char *filename)
- {
- struct stat st;
- if(stat(filename , &st) < 0) {
- fprintf(stderr , "stat localfile failed, path:%s\n",filename);
- return -1;
- }
- int fd = open(filename, O_RDONLY);
- if( fd < 0 ) {
- fprintf(stderr , "open localfile failed, path:%s\n",filename);
- return -1;
- }
- //clear cache by posix_fadvise
- if( posix_fadvise(fd,0,st.st_size,POSIX_FADV_DONTNEED) != 0) {
- printf("Cache FADV_DONTNEED failed, %s\n",strerror(errno));
- }
- else {
- printf("Cache FADV_DONTNEED done\n");
- }
- return 0;
- }
我们看到,清除之后,缓存中在也没有bean_1文件的页面了。
后记:
这里面的水比较深,很多地方可以扩展开来,比如缓存到什么程度,操作系统开始出面清理缓存,在比如posix_fadvise的kernel实现,在比如介绍mincore系统调用的时候,我们发现,多个系统调用组合才能得到文件的缓存信息,这太慢了,Chris Frost提出了一个新的系统调用fincore,感兴趣的可以查看http://libprefetch.cs.ucla.edu/及https://lkml.org/lkml/2013/2/15/44。 另外,低于mincore系统调用,只返回是否在文件对应对页是否存在在缓存中,这太浪费了,明明可以把是否dirty一并返回,我今天一直纠结与如何返回文件页面在缓存的dirty情况,很蛋疼,没解决。实际上mincore完全可以顺路返回这个值。毕竟int有32bit,只用一个bit太浪费了。不能展的太开,否则,就收敛不了了,另外,我的功能还不到。
fs and page cache.pdf
参考文献
1 posix_fadvise清除缓存的误解和改进措施
2 利用posix_fadvise清理系统中的文件缓存