和程序员聊聊PC下的内存地址

引子:不考虑对图形处理能力很高的游戏玩家,日常居家、公司办公条件下PC的CPU处理能力早已过剩,影响PC操作速度感受的主要因素其实是物理内存大小和硬盘速度,这里的硬盘是暂指目前仍占应用主流的机械硬盘,SSD和混合硬盘先不谈。除了使用时间较长、磨损比较厉害的待淘汰机械硬盘外,硬盘间的速度相差其实并不大(比如5400转和7200转硬盘、缓存相差一倍的硬盘之间性能差距往往并达不到倍数关系)。而物理内存,当一个PC系统使用内存达到一定临界值后,由于需要和硬盘这样的低速外部存储交换内存数据,速度差距与相同应用情形而物理内存配置大1倍的系统而言往往超过1倍甚至10倍以上。我自己的实际例子:公司工作用的PC由于只有2GiB(1Gi=1024*1024*1024)物理内存,而实际工作中需要打开虚拟机并运行很多应用(特别是比较吃内存的Chrome浏览器),有时打开一个新程序需要瞪眼等待30秒以上,等待期间硬盘访问指示灯持续亮着,表示硬盘正在经受折磨,而在内存充裕情况下我打开同样的应用程序也就是1-3秒内完成。

既然内存这么重要,那就聊聊内存吧,内存话题太丰富了,专著就已经不少。作为软件产业链比较上游的C程序员来说,内存地址是常涉及的一个概念,不复杂的嵌入式系统(处理器平台无MMU支持,MMU指存储器管理单元)中由于内存地址概念比较简单暂不讨论,而稍微复杂点的PC系统内存地址很有代表性,本文就从PC内存地址聊起,试图达到澄清一些入门概念,激发一些读者技术探究的兴趣,给他们扫清一些外围障碍,从而为他们进一步深入了解CPU内存映射细节和具体的内存管理算法打下基础。当然,专家们扫略此文后若有指正,也欢迎不吝板砖:binaryhead@sina.com,呵呵。

1、物理内存,在不引起误解的情况下,是指非专业人士、专业人士都懂的——内存条,比如SDRAM、DDR/DDR2/DDR3/DDR4/DDR5等内存条。 

2、虚拟地址(如果存在的话,见后面4)是程序员视角的地址,比如C语言中的指针值,汇编程序员使用CPU指令产生的寻址。

3、物理地址是CPU芯片地址引脚(假定内存控制器没有集成在CPU中)发出的访存地址,物理地址还要被内存控制机构(一般在北桥芯片中)进行译码或映射(映射的一个例子见本文最后17部分提到的“物理内存回收”),可能实际指向DRAM(就是前面提到的内存条),也可能通过一些外部总线(比如PCI)指向各种外部设备。对外部设备的这种访问一般也称为MMIO:Memory Mapped I/O——内存映射I/O。另外特别提一下,x86支持用I/O指令(IN、OUT)寻址的另外的独立64K I/O地址空间,该地址空间不要和这里物理内存地址混淆。

4、虚拟地址一般通过CPU的分页机制映射到物理地址,如果分页机制未使能,此时虚拟地址就是物理地址。所谓的分页机制,简单来说就是把虚拟地址空间按固定大小(比如4KiB,1Ki=1024)划分成虚拟页面,物理内存也按和虚拟页面同等大小划分成物理页面。虚拟页面到物理页面之间的映射通过多级映射表来完成的,所谓多级映射,具体来说x86是2级,x86 PAE下是3级,目前x64支持48位虚拟地址因而需要4级映射,可以预见,假如20年后48位虚拟地址又不够用了,而那时若仍采用4KiB页面,则需要5级映射来实现57位虚拟地址支持!因为有了映射机制的存在,我们就可以达到:保护、隔离、共享等目标。①、保护,通过虚拟页属性来实现,比如简单的两级特权保护:用户页属性和监管页属性。标志为用户页属性的虚拟页面中运行的应用程序并不能直接访问标志为监管页属性的虚拟内存页面,标志为监管页属性的虚拟内存页面一般驻留着操作系统内核代码和数据。②、隔离,通过不同应用程序进程的虚拟地址空间(尽管相同却)映射到不同的实际物理页面来实现,此时每个应用的虚拟地址空间也称为自己的私有地址空间。隔离同时也进一步扩大了整个系统的可访问虚拟地址空间,实际上一个系统的可访问地址空间总和=进程数×进程私有地址空间+所有进程可见的公共地址空间,这是一个天文数字,因为称为虚拟内存再准确不过了。③、共享,操作系统只需把同一个物理页面映射到不同的进程的虚拟地址空间中就能实现这些进程共享这一物理页面。

