Linux 虚拟地址空间如何分布

Linux 虚拟地址空间如何分布,第1张

一个进程的虚拟地址空间主要由两个数据结来描述。一个是最高层次的:mm_struct,一个是较高层次的:vm_area_structs。最高层次的mm_struct结构描述了一个进程的整个虚拟地址空间。较高层次的结构vm_area_truct描述了虚拟地址空间的一个区间(简称虚拟区)。

1. MM_STRUCT结构

mm_strcut 用来描述一个进程的虚拟地址空间,在/include/linux/sched.h 中描述如下:

struct mm_struct {

struct vm_area_struct * mmap /* 指向虚拟区间(VMA)链表 */

rb_root_t mm_rb/*指向red_black树*/

struct vm_area_struct * mmap_cache/* 指向最近找到的虚拟区间*/

pgd_t * pgd /*指向进程的页目录*/

atomic_t mm_users /* 用户空间中的有多少用户*/

atomic_t mm_count /* 对"struct mm_struct"有多少引用*/

int map_count /* 虚拟区间的个数*/

struct rw_semaphore mmap_sem

spinlock_t page_table_lock /* 保护任务页表和 mm->rss */

struct list_head mmlist /*所有活动(active)mm的链表 */

unsigned long start_code, end_code, start_data, end_data

unsigned long start_brk, brk, start_stack

unsigned long arg_start, arg_end, env_start, env_end

unsigned long rss, total_vm, locked_vm

unsigned long def_flags

unsigned long cpu_vm_mask

unsigned long swap_address

unsigned dumpable:1

/* Architecture-specific MM context */

mm_context_t context

}

对该结构进一步说明如下:

在内核代码中,指向这个数据结构的变量常常是mm。

每个进程只有一个mm_struct结构,在每个进程的task_struct结构中,有一个指向该进程的结构。可以说,mm_struct结构是对整个用户空间的描述。

一个进程的虚拟空间中可能有多个虚拟区间(参见下面对vm_area_struct描述),对这些虚拟区间的组织方式有两种,当虚拟区较少时采用单链表,由mmap指针指向这个链表,当虚拟区间多时采用“红黑树(red_black

tree)”结构,由mm_rb指向这颗树。在2.4.10以前的版本中,采用的是AVL树,因为与AVL树相比,对红黑树进行 *** 作的效率更高。

因为程序中用到的地址常常具有局部性,因此,最近一次用到的虚拟区间很可能下一次还要用到,因此,把最近用到的虚拟区间结构应当放入高速缓存,这个虚拟区间就由mmap_cache指向。

指针pgt指向该进程的页目录(每个进程都有自己的页目录,注意同内核页目录的区别),当调度程序调度一个程序运行时,就将这个地址转成物理地址,并写入控制寄存器(CR3)。

由于进程的虚拟空间及其下属的虚拟区间有可能在不同的上下文中受到访问,而这些访问又必须互斥,所以在该结构中设置了用于P、V *** 作的信号量mmap_sem。此外,page_table_lock也是为类似的目的而设置。

虽然每个进程只有一个虚拟地址空间,但这个地址空间可以被别的进程来共享,如,子进程共享父进程的地址空间(也即共享mm_struct结构)。所以,用mm_user和mm_count进行计数。类型atomic_t实际上就是整数,但对这种整数的 *** 作必须是“原子”的。

另外,还描述了代码段、数据段、堆栈段、参数段以及环境段的起始地址和结束地址。这里的段是对程序的逻辑划分,与我们前面所描述的段机制是不同的。

mm_context_t是与平台相关的一个结构,对i386 几乎用处不大。

在后面对代码的分析中对有些域给予进一步说明。

2. VM_AREA_STRUCT 结构

vm_area_struct描述进程的一个虚拟地址区间,在/include/linux/mm.h中描述如下:

struct vm_area_struct

struct mm_struct * vm_mm /* 虚拟区间所在的地址空间*/

unsigned long vm_start/* 在vm_mm中的起始地址*/

unsigned long vm_end /*在vm_mm中的结束地址 */

/* linked list of VM areas per task, sorted by address */

struct vm_area_struct *vm_next

pgprot_t vm_page_prot /* 对这个虚拟区间的存取权限 */

unsigned long vm_flags/* 虚拟区间的标志. */

rb_node_t vm_rb

