计算机内存管理之虚拟内存

本文探讨了虚拟内存的原理及其在保护进程内存隔离中的作用,深入剖析了内存分页机制、TLB缓存、多级页表的优势,以及Linux内核空间的分页与非线性映射。介绍了Linux内存管理中的buddy算法和内存保护策略,包括页表结构和访问权限控制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、虚拟内存是什么,为什么要有虚拟内存

如果进程直接访问物理内存,那么不同进程是能够直接访问其它进程的内存空间的,这种行为不安全。
为保证不同进程之间内存空间不可见,实现进程内存隔离,需要有一种机制,能够将不同进程的内存物理空间实现隔离。

这个机制就是 虚拟内存,为不同的进程分配各自的虚拟地址,这些虚拟地址落在不同的物理地址,实现了不同进程使用的地址隔离。而虚拟地址怎么映射到不同的物理地址,上层程序不需要关心。对进程来讲,他感觉自己能够访问到完整的整个内存空间。

虚拟地址与不同的物理地址映射的机制,由操作系统提供。程序访问虚拟地址的时候,由操作系统将虚拟地址转换不同的物理地址。

处理器中的内存管理单元MMU)负责管理这种映射关系。提供虚拟地址和物理地址的映射、内存访问权限保护和cache缓存控制等硬件支持。

主要有两种方式,分别是内存分段和内存分页,分段是比较早提出的,我们先来看看内存分段。

2、内存分段

最初是为了解决8086 16位处理器寻址20内存空间的问题。8086上涉及4个寄存器,

CS(Code Segment):代码段寄存器;
DS(Data Segment):数据段寄存器;
SS(Stack Segment):堆栈段寄存器;
ES(Extra Segment):附加段寄存器。

每个程序在执行前,都要决定程序的代码段、数据段、堆栈段需要用到内存的哪些位置,并设定CS DS SS寄存器来指定这些位置。
将程序所需要的内存空间大小的虚拟空间,通过映射机制映射到某个物理地址空间(映射的操作由硬件完成)。

分段系统的逻辑地址结构由段号(段名)和段内地址(段内偏移量)所组成。

缺点:
1、分段内存映射单位是整个程序,占用内存空间大;如果内存空间不够,还要将整个程序所需要的空间换入换出到磁盘空间,这个动作造成大量磁盘访问,严重降低程序运行速度;事实上,大多程序运行时所需数据只是很小一部分,不需要整体的写入和写出。
2、内存碎片,同样会导致内存swap。

3、内存分页

内存分页能够解决分段的缺点。

分页模式下,程序不是一次性全部加载到物理内存中,暂时没用到的数据和代码保存在磁盘中,程序运行需要用到这些数据时,再从磁盘中加载到内存中。这样不会产生大量的磁盘写入写出操作。

内存分页就是将物理地址分成一页一页固定尺寸的大小。在linux下,每页的大小是4KB。
虚拟地址到物理地址的映射关系,由页表来完成。页表存储在内存中,MMU通过查询页表,负责将虚拟地址转换为物理地址。
分页机制下,虚拟地址分为两部分,页号和页内偏移(简单来讲,实际还有很多标志位)。页号作为页表的索引,通过页号查询到页表中存储的页的物理内存起始地址,页的物理地址加上页内偏移,就组成了实际物理地址。

3.1 TLB

上述内存分页的转换表保存在内存中。
为加快地址转换速度,减少地址转换的时钟周期,CPU会维护一个转换旁路缓存(Translation Lookaside Buffer)TLB。
TLB是MMU的核心部件,缓存了少量的虚拟地址与物理地址的转换关系,是一个保存转换表的cache。也被称为快表。
TLB添加一项ASID(Address Space ID)的匹配。ASID就类似进程ID一样,用来区分不同进程的TLB表项。进程ID和每个进程的ASID一般是不相等的,每创建一个新进程,就为之分配一个新的ASID。当ASID分配完后,flush所有TLB,重新分配ASID。
内核空间是所有进程共享,进程A切换进程B的时候,如果进程B访问的地址位于内核空间,完全可以使用进程A缓存的TLB。但是现在由于ASID不一样,导致TLB miss。我们针对内核空间这种全局共享的映射关系称之为global映射。

