VMA使用

VMA使用


用户地址空间

用户层进程的虚拟地址空间是linux的一个重要抽象:它向每个运行进程提供了同样的系统视图,这使得多个进程可以同时运行,而不会相互干扰。
本文讨论内核管理用户虚拟地址空间的方法,由于一下种种原因,这比内核地址空间的管理更复杂。

  • 每个应用程序都有自身的地址空间,与所有其它的应用程序分隔开。
  • 通常在巨大的线性地址空间中,只有很少的段可用于各个用户空间进程,这些段彼此有一定的距离。内核需要一些数据结构来有效管理这些段。
  • 地址空间只有极小的一部分与物理内存页直接关联。不经常使用的段,只有当需要时才会与物理页帧进行关联。
  • 内核信任自身,但无法信任用户进程。因此,各个操作用户地址空间的操作都伴随着各种检查,以确保程序的权限不会超出应有的限制。
  • fork-exec模型在UNIX操作系统中用于产生新进程。如果实现得较为粗劣,该模型的功能将大打折扣。因此内核借助与一些技巧,来尽可能高效的管理用户地址空间。
进程地址空间的布局

虚拟地址空间中包含了若干区域。其分布方式是特定于体系结构的,但所有方法都有下列共同部分。

  • 代码段,当前运行代码的二进制代码。
  • 动态库,程序使用的动态库代码。
  • 堆。
  • 栈。
  • 环境变量和命令行参数的段。
  • 将文件内容映射到虚拟地址空间中的内存映射。

既然出现了这么多的区域,所以内核就将各个区域称为VMA(virtual memoryAreas),每个区域使用一个名为struct vm_area_struct的数据结构进行管理。

VMA数据结构

vm_area_struct结构体描述了指定地址空间内连续区间上的一个独立内存范围。内核将每个内存范围作为一个单独的内存对象管理,每个内存对象都拥有一致的属性,比如访问权限等,另外,相应的操作也都一致。注意在同一个地址空间内的不同内存区域不能重叠。

<include/linux/mm_types.h>
struct vm_area_struct {
	/* The first cache line has the info for VMA tree walking. */

    /* 线性区内的第一个线性地址 */
	unsigned long vm_start;		/* Our start address within vm_mm. vm_mm内的起始地址 */
    /* 线性区之外的第一个线性地址 */
	unsigned long vm_end;		/* The first byte after our end address
					   within vm_mm. 在vm_mm内结束地址之后的第一个字节的地址 */

	/* linked list of VM areas per task, sorted by address 各进程的虚拟内存区域链表,按地址排序 */
    /* 整个链表会按地址大小递增排序 */
    /* vm_next: 线性区链表中的下一个线性区 */
    /* vm_prev: 线性区链表中的上一个线性区 */
	struct vm_area_struct *vm_next, *vm_prev;

    /* 用于组织当前内存描述符的线性区的红黑树的结点 */
	struct rb_node vm_rb;

	/*
	 * Largest free memory gap in bytes to the left of this VMA.
	 * Either between this VMA and vma->vm_prev, or between one of the
	 * VMAs below us in the VMA rbtree and its ->vm_prev. This helps
	 * get_unmapped_area find a free area of the right size.
	 */
	/* 此vma的子树中最大的空闲内存块大小(bytes) */
	unsigned long rb_subtree_gap;

	/* Second cache line starts here. */

    /* 指向所属的内存描述符 */
	struct mm_struct *vm_mm;	/* The address space we belong to. 所属地址空间 */
    /* 页表项标志的初值,当增加一个页时,内核根据这个字段的值设置相应页表项中的标志 */
    /* 页表中的User/Supervisor标志应当总被置1 */
	pgprot_t vm_page_prot;		/* Access permissions of this VMA. 该区域虚拟内存区域的访问权限 */
    /* 线性区标志
     * 读写可执行权限会复制到页表项中,由分页单元去检查这几个权限
     */
	unsigned long vm_flags;		/* Flags, see mm.h. 标志 */

	/*
	 * For areas with an address space and backing store,
	 * linkage into the address_space->i_mmap interval tree.
	 * 对于有adress_space和后备存取器的区域来说,
	 * shared连接到address_space->i_mmap优先树
	 */
	struct {
		struct rb_node rb;
		unsigned long rb_subtree_last;
	} shared;