/*

* For areas with an address space and backing store,

* one of the address_space->i_mmap{,shared} lists,

* for shm areas, the list of attaches, otherwise unused.

*/

struct vm_area_struct *vm_next_share

struct vm_area_struct **vm_pprev_share

/*对这个区间进行 *** 作的函数 */

struct vm_operations_struct * vm_ops

/* Information about our backing store: */

unsigned long vm_pgoff/* Offset (within vm_file) in PAGE_SIZE

units, *not* PAGE_CACHE_SIZE */

struct file * vm_file /* File we map to (can be NULL). */

unsigned long vm_raend/* XXX: put full readahead info here. */

void * vm_private_data/* was vm_pte (shared mem) */

}

vm_flag是描述对虚拟区间的 *** 作的标志,其定义和描述如下

标志名描述

VM_DENYWRITE 在这个区间映射一个打开后不能用来写的文件。

VM_EXEC 页可以被执行。

VM_EXECUTABLE 页含有可执行代码。

VM_GROWSDOWN 这个区间可以向低地址扩展。

VM_GROWSUP 这个区间可以向高地址扩展。

VM_IO 这个区间映射一个设备的I/O地址空间。

VM_LOCKED 页被锁住不能被交换出去。

VM_MAYEXEC VM_EXEC 标志可以被设置。

VM_MAYREAD VM_READ 标志可以被设置。

VM_MAYSHAREVM_SHARE 标志可以被设置。

VM_MAYWRITEVM_WRITE 标志可以被设置。

VM_READ 页是可读的。

VM_SHARED 页可以被多个进程共享。

VM_SHM页用于IPC共享内存。

VM_WRITE页是可写的。

较高层次的结构vm_area_structs是由双向链表连接起来的,它们是按虚地址的降顺序来排列的,每个这样的结构都对应描述一个相邻的地址空间范围。之所以这样分割,是因为每个虚拟区间可能来源不同,有的可能来自可执行映象,有的可能来自共享库,而有的则可能是动态分配的内存区,所以对每一个由vm_area_structs结构所描述的区间的处理 *** 作和它前后范围的处理 *** 作不同。因此Linux

把虚拟内存分割管理,并利用了虚拟内存处理例程(vm_ops)来抽象对不同来源虚拟内存的处理方法。不同的虚拟区间其处理 *** 作可能不同,Linux在这里利用了面向对象的思想,即把一个虚拟区间看成一个对象,用vm_area_structs描述了这个对象的属性,其中的vm_operation结构描述了在这个对象上的 *** 作,其定义在/include/linux/mm.h中:

/*

* These are the virtual MM functions - opening of an area, closing and

* unmapping it (needed to keep files on disk up-to-date etc), pointer

* to the functions called when a no-page or a wp-page exception occurs.

*/

struct vm_operations_struct {

void (*open)(struct vm_area_struct * area)

void (*close)(struct vm_area_struct * area)

struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, int unused)

}

vm_operations结构中包含的是函数指针;其中,open、close分别用于虚拟区间的打开、关闭,而nopage用于当虚存页面不在物理内存而引起的“缺页异常”时所应该调用的函数。

3.红黑树结构

Linux内核从2.4.10开始,对虚拟区的组织不再采用AVL树,而是采用红黑树,这也是出于效率的考虑,虽然AVL树和红黑树很类似,但在插入和删除节点方面,采用红黑树的性能更好一些,下面对红黑树给予简单介绍。

一颗红黑树是具有以下特点的二叉树:

每个节点着有颜色,或者为红,或者为黑

根节点为黑色

如果一个节点为红色,那么它的子节点必须为黑色

从一个节点到叶子节点上的所有路径都包含有相同的黑色节点数

看这篇文章之前需要知道一个概念

虚拟内存系统通过将虚拟内存分割为称作虚拟页(Virtual Page,VP)大小固定的块,一般情况下,每个虚拟页的大小默认是4096字节。同样的,物理内存也被分割为物理页(Physical Page,PP),也为4096字节。

在LINUX中我们可以使用mmap用来在进程虚拟内存地址空间中分配地址空间,创建和物理内存的映射关系。

映射关系可以分为两种

1、文件映射

磁盘文件映射进程的虚拟地址空间,使用文件内容初始化物理内存。

2、匿名映射

初始化全为0的内存空间。

而对于映射关系是否共享又分为

1、私有映射(MAP_PRIVATE)

