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树和红黑树很类似,但在插入和删除节点方面,采用红黑树的性能更好一些,下面对红黑树给予简单介绍。

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

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

根节点为黑色

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

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

考虑这样一种常见的情况:用户进程调用malloc()动态分配了一块内存空间,再对这块内存进行访问。这些用户空间发生的事会引发内核空间的那些反映?本文将简单为您解答。1.brk系统调用服务例程malloc()是一个API,这个函数在库中封装了系统调用brk。因此如果调用malloc,那么首先会引发brk系统调用执行的过程。brk()在内核中对应的系统调用服务例程为SYSCALL_DEFINE1(brk, unsigned long, brk),参数brk用来指定heap段新的结束地址,也就是重新指定mm_struct结构中的brk字段。brk系统调用服务例程首先会确定heap段的起始地址min_brk,然后再检查资源的限制问题。接着,将新老heap地址分别按照页大小对齐,对齐后的地址分别存储与newbrk和okdbrk中。brk()系统调用本身既可以缩小堆大小,又可以扩大堆大小。缩小堆这个功能是通过调用do_munmap()完成的。如果要扩大堆的大小,那么必须先通过find_vma_intersection()检查扩大以后的堆是否与已经存在的某个虚拟内存重合,如何重合则直接退出。否则,调用do_brk()进行接下来扩大堆的各种工作。 SYSCALL_DEFINE1(brk, unsigned long, brk) { unsigned long rlim, retvalunsigned long newbrk, oldbrkstruct mm_struct *mm = current->mmunsigned long min_brkdown_write(&mm->mmap_sem)#ifdef CONFIG_COMPAT_BRK min_brk = mm->end_code#else min_brk = mm->start_brk#endif if (brk <min_brk) goto outrlim = rlimit(RLIMIT_DATA)if (rlim <RLIM_INFINITY &&(brk - mm->start_brk) + (mm->end_data - mm->start_data) >rlim) newbrk = PAGE_ALIGN(brk)oldbrk = PAGE_ALIGN(mm->brk)if (oldbrk == newbrk) goto set_brkif (brk brk) { if (!do_munmap(mm, newbrk, oldbrk-newbrk)) goto set_brkgoto out} if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE)) goto outif (do_brk(oldbrk, newbrk-oldbrk) != oldbrk) goto outset_brk: mm->brk = brkout: retval = mm->brkup_write(&mm->mmap_sem)return retval} brk系统调用服务例程最后将返回堆的新结束地址。2.扩大堆用户进程调用malloc()会使得内核调用brk系统调用服务例程,因为malloc总是动态的分配内存空间,因此该服务例程此时会进入第二条执行路径中,即扩大堆。do_brk()主要完成以下工作:1.通过get_unmapped_area()在当前进程的地址空间中查找一个符合len大小的线性区间,并且该线性区间的必须在addr地址之后。如果找到了这个空闲的线性区间,则返回该区间的起始地址,否则返回错误代码-ENOMEM;2.通过find_vma_prepare()在当前进程所有线性区组成的红黑树中依次遍历每个vma,以确定上一步找到的新区间之前的线性区对象的位置。如果addr位于某个现存的vma中,则调用do_munmap()删除这个线性区。如果删除成功则继续查找,否则返回错误代码。3.目前已经找到了一个合适大小的空闲线性区,接下来通过vma_merge()去试着将当前的线性区与临近的线性区进行合并。如果合并成功,那么该函数将返回prev这个线性区的vm_area_struct结构指针,同时结束do_brk()。否则,继续分配新的线性区。4.接下来通过kmem_cache_zalloc()在特定的slab高速缓存vm_area_cachep中为这个线性区分配vm_area_struct结构的描述符。5.初始化vma结构中的各个字段。6.更新mm_struct结构中的vm_total字段,它用来同级当前进程所拥有的vma数量。7.如果当前vma设置了VM_LOCKED字段,那么通过mlock_vma_pages_range()立即为这个线性区分配物理页框。否则,do_brk()结束。可以看到,do_brk()主要是为当前进程分配一个新的线性区,在没有设置VM_LOCKED标志的情况下,它不会立刻为该线性区分配物理页框,而是通过vma一直将分配物理内存的工作进行延迟,直至发生缺页异常。3.缺页异常的处理过程经过上面的过程,malloc()返回了线性地址,如果此时用户进程访问这个线性地址,那么就会发生缺页异常(Page Fault)。整个缺页异常的处理过程非常复杂,我们这里只关注与malloc()有关的那一条执行路径。当CPU产生一个异常时,将会跳转到异常处理的整个处理流程中。对于缺页异常,CPU将跳转到page_fault异常处理程序中: //linux-2.6.34/arch/x86/kernel/entry_32.S ENTRY(page_fault) RING0_EC_FRAME pushl $do_page_fault CFI_ADJUST_CFA_OFFSET 4 ALIGN error_code: ………… jmp ret_from_exception CFI_ENDPROC END(page_fault) 该异常处理程序会调用do_page_fault()函数,该函数通过读取CR2寄存器获得引起缺页的线性地址,通过各种条件判断以便确定一个合适的方案来处理这个异常。3.1.do_page_fault()该函数通过各种条件来检测当前发生异常的情况,但至少do_page_fault()会区分出引发缺页的两种情况:由编程错误引发异常,以及由进程地址空间中还未分配物理内存的线性地址引发。对于后一种情况,通常还分为用户空间所引发的缺页异常和内核空间引发的缺页异常。内核引发的异常是由vmalloc()产生的,它只用于内核空间内存的分配。显然,我们这里需要关注的是用户空间所引发的异常情况。这部分工作从do_page_fault()中的good_area标号处开始执行,主要通过handle_mm_fault()完成。 //linux-2.6.34/arch/x86/mm/fault.c dotraplinkage void __kprobes do_page_fault(struct pt_regs *regs, unsigned long error_code) { ………… good_area: write = error_code &PF_WRITEif (unlikely(access_error(error_code, write, vma))) { bad_area_access_error(regs, error_code, address)return} fault = handle_mm_fault(mm, vma, address, write ? FAULT_FLAG_WRITE : 0)………… } 3.2.handle_mm_fault()该函数的主要功能是为引发缺页的进程分配一个物理页框,它先确定与引发缺页的线性地址对应的各级页目录项是否存在,如何不存在则分进行分配。具体如何分配这个页框是通过调用handle_pte_fault()完成的。 int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma, unsigned long address, unsigned int flags) { pgd_t *pgdpud_t *pudpmd_t *pmdpte_t *pte………… pgd = pgd_offset(mm, address)pud = pud_alloc(mm, pgd, address)if (!pud) return VM_FAULT_OOMpmd = pmd_alloc(mm, pud, address)if (!pmd) return VM_FAULT_OOMpte = pte_alloc_map(mm, pmd, address)if (!pte) return VM_FAULT_OOMreturn handle_pte_fault(mm, vma, address, pte, pmd, flags)} 3.3.handle_pte_fault()该函数根据页表项pte所描述的物理页框是否在物理内存中,分为两大类:请求调页:被访问的页框不再主存中,那么此时必须分配一个页框。写时复制:被访问的页存在,但是该页是只读的,内核需要对该页进行写操作,此时内核将这个已存在的只读页中的数据复制到一个新的页框中。用户进程访问由malloc()分配的内存空间属于第一种情况。


欢迎分享,转载请注明来源:夏雨云

原文地址:https://www.xiayuyun.com/zonghe/148343.html

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

发表评论

登录后才能评论

评论列表(0条)

    保存