3.2 TTW

转换表漫游(Translation Table walk)TTW。当在TLB中没有查询到对应的地址转换关系条目时,需要通过查询内存中转换表来获取映射关系。这个动作叫做转换表漫游。

3.3 多级页表

每个进程都要单独维护一个页表,多级页表是为了解决什么问题的呢?
先举个例子,假设虚拟地址空间大小为4G,在linux下,每页的大小是4k,那么页表项就是2^20个,如果每个页表占用4B,那么整个页表占用4M内存。那么问题来了
1、每个进程都要维护一个页表,也就是每个进程都要占用4M内存大小,整个开销是很大的。那么多级页表能解决这个问题吗,是的!
如上所说,使用一级页表,每个进程占用4M内存大小。假设使用2级页表,将2^20个一次页表分成1024组,每组包含1024个页表,如此形成二级分页。
也就是32位地址,10位表示二级页号,10位表示1级页号,12位表示页内偏移。寻址时先寻址二级页,在二级页表查找到一级页并索引到实际物理地址。
这时候会发现,采用二级页表,内存需要维护4k二级页表+4M一级页表,内存占用不是更大了么?不是的!
计算机的局部性原理,每个进程都分配了4G的虚拟地址空间,但是大部分进程使用的空间都远未达到4G。因此大部分二级页表下的一级页表项是空的,不需要占用空间
所以说多级页表可以节省内存
2、使用一级页表的话,每个进程的页表项需要在内存中连续存储,这个对内存管理是增加很大开销的。尤其是内存碎片多、内存空间不足等情况下。下面看看多级页表为什么能解决这个问题:
多级页表实际上是对每个一级页表项增加了索引,可以通过上层页表索引到下层页表。因此只需要顶层页表在物理内存中连续、低层不同页表不需要连续 只有页表中的项连续就OK了,最底层页表项不需要在物理空间上连续。

4、linux中的分页机制:

linux采用四级分页机制,即

页全局目录(Page Global Directory)
页上级目录(Page Upper Directory)
页中间目录(Page Middle Directory)
页表(Page Table)

对于不同的物理架构,这个分页机制也是兼容的。例如对于i386而言,仅采用二级页表,即页上层目录和页中层目录长度为0。

linux主要使用分页机制管理内存,而使用了较少的分段机制。

给每一个进程分配一块不同的物理地址空间,这确保了可以有效地防止寻址错误。
允许将某页内存存在磁盘上。

在硬件上会有一个叫做页表基地址寄存器,它存储PGD页表的首地址。MMU就是根据页表基地址寄存器从PGD页表一路查到PTE,最终找到物理地址(PTE页表中存储物理地址)。
CR3含有存放页目录表页面的物理地址,因此CR3也被称为PDBR。CR3寄存器的改变与操作系统的关联主要是由于进程切换,每当进程切换时,CR3的内容需要被操作系统修改

4.1 线性映射与非线性映射

linux在内核空间执行内存管理,实际上给内核分配的虚拟地址空间是1G,如果实际物理空间小于1G,可直接将物理空间线性映射到内核空间,提高访问速度。
但是如果物理空间大于1G,内核无法在1G虚拟内存空间直接线性映射高于1G的物理地址空间,这就需要非线性映射,通常不映射,只有使用时候才映射,kmap等。
线性空间最大只能896M.

x86内核空间划分:

保留区
专用页面映射区
高端内存映射区
vmalloc虚拟内存分配区
物理内存映射区(内部最底层划分DMA区)

实际上,物理内存的0~896M线性映射到内核空间的物理内存映射区;
物理内存大于896M的空间,由内核空间的高端内存映射区管理。

arm内核空间划分:

向量表
vmalloc
DMA+常规区域内存映射
高端内存映射区
内核模块

4.2 buddy算法

DMA、常规内存、高端内存这三个区域都用buddy算法管理,将空闲页面以2的n次方为单位进行管理,因此,linux最底层内存申请都是以2n为单位的。buddy算法最主要优点是避免了外部碎片,任何时候,区域中的空闲内存都能以2n进行拆分或合并。
/proc/buddyinfo会显示每个区域中2^n的空闲页面的分布情况:

