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 等系统上使用,在这下载编译:

http://hoytech.com/vmtouch/

今天简单看了下 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 出去。

以上为个人分析结果,有任何问题欢迎指正咯 ^_^