page cache诊断控制工具 vmtouch 源代码分析
本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/vmtouch-code-analysis/
vmtouch 是一个 portable 的 page cache 诊断和控制工具,可以查看文件或者设备中有多少在 page cache 中,知道之后对这些在 page cache 中的内存引用可以避免 page fault,支持将文件的内容从 page cache 逐出,同时还可以将文件手工 touch 到 page cache 中,支持 lock 文件部分区域到 memory 中防止被交换出去从而提高。
vmtouch 可以在 Linux、BSD 等系统上使用,在这下载编译:
今天简单看了下 vmtouch 的代码,发现还比较简单,自己写个类似的程序验证之后,将代码分析结果写下。vmtouch 的代码比较少,我只贴出最关键的一个函数 vmtouch_file(关键部分已经高亮显示),这个函数做 分析 page cache 使用、touch、lock 的操作,其它部分只是加了读了目录的遍历处理之类的。
int64_t bytes2pages(int64_t bytes) {
return (bytes+pagesize-1) / pagesize;
}
int aligned_p(void *p) {
return 0 == ((long)p & (pagesize-1));
}
int is_mincore_page_resident(char p) {
return p & 0x1;
}
void vmtouch_file(char *path) {
int fd;
void *mem;
struct stat sb;
int64_t len_of_file;
int64_t pages_in_file;
int i;
int res;
res = o_followsymlinks ? stat(path, &sb) : lstat(path, &sb);
if (res) {
warning("unable to stat %s (%s), skipping", path, strerror(errno));
return;
}
if (S_ISLNK(sb.st_mode)) {
warning("not following symbolic link %s", path);
return;
}
if (sb.st_size == 0) return;
if (sb.st_size > o_max_file_size) {
warning("file %s too large, skipping", path);
return;
}
len_of_file = sb.st_size;
retry_open:
fd = open(path, O_RDONLY, 0);
if (fd == -1) {
if (errno == ENFILE || errno == EMFILE) {
increment_nofile_rlimit();
goto retry_open;
}
warning("unable to open %s (%s), skipping", path, strerror(errno));
return;
}
mem = mmap(NULL, len_of_file, PROT_READ, MAP_SHARED, fd, 0);
if (mem == MAP_FAILED) {
warning("unable to mmap file %s (%s), skipping", path, strerror(errno));
close(fd);
return;
}
if (!aligned_p(mem)) fatal("mmap(%s) wasn't page aligned", path);
pages_in_file = bytes2pages(len_of_file);
total_pages += pages_in_file;
if (o_evict) {
if (o_verbose) printf("Evicting %s\n", path);
#if defined(__linux__)
if (posix_fadvise(fd, 0, len_of_file, POSIX_FADV_DONTNEED))
warning("unable to posix_fadvise file %s (%s)", path, strerror(errno));
#elif defined(__FreeBSD__) || defined(__sun__)
if (msync(mem, len_of_file, MS_INVALIDATE))
warning("unable to msync invalidate file %s (%s)", path, strerror(errno));
#else
fatal("cache eviction not (yet?) supported on this platform");
#endif
} else {
char mincore_array[pages_in_file];
int64_t pages_in_core=0;
double last_chart_print_time=0.0, temp_time;
// 3rd arg to mincore is char* on BSD and unsigned char* on linux
if (mincore(mem, len_of_file, (void*)mincore_array)) fatal("mincore %s (%s)", path, strerror(errno));
for (i=0; i<pages_in_file; i++) {
if (is_mincore_page_resident(mincore_array[i])) {
pages_in_core++;
total_pages_in_core++;
}
}
if (o_verbose) {
printf("%s\n", path);
last_chart_print_time = gettimeofday_as_double();
print_page_residency_chart(stdout, mincore_array, pages_in_file);
}
if (o_touch) {
for (i=0; i<pages_in_file; i++) {
junk_counter += ((char*)mem)[i*pagesize];
mincore_array[i] = 1;
if (o_verbose) {
temp_time = gettimeofday_as_double();
if (temp_time > (last_chart_print_time+CHART_UPDATE_INTERVAL)) {
last_chart_print_time = temp_time;
print_page_residency_chart(stdout, mincore_array, pages_in_file);
}
}
}
}
if (o_verbose) {
print_page_residency_chart(stdout, mincore_array, pages_in_file);
printf("\n");
}
}
if (o_lock) {
if (mlock(mem, len_of_file))
fatal("mlock: %s (%s)", path, strerror(errno));
}
if (!o_lock && !o_lockall) {
if (munmap(mem, len_of_file)) warning("unable to munmap file %s (%s)", path, strerror(errno));
close(fd);
}
}
稍微有点基础就可以看明白了,先 mmap 映射文件到当前进程,按 page size 对齐之后,调用 mincore 函数就可以得到文件中每一个 page 是否在 page cache 中,结果保存在 mincore_array 数组中,该数据中每个字节的第一位即表示是否在 page cache 中。
将文件内容逐出(指定 o_evict)出 page cache 是通过 posix_fadvise 函数调用 fadvise 系统调用来实现的(BSD通过 msync 实现,这个在 Linux 上没有效果)。fadvise 系统调用可以告诉 kernel 要操作的文件在接下来要干什么,kernel 可以提前做一些操作而提高性能,Linux kernel 里实现了以下几种控制方式:
POSIX_FADV_NORMAL - 正常操作,对文件使用底层设备的默认 readahead 值;
POSIX_FADV_SEQUENTIAL - 顺序I/O,对文件使用两倍的 readahead 值;
POSIX_FADV_RANDOM - 随机I/O,禁用文件上的 readahead;
POSIX_FADV_NOREUSE - 只使用一次
POSIX_FADV_WILLNEED - 很快需要使用,对文件使用非阻塞读到 page cache
POSIX_FADV_DONTNEED - 不再需要使用文件,从 page cache 中逐出
posix_fadvise 加 POSIX_FADV_DONTNEED 参数就可以将文件从 page cache 中逐出,需要注意的是如果需要确保文件从 page cache 中逐出,还需要在调用 fadvise 之前用 fsync/fdatasync/sync_file_range 之类的函数将 dirty page 清理。
下面是我在 Linux 下用 posix_fadvise 的一个测试程序测试的结果:
[root@localhost ~]# echo 3 > /proc/sys/vm/drop_caches [root@localhost ~]# free total used free shared buffers cached Mem: 374092 61832 312260 0 136 5060 -/+ buffers/cache: 56636 317456 Swap: 707576 436 707140 [root@localhost ~]# dd if=/dev/zero of=test bs=1024k count=100 记录了100+0 的读入 记录了100+0 的写出 104857600字节(105 MB)已复制,22.5514 秒,4.6 MB/秒 [root@localhost ~]# free total used free shared buffers cached Mem: 374092 168960 205132 0 564 109816 -/+ buffers/cache: 58580 315512 Swap: 707576 436 707140 [root@localhost ~]# ./fadvise test POSIX_FADV_DONTNEED OK [root@localhost ~]# free total used free shared buffers cached Mem: 374092 63932 310160 0 580 7424 -/+ buffers/cache: 55928 318164 Swap: 707576 436 707140
从 free 命令的结果可以很明显的看到,dd 之后基本文件都在 page cache 中,fadvise 之后从 page cache 中正确逐出。
接着是 vmtouch 中的 touch 操作(指定 o_touch)就更简单了,对 mmap 到的地址直接遍历引用,不在 page cache 的内容会自动产生 page fault 到 page cache 中。
lock 内存(指定 o_lock)也则直接使用 mlock 函数来实现,mlock 对于对安全性和实时性有很高要求的程序非常有用,可以保证指定的文件区域在内存中,不被 swap 出去。
以上为个人分析结果,有任何问题欢迎指正咯 ^_^