	/*
	 * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
	 * list, after a COW of one of the file pages.	A MAP_SHARED vma
	 * can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack
	 * or brk vma (with NULL file) can only be in an anon_vma list.
	 */
	/* 
     * 指向匿名线性区链表头的指针,这个链表会将此mm_struct中的所有匿名线性区链接起来
     * 匿名的MAP_PRIVATE、堆和栈的vma都会存在于这个anon_vma_chain链表中
     * 如果mm_struct的anon_vma为空,那么其anon_vma_chain也一定为空
     */
	struct list_head anon_vma_chain; /* Serialized by mmap_sem &
					  * page_table_lock */
    /* 指向anon_vma数据结构的指针,对于匿名线性区,此为重要结构 */
	struct anon_vma *anon_vma;	/* Serialized by page_table_lock */

	/* Function pointers to deal with this struct. */
    /* 指向线性区操作的方法,特殊的线性区会设置,默认会为空 */
	const struct vm_operations_struct *vm_ops;

	/* Information about our backing store: 后备存取器有关信息 */
    /* 如果此vma用于映射文件,那么保存的是在映射文件中的偏移量。
     * 如果是匿名线性区,它等于0或者vma开始地址对应的虚拟页框号(vm_start >> PAGE_SIZE),
     * 这个虚拟页框号用于vma向下增长时反向映射的计算(栈) 
     */
	unsigned long vm_pgoff;		/* Offset (within vm_file) in PAGE_SIZE
					   units */
    /* 指向映射文件的文件对象,也可能指向建立shmem共享内存中返回的struct file,
     * 如果是匿名线性区,此值为NULL或者一个匿名文件(这个匿名文件跟swap有关?待看) 
     */
	struct file * vm_file;		/* File we map to (can be NULL). */
	void * vm_private_data;		/* was vm_pte (shared mem) */

#ifndef CONFIG_MMU
	struct vm_region *vm_region;	/* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
	struct mempolicy *vm_policy;	/* NUMA policy for the VMA */
#endif
	struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
};

由于这些地址空间归属于各个用户进程,所以在用户进程的struct mm_struct数据结构中也有相应的成员,用于对这些VMA进程管理。

struct mm_struct {
    /* 指向线性区对象的链表头,链表是经过排序的,按线性地址升序排列,里面包括了匿名映射线性区和文件映射线性区 */
	struct vm_area_struct *mmap;		/* list of VMAs 虚拟内存区域列表 */
    /* 指向线性区对象的红黑树的根,一个内存描述符的线性区会用两种方法组织,链表和红黑树,红黑树适合内存描述符有非常多线性区的情况 */
    struct rb_root mm_rb;               /* 虚拟内存区域红黑树 */
    ...
};

每个VMA都要连接到mm_struct中的链表和红黑树中,以便查找。

  • mmap形成一个单链表,进程中所以的VMA都链接到这个链表中,链表头是mm_struct->mmap,节点元素是vm_area_struct->vm_next和vm_prev。
  • mm_rb是红黑树的根节点,子节点为vm_area_struct->rb,红黑树的键值为vm_area_struct->vm_end,每个进程有一颗VMA的红黑树。

VMA按照起始地址一递增的方式插入mm_struct->mmap链表中。当进程拥有大量的VMA时,扫描链表和查找特定的VMA是非常低效的操作,例如在云计算的机器中,所以内核中通常要靠红黑树来协助,以便提高查找速度。

操作VMA
1.查找VMA

通过虚拟地址addr来查找VMA是内存中常用的操作。

1.1 find_vma() 函数根据给定地址addr查找满足如下条件之一的VMA:

  • addr在VMA空间范围内,即vma->vm_start <= addr < vma->vm_end。
  • 距离addr最近并且VMA的结束地址大于addr的一个VMA。
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
	struct rb_node *rb_node;
	struct vm_area_struct *vma;

    //1
	/* Check the cache first. */
	vma = vmacache_find(mm, addr);
	if (likely(vma))
		return vma;

    //2
	rb_node = mm->mm_rb.rb_node;

	while (rb_node) {
		struct vm_area_struct *tmp;

		tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);

		if (tmp->vm_end > addr) {
			vma = tmp;
			if (tmp->vm_start <= addr)
				break;
			rb_node = rb_node->rb_left;
		} else
			rb_node = rb_node->rb_right;
	}

    //3
	if (vma)
		vmacache_update(addr, vma);
	return vma;
}

