程序员必读:CPU内存访问的底层原理与优化策略

引言

内存管理是计算机系统中的核心技术之一,它直接关系到系统的性能和稳定性。从早期的Intel 8086处理器到现代的64位多核CPU,内存管理机制经历了从简单到复杂、从低效到高效的发展历程。这些设计理念不仅在操作系统内核中得到了广泛应用,也深刻影响了许多优秀的开源项目。

本文将深入剖析CPU与内存访问之间的工作机制发展和原理,通过回顾历史演进过程,帮助读者理解现代计算机系统中内存管理的核心思想。通过对本文的学习,您将了解到许多现代开源项目是如何借鉴这些经典设计理念来构建高性能系统的。

我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili ,也欢迎您了解我的开源项目 mini-redis:github.com/shark-ctrl/...

为方便与读者交流,现已创建读者群。关注上方公众号获取我的联系方式,添加时备注加群即可加入。

内存管理的发展史

intel-8086款cpu

计算机发展初期,intel-cpu 8086通过针脚获取内存数据,见下图,需要了解的是,因为时代发展的局限性,intel-8086 cpu针脚地址总线和数据总线是共享这几对针脚:

同时8086是16位的cpu,即数据总线也有16位,这意味着它一次性可以读取16bit的数据。因为数据总线和地址总线共享针脚,所以总线的名称为AD0~AD15。同时地址还有A16~A19这高4位,所以地址总线实际是有20位,换算一下:

ini 复制代码
第一步:20bit=2^20=1048576
第二步: 1048576 代表每个字节=1048576/1024/1024=1M

由此可得,地址总线大约可以寻址1M的内存空间:

虽然地址总线可以寻址1M以内的数据,但是发展初期的寄存器也只有16位换算下来也就是65536也就是64kb的数据,考虑到这一局限性,设计者们提出内存分段的方式进行寻址,大体规则为:

  1. 每个内存段最大64kb
  2. 新增段寄存器,指向内存段的起始地址
  3. CPU通过段寄存器中的段基址加上偏移量进行寻址
  4. 分为数据段、代码段、堆栈段、附加段等不同类型的段

随着时间的推移,cpu的引脚越来越多,位数也越来越多,到了80386CPU已支持32位,此时地址总线和数据总线不再共享引脚了。

虚拟内存

因为寻址空间更多,计算机提出了多进程的概念,即单位时间内可以边听音乐、打游戏、聊天。考虑到每个进程都需要用到庞大内存空间,多进程全部放在内存空间肯定是不现实的。于是基于Unix系统之上提出了沿用至今的虚拟内存的设计理念,在虚拟地址空间的设计理念下,所有进程在逻辑上感觉自己获得的是一个完整的内存,注意是逻辑上的地址空间,到了真正实际使用物理内存空间时,由操作系统统一申请内存页,默认情况下32位的操作系统页大小为4kb:

在虚拟内存系统中,每个进程都拥有独立的虚拟地址空间,这为进程提供了内存保护和隔离。虚拟地址空间通常被划分为用户空间和内核空间,其中内核空间对所有进程都是相同的,而用户空间则因进程而异。这种设计不仅提高了系统的安全性,还使得内存管理更加灵活。

对此,设计者还新增了一个名为cr3的寄存器统一管理内存的页表,该表设计结构为:

  1. 页目录:记录所有的页表
  2. 页表:记录实际页的地址

当我们进程需要实际使用物理内存时,操作系统会基于其使用的大小统一为其分配物理内存,cpu则是基于cr3寄存器完成地址翻译:

  1. 通过页目录定位到页表
  2. 通过页表定位到实际的物理页帧
  3. 结合页内偏移量定位到实际数据

因所有的进程都有各自的虚拟地址管理,当操作新进程的时候,只需将cr3寄存器指向的地址改为新进程的页目录基地址即可,这就是我们常说的分页式内存管理:

但是随着发展,计算机需要运行的内存越来越多,内存空间有些捉襟见肘了,于是设计者提出了LRU缓存置换算法,该算法将最近没有用到的内存数据置换到硬盘空间以容纳新的进程内存分配需求,当cpu实际再次访问到这个数据的时候,操作系统会优先从内存中尝试加载数据,若没有数据则发起缺页中断将数据重新加载回内存中,这也就是著名的内存分页交换算法:

页面置换算法是操作系统内存管理的重要组成部分,除了LRU(Least Recently Used)算法外,还有FIFO(First In First Out)、LFU(Least Frequently Used)等多种算法。这些算法在不同场景下各有优劣,操作系统会根据具体需求选择合适的置换策略。

随着时代的发展,cpu也由32位发展至64位,内存也逐渐由4G变为16甚至是32、48G,但该算法依旧沿用至今,显见其设计理念之出色。

虚拟地址翻译缓存的诞生

MMU地址翻译的局限性

上文提及地址地址转换过程,本质上MMU(内存管理单元)通过cr3寄存器获取数据的页目录索引、页表索引、偏移量之后进行地址解析转译,对应需要进行多次的内存访问:

  1. 读取页目录项(在32位系统中通常需要1次内存访问,64位系统中可能需要4次或更多)
  2. 读取页表项(1次内存访问)
  3. 基于页内偏移量访问实际物理内存中的数据(1次内存访问)

