当前位置: 首页 > news >正文

内存管理【Linux操作系统】

文章目录

  • 简单谈一下物理内存管理
    • 页框
    • 为什么要把物理内存划分成一个一个固定大小的页框使用?
    • 对页框进行描述
    • 对页框进行组织管理
    • 虚拟地址→物理地址(真实的页表)
      • 真实的页表
      • 那我们如何把虚拟地址→物理地址呢?
      • 页表懒加载时,如何确定虚拟地址对应的物理地址是否已经准备好了?
    • 真实的页表中的标志位
      • 页表的标志位如何保存?
      • 什么时候对页表的标志位进行初始化?
    • 缺页中断相关问题
      • 那么操作系统是如何区分越界访问和缺页中断的呢?

简单谈一下物理内存管理

页框

操作系统管理(使用)物理内存的基本单位是页框【就是一个物理内存块,一般4kb大小
为什么一般是4kb大小?
因为计算机科学家研究出4kb效率最高

就像操作系统文件管理时,使用磁盘空间的基本单位也是数据块(一般也是4kb)一样

所以磁盘中的数据加载到物理内存,就非常方便了,只需要对应的数据块中的内容,加载到对应页框里面就行了



为什么要把物理内存划分成一个一个固定大小的页框使用?

而不是需要多少,就分配多少?

  • 硬件支持与地址转换
    • MMU 与页表机制:
      现代 CPU 的内存管理单元(MMU)通过页表将虚拟地址转换为物理地址,而这一过程必须以固定大小的页(如 4KB、2MB、1GB)为基本单位。操作系统必须遵循硬件的这一设计,否则无法高效完成地址转换
    • TLB 缓存效率:
      缓冲器(TLB)缓存虚拟页到物理页框的映射。若内存分配不以页为单位,TLB 的命中率会大幅下降,导致性能急剧降低

  • 为了提高物理内存的使用率,减少内存碎片
    如果是一个一个小块地使用物理内存,那么使用物理内存时,一次最多浪费一个页框的物理内存
    即最多只会出现内部碎片(即一个页框内部没有存满数据还有空隙),不会出现外部碎片(分散的小块内存难以合并)
    • 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中,当进程访问虚拟地址时,若未找到对应的数据或代码,操作系统通过以下机制区分是野指针(无效地址)还是缺页错误(有效地址但数据未加载):

  1. 虚拟地址空间的合法性检查
    每个进程的虚拟地址空间由内核维护,包含多个映射区域(如代码段、堆、栈、共享库等)。这些区域通过vm_area_struct结构体描述,记录每个区域的起始地址、结束地址、权限(读/写/执行)等。
    当发生页错误(Page Fault)时,内核首先检查触发错误的虚拟地址是否属于某个已存在的vm_area_struct区域:
    合法地址:若地址在某个区域范围内,说明是有效地址,可能是缺页错误或权限错误。
    非法地址:若地址不在任何区域范围内,直接判定为野指针访问,触发SIGSEGV信号(段错误),进程通常终止。

  2. 页表项(Page Table Entry, PTE)状态分析
    若地址合法,内核进一步检查页表项(PTE)的状态:
    页面未加载(缺页错误):
    PTE的**存在位(Present Bit)**为0,但地址对应的区域有效(如文件映射、匿名内存或交换空间)。
    内核会尝试从磁盘(如交换分区或文件)加载页面到物理内存,并更新页表。
    权限错误:
    PTE存在(Present Bit=1),但操作违反权限(如写只读页)。
    触发SIGSEGV信号(例如尝试修改代码段的只读页)。

  3. 错误类型的最终判定
    野指针:地址不在任何vm_area_struct区域 → SIGSEGV。
    缺页错误:地址合法且属于某个区域,但页面未加载 → 触发页面调度(Page-in)。
    权限错误:地址合法但操作违反权限 → SIGSEGV。

  4. 内核处理流程(简化版)
    Page Fault发生

    CPU将错误地址和原因(读/写/执行)传递给内核

    内核检查地址是否在进程的vm_area_struct链表中
    ├─ 不在 → 触发SIGSEGV(野指针)
    └─ 存在 → 检查PTE状态
    ├─ 页面未加载(Present Bit=0)→ 调入页面(缺页处理)
    └─ 权限错误 → 触发SIGSEGV


总结

  • 野指针:访问未映射的虚拟地址 → 内核直接终止进程。
  • 缺页错误:访问已映射但未加载的地址 → 内核透明加载数据,进程无感知。
  • 权限错误:访问合法地址但违反权限 → 内核终止进程。
    操作系统通过虚拟地址空间管理和页表状态分析,精准区分不同类型的错误,确保进程行为受控且内存访问高效安全。

相关文章:

  • .Net Framework 4/C# 面向对象编程进阶
  • 【2025】通过idea把项目到私有仓库(3)
  • 宏基因组产品升级——微生物菌群木质素降解能力评估!
  • 中科君芯JFG150N40B 40V-N沟道增强模式功率驱动器
  • Go语言依赖管理与版本控制-《Go语言实战指南》
  • [蓝桥杯]最大比例
  • [蓝桥杯]三元组中心问题
  • 如何在mac上安装podman
  • 机器学习监督学习sklearn实战三:八种算法对印第安人糖尿病预测数据进行分类和比较
  • 在WPS中如何启用宏VBA wps.vba.exe下载和安装
  • 归一化 Normalization 技术概述、优化思路
  • 使用cursor 编辑器开发 Vue项目,配置ESlint自动修复脚本,解决代码不规范引起的报错无法运行项目问题
  • 使用TypeScript构建一个最简单的MCP服务器
  • 开源库 API 化平台 (ALLBEAPI) - 让优秀工具触手可及!
  • PowerBI企业运营分析—全动态盈亏平衡分析
  • VS Code开发项目,配置ESlint自动修复脚本
  • Ubuntu 25.10 将默认使用 sudo-rs
  • 在CSDN发布AWS Proton解决方案:实现云原生应用的标准化部署
  • Solana Web3 快速入门:创建并获取钱包账户的完整指南
  • iOS UIActivityViewController 组头处理
  • 建立一个购物网站/seo网站推广收费
  • 长沙做网站建设公司/云南网络营销seo
  • 张家港普通网站建设/如何交换友情链接
  • 网站建设职能/手机seo快速排名
  • 网站建设规划书的空间/营销助手
  • 珠海医疗网站建设公司/打开百度首页