3.数据结构(vm_area_struct,优先树)
struct mm_struct提供了进程在内存中布局的所有必要信息。另外,它还包括下列成员,用于管理用户进程在虚拟地址空间中的所有内存区域。(其中管理虚拟地址空间所有内存区域)
<mm_types.h> struct mm_struct { struct vm_area_struct * mmap; /* 虚拟内存区域列表 */ struct rb_root mm_rb; struct vm_area_struct * mmap_cache; /* 上一次find_vma的结果 */ ... }
树和链表
每个区域都通过一个vm_area_struct实例描述,进程的各区域按两种方法排序。
(1) 在一个单链表上(开始于mm_struct->mmap)。
(2) 在一个红黑树中,根结点位于mm_rb。
mmap_cache缓存了上一次处理的区域。
用户虚拟地址空间中的每个区域由开始和结束地址描述。现存的区域按起始地址以递增次序被归入链表中。扫描链表找到与特定地址关联的区域,在有大量区域时是非常低效的操作(数据密集型的应用程序就是这样)。因此vm_area_struct的各个实例还通过红黑树管理,可以显著加快扫描速度。
虚拟内存区域的表示
每个区域表示为vm_area_struct的一个实例,其定义(简化形式)如下:(每个虚拟内存区域实际管理者)
<mm_types.h> struct vm_area_struct { struct mm_struct * vm_mm; /* 所属地址空间。 */ unsigned long vm_start; /* vm_mm内的起始地址。 */ unsigned long vm_end; /* 在vm_mm内结束地址之后的第一个字节的地址。 */ /* 各进程的虚拟内存区域链表,按地址排序 */ struct vm_area_struct *vm_next; pgprot_t vm_page_prot; /* 该虚拟内存区域的访问权限。 */ unsigned long vm_flags; /* 标志,如下列出。 */ struct rb_node vm_rb; /* 对于有地址空间和后备存储器的区域来说, shared连接到address_space->i_mmap优先树, 或连接到悬挂在优先树结点之外、类似的一组虚拟内存区域的链表, 或连接到address_space->i_mmap_nonlinear链表中的虚拟内存区域。 */ union { struct { struct list_head list; void *parent; /* 与prio_tree_node的parent成员在内存中位于同一位置 */ struct vm_area_struct *head; } vm_set; struct raw_prio_tree_node prio_tree_node; } shared; /* *在文件的某一页经过写时复制之后,文件的MAP_PRIVATE虚拟内存区域可能同时在i_mmap树和 *anon_vma链表中。MAP_SHARED虚拟内存区域只能在i_mmap树中。 *匿名的MAP_PRIVATE、栈或brk虚拟内存区域(file指针为NULL)只能处于anon_vma链表中。 */ struct list_head anon_vma_node; /* 对该成员的访问通过anon_vma->lock串行化 */ struct anon_vma *anon_vma; /* 对该成员的访问通过page_table_lock串行化 */ /* 用于处理该结构的各个函数指针。 */ struct vm_operations_struct * vm_ops; /* 后备存储器的有关信息: */ unsigned long vm_pgoff; /* (vm_file内)的偏移量,单位是PAGE_SIZE,不是PAGE_CACHE_SIZE */ struct file * vm_file; /* 映射到的文件(可能是NULL)。 */ void * vm_private_data; /* vm_pte(即共享内存) */ };
各个成员的语义如下。
vm_mm是一个反向指针,指向该区域所属的mm_struct实例。
vm_start和vm_end指定了该区域在用户空间中的起始和结束地址。
进程所有vm_area_struct实例的链表是通过vm_next实现的,而与红黑树的集成则通过vm_rb实现。
vm_page_prot存储该区域的访问权限。
vm_flags是描述该区域的一组标志。我将在下文讨论可以设置的标志。
从文件到进程的虚拟地址空间中的映射,可通过文件中的区间和内存中对应的区间唯一地确定。为跟踪与进程关联的所有区间,内核使用了如上所述的链表和红黑树。
但还必须能够反向查询:给出文件中的一个区间,内核有时需要知道该区间映射到的所有进程。这种映射称作共享映射(shared mapping)。为提供所需的信息,所有的vm_area_struct实例都还通过一个优先树管理,包含在shared成
员中。细节将在下文讨论。
anon_vma_node和anon_vma用于管理源自匿名映射(anonymous mapping)的共享页。指向相同页的映射都保存在一个双链表上,anon_vma_node充当链表元素。有若干此类链表,具体的数目取决于共享物理内存页的映射集合的数目。anon_vma成员是一个指向与各链表关联的管理结构的指针,该管理结构由一个表头和相关的锁组成。
匿名映射和共享映射详细讨论见这里。 vm_ops是一个指针,指向许多方法的集合,这些方法用于在区域上执行各种标准操作。
<mm.h> struct vm_operations_struct { void (*open)(struct vm_area_struct * area); void (*close)(struct vm_area_struct * area); int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf); struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, int *type); ... };
在创建和删除区域时,分别调用open和close。这两个接口通常不使用,设置为NULL指针。
但fault是非常重要的。如果地址空间中的某个虚拟内存页不在物理内存中,自动触发的缺页异常处理程序会调用该函数,将对应的数据读取到一个映射在用户地址空间的物理内存页中。
nopage是内核原来用于响应缺页异常的方法,不如fault那么灵活。出于兼容性的考虑,该成员仍然保留,但不应该用于新的代码。
vm_pgoffset指定了文件映射的偏移量,该值用于只映射了文件部分内容时(如果映射了整个文件,则偏移量为0)。偏移量的单位不是字节,而是页(即PAGE_SIZE)。
vm_file指向file实例,描述了一个被映射的文件(如果映射的对象不是文件,则为NULL指针)。
取决于映射类型,vm_private_data可用于存储私有数据,不由通用内存管理例程操作。内核只确保在创建新区域时该成员初始化为NULL指针。当前,只有少数声音和视频驱动程序使用了该选项。
vm_flags存储了定义区域性质的标志。这些都是<mm.h>中声明的预处理器常数。
VM_READ、VM_WRITE、VM_EXEC、VM_SHARED分别指定了页的内容是否可以读、写、执行,或者由几个进程共享。
VM_GROWSDOWN和VM_GROWSUP表示一个区域是否可以向下或向上扩展(到更低或更高的虚拟地址)。由于堆自下而上增长,其区域需要设置VM_GROWSUP。VM_GROWSDOWN对栈设置,该区域自顶向下增长。
如果区域很可能从头到尾顺序读取,则设置VM_SEQ_READ。VM_RAND_READ指定了读取可能是随机的。这两个标志用于“提示”内存管理子系统和块设备层,以优化其性能(例如,如果访问是顺序的,则启用页的预读。)
如果设置了VM_DONTCOPY,则相关的区域在fork系统调用执行时不复制。
优先查找树
优先查找树(priority search tree)用于建立文件中的一个区域与该区域映射到的所有虚拟地址空间之间的关联。
1.附加的数据结构
每个打开文件(和每个块设备,因为这些也可以通过设备文件进行内存映射)都表示为struct file的一个实例。该结构包含了一个指向地址空间对象struct address_space的指针。该对象是优先查找树(prio tree)的基础,而文件区间与其映射到的地址空间之间的关联即通过优先树建立。
<fs.h> struct address_space { struct inode *host; /* owner: inode, block_device */ ... struct prio_tree_root i_mmap; /* 私有和共享映射的树 */ struct list_head i_mmap_nonlinear;/*VM_NONLINEAR映射的链表 */ ... }
<fs.h> struct file { ... struct address_space *f_mapping; ... }
此外,每个文件和块设备都表示为struct inode的一实例。struct file是通过open系统调用打开的文件的抽象,与此相反,inode表示文件系统自身中的对象。
<fs.h> struct inode { ... struct address_space *i_mapping; ... }
inode是一个特定于文件的数据结构,而file则是特定于给定进程的。
这些数据结构彼此关联,图4-7给出了内存中各个结构之间关联的概述。请注意,图中树的表示只是象征性的,没有反映实际上比较复杂的树的布局。
![](https://img2022.cnblogs.com/blog/2355803/202203/2355803-20220324203838512-412522273.png)
在这里只要知道以下内容就足够了:address_space是优先树的基本要素,而优先树包含了所有相关的vm_area_struct实例,描述了与inode关联的文件区间到一些虚拟地址空间的映射。由于每个struct vm_area的实例都包含了一个指向所属进程的mm_struct的指针,关联就已经建立起来了!要注意,vm_area_struct还可以通过以i_mmap_nonlinear为表头的双链表与一个地址空间关联。这是非线性映射(nonlinear mapping)所需要的,详细讨论见。
2. 优先树的表示
struct raw_prio_tree_node { struct prio_tree_node *left; struct prio_tree_node *right; struct prio_tree_node *parent; }; struct prio_tree_node { struct prio_tree_node *left; struct prio_tree_node *right; struct prio_tree_node *parent; unsigned long start; unsigned long last; /* last location _in_ interval */ }; struct prio_tree_root { struct prio_tree_node *prio_tree_node; unsigned short index_bits; unsigned short raw; /* * 0: nodes are of type struct prio_tree_node * 1: nodes are of type raw_prio_tree_node */ };
优先树用来管理表示给定文件中特定区间的所有vm_area_struct实例。这要求该数据结构不仅能够处理重叠,还要能处理相同的文件区间。如图4-8所示:两个进程将一个文件的[7, 12]区域映射到其虚拟地址空间中,而第3个进程映射了区间[10, 30]。
![](https://img2022.cnblogs.com/blog/2355803/202203/2355803-20220324213407739-1964162169.png)
重叠区间的管理称不上是个问题。区间的边界提供了一个唯一索引,可用于将各个区间存储在一个唯一的树结点中。我不会详细讨论内核的实现方式,因为这与基数树非常相似。只要知道:如果区间B、C和D完全包含在另一个区间A中,那么A将是B、C和D的父结点。
但如果多个相同区间被归入优先树,会发生什么情况?各个优先树结点表示为一个raw_prio_tree_node实例,该实例直接包含在各个vm_area_struct实例中。回忆前文,该实例与一个vm_set实例在同一个联合中。这可以将一个vm_set(进而vm_area_struct)的链表与一个优先树结点关联起来。图4-9说明了内存中这种关联的具体情况。
![](https://img2022.cnblogs.com/blog/2355803/202203/2355803-20220324213851765-1283450451.png)
在区间插入到优先树时,内核进行如下操作。
在vm_area_struct实例链接到优先树中作为结点时,raw_prio_tree_node用于建立必要的关联。为检查是否树中已经有同样的vm_area_struct,内核利用下述事实。vm_set的parent成员与raw_prio_tree_node结构的最后一个成员是相同的,这些数据结构可据此进行协调。由于parent在vm_set内并不使用,内核可以使用parent != NULL,来检查当前的vm_area_struct实例是否已经在树中。raw_prio_tree_node的定义还确保了在share联合内部的内存布局中,vmset的head成员与prio_tree_node不重叠,因此二者尽管在同一个联合之中,也可以同时使用。因此内核使用vm_set.head指向属于一个共享映射的vm_area_struct实例列表中的第一个实例。
这段话比较难理解,下面的解释翻译于该版本Linux源码的注释:
对于映射一组唯一文件页面的每个 vma,即唯一 [radix_index,heap_index] 值,我们有一个对应的优先级搜索树节点。如果多个 vmas 具有相同的 [radix_index, heap_index] 值,则其中一个用作树节点,其他的存储在 vm_set 列表中。树节点使用 vm_set.head 指向列表的第一个 vma(头),即只有树节点share成员是raw_prio_tree_node,其余是vm_set。
总结:每一个inode都有一个唯一的address_space,每一个address_space都有唯一一个专属prio_tree_root,其上的结点类型为prio_tree_node,vm_area_struct中share中的raw_prio_tree_node类型成员在插入时会转化为prio_tree_node,插入方法如上文讨论,类似于基准树,并且上文中的区间的左区间保存在vma->vm_pgoff,右区间可以通过vma计算出来,这些在插入时都是必要信息