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()实现流程:
- 查找缓存,若在缓存中找到,则直接返回。在task_struct结构中,有一个存放最近访问过的VMA的数组vmacache[VMACACHE_SIZE],可以存放几个最近使用的VMA,充分利用局部性原理。
- 根据红黑树进行查找。这是一个典型的红黑树查找方式,根据键值选择左节点或者右节点。
- 若找到满足要求的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()实现流程
- 寻找插入位置,寻找的过程很有意思,运用了二级指针和三级指针作为形参。
- 若该区域是一个记账VMA区域(没懂),这个机制还没遇到过。
- 如果VMA是匿名映射区,则设置vma->vm_pgoff等于区域起始页码。
- 插入到链表和红黑树中,若是文件映射,加入到文件的基数树中。
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;
}