考虑到内存访问速度远远低于CPU读写速度,且现如今64位系统需要使用到4级甚至5级页表,于是就提出了为MMU分配一份缓存空间的想法。

TLB缓存空间的诞生

于是就有了TLB(Translation Lookaside Buffer,转换旁路缓冲器)的设计,整体设计理念是利用局部性原理,即之前用到的数据下一次大概率会用到,所以基于cr3寄存器得到的虚拟地址和翻译后的物理地址构成映射直接写入到TLB缓存中。例如我们现在有虚拟地址0x12345678,其高20位对应0x12345000定位到真实的物理页13bc1000,结合最后12位的页内偏移量得到实际物理地址:13bc1678:

TLB通常被实现为内容可寻址存储器(CAM),可以并行搜索所有条目,这使得TLB查找速度非常快,通常只需要一个时钟周期。现代CPU中的TLB通常分为指令TLB(ITLB)和数据TLB(DTLB),分别缓存指令地址和数据地址的转换结果。

后续进行内存访问时,MMU的执行流程为:

  1. 查看虚拟地址是否存在于TLB中,若存在则直接访问(TLB命中),反之执行步骤2
  2. 进行完整的页表遍历地址翻译生成实际的物理内存信息并缓存到TLB中
  3. 基于上述步骤得到的物理内存访问实际内存数据

缓存映射的设计

对于这份缓存的设计,为避免多地址映射位置冲突,采用取模定位分组再遍历的方式进行读写缓存,保证效率的同时又避免冲突。例如我们将缓存分为4组,此时要写入虚拟地址0x12345678的缓存信息,我们就可以基于虚拟页号对组数取模定位到分组0,查看分组0中空闲的位置将映射信息写入:

这种组织方式被称为组相联映射,是直接映射和全相联映射的折中方案。它既避免了直接映射中不同内存块映射到同一缓存位置的冲突问题,又不像全相联映射那样需要昂贵的硬件支持。

置换算法的优化

但是缓存空间是有限的,在多进程环境中,同一个虚拟页翻译的物理地址都是不同的,所以当进行新的缓存替换的时候,当前进程对应的缓存数据需要被清理掉,而内核页面的缓存翻译信息可以保留。因此,最终的TLB缓存设计就是在原有的映射基础上增加一个ASID(Address Space Identifier,地址空间标识符)来区分不同进程的页表映射,实现进程间TLB缓存的隔离。

当发生进程切换时,CPU可以基于ASID判断TLB条目是否属于当前进程,只有匹配的条目才会被使用。这样既避免了进程切换时刷新整个TLB的开销,又保证了内存访问的正确性。

小结

本文以CPU访问内存为切入点,讲解了从Intel 8086到现代CPU的内存管理发展史。文章首先介绍了早期CPU受限于硬件设计而采用的分段式内存管理,随后阐述了为支持多进程而提出的虚拟内存概念,以及基于页面的内存管理机制。考虑到虚拟地址到物理地址转换的性能开销,文章进一步介绍了TLB(Translation Lookaside Buffer)缓存的设计与实现,包括其组织结构、工作原理以及在多进程环境下的优化策略。

通过本文的介绍,我们可以看到现代操作系统和CPU在内存管理方面的设计思想,这些设计理念不仅在操作系统内核中得到了应用,也在许多开源项目中有所体现,如Redis、Nginx等高性能系统都借鉴了这些思想来优化内存访问性能。

理解这些底层原理有助于我们在实际开发中编写更高效的代码,特别是在处理大规模数据或对性能有严格要求的场景中,能够更好地利用硬件特性来优化程序性能。

我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili ,也欢迎您了解我的开源项目 mini-redis:github.com/shark-ctrl/...

为方便与读者交流,现已创建读者群。关注上方公众号获取我的联系方式,添加时备注加群即可加入。

参考

《趣话计算机底层技术》

本文使用 markdown.com.cn 排版

相关推荐
用户6120414922132 小时前
springmvc做的学生考勤管理系统
javascript·后端·spring
IT_陈寒2 小时前
SpringBoot性能翻倍的7个隐藏配置,90%开发者从不知道!
前端·人工智能·后端
月夕·花晨3 小时前
Gateway -网关
java·服务器·分布式·后端·spring cloud·微服务·gateway
绝无仅有3 小时前
面试之MySQL 高级实战& 优化篇经验总结与分享
后端·面试·github
绝无仅有3 小时前
某云大厂面试之Go 实际问题及答案
后端·面试·github
程序员爱钓鱼11 小时前
Go语言实战案例 — 工具开发篇:实现一个图片批量压缩工具
后端·google·go
ChinaRainbowSea13 小时前
7. LangChain4j + 记忆缓存详细说明
java·数据库·redis·后端·缓存·langchain·ai编程
舒一笑13 小时前
同步框架与底层消费机制解决方案梳理
后端·程序员