find_vma()实现流程:

  1. 查找缓存,若在缓存中找到,则直接返回。在task_struct结构中,有一个存放最近访问过的VMA的数组vmacache[VMACACHE_SIZE],可以存放几个最近使用的VMA,充分利用局部性原理。
  2. 根据红黑树进行查找。这是一个典型的红黑树查找方式,根据键值选择左节点或者右节点。
  3. 若找到满足要求的VMA,则更新缓存。注意更新缓存时数组下标的哈希计算。基于页码,为具有良好局部性和随机访问的工作负载提供良好的命中率。

1.2 find_vma_prev() 函数的逻辑与find_vma()一样,但是返回VMA的前继成员。

struct vm_area_struct *
find_vma_prev(struct mm_struct *mm, unsigned long addr,
			struct vm_area_struct **pprev)
{
	struct vm_area_struct *vma;

	vma = find_vma(mm, addr);
	if (vma) {
		*pprev = vma->vm_prev;
	} else {
		struct rb_node *rb_node = mm->mm_rb.rb_node;
		*pprev = NULL;
		/* 遍历找到最大的节点 */
		while (rb_node) {
			*pprev = rb_entry(rb_node, struct vm_area_struct, vm_rb);
			rb_node = rb_node->rb_right;
		}
	}
	return vma;
}

pprev参数存放小于addr的VMA指针。

1.3 find_vma_intersection() 函数用于查找start_addr、end_addr和现存的VMA有重叠的一个VMA,它基于find_vma()实现。

static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)
{
	struct vm_area_struct * vma = find_vma(mm,start_addr);

	if (vma && end_addr <= vma->vm_start)
		vma = NULL;
	return vma;
}
2.插入VMA

insert_vm_struct()是内核提供的插入VMA的核心API函数。

int insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vma)
{
	struct vm_area_struct *prev;
	struct rb_node **rb_link, *rb_parent;

    //1.
	if (find_vma_links(mm, vma->vm_start, vma->vm_end,
			   &prev, &rb_link, &rb_parent))
		return -ENOMEM;
    //2.
	if ((vma->vm_flags & VM_ACCOUNT) &&
	     security_vm_enough_memory_mm(mm, vma_pages(vma)))
		return -ENOMEM;

	/*
	 * The vm_pgoff of a purely anonymous vma should be irrelevant
	 * until its first write fault, when page's anon_vma and index
	 * are set.  But now set the vm_pgoff it will almost certainly
	 * end up with (unless mremap moves it elsewhere before that
	 * first wfault), so /proc/pid/maps tells a consistent story.
	 *
	 * By setting it to reflect the virtual start address of the
	 * vma, merges and splits can happen in a seamless way, just
	 * using the existing file pgoff checks and manipulations.
	 * Similarly in do_mmap_pgoff and in do_brk.
	 */
	//3.
	if (vma_is_anonymous(vma)) {
		BUG_ON(vma->anon_vma);
		vma->vm_pgoff = vma->vm_start >> PAGE_SHIFT;
	}

    //4.
	vma_link(mm, vma, prev, rb_link, rb_parent);
	return 0;
}

insert_vm_struct()实现流程

  1. 寻找插入位置,寻找的过程很有意思,运用了二级指针和三级指针作为形参。
  2. 若该区域是一个记账VMA区域(没懂),这个机制还没遇到过。
  3. 如果VMA是匿名映射区,则设置vma->vm_pgoff等于区域起始页码。
  4. 插入到链表和红黑树中,若是文件映射,加入到文件的基数树中。
3.合并VMA

在新的VMA被加入到进程的地址空间时,内核会检查它是否可以与现存的VMA进行合并。vma_merge()函数实现将一个新的VMA和附近的VMA合并的功能。

struct vm_area_struct *vma_merge(struct mm_struct *mm,
			struct vm_area_struct *prev, unsigned long addr,
			unsigned long end, unsigned long vm_flags,
			struct anon_vma *anon_vma, struct file *file,
			pgoff_t pgoff, struct mempolicy *policy,
			struct vm_userfaultfd_ctx vm_userfaultfd_ctx)
{
    /* 计算出区域大小,单位是页 */
	pgoff_t pglen = (end - addr) >> PAGE_SHIFT;
	struct vm_area_struct *area, *next;
	int err;

	/*
	 * We later require that vma->vm_flags == vm_flags,
	 * so this tests vma->vm_flags & VM_SPECIAL, too.
	 */
	/* VM_SPECIAL指的是non-mergable, non-mlock的VMAs */
	if (vm_flags & VM_SPECIAL)
		return NULL;

