vmtouch实现原理解析

vmtouch是一个很好用的小工具,可以用来查询文件是否在内存中的有缓存,也可以将文件导入缓存或者锁定缓存。

工具源码:https://github.com/hoytech/vmtouch

 

1.vmtouch的使用

先来看看这个工具如何使用的

 

2.vmtouch原理

首先我们来看vmtouch.c文件,这个工具就这一个源码文件,可见有多简洁。

main函数很简单,先进行参数解析,然后执行 for (i=0; i<argc; i++) vmtouch_crawl(argv[i]);

由于该可以同时分析多个文件。主要实现函数在vmtouch_crawl中。

vmtouch可以解析整个目录下的所有文件,所以在vmtouch_crawl中会对目录中所有文件进行历遍,最后由vmtouch_file(path);函数执行具体文件的解析。

下面我把vmtouch_file函数中几个关键点列出来:

void vmtouch_file(char *path) {
    fd = open(path, open_flags, 0);  
    mem = mmap(NULL, len_of_range, PROT_READ, MAP_SHARED, fd, offset);
    mincore(mem, len_of_range, (void*)mincore_array)
}

最关键的就上面那三个步骤,首先是打开文件,然后进行mmap映射。mmap映射是将文件内容映射到进程地址空间的内存中。接着调用了mincore函数。这个函数就是vmtouch能获得文件缓存情况的关键。

mincore函数的定义:

#include <unistd.h>
#include <sys/mman.h>

int mincore(void *addr, size_t length, unsigned char *vec);

参数有三个:

1.addr:地址

2.length:长度

3.vec:vec指向一个数组,数组成员必须有(length+PAGE_SIZE-1) / PAGE_SIZE个,也就是每个页对应一个字节,该字节最后一个bit如果是1,就表示这个页在缓存中,否则就不再缓存中。

描述:

mincore返回一个数组,该数组表示文件的每个页,在进程的虚拟地址空间中,是否有实际物理内存的缓存。内核会返回该地址起始页到结束页在内存中的驻留情况。地址(addr)必须是页对齐长度(length)不需要页对齐,但返回的状态是整个页的状态。当然这里返回的状态是瞬时的状态,如果没有锁定内存,那么随时可能被交换出去。

3.mincore内核实现原理

mincore是一个系统调用,进入内核后,其实现代码在/kernel/mm/mincore.c

SYSCALL_DEFINE3(mincore, unsigned long, start, size_t, len,
		unsigned char __user *, vec)
{
    pages = len >> PAGE_SHIFT;
	pages += (offset_in_page(len)) != 0;
    ......
    while (pages) {
		/*
		 * Do at most PAGE_SIZE entries per iteration, due to
		 * the temporary buffer size.
		 */
		down_read(&current->mm->mmap_sem);
		retval = do_mincore(start, min(pages, PAGE_SIZE), tmp);
		up_read(&current->mm->mmap_sem);

		if (retval <= 0)
			break;
		if (copy_to_user(vec, tmp, retval)) {
			retval = -EFAULT;
			break;
		}
		pages -= retval;
		vec += retval;
		start += retval << PAGE_SHIFT;
		retval = 0;
	}
}

参数和应用曾一样,起始地址、长度和页状态描述的数组。这里起始地址由指针类型变为长整型。

首先根据长度来计算所要解析的页的个数,然后调用do_mincore来完成解析。

static long do_mincore(unsigned long addr, unsigned long pages, unsigned char *vec)
{
	struct vm_area_struct *vma;
	unsigned long end;
	int err;
	struct mm_walk mincore_walk = {
		.pmd_entry = mincore_pte_range,
		.pte_hole = mincore_unmapped_range,
		.hugetlb_entry = mincore_hugetlb,
		.private = vec,
	};

	vma = find_vma(current->mm, addr);
	if (!vma || addr < vma->vm_start)
		return -ENOMEM;
	mincore_walk.mm = vma->vm_mm;
	end = min(vma->vm_end, addr + (pages << PAGE_SHIFT));
	err = walk_page_range(addr, end, &mincore_walk);
	if (err < 0)
		return err;
	return (end - addr) >> PAGE_SHIFT;
}

do_mincore的参数是起始地址、页个数和页状态数组。

首先定义一个mm_walk结构体,该结构体在walk_page_range进行历遍进程页表树的时候执行pmd、ptd回调处理函数,也提供私有数据处理。这里的private就指向我们存储结果的页状态描述数组。

find_vma函数,根据起始地址查找进程地址空间的vma。这里的vma就是struct vm_area_struct,内核用来管理进程地址空间。一个vma就是一块连续的线性地址空间的抽象,它拥有自身的权限(可读,可写,可执行等等) ,每一个虚拟内存区域都由一个相关的struct vm_area_struct结构来描述。这里不展开细说,后续单独开一篇。

