文章目录
简单谈一下物理内存管理
页框
操作系统管理(使用)物理内存的基本单位是页框【就是一个物理内存块,一般4kb大小
】
为什么一般是4kb大小?
因为计算机科学家研究出4kb效率最高
就像操作系统文件管理时,使用磁盘空间的基本单位也是数据块(一般也是4kb)一样
所以磁盘中的数据加载到物理内存,就非常方便了,只需要对应的数据块中的内容,加载到对应页框里面就行了
为什么要把物理内存划分成一个一个固定大小的页框使用?
而不是需要多少,就分配多少?
- ①
硬件支持与地址转换
- MMU 与页表机制:
现代 CPU 的内存管理单元(MMU)通过页表将虚拟地址转换为物理地址,而这一过程必须以固定大小的页(如 4KB、2MB、1GB)为基本单位。操作系统必须遵循硬件的这一设计,否则无法高效完成地址转换 - TLB 缓存效率:
缓冲器(TLB)缓存虚拟页到物理页框的映射。若内存分配不以页为单位,TLB 的命中率会大幅下降,导致性能急剧降低
- MMU 与页表机制:
- ②
为了提高物理内存的使用率,减少内存碎片
如果是一个一个小块地使用物理内存,那么使用物理内存时,一次最多浪费一个页框的物理内存
即最多只会出现内部碎片(即一个页框内部没有存满数据还有空隙),不会出现外部碎片(分散的小块内存难以合并)- 1.如果申请的物理内存很小,那么给它一个页框就够了
- 2.如果一次申请的物理内存很大,那么给就给它n个页框,前n-1个页框一定是全部存满数据的,第n个页框可能存不满,但就算只存了一个比特位的数据又怎样
也就浪费一个页框而已,后续使用完成之后,回收非常方便
- ③.
为了提高内存管理效率
- 元数据开销:
操作系统需要记录内存的分配状态(如空闲或占用) - 分配/释放速度:
以页为单位分配内存,操作系统只需操作页表或空闲页链表修改一下标志位,复杂度为 (O(1));而随机大小的内存块管理需要遍历更复杂的结构,效率低下
- 元数据开销:
- ④
磁盘 I/O 的协同设计
- 页缓存对齐:
Linux 将文件数据缓存在内存中,缓存的单位是页(4KB),与磁盘块(Block,通常为 4KB)对齐。
这使得:
内存和磁盘的数据交换效率更高(减少读写次数)
- 页缓存对齐:
对页框进行描述
操作系统中存在非常多的页框,每个页框的使用情况,属于谁,是否要释放等等信息操作系统都要知道
所以操作系统一定要管理页框
为了描述页框,操作系统内定义了:struct page
因为页框的个数很多,所以struct page结构体变量也很多
所以一个struct page对象的大小要尽可能的小,一般不到40字节
注意:
因为页框是物理内存块,而且页框是操作系统开机之后就一直存在的,所以描述页框的struct page也是一直存在,不会被释放的,除非操作系统关机
struct page里面我们需要关注的成员变量
①
unsigend long flags
:位图标志位,记录了各种各样的标志,描述页框的状态
其中就有一个:表示该页框是否被使用
所以释放(申请)物理内存(页框),就只需要改对应页框的标志位就可以了②
int _mapcount
:引用计数,表示该页框被多少个进程使用
可以用于实现写时拷贝
比如:
子进程继承父进程的结构体的时候,因为结构体对象都是存储在物理内存中的,所以增加一下引用计数即可
对页框进行组织管理
操作系统中是,把struct page
结构体变量放进类似于数组
的数据结构中进行组织的
所以每一个struct page就有了下标,每一个页框就有了编号
更重要的是:
每个页框有了编号的话,那么物理地址就有了
因为
如果页框号为0的页框的起始物理地址为0,页框又是连续存放的,而且页框大小都输固定的(一般都是4kb)
所以
页框号为n的页框的起始地址=n*4*1024
(4kb=4*1024字节)
所以:
①我们只要有页框号就知道页框的起始物理地址
②只要有物理地址,
物理地址/4kb
,再取整就能得到页框号,就能找到对应的页框
所以我们只要找到struct page
结构体变量(page里面记录了自己的下标),就可以找到它对应的页框
所以进程使用物理内存,只需要记录对应的n个struct page
的地址就行了
存放所有struct page结构体变量的数组也是存储在物理内存中的
操作系统里面定义了一个全局的指针
指向这个数组的起始地址
这样在操作系统的任何地方,就都可以很方便地找到每一个struct page
,就可以知道每一个页框的使用情况
虚拟地址→物理地址(真实的页表)
真实的页表
我们之前说:
页表里面的页表项是直接左边存储虚拟地址,右边存储物理地址
但是这样其实是不行的,因为这样空间消耗太大了
如果全部的虚拟地址和物理地址都建立映射,那么就要花费至少真实的物理地址总大小的16倍的内存(因为一个字节就有一个地址,一个地址至少占4字节)
就算一个进程映射不完,但是我们现在的一些3A游戏运行时,占的内存也有7-8G,物理内存光放页表都放不下来
所以
真实的页表是:
[以32位操作系统的页表为例]
一个进程有一个对应的页目录,一个页目录里面一共1024个页目录项
页目录项里面存储的是某个页表的起始物理地址
一个页表里面也一共有1024个页表项,
页表项里面存储的是,某个页框的起始物理地址
所以一个真实的“页表”的大小最大只需要: 4*4 *1024 * 1024=16MB
因为这个就可以把整个物理内存的物理地址全部映射完
但是,一个进程可能使用全部都物理地址(内存)吗?
不可能!!!
所以其实肯定不可能页目录表里面的1024个页表全部都被使用,所以真实页表的大小一般远小于16MB
那我们如何把虚拟地址→物理地址呢?
32位平台下,一个地址占4个字节,32个比特位
所以:
①虚拟地址的前(从左→右)
10个比特位
,存储的就是页目录表的下标
,用于在页目录里面指定,要用那个页目录表项,就找到了页表[因为1024=2^10]②虚拟地址的
中间10个比特位
,存储的就是页表的下标
,用于在页表中指定,要用那个页表项,就找到了页框③虚拟地址
最后12个比特位
,就是表示相对于对应的页框的起始物理地址的偏移量[因为2^12=4096=4kb]
这样虚拟地址就可以转换成物理地址了
所以上面的映射说明,虚拟地址和物理地址其实有一定的关系?
其实没有,虚拟地址和物理地址建立映射之前,没有任何联系
,它们之间是完全解藕的
只有映射了之后,才通过上面说的结构有了联系
那为什么上面的结构能够映射呢?
那是因为编译器编译形成可执行文件的时候,就已经进行了虚拟地址的编址,所以可执行程序的代码和数据,加载到物理内存时也是以4kb为单位的
所以加载时,
就随意加载到哪个页框
,只要得知这个页框的起始物理地址即可,因为代码/数据加载到了页框,所以页框的起始物理地址就有了对应的虚拟地址了再根据页框的起始物理地址对应的虚拟地址的中间10位,先创建(查找)一张页表,再把页框起始物理地址,填充到页表对应的页表项里
再根据可执行文件编好的虚拟地址填充,把页表的物理地址填充到对应的(
即页目录里面不一定是从0→1023连续填充的,可能隔开
)页目录的下标中所以建立虚拟地址和物理地址一开始加载代码和数据时,建立映射的时候是填充页表→填充页目录表,当然如果懒加载了也没问题
就算只加载进程的入口函数(一般是_start函数)的地址,也没事
因为如果MMU硬件顺着虚拟地址找到的物理地址还没填充数据,那直接填充不就行了
所以
一般情况下,虚拟地址和物理地址的映射,在程序加载到内存时,就已经建立好了
如下图
页表懒加载时,如何确定虚拟地址对应的物理地址是否已经准备好了?
页表懒加载
即进程加载代码和数据的时候,不一定运行了就会把所有的代码和数据加载进来
只要能够在需要的时候,能及时加载进来就可以了
即:
如何知道虚拟地址对应的物理地址中,有没有需要的数据呢?
如果懒加载(以进程最开始的为例,运行过程中懒加载也是类似):
那就是拿着进程PCB中存储的入口函数(_start函数)的虚拟地址,创建页目录表,再创建一张页表
再根据前10比特位,把页表起始物理地址填充到页目录表的指定下标位置
再根据中间10比特位,查询页表
但是不填充具体页框的物理地址,而是直接填nullptr(至少前20比特位为全0,因为可能顺便设置页表标志位
)这样运行进程时,MMU查找到页表时,发现是页框地址是nullptr,那就是这个页框的代码和数据还没有准备好,
自然就触发缺页中断了
综上:
- 硬件上:物理内存是没有权限控制的(即谁都可以读写)只要知道物理地址就能读写
- 软件上:物理内存才有权限控制,所以物理内存的权限控制是软件做的
真实的页表中的标志位
页表的标志位如何保存?
32位下真实的页表是:
页目录表里面存了1024张页表的起始地址,一个页表里面存了1024个页框的起始物理地址
但是我们之前不是还说过,页表里面有标志位,来进行权限管理之类的操作吗?
如果我们细心观察就会发现:
所有的页框的起始物理地址的最低的12个比特位都是0
因为2^12=4096=4kb
而且每个页框的大小都是4kb,所以其实每个页框的起始物理地址都是4kb的整数倍
所以最低的12个比特位是用来存储:从0到4095的
例如:
第一个页框的起始物理地址为全0
第二个页框的起始物理地址为
00000000000000000001000000000000
第3个页框:
00000000000000000010000000000000
…
而
页表里面的页表项只存储页框的起始物理地址
那我们可不可以利用这空的12个比特位,来存储页表的相关标志位呢?
如下图,就是Linux操作系统中页表实现的一部分
因为页表项和页目录项的数据类型是
unsigned long
所以页表和页目录表本质就是元素为unsigned long类型的数组
什么时候对页表的标志位进行初始化?
可执行文件加载到内存的时候!!!
因为编译器编译形成可执行文件的时候,就已经分好了数据节了
而数据节就已经有权限位了
只要加载进来就可以直接初始化!!!
查页表的标志位和权限位,看是否有权限,也是MMU硬件在虚拟地址→物理地址时做的
所以如果因为权限位/标志位(修改代码区,懒加载导致的需要的代码数据还没加载到内存等问题)
导致地址转换失败,本质就是CPU运算出错(因为MMU就是CPU的一部分)
CPU就会在状态寄存器中打上对应的标志位,在合适的时候触发软中断切换到操作系统给进程发送中断信号
缺页中断相关问题
产生缺页中断的情况就是:尝试访问的页框中的数据不在物理内存中
(注意:野指针/权限错误不叫缺页中断,它们是错误
)
①
因为懒加载的原因
,虚拟地址找到的页表中还没有存放对应页框的起始物理地址(存的是nullptr)②
因为内存不足时
,挂起导致的进程一部分代码和数据换出(本质就是对应页框中的数据换出),此时虚拟地址找到的页表指向的是换出的页框,页表项中有页框的起始物理地址,但是没有代码和数据
所以有了缺页中断
new和malloc的时候,其实就只需
- ①申请并填充vm_aere_struct节点
- ②填充页目录项和对应的页表项
- ③存放在页表中的页框的起始物理地址为全0
- ④是否命中的标志位置为0
进程申请物理内存要做的操作都是与上面类似的
为什么这么做?
1.这样就在申请了但还没使用的区间中,暂时性的节省物理内存
要使用时缺页中断就可以了2.进程只管自己的进程地址空间和页表,物理内存的操作都归操作系统管理
这样就把进程管理和内存管理解藕了
同理未初始化的虚拟内存块(变量,数组等)也和还没被使用的new出来的空间一样,还没有真正与之对应的物理内存块
那么操作系统是如何区分越界访问和缺页中断的呢?
因为虚拟地址触发越界访问和缺页中断时,都是虚拟地址与对应的物理地址还没有真正建立映射
其实很简单:就是代码和数据的范围
因为进程地址空间中,真正有虚拟和物理地址映射的虚拟内存区域都在vm_area_struct
链表里面存着
操作系统通过vm_area_struct至少也大概知道(因为不可能真去遍历链表)进程的代码和数据的范围
所以不在这个链表节点范围的肯定就是越界访问了
具体细节:
在Linux中,当进程访问虚拟地址时,若未找到对应的数据或代码,操作系统通过以下机制区分是野指针(无效地址)还是缺页错误(有效地址但数据未加载):
虚拟地址空间的合法性检查
每个进程的虚拟地址空间由内核维护,包含多个映射区域(如代码段、堆、栈、共享库等)。这些区域通过vm_area_struct结构体描述,记录每个区域的起始地址、结束地址、权限(读/写/执行)等。
当发生页错误(Page Fault)时,内核首先检查触发错误的虚拟地址是否属于某个已存在的vm_area_struct区域:
合法地址:若地址在某个区域范围内,说明是有效地址,可能是缺页错误或权限错误。
非法地址:若地址不在任何区域范围内,直接判定为野指针访问,触发SIGSEGV信号(段错误),进程通常终止。页表项(Page Table Entry, PTE)状态分析
若地址合法,内核进一步检查页表项(PTE)的状态:
页面未加载(缺页错误):
PTE的**存在位(Present Bit)**为0,但地址对应的区域有效(如文件映射、匿名内存或交换空间)。
内核会尝试从磁盘(如交换分区或文件)加载页面到物理内存,并更新页表。
权限错误:
PTE存在(Present Bit=1),但操作违反权限(如写只读页)。
触发SIGSEGV信号(例如尝试修改代码段的只读页)。错误类型的最终判定
野指针:地址不在任何vm_area_struct区域 → SIGSEGV。
缺页错误:地址合法且属于某个区域,但页面未加载 → 触发页面调度(Page-in)。
权限错误:地址合法但操作违反权限 → SIGSEGV。内核处理流程(简化版)
Page Fault发生
↓
CPU将错误地址和原因(读/写/执行)传递给内核
↓
内核检查地址是否在进程的vm_area_struct链表中
├─ 不在 → 触发SIGSEGV(野指针)
└─ 存在 → 检查PTE状态
├─ 页面未加载(Present Bit=0)→ 调入页面(缺页处理)
└─ 权限错误 → 触发SIGSEGV
总结
- 野指针:访问未映射的虚拟地址 → 内核直接终止进程。
- 缺页错误:访问已映射但未加载的地址 → 内核透明加载数据,进程无感知。
- 权限错误:访问合法地址但违反权限 → 内核终止进程。
操作系统通过虚拟地址空间管理和页表状态分析,精准区分不同类型的错误,确保进程行为受控且内存访问高效安全。