5、线性地址,线性地址一般是相对“逻辑地址”概念而言的。熟悉x86实模式的人会有“逻辑地址”的记忆,因为x86实模式下分段机制扮演重要角色,跨段的地址往往采用【段:段内偏移】的形式,这就是“远指针”概念的由来,如果读者对早期DOS下C语言编程熟悉,应对远指针这一概念有记忆。尽管现代操作系统(如Linux、Windows)在x86/x64保护模式下都采用平坦地址模型,但由于x86/x64段机制无条件启用(事实上,x86保护模式下分页机制反而在CPU硬件实现上是操作系统可选开启的,虽然操作系统总是开启分页机制!),保护模式下的远指针指向的地址就是逻辑地址,逻辑地址经分段机制转换之后的地址就是线性地址。线性地址中的线性还有另外一层重要涵义,就是没有间断、连续的意思。前面4中介绍的分页虚拟地址提到,每个应用程序进程有自己的私有地址空间,每个应用程序进程的私有(虚拟)地址空间是连续的、相同的(比如都是从0x00000000-0x7FFFFFFF这样的2GiB虚拟内存空间,1Gi=1024*1024*1024,下同),一个进程的两个连续的虚拟页面完全可能映射到毫不相邻的两个物理页面。实际上,由于历史遗留的原因,可当作RAM方式访问的物理内存地址空间还真就是不连续的,比如物理内存地址空间0x000A0000-0x000FFFFF(这一早些年DOS时代的上位内存区,UMA)到现在也一般不直接映射到DRAM中,因而DRAM的这一地址区域空间是被浪费掉的,虽然只有区区的384KiB!当然正因为只有区区384KiB,所以犯不着由硬件内存控制机构来回收,内存回收见本文最后第17部分。提醒一下C程序员们,尽管每个进程有自己连续的私有地址空间,但还是要遵循基本的编程准则:除非有广而告之的权威保证,不要试图访问自己未分配的虚拟内存地址,这里的分配包括程序设计时分配的变量和程序运行时通过malloc动态分配的堆空间。比如,在Windows(NT系列及后代)下,每个进程的0x00000000-0x0000FFFF这64KiB虚存是操作系统用于坏指针的捕捉区域,尝试访问这一区域将直接导致程序出错。

6、综合前面1-5:逻辑地址(经段转换到)-->线性地址(如果分页使能就是虚拟地址,此时虚拟地址经分页映射到)-->物理地址(经译码转换到)-->内存条上的物理内存地址或MMIO地址。

7、对于操作系统设计者而言,由CPU硬件分页机制实现的虚拟内存管理具有最大的跨CPU体系结构(比如x86/x64、PowerPC、ARM、MIPS、SPARC等)通用性,因而硬件MMU大多情况下是指CPU硬件的分页机制。正因为如此,尽管x86/x64的分段机制支持4级特权(R0、R1、R2、R3),但x86/x64上运行的操作系统出于移植到其它CPU体系结构考虑,只使用了两级特权:R0和R3,x86/x64上的分页机制就支持两级虚拟页面属性特权保护,即前面4中提到的:用户虚拟页面属性和监管虚拟页面属性,并分别和段特权的R3和R0相对应。

8、通常说的32位CPU是指CPU的通用寄存器的位宽度和虚拟地址(支持分页时)位宽度是32位的。类似地,64位CPU是指CPU的通用寄存器的位宽度和虚拟地址位宽度是64位的。

9、32位CPU(如x86)支持的实际物理地址宽度可能大于32位,就是说32位虚拟地址CPU架构并不必然意味着32位的物理地址,物理地址可以少于或多于32位。例子:Intel从1995年推出的Pentium Pro开始即支持36位物理地址(即64GiB物理地址空间),当然此时需要通过CPU的PAE(物理地址扩展)机制实现36位地址访问。x86(相对x64而言)支持数据执行阻止(即DEP,Windows叫法;硬件厂商AMD叫NX:No-eXecute,Intel叫XD:eXcute-Disable)需要PAE使能,原因很简单:32位x86在PAE使能后,虚拟地址到物理地址转换过程中产生的中间地址都是64位的,而DEP在CPU硬件实现上就是通过这些64位地址的最高位是1还是0来实现的,如果该位是1就是意味着该页是数据,不能被当作指令执行。

