地址空间 和地址生成
地址空间分为物理地址空间和逻辑地址空间
- 物理地址空间 是可以直接访问到具体主存地址的,面向硬件的
- 逻辑地址空间则是面向应用程序的,类似于偏移量。以C程序为例,其编译出来的程序,其中的地址都是逻辑地址。程序本身并不会固定物理地址。
但是在加载到内存运行的时候,每个应用程序都是要分配对应的物理地址空间的。这个过程对应用程序本身是不可见的,这是操作系统的工作。
在应用程序执行的时候,cpu里的mmu(内存管理单元)负责查找逻辑地址的映射表,找出逻辑地址和物理地
地址的安全检测
之所以只给应用程序逻辑地址,使其无法直接操作物理地址。一是为了屏蔽底层硬件的操作,为应用程序提供便利。二则是为了 安全。
对于cpu而言,内核一定是被信任的。应用程序则不是。应用程序可能因为各种原因导致出现一些非法的操作。比如访问了其他应用程序的物理地址空间。
操作系统会给每个应用程序指定起始访问地址和地址的长度,这样就确定了一个仅供此应用程序访问的空间。一旦其想访问此空间之外的地址,就会引发内存异常。
内存碎片
- 内部碎片 是由于采用固定大小的内存分区,当一个进程不能完全使用分给它的固定内存区域时就产生了内部碎片,通常内部碎片难以完全避免;
- 外部碎片是由于某些未分配的连续内存区域太小,以至于不能满足任意进程的内存分配请求,从而不能被进程利用的内存区域。
连续内存分配
内存分配的三种算法
在内存中,某个应用程序运行结束后,内存会被回收,这块区域就成了空闲区域。但是由于每隔进程被回收的时刻都不相同。导致会在内存中出现很多碎片和空隙。 在 新来的应用程序需要空间的时候,操作系统会有多种方式从这些空闲的空间中找到最合适的那个。
1 最先适配 :
从内存中按照地址顺序找到第一个满足 应用程序需求的 空闲空间。
优点:实现简单,并且容易产生更大的空闲块(没有被破坏)
缺点:容易产生外碎片,并且随着时间这个特性会加剧。
2 最优适配 :
从内存中找到一个最接近应用程序需求的空间。
优点:对于大多数小内存分配的情况比较合适,比较简单。避免了分割大的空闲块,并且最小化外部碎片产生的尺寸。
缺点:将外碎片分配得比较细,重分配慢,而且容易产生很多没用的微小碎片。
3 最差适配 :
从内存中找到一个最大的能满足需求的空间。
优点:假如分配是中等尺寸效果最好
缺点:易于破坏大的空闲块以致于大分区无法被分配
以上三种算法各有优势。但都无法彻底解决碎片问题。
碎片紧凑和分区兑换
碎片紧凑
也称作压缩式碎片整理。其实就是内存的重新拷贝,将处于等待状态的进程的对应地址空间上的数据拷贝到新的空闲空间中,这样可以让碎片被充分利用起来。 但是这个前提是,程序中没有绝对地址的引用,否则是无法进行移动的。 而且 这个开销其实也是很大的。
分区对换
分区对换是 将等待状态进程的数据先存储到外存中。需要运行此进程时,又将内存中的其他程序存储到外存,从而留下足够的空间够此进程运行。
非连续内存分配
诞生背景: ,在连续内存分配中,无论哪种方式都会产生碎片。而且要求内存是连续的,分配效率不高。
非连续内存分配主要通过对非连续的内存进行分段、分页。以段和页的形式提供给程序使用。
分段
分段方便了用户的使用
在进程的空间中,其实包含了堆、栈、用户数据、代码等空间的区别。使用分段的形式,可以将这些空间有效得隔离开来,并使得内存可以在不连续的空间上进行分配。
分段是逻辑上的。
段可以被共享,一个段在不同的进程内可具有不同的段号。可以设立共享段表来集中管理贡献段表。
分页
分页提高了内存的利用率。
分页是物理上的。将逻辑地址空间划分为相同大小的基本数据单位。通常是4k。
通过分页的设计,使得使用按页分配内存的时候,内存碎片只会产生在最后一页。
段表和页表
由于段是不一定连续的,为了让程序能够找到每个段的位置,系统为每个进程设立了一个段表。进程空间中的每个段在该段表中都有个表项,其中记录了该段的起始地质和段的长度。
段表和页表都可以通过高速缓冲 提高查找效率,这就是 快表,类似表的缓存。
页表是一种特殊的数据结构,放在系统空间的页表区,存放逻辑页与物理页帧的对应关系。 每一个进程都拥有一个自己的页表,PCB表中有指针指向页表
段页式管理
通过给每个段增加一个页表,实现段页式的管理。能够有效利用内存,但是增大了维护成本。
这样获取数据需要访问三次内存。 先访问段表,再访问页表,再访问数据。
虚拟存储
无论是连续分配还是非连续分配,都要求程序一次性装入内存。但如果程序太大,会无法装入内存。
-
-
- 局部性原理
-
-
-
-
- 程序的执行总是呈现局部性。即,在一个较短的时间段内,程序的执行仅限于某个部分;相应的,它所访问的存储空间也局限于某个区域。
- 时间局限性:如果程序中的某条指令一旦执行, 则不久以后该指令可能再次执行;如果某数据被访问过, 则不久以后该数据可能再次被访问。
- 空间局限性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问;
-
-
通过局部性原理,我们可以得知有时候并不需要将所有页面都加载进内存。
1. 分页虚拟存储
分页虚拟存储 是在分页系统的基础上增加请求调页和页面置换功能。
在进程装入内存时,并不是一次性加载所有页面。而是装入若干个,之后根据需要动态加载其他页面。
如果内存满了,根据某种算法淘汰某个页,加载新的页面。
分页虚拟存储使用的页表比原先的页表要多出一些标志位。
在置换页面时,如果旧页面被修改过,需要及时更新到外存(磁盘)中。
1.1 页面置换算法
- 先进先出
每次淘汰最先进入内存的页。易于实现
- 最近最久未使用 (LRU) 算法
淘汰最近一段时间内最少使用的一页
- 简单clock 置换算法
为每页设置访问位,每当该页面被访问,则访问位加1.
需要淘汰页面的时候,遍历页队列,如果某页访问位为0则淘汰该页。否则则将访问位 减一。
- 改进型clock 置换算法
由于置换被修改过的页面需要将其更新到外存,这个开销不小。改进型clock 算法 在选择置换页面时,还考虑到置换的代价。 不同于简单clock ,改进clock 将会选择 即是未被使用的页面又是未被修改过的页面作为最先淘汰的页面。
2. 抖动
如果运行进程的大部分时间都用于页面的换入/换出,而几乎不能完成任何有效的工作,则称此进程处于抖动状态。
为了预防抖动:
- 采用局部置换策略,某进程需要加载新页时只允许替换该进程自身空间内的其他页面
- 利用工作集: 在调度新程序前,需要为页面不足的进程分配新的物理块,以降低其却页率,之后再考虑加载新程序。
- 工作集:指进程在执行过程中,从时间t开始的某段时间间隔Δ里进程实际访问的页面集合,记为w(t,Δ)。时间间隔Δ是工作集的窗口尺寸。
- 利用L=S准则调节缺页率。L缺页之间的平均时间,S平均缺页服务时间。若L远大于S,说明很少发生缺页,可以加载新程序。若L远远小于S则代表频繁缺页,应避免再加载新程序。
- 选择挂起某些进程:通过选择优先级最低的进程或者缺页进程、最近激活的进程、驻留集最小/最大的进程等方式挑选一些进程并挂起,给其他进程腾出加载新页的空间。
3. 分段虚拟存储
类似于分页,加载进内存时并不需要加载所有的段,加载部分即可。当请求的段不存在时,产生缺段中断并加载新的段。
段的动态连接: 在运行前无法预知程序需要的段,与其在运行前连接所有的段,还不如在运行时需要的时候才将段连接上。
linux 中的内存管理
linux 采用虚拟存储,用户需要对其虚拟地址空间进行寻址,必须通过二级页表转换得到物理地址。
(在不同类型的处理机中,通过宏定义可以屏蔽一些细节,使内核无需使用特定的方式寻址。)
在少于特定阈值时,页通过链表连接。否则将使用AVL树构造,以保证高的检索效率。
页表的管理: linux 假定页表时分三级管理的。一级页表只占用一个页,其中存放了二级页表的入口指针。二级页表则存放三级页表的入口指针。
页面分配与回收
- linux 空闲页管理采用的是伙伴系统。
什么是伙伴系统?
伙伴系统的宗旨就是用最小的内存块来满足内核的对于内存的请求。在最初,只有一个块,也就是整个内存,假如为1M大小,而允许的最小块为64K,那么当我们申请一块200K大小的内存时,就要先将1M的块分裂成两等分,各为512K,这两分之间的关系就称为伙伴,然后再将第一个512K的内存块分裂成两等分,各位256K,将第一个256K的内存块分配给内存,这样就是一个分配的过程。
缺点:每次分配的内存都是2的整数次幂,会产生多余的空间。
分配:不断2分块一个2的整数幂大小的块,直到达到可以满足需求大小的最小块。
回收: 为了让系统找到大的空间分配,在回收时尽可能将小块合并成大块。寻找和其大小相同的“伙伴“块并合并。
页面换入换出
当挑选出准备换出的页面时,先将内容写入磁盘,并且修改内存的存在位,但是占据的内存不立即释放,而是将其page结构存放在一个swap cache队列。从活跃状态变为不活跃状态。这样,如果再发生被换出后需要立刻访问的情况,就可从swap cache队列中找到页面。这样极大减少了抖动。
额外减小开销的方法:
在准备换出页面的时候,不一定将内容写入磁盘。将页面分为clean,dirty。clean是指内存换入该页面后从未写入,与盘上一致。dirty是不一致的,但是也不需要立刻写出去,可先断开映射表,经过一段时间老化后再写,从而变成clean。clean可缓冲到必要时再回收,且回收clean代价小。
在内核内有一个kswapd 的守护进程,专门负责检查维护系统的空闲页面数并选择是否换出页面。直到每次换出或丢弃的页面达到6.
页面错误的处理:
发生页面错误会先调用do_page_fault 函数并判断错误类型然后调用对应的处理函数。
- 缺页且该页从未被映射到虚拟地址空间: 调用 do_no_page。如果没有预定义的处理函数,将为该页面申请新页面并加载到内存。 如果发生错误的页面是可写的,应标记该页为脏页。
- 缺页但该页面曾经映射到虚拟地址空间(该页现应该在swap cache中),调用do_swap_page函数将该页从swap cache调入内存。
- 向共享页面写: 调用do_wp_page ,(copy-on-write 机制)将共享页面的内容复制到新申请的页面中去。
页缓存
这个简单理解,就是为了加快对磁盘文件的访问。在内存划出部分空间作为缓存。在需要访问某个页面时,先访问页面缓存,如果没有击中缓存则去磁盘读取并加入 到缓存。当某个页面不再被任何进程需要时就将其从页面缓存中删除。
swap cache
从磁盘读取的页面会被加入页缓存。但如果该页并不是真正文件,而是通过malloc 方式申请出来的页面。在不被进程使用时,就可以加入swap cache等待回收。
这里的swap cache 并不是 swap 分区
内核cache
slab.c 专门负责给内核分配内存。 之所以需要单独给内核使用不同的方式分配,一时因为内核使用物理地址,效率高。二是为了 伙伴系统对内存造成浪费,因为内核经常为一些数据结构分配内存,而这些结构大小往往不同,伙伴系统分配起来比较困难。
Slab并非是脱离伙伴关系而独立存在的一种内存分配方式,slab仍然是建立在页面基础之上,换句话说,Slab将页面(来自于伙伴关系管理的空闲页面链表)撕碎成众多小内存块以供分配