多进程间数据共享,修改不反应到磁盘实际文件,是一个copy-on-write(写时复制)的映射方式。

2、共享映射(MAP_SHARED)

多进程间数据共享,修改反应到磁盘实际文件中。

因此总结起来有4种组合

1、私有文件映射

多个进程使用同样的物理内存页进行初始化,但是各个进程对内存文件的修改不会共享,也不会反应到物理文件中

2、私有匿名映射

mmap会创建一个新的映射,各个进程不共享,这种使用主要用于分配内存(malloc分配大内存会调用mmap)。

例如开辟新进程时,会为每个进程分配虚拟的地址空间,这些虚拟地址映射的物理内存空间各个进程间读的时候共享,写的时候会copy-on-write。

3、共享文件映射

多个进程通过虚拟内存技术共享同样的物理内存空间,对内存文件 的修改会反应到实际物理文件中,他也是进程间通信(IPC)的一种机制。

4、共享匿名映射

这种机制在进行fork的时候不会采用写时复制,父子进程完全共享同样的物理内存页,这也就实现了父子进程通信(IPC).

这里值得注意的是,mmap只是在虚拟内存分配了地址空间,只有在第一次访问虚拟内存的时候才分配物理内存。

在mmap之后,并没有在将文件内容加载到物理页上,只上在虚拟内存中分配了地址空间。当进程在访问这段地址时,通过查找页表,发现虚拟内存对应的页没有在物理内存中缓存,则产生"缺页",由内核的缺页异常处理程序处理,将文件对应内容,以页为单位(4096)加载到物理内存,注意是只加载缺页,但也会受 *** 作系统一些调度策略影响,加载的比所需的多。

1.write

因为物理内存是有限的,mmap在写入数据超过物理内存时, *** 作系统会进行页置换,根据淘汰算法,将需要淘汰的页置换成所需的新页,所以mmap对应的内存是可以被淘汰的(若内存页是"脏"的,则 *** 作系统会先将数据回写磁盘再淘汰)。这样,就算mmap的数据远大于物理内存, *** 作系统也能很好地处理,不会产生功能上的问题。

2.read

从图中可以看出,mmap要比普通的read系统调用少了一次copy的过程。因为read调用,进程是无法直接访问kernel space的,所以在read系统调用返回前,内核需要将数据从内核复制到进程指定的buffer。但mmap之后,进程可以直接访问mmap的数据(page cache)。

测试结果来源于: 深入剖析mmap-从三个关键问题说起

1.读性能分析

场景:对2G的文件进行顺序写入

可以看到mmap在100byte写入时已经基本达到最大写入性能,而write调用需要在4096(也就是一个page size)时,才能达到最大写入性能。

从测试结果可以看出,在写小数据时,mmap会比write调用快,但在写大数据时,反而没那么快。

2.写性能分析

场景:对2G的文件进行顺序读取(为了避免磁盘对测试的影响,2G文件都缓存在pagecache中)

由上可以看出,在read上面,mmap的性能还是非常好的。

优点如下:

1、对文件的读取 *** 作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。

2、实现了用户空间和内核空间的高效交互方式。两空间的各自修改 *** 作可以直接反映在映射的区域内,从而被对方空间及时捕捉。

3、提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。同时,如果进程A和进程B都映射了区域C,当A第一次读取C时通过缺页从磁盘复制文件页到内存中;但当B再读C的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可直接使用已经保存在内存中的文件数据。

4、可用于实现高效的大规模数据传输。内存空间不足,是制约大数据 *** 作的一个方面,解决方案往往是借助硬盘空间协助 *** 作,补充内存的不足。但是进一步会造成大量的文件I/O *** 作,极大影响效率。这个问题可以通过mmap映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效。

缺点如下:

1.文件如果很小,是小于4096字节的,比如10字节,由于内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。虽然被映射的文件只有10字节,但是对应到进程虚拟地址区域的大小需要满足整页大小,因此mmap函数执行后,实际映射到虚拟内存区域的是4096个字节,11~4096的字节部分用零填充。因此如果连续mmap小文件,会浪费内存空间。

3.如果更新文件的 *** 作很多,会触发大量的脏页回写及由此引发的随机IO上。所以在随机写很多的情况下,mmap方式在效率上不一定会比带缓冲区的一般写快。


欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/yw/6155619.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-03-16
下一篇2023-03-16

发表评论

登录后才能评论

评论列表(0条)

    保存