我们只要知道,查询到文件映射在进程空间的那个vma结构,我们就可以知道文件数据具体在进程虚拟地址空间的映射情况了。

接下去的流程:

do_mincore
|--->walk_page_range  这里会找到该文件相关的所有VMA
|------->__walk_page_range
|------------>walk_pgd_range

static int walk_pgd_range(unsigned long addr, unsigned long end,
			  struct mm_walk *walk)
{
	pgd_t *pgd;
	unsigned long next;
	int err = 0;

	pgd = pgd_offset(walk->mm, addr);
	do {
		next = pgd_addr_end(addr, end);
		if (pgd_none_or_clear_bad(pgd)) {
			if (walk->pte_hole)
				err = walk->pte_hole(addr, next, walk);
			if (err)
				break;
			continue;
		}
		if (walk->pmd_entry || walk->pte_entry)
			err = walk_pud_range(pgd, addr, next, walk);
		if (err)
			break;
	} while (pgd++, addr = next, addr != end);

	return err;
}

这个被映射的文件,在进程的虚拟地址空间中,如果已经被缓存到内存中,那么一定会在进程的页表中有相应的页表记录了物理地址和虚拟地址映射关系。所以我们这里要找到管理文件映射的页表。(这里有一个疑问,为什么通过mmap映射文件之后,进程页表会把该文件在缓存中的页自动更新到自己的页表中?)

pgd是一级页表,我们的linux内核使用了二级映射。因此一个pgd项可以表示4MB大小的内存空间。现在文件被映射到进程地址空间,由多个VMA管理多个连续的地址空间。因此我们要找每一个vma中的每一个落在地址范围内的pgd,然后在pgd中找到地址范围内的下一级页表。

先来看walk_pgd_range函数,这里面首先根据进程的mm_struct结构来获得进程pgd地址。然后根据地址偏移计算出addr所在的pgd项的偏移。pgd = pgd_offset(walk->mm, addr);

得到了这个VMA中我们所要的地址的pgd项,然后就在该pgd中查找页表项,判断有没有映射关系就可以了。

这里是walk_pud_range来解析下一级的页目录项,而实际上并没有作用,因为内核只使用了二级页表,。。。

 

最后会通过struct mm_walk中的回调函数mincore_pte_range来处理最后一级页表。

static int mincore_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end,
			struct mm_walk *walk)
{
	spinlock_t *ptl;
	struct vm_area_struct *vma = walk->vma;
	pte_t *ptep;
	unsigned char *vec = walk->private;
	int nr = (end - addr) >> PAGE_SHIFT;
	ptl = pmd_trans_huge_lock(pmd, vma);
	if (ptl) {
		memset(vec, 1, nr);
		spin_unlock(ptl);
		goto out;
	}
	if (pmd_trans_unstable(pmd)) {
		__mincore_unmapped_range(addr, end, vma, vec);
		goto out;
	}
	ptep = pte_offset_map_lock(walk->mm, pmd, addr, &ptl);
	for (; addr != end; ptep++, addr += PAGE_SIZE) {
		pte_t pte = *ptep;
		if (pte_none(pte))
			__mincore_unmapped_range(addr, addr + PAGE_SIZE,
						 vma, vec);
		else if (pte_present(pte))
			*vec = 1;
		else { /* pte is a swap entry */
			swp_entry_t entry = pte_to_swp_entry(pte);
			if (non_swap_entry(entry)) {
				/*
				 * migration or hwpoison entries are always
				 * uptodate
				 */
				*vec = 1;
			} else {
#ifdef CONFIG_SWAP
				*vec = mincore_page(swap_address_space(entry),
						    swp_offset(entry));
#else
				WARN_ON(1);
				*vec = 1;
#endif
			}
		}
		vec++;
	}
	pte_unmap_unlock(ptep - 1, ptl);
out:
	walk->private += nr;
	cond_resched();
	return 0;
}

参数还是三个:pmd(二级页目录),起始地址,结束地址,mm_walk结构

一个for循环,一页一页进行处理。先pte_none判断是否为空,然后pte_present判断是否有,最后判断是否已经被交换出去。

然后判断玩这个pgd,进行下一个pgd。判断完这个VMA,就进行下一个VMA。直到地址范围都检索结束。然后我们就得到了该文件在缓存中的情况了!

 

至此分析完成mincore在内核中的实现过程。

接下去再来看之前提的那个问题。为什么mmap文件之后,进程页表就能记录到在缓存中的页?

 

未完待续......

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值