10、通常说的32操作系统是指运行在32位CPU上的操作系统或者64位CPU的32位工作模式下。正如前面9中提到的,32位操作系统在支持PAE的32位x86上实际可访问的物理内存可能超过32位,比如达到36位的64GiB。实际上,一些32位Window系统,如Windows 2000的高端版本、Windows Server 2003和Windows Server 2008的高端版本支持32GiB/64GiB的物理内存。Windows客户系列操作系统,包括32位的Windows XP(如SP3)、Windows7、Windows8由于支持DEP而无条件开启了PAE,实际具备4GiB以上物理内存访问能力,但由于所谓的“一些内核驱动程序在支持大于4GiB物理内存时工作不正常从而造成操作系统不稳定”等原因,Microsoft基于看似为用户着想的“稳妥”策略,只让这些客户系列操作系统最大只支持到4GiB物理内存,造成实际刚好配置4GiB物理内存的x86/x64系统上运行这些32位客户操作系统的尴尬,见本文最后的17部分。不过在实际内存大于4GiB时,Windows应用程序员仍可能通过AWE(Address Windowing Extensions)方式利用4GiB以上的物理内存,注意这里的措辞:利用。我有点疑惑,因为这些版本Windows一方面声称不支持大于4GiB物理内存而实际却能通过AWE方式访问大于4GiB的物理内存,看似自相矛盾。当然从程序员角度来说,采用AWE显然是程序有意而为之的行为,且按MSDN说法,进程在自己的私有虚拟地址空间访问窗口(此时此窗口已被锁定)直接独占式地映射到对应的已分配物理内存上,其它进程在该进程解除虚存映射并释放已分配的物理内存前,是不能访问这些物理内存的,从而这些内存不会(因为没有必要)被操作系统内存管理器交换到外存上去,因此具有极大的性能优势甚至是保密特性(应用进程不能控制自己位于虚存中的机密数据被操作系统交换到外存的分页文件上的行为,专家认为是不保密的行为)。这样看来,因为使用AWE是程序的有意行为,前面自相矛盾的两方面似乎也不完全对立,呵呵。

11、32位Windows(NT内核系列:NT/2000/XP/7/8以及Server2003/2008)虚拟地址空间划分:2GiB的每个进程私有地址空间(0x00000000-0x7FFFFFFF)+2GiB内核专用地址空间(0x80000000-0xFFFFFFFF)。/3GB启动选项使能时,3GiB的每个进程私有地址空间(0x00000000-0xBFFFFFFF)+1GiB的内核专用地址空间(0xC0000000-0xFFFFFFFF)。而要完全使用3GiB私有进程地址空间(即大于通常的2GiB进程私有地址空间)时,应用程序链接时需要使用/LARGEADDRESSAWARE选项,本文的16部分有对/LARGEADDRESSAWARE的进一步描述。

12、32位Linux的虚拟地址空间划分:3GiB的每个进程私有地址空间(0x00000000-0xBFFFFFFF)+1GiB的内核专用地址空间(0xC0000000-0xFFFFFFFF)。

13、64位Windows在x64上虚拟地址空间划分(目前仍在进化中,所以后面的范围权当理解原理的参考吧):8~128TiB(1Ti=1024*1024*1024*1024,下同)的每个进程私有地址空间(0x0000000000000000-0x000007FFFFFFFFFF,Win8.1-64bit:0x0000000000000000-0x00007FFFFFFFFFFF)+8~128TiB的内核专用地址空间(其中,Win7_7601-64bit:0xFFFFF6800000000-0xFFFFFFFFFFFFFFFF共9.5TiB,Win8.1-64bit、Win10Preview_9841-64Bit:0xFFFFC00000000000-0xFFFFFFFFFFFFFFFF共64TiB)。内核地址空间之所以采用这种“奇怪”的格式(AMD/Intel称为“规范”格式),参见:https://en.wikipedia.org/wiki/X86-64#Canonical_form_addresses

14、64位Linux在x64上的虚拟地址空间划分一步到位:128TiB的每个进程私有地址空间(0x0000000000000000-0x00007FFFFFFFFFFF)+128TiB的内核专用地址空间(0xFFFF80000000000-0xFFFFFFFFFFFFFFFF)。参见:https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt

15、11-14的“每个进程私有地址空间”对应用程序员来说意味着最多可同时寻址访问的“(虚拟)内存”上限,比如对C程序员来说意味着最多可连续用malloc函数在进程堆上申请分配的内存上限(这里为讨论方便,排除malloc之后调用free释放的情况)。由于进程本身的代码、数据以及本进程调用的动态库也在当前进程的私有地址空间中,malloc实际可分配的(虚拟)内存比“每个进程私有地址空间”少一些(一般来说至少要少几十MiB,1Mi=1024*1024)。更进一步,因为这些动态库的存在及动态库载入地址生成的历史原因,加上前面11中的内容,我提醒程序员们:尽管32位应用程序的私有虚拟地址空间可能达到2GiB、3GiB或4GiB(4GiB请阅读后面16中的内容),但由于动态库在虚拟地址空间中位置的不理想特点,比如可能“悬挂”在进程私有地址中间位置,malloc单次最大申请分配的内存不要太大,比如2GiB,因为那样的话,尽管空闲虚拟内存总空间可能有2GiB甚至3GiB以上,但实际上你单次能成功分配的虚存大小可能要远小于这个数。至于在64位操作系统下,由于进程私有虚拟空间相对32位操作系统而言提高了至少3个数量级,并且因为64位应用程序新近开发的特点(历史包袱没有),所以你就尽情地用malloc分配虚拟空间吧,没有前面的单次分配大小限制,唯一要担心的是硬盘不够大,容不下因malloc向操作系统提交大量虚存分配要求后,被操作系统扩大了的分页文件!

