内存映射提供了一种共享对象的机制,以避免浪费内存资源 。一个对象被映射到虚拟内存的一个区域,或者作为一个共享对象,或者作为一个私有对象 。
如果一个进程把一个共享对象映射到它的虚拟地址空之间的一个区域,那么这个进程对这个区域的任何写操作对同样把这个共享对象映射到它们的虚拟内存的其他进程也是可见的 。相反,对映射到私有对象的区域的任何写操作对其他进程是不可见的 。映射到共享对象的虚拟内存区域称为共享区域 。同样,也有私人区域 。
为了节省内存,私有对象的生命周期与共享对象的生命周期基本相同(物理内存中只保存私有对象的一个副本),并采用写时复制的技术来处理多个进程的写冲突 。
只要没有进程试图写入自己的私有区域,多个进程就可以继续共享物理内存中私有对象的单个副本 。然而,每当进程试图写入私有区域中的页面时,它将触发保护异常 。在上图中,进程B试图写入私有区域的页面,这触发了一个保护异常 。异常处理程序将在物理内存中创建这个页面的新副本,更新PTE以指向这个新副本,然后恢复这个页面的可写权限 。
另一个典型的例子是fork()函数,它用于创建子进程 。当前进程调用fork()函数时,内核会为新进程创建各种必要的数据结构,并为其分配一个唯一的PID 。为了给新进程创建虚拟内存,它复制了当前进程的mm_struct、vm_area_struct和页表的原始副本 。并将两个进程的每个页面标记为只读,并将两个进程的每个区域标记为私有区域(写入时复制) 。
这样,父进程和子进程的虚拟内存空是完全一致的,只有当这两个进程中的任何一个在写的时候,才能使用写时复制来保证每个进程的虚拟地址空之间的私有抽象概念 。
存储器分配虽然内存映射(mmap()函数)可以用来创建和删除虚拟内存区域,以满足运行时动态内存分配的问题 。但是,为了更好的可移植性和方便性,需要更高层次的抽象,即动态内存分配器 。
动态内存分配器维护进程的虚拟内存区域,也称为“堆” 。内核还维护一个指向堆顶部的指针brk(break) 。动态内存分配器将堆视为连续虚拟内存块的集合,每个块有两种状态,已分配和空空闲 。分配的块是为应用程序显式保留的,而空空闲块可用于分配,其空空闲状态为,直到被应用程序显式分配 。分配的块要么由应用程序显式释放,要么由垃圾收集器释放 。
本文只解释动态内存分配的一些概念,动态内存分配的实现不在本文讨论范围之内 。感兴趣的话可以参考dlmalloc的源代码,这是Doug Lea(写Java发契约的那个)实现的一个设计巧妙的内存分配器,源代码中有很多注释 。
内存碎片空堆之间利用率低的主要原因是一种叫做碎片化的现象 。当有未使用的内存,但该内存无法满足分配请求时,就会出现碎片 。有两种类型的碎片:
内部碎片:当分配的块大于有效负载时发生 。例如,程序请求一个5字块(这里我们不关心字的大小,假设一个字是4个字节,堆的大小是16个字,保证边界双字对齐),内存分配器为了保证空 free block是双字边界对齐,不得不分配一个6字块(具体实现中对齐的规定可能略有不同,但对齐肯定会存在) 。在这个例子中,分配的块是6个字,有效载荷是5个字,内部片段是分配的块减去有效载荷,即1个字 。
外部碎片:当空空闲内存足以满足分配请求,但没有任何单个空空闲块大到足以处理该请求时,就会发生这种情况 。外部碎片难以量化且不可预测,所以分发商通常会尝试通过启发式策略维护少量的大空空闲块,而不是维护大量的小空空闲块 。分配器还会根据策略和分配请求的匹配划分空空闲块和合并空空闲块(它们必须是相邻的) 。
空自由链表分配器被组织成一个连续的已分配块和空空闲块序列,称为空空闲链表 。空自由链表分为隐式空自由链表和显式空自由链表 。
隐式空空闲链表是一个单向链表,每个空空闲块只通过头中的size字段隐式连接 。
显式空空闲链表是指空空闲块被组织成某种形式的显式数据结构(为了更高效地合并和划分空空闲块) 。比如把堆组织成一个双向空空闲链表,每个空空闲块包含一个前任节点的指针和一个后继节点的指针 。
有几种策略可以找到空空闲块:
第一次适配:从头搜索空空闲链表,选择最先遇到的合适的空空闲块 。它的优点是倾向于在链表后面保留大的空空闲块,缺点是倾向于在链表前面附近留下碎片 。
推荐阅读
- 图文解说 施乐3117加粉只需10分钟新系统了解的东西
- word文档斜杠怎么打系统教程推荐
- 苹果系统上QQ音乐怎么更换皮肤[技能提升]
- 摩尔庄园手游邻居系统解锁条件一览-邻居系统怎么开启让您技能天下无双
- 原神元素能量怎么回复-元素能量系统机制介绍出神入化
- apple-watch怎么升级系统精选视频
- Win7系统更改磁盘卷标排列顺序的操作步骤是什么? 如何更改硬盘驱动器的卷标
- 华为手机系统更新怎么关闭视频学习
- oppo手机系统更新精选视频
- 华为手机系统更新在哪里视频学习