本文同步自(如浏览不正常请点击跳转):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 出去。
以上为个人分析结果,有任何问题欢迎指正咯 ^_^