16、对64位Windows作一个补充:64位Windows提供对原有32位应用程序的较好兼容性支持,这通过WoW64机制实现。按照前面13中的内容,64位Windows的内核空间远远高于每个用户进程的低8TiB私有虚拟地址空间,完全犯不着像32位Windows内核那样,再来挤占有限的低32位虚存地址空间。因此,在64位Windows上,32位应用程序的私有地址空间扩展到完全的4GiB,不过需要注意的是,若要完全使用4GiB私有虚存空间,32位应用程序需要在链接时给出/LARGEADDRESSAWARE选项。由于该链接选项实际最终体现在PE可执行程序中、程序特征字中的一个位上,理论上是可以通过工具对现有遗留的32位Windows二进制应用程序进行修改,设置LARGEADDRESSAWARE位,以期获得提升到4GiB私有虚拟内存空间带来的益处,但这些遗留二进制程序如果本身有自校验功能并在觉察到自己被“非法”修改后拒绝工作,果真如此的话就违背二进制修改的初衷了。

17、最后讨论一下配备4GiB物理内存PC安装Windows操作系统时的尴尬处境,之所以说尴尬是因为:一方面,配备4GiB以上物理内存时,我们除了装64位的Windows(如果选择Windows的话),肯定无条件选择64位版本;另一方面,配备2GiB(包括3GiB)或以下物理内存的机器,对那些内存要求不高、并不密集进行64位整数运算的32位应用程序而言,原生的32位Windows操作系统性能甚至要优于64位Windows。4GiB物理内存配置刚好是不上不下所以显得尴尬。一个实例:本人2014年8月份买的Lenovo G510电脑,配置4GiB物理内存,安装了32位Windows 7操作系统,操作系统报告可用内存居然是2.43G,这差不多创下了我所了解的4GiB物理内存在32位操作系统上报告可用内存的最低记录了。原因前面已经提到(请阅读第3、10部分):外设MMIO占用了我大量的物理内存地址空间。另一方面,Intel早在2006年推出的965芯片组就开始支持MMIO占用内存的回收了(参见http://www.intel.com/Assets/PDF/datasheet/313053.pdf,第62页),但内存回收地址区间位于4GiB物理地址空间之上(这样才有实际意义,实际BIOS在内存检测时都是设置内存控制器,使内存回收地址区间紧接在已安装的物理内存之上),而前面提到微软出于“稳妥”策略限制,尽管启用了PAE,32位客户版本Windows并不直接支持大于4GiB的物理内存,所以造成我接近40%的物理内存浪费。忍无可忍,重装Windows 7 64位版,结果Windows显示可用内存增加到4012MiB,浪费的1526MiB物理内存完全被回收!

难免会有喜欢刨根问底的人,所以最后解释一下回收1526MiB物理内存的由来。使用工具读取G510机器北桥内存控制器一些寄存器的值,其中,REMAPBASE寄存器的值是0x100000000,REMAPLIMIT寄存器的值是0x15F500000,考虑到REMAPLIMIT寄存器最低20位应视为全1,因此实际回收空间是0x100000000-0x15F5FFFFF,正好1526MiB。TOLUD寄存器的值是0x9FA00000,TOM寄存器的值当然是0x100000000,而0x9FA00000-0xFFFFFFFF是1542MiB,那16MiB哪去了?答案是:让Intel可管理引擎给“偷”了。证据:MESEG_BASE寄存器值是0x00000000FF000000,可管理引擎分配(偷走)的物理内存总是位于物理内存的顶部,因此0x9FA00000-0xFF000000正好是1526MiB,0xFF000000-0xFFFFFFFF这一区间就是被可管理引擎“偷走”的物理内存,正好是16MiB。


最最最后,给有志于揣摩PC机BIOS工作原理的读者留一个习题,如果有一天在我的G510机器上再添加4GiB物理内存,物理内存总共达到8GiB,请预测一下BIOS会把REMAPBASE、REMAPLIMIT、TOM、MESEG_BASE这些寄存器的值设置成多少?

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值