Node 0, zone      DMA      3      3      1      1      1      0      1      0      0      0      0 
Node 0, zone    DMA32      5      6      6      8      7      6      4      6      8      9    325 
Node 0, zone   Normal   4795   4161   2183    639    301     84     25      9     13      4   2711 

5、内存保护:

1、虚拟内存就是一种内存保护机制:实现不同进程访问的物理内存隔离。
2、同一任务内,定义四种不同的特权级别,用于限制对内存的访问权限。特权级分为4级,特权级0、1、2、3。
CS代码段寄存器,一般用于存放代码,它就含有一个特殊的两位字段,用以标识CPU当前的特权级别(CurrentPrivilegeLevel,CPL),值为0代表最高优先级,值为3代表最低优先级。在linux和windows均只使用特权级0和特权级3。分别称为内核态和用户态。

例如:
1、页目录项中的字段 User/Supervisor标志 ,表示了访问页或页表所需要的的特权级。若这个标志为0,只有当CPL小于3(这意味着对于Linux而言,处理器处于内核态)时才能对页寻址;若该标志为1,则总能对页寻址。
2、与段的3种存取权限(读,写,执行)不同的是,页的存取权限只有两种(读,写)。如果页目录项或页表项的Read/Write标志等于0,说明相应的页表或页是只读的,否则是可读写的。

6、总结

文章讲述了计算机内存管理之虚拟内存,在此再次总结:

为了保证进程之间的内存地址相互隔离,于是操作系统就为每个进程独立分配一套虚拟地址空间,每个程序只关心自己的虚拟地址就可以,实际上大家的虚拟地址都是一样的,但分布到物理地址内存是不一样的。作为程序,也不用关心物理地址的事情。

每个进程都有自己的虚拟空间,而物理内存只有一个,所以当启用了大量的进程,物理内存必然会很紧张,于是操作系统会通过内存交换技术,把不常使用的内存暂时存放到硬盘(换出),在需要的时候再装载回物理内存(换入)。

那既然有了虚拟地址空间,那必然要把虚拟地址「映射」到物理地址,这个事情通常由操作系统来维护。

那么对于虚拟地址与物理地址的映射关系,可以有分段和分页的方式,同时两者结合都是可以的。

内存分段是根据程序的逻辑角度,分成了栈段、堆段、数据段、代码段等,这样可以分离出不同属性的段,同时是一块连续的空间。但是每个段的大小都不是统一的,这就会导致内存碎片和内存交换效率低的问题。

于是,就出现了内存分页,把虚拟空间和物理空间分成大小固定的页,如在 Linux 系统中,每一页的大小为 4KB。由于分了页后,就不会产生细小的内存碎片。同时在内存交换的时候,写入硬盘也就一个页或几个页,这就大大提高了内存交换的效率。

再来,为了解决简单分页产生的页表过大的问题,就有了多级页表,它解决了空间上的问题,但这就会导致 CPU 在寻址的过程中,需要有很多层表参与,加大了时间上的开销。于是根据程序的局部性原理,在 CPU 芯片中加入了TLB,负责缓存最近常被访问的页表项,大大提高了地址的转换速度。

Linux 系统主要采用了分页管理,但是由于 Intel 处理器的发展史,Linux 系统无法避免分段管理。于是 Linux 就把所有段的基地址设为 0,也就意味着所有程序的地址空间都是线性地址空间(虚拟地址),相当于屏蔽了 CPU 逻辑地址的概念,所以段只被用于访问控制和内存保护。

内存完整性:由于虚拟内存到物理内存的映射,每个进程都认为自己占用了4G完整内存,并认为内存是连续的,每个进程都认为自己获取的内存是一块连续的地址,故各进程不需要关心内存问题。

安全性:通过虚拟内存的映射,实现各进程间访问的物理内存的隔离;另外,在虚拟内存映射到物理内存的查表过程中,页表中还记录了内存的访问权限,实现了内存的访问权限控制。

内存swap:linux 系统引入swap分区,在可用物理内存不足时,将暂时不用的内存数据拷贝到存储介质(磁盘)上,让出内存让出给优先进程使用,进程使用完,再将原数据从磁盘加载到内存中。通过这种swap技术,linux系统可以让进程有“无限”内存可以用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值