最近的时光,闲暇下来的时候基本是在看PostgreSQL的source code,最近在看storage,其中file部分整体看的比较快,因为有2年的文件系统的实战经验。但是自己当年,不会的东西太多,没有能力细细的研读kernel 和相关的东西。我看到了fadvise 这个接口,然后展开学习了这个接口。相当的东西都是学习网上其他前辈的的,很多源自淘宝的褚霸 霸爷,向相关的前辈致敬。
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/
然后我们可以用configure ;make ;make install将这个包安装,这个包提供了一个叫linux-fincore的二进制可执行文件,可以用来看文件页面在内存的驻留情况,给出了统计信息:
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;
我们去掉了那些异常判断,去掉不相关的展现逻辑,核心代码就是这么几行。 open获得文件描述符,stat获取文件的长度,页面的大小是4K ,有了长度,我们就知道了,我们需要多少个int来存放结果。mmap建立映射关系,mincore获取文件页面的驻留情况,从起始地址开始,长度是filesize,结果保存在mincore_vec数组里。如果mincore[page_index]&1 == 1,那么表示该页面驻留在内存中,否则没有对应缓存。
vmtouch也是一个相关的工具,也提供类似的功能,代码路径在:,只有一个文件,直接编译即可:
-
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
这部分内核代码位于fs/drop_caches.c里面。调用这个之后,cache部分的内存会被释放。其实是没有关系,纵然cache占满了内存,运行一个大程序也不会出现内存不足,因为操作系统可以回收这部分的内存。
除了这条粗暴的方法外,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
Linux相当于承认自己不够聪明,请用户给它点提示。 它支持用户提那些内容的提示呢?兄弟们可以自行man 一下,常用的是:
-
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_WILLNEED 相当与告知Linux,这个文件,在不久的将来我要用,请帮我准备好相应的页面(从磁盘读入内存),本质相当与告诉linux预读。
第二个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
-
}
淘宝的霸爷深入分析了POSIX_FADV_DONTNEED相关的内核代码。关心实现的可以去读霸爷的http://blog.yufeng.info/archives/1917。
现在有个这个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;
-
}
除此意外,vmtouch的 -e选项也提供了类似的功能,还有linux-ftools 里面的linux-fadvise也有类似的功能,本质都是posix_fadvise。
我们看到,清除之后,缓存中在也没有bean_1文件的页面了。
后记:
这里面的水比较深,很多地方可以扩展开来,比如缓存到什么程度,操作系统开始出面清理缓存,在比如posix_fadvise的kernel实现,在比如介绍mincore系统调用的时候,我们发现,多个系统调用组合才能得到文件的缓存信息,这太慢了,Chris Frost提出了一个新的系统调用fincore,感兴趣的可以查看及。 另外,低于mincore系统调用,只返回是否在文件对应对页是否存在在缓存中,这太浪费了,明明可以把是否dirty一并返回,我今天一直纠结与如何返回文件页面在缓存的dirty情况,很蛋疼,没解决。实际上mincore完全可以顺路返回这个值。毕竟int有32bit,只用一个bit太浪费了。不能展的太开,否则,就收敛不了了,另外,我的功能还不到。
fs and page cache.pdf
参考文献
1 posix_fadvise清除缓存的误解和改进措施
2
阅读(2581) | 评论(0) | 转发(0) |