	if (prev)
		next = prev->vm_next;
	else
		next = mm->mmap;
	area = next;
	if (area && area->vm_end == end)		/* cases 6, 7, 8 */
		next = next->vm_next;

	/* verify some invariant that must be enforced by the caller */
	VM_WARN_ON(prev && addr <= prev->vm_start);
	VM_WARN_ON(area && end > area->vm_end);
	VM_WARN_ON(addr >= end);

	/*
	 * Can it merge with the predecessor?
	 */
	/* 判断能否与前继区域合并 
     * 合并条件:1.插入区域的起始地址与前一区域的结束地址相等
     *           2.在NUMA架构中,判断两个区域的vm_policy是否相等
     *           3.判断prev是否满足合并(vm_flag、vm_file、...)
     * 真正的合并工作在__vma_adjust()函数中进行,太长了,看不下去
     */
	if (prev && prev->vm_end == addr &&
			mpol_equal(vma_policy(prev), policy) &&
			can_vma_merge_after(prev, vm_flags,
					    anon_vma, file, pgoff,
					    vm_userfaultfd_ctx)) {
		/*
		 * OK, it can.  Can we now merge in the successor as well?
		 */
		/* 理想情况是新插区域的结束地址等于next节点的起始地址,那么前后节点prev和next可以合并在一起
		 * 这里判断条件多了一个prev与next的判断
		 */
		if (next && end == next->vm_start &&
				mpol_equal(policy, vma_policy(next)) &&
				can_vma_merge_before(next, vm_flags,
						     anon_vma, file,
						     pgoff+pglen,
						     vm_userfaultfd_ctx) &&
				is_mergeable_anon_vma(prev->anon_vma,
						      next->anon_vma, NULL)) {
							/* cases 1, 6 */
			err = __vma_adjust(prev, prev->vm_start,
					 next->vm_end, prev->vm_pgoff, NULL,
					 prev);
		} else					/* cases 2, 5, 7 */
			err = __vma_adjust(prev, prev->vm_start,
					 end, prev->vm_pgoff, NULL, prev);
		if (err)
			return NULL;
		khugepaged_enter_vma_merge(prev, vm_flags);
		return prev;
	}

	/*
	 * Can this new request be merged in front of next?
	 */
	/* 判断能否与后继区域合并 */
	if (next && end == next->vm_start &&
			mpol_equal(policy, vma_policy(next)) &&
			can_vma_merge_before(next, vm_flags,
					     anon_vma, file, pgoff+pglen,
					     vm_userfaultfd_ctx)) {
		if (prev && addr < prev->vm_end)	/* case 4 */
			err = __vma_adjust(prev, prev->vm_start,
					 addr, prev->vm_pgoff, NULL, next);
		else {					/* cases 3, 8 */
			err = __vma_adjust(area, addr, next->vm_end,
					 next->vm_pgoff - pglen, NULL, next);
			/*
			 * In case 3 area is already equal to next and
			 * this is a noop, but in case 8 "area" has
			 * been removed and next was expanded over it.
			 */
			area = next;
		}
		if (err)
			return NULL;
		khugepaged_enter_vma_merge(area, vm_flags);
		return area;
	}

	return NULL;
}
4.创建VMA
unsigned long
get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
		unsigned long pgoff, unsigned long flags)
{
	unsigned long (*get_area)(struct file *, unsigned long,
				  unsigned long, unsigned long, unsigned long);

	unsigned long error = arch_mmap_check(addr, len, flags);
	if (error)
		return error;

	/* Careful about overflows.. */
	if (len > TASK_SIZE)
		return -ENOMEM;

	get_area = current->mm->get_unmapped_area;
	if (file) {
		if (file->f_op->get_unmapped_area)
			get_area = file->f_op->get_unmapped_area;
	} else if (flags & MAP_SHARED) {
		/*
		 * mmap_region() will call shmem_zero_setup() to create a file,
		 * so use shmem's get_unmapped_area in case it can be huge.
		 * do_mmap_pgoff() will clear pgoff, so match alignment.
		 */
		pgoff = 0;
		get_area = shmem_get_unmapped_area;
	}

	addr = get_area(file, addr, len, pgoff, flags);
	if (IS_ERR_VALUE(addr))
		return addr;

	if (addr > TASK_SIZE - len)
		return -ENOMEM;
	if (offset_in_page(addr))
		return -EINVAL;

	error = security_mmap_addr(addr);
	return error ? error : addr;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值