《Windows核心编程系列》十二谈谈Windows内存体系结构
Windows内存体系结构
理解Windows内存体系结构是每一个励志成为优秀的Windows程序员所必须的。
进程虚拟地址空间
每个进程都有自己的虚拟地址空间。对于32位操作系统来说,它的地址空间是4GB。这是因为32位指针可以表示从0x00000000到0xFFFFFFFF之间的任一值。对于64位的操作系统来说有0--2的64次方之间的任一值。
由于每个就进程都有自己的地址空间,因此每个进程都只能访问属于自己的地址空间而不能访问其他进程的空间。这保护了进程,也是之所以我们说进程是资源分配和保护的基本单位的原因。
每个进程都有自己的私有地址空间,进程A可以访问它的地址空间0x11009333处的地址。进程B当然也有这个地址,但进程B的此地址处存储的与进程A的数据完全不同。它们没有任何关系。进程A不能访问进程B的地址空间。反之亦然。
每个进程的地址空间都被分成多个分区。由于地址空间分区依赖于操作系统底层实现,因此各个版本的Windows的分区各不相同。此处我们主要介绍32位和64位的Windows内核下的分区情况。
一:空指针赋值区
这一分区是进程地址空间从0x00000000到0x0000FFFF的区域。共64KB。该分区的作用是帮助程序员捕获对空指针的赋值。如果进程中的线程试图访问此区域将会导致访问违规。(原因:此区域未调拨空间,访问未调拨空间的地址将会导致访问违规)。这种情况经常由调用API申请空间失败引起,如malloc。但未对指针进行检查。
没有任何方法能够访问到此区域的虚拟内存。
二:用户模式模式分区。
这一部分空间是进程地址空间的驻地。它的大小也取决于cpu体系结构。
X86cpu体系下的用户模式分区从0x00010000----0x7FFEFFFF总共2GB。进程的大部分数据都保存在这一分区。这其中包括程序加载的dll。
有些用户程序需要大于2G的用户模式地址空间。为此Windows提供了一种大用户分区模式来增大用户模式分区,但最多为3GB。要启动大用户分区模式可以查询其他资料,此处不再介绍。
当32位的应用程序在64位下的环境下运行时,Windows会让此程序在地址空间沙箱中运行。系统将64位地址的高33位都设为0,这样就截断成了32位地址,也就将可用的地址空间限制在最底部的2GB中。对于大多数的程序来说,2GB的地址空间已经足够,为了让64位应用程序能够访问整个用户模式分区,必须用/LARGEADDRESSAWARE连接器开关来链接应用程序。
三:内核模式分区
这一分区是操作系统代码的驻地。系统需要这一部分空间来存放内核代码、设备驱动程序代码、设备输入输出高速缓存、非分页缓冲池分配表、进程页面表等。驻留在这一分区任何东西为所有进程共有,被映射到所有进程地址空间中。如果一个应用程序试图访问这一区域将会导致访问违规。
当系统创建一个进程同时为其创建它地址空间时,此地址空间中大部分都是闲置的。为了使用这部分地址空间,我们必须调用VirtualAlloc来分配其中的区域。分配区域的操作被称为预定。
当应用程序预定地址空间区域时,系统会确保预定的区域的起始地址正好是分配粒度的整数倍。分配粒度根据不同的平台而有所不同。现在所有的平台都是用相同的分配粒度。大小为64KB。
而对于预定的地址空间的大小,系统会确保区域的大小正好是系统页面大小的整数倍。X86和x64系统使用的页面大小为4KB。
如果应用程序预定一块大小为10KB的地址空间区域。那么系统会将该请求取整到页面大小的整数倍。在x86和x64系统中系统会预定一块大小为12KB的区域。
当程序不再需要访问所预定的地址空间区域时,应该释放该区域可以调用VirtualFree函数来完成。
调拨物理存储区
前面预定的地址空间仅仅是标记此块空间已有人使用,其他程序不能再次预定此块区域。为了使用所预定的地址空间区域,我们还必须分配物理存储器,并将存储器映射到所预定的区域。这个过程叫做调拨物理存储器。
可以通过调用VirtualAlloc来调拨物理存储器。物理存储器的调拨是以页面为单位来调拨的。并不需要为所有预定的区域都调拨物理空间。可以仅调拨需要使用的区域。调拨后程序就可以访问内存空间了。
当程序不需要访问所预定的区域中已调拨的物理存储器,应该释放物理存储器。这个过程被称为撤销调拨物理存储器。这是通过调用VirtualFreee函数来完成。
物理存储器和页交换文件
如今的操作系统可以对磁盘进行虚化,来扩展内存,这部分区域被称为分页文件或页交换文件,其中包含虚拟内存,可用程序使用。
内存和磁分页文件共同构成了总内存。
页交换文件增大了应用程序可用内存的总量。实际上,这时操作系统与cpu分工协作,把内存中的一部分保存到页交换文件,并在应用程序的时候再将页交换文件中的对应部分载入内存。
当应用程序调用VirtualAlloc函数来把物理存储器调拨给地址空间区域时,该空间实际上是从硬盘的页交换文件中得到的。
为了能够使用虚拟内存,当线程试图访问存储器中的一个字节中,cpu必须知道该字节是在内存中还是在磁盘上。
当线程试图访问所属进程地址空间中的一块区域时,有可能出现两种情况:
一:要访问的区域就在内存中。此时cpu会把数据的虚拟地址映射到内存的物理地址,然后访问内存。
二:不在内存中,而是位于页交换文件中。这次不成功的访问将会触发缺页中断。发生缺页中断时中断处理程序会在内存中找到一个闲置的页面,然后将数据从页交换文件复制到内存中。
当用户执行一个程序时,系统会打开应用程序对应的exe文件。计算出应用程序的代码和数据的大小。然后系统会预定一块地址空间,并注明与该区域相关联的物理存储器就是exe文件本身。系统并没有从页交换文件中分配空间,而是将exe文件作为程序预定空间的后备存储器。这样一来,程序载入很快,页交换文件也可以保持在一个合理的大小。
当一个程序位于硬盘上的文件映像作为地址空间区域对应的物理存储器时,我们称这个文件映像为内存映射文件。当载入一个dll或exe时,系统会自动预定地址空间并把文件映像映射到该区域。除此之外系统还允许我们手动将数据文件映射到地址空间。
Windows可以使用多个页交换文件。如果多个页交换文件位于不同的物理硬盘上,那么系统就可以运行得更快。我们可以在设置页交换文件大小。
页面保护属性
我们可以给每个已分配的物理存储页指定不同的页面保护属性。
页面属性有以下几种:
PAGE_NOACCESS: 不可访问。试图访问或执行页面中的代码 将导致访问违规。
PAGE_READONLY: 只读。试图写入页面或执行页面代码将引发访问违规。
PAGE_READWRITE: 读写。试图的执行页面中的代码将引发访问违规。
PAGE_EXECUTE: 可执行。试图读写页面将导致访问违规。
PAGE_EXECUTE_READ: 执行只读。试图写入页面将导致访问违规。
PAGE_EXECUTE_READWRITE:允许对页面进行任何操作。
PAGE_WRITECOPY: 试图执行页面代码将导致访问违规。试图写入页将使系统在页交换文件中为此页面创建一份副本。
PAGE_EXECUTE_WRITECOPY: 对页面执行任何操作都不会导致访问违规。试图写入页面将导致系统从页交换文件为此页面创建一份副本。
Windows通过页面保护属性对物理存储页面进行保护。如果cpu试图执行某个页面中的代码,而该页又没有PAGE_EXECUTE_*保护属性,那么cpu会抛出访问违规异常。
写时复制
PAGE_WRITECOPY与PAGE_EXECUTE_WRITECOPY的作用是节省内存和页交换文件的使用。
Windows允许多个进程共享同一块存储器。所有的进程进程会共享程序的代码页和数据页,但只能读取或执行而不能写入。如果每个进程实例修改并写入一个存储页,这等于是修改了其他程序实例正在使用的存储页,最终将导致混乱。让所有的应用程序实例共享相同的存储页能够极大的提升了系统的性能。
为了避免此种情况,os会给共享的可写存储页指定写时复制属性。当系统把一个exe或dll映射到进程地址空间中时,会计算出多少页面是可写的。计算操作是根据页面的保护属性来进行的。包含代码的页面具有PAGE_EXECUTE_READ属性,包含数据的页面具有PAGE_READWRITE属性。计算出可写页面所占的空间大小后,系统会从页交换文件预定存储空间来容纳这些可写页面。
当线程试图写入一个页面时,系统会执行以下操作:
1:在内存中找到一个闲置页面,并将其后备存储器标明为页交换文件,这次是真正的从页交换文件中分配。
2:系统把线程想要修改的页面复制到闲置页面中。并将闲置页面标记为PAGE_READWRITE或PAGE_EXECUTE_READWRITE属性。
3:系统不会对原始页面做任何更改。然后更新页面表,原来的虚拟地址现在就映射到这个新的页面了。
4:线程将对新页面进行写入。
每个进程在修改共享页面时都会执行操作,而不会对共享页面有任何影响。
内存区域的类型
闲置区域 区域的虚拟地址没有任何后备存储器。该地址尚未预定,应用程序既可以从它的基地址开始预订区域,也可以从闲置区域的任何位置开始。
私有区域:区域的虚拟地址以系统页交换文件为后备存储器。
映像区域:区域的虚拟地址刚开始时是以映像文件如exe或dll文件作为后备存储器。但以后就不一定是。由于写入的页面具有写时复制属性,写时复制机制会改用页交换文件作为后备存储器。
已映射区域:区域的虚拟地址刚开始以内存映射文件为后备存储器。以后可能就不一定以页交换文件为后备存储器。
内存对齐
Cpu只有访问对齐后的数据执行效率才是最高的。
数据的地址模数据的大小如果结果是0,那么数据就是对齐的。
如果cpu要访问的数据没有对齐将会出现两种结果:
一:cpu会引发一个异常。
二:通过多次访问来取得未对齐的数据。
显然cpu多次访问内存会影响到程序的性能。为了得到最佳的应用程序性能,我们在编写代码时尽量让数据对齐。
X86环境下的vc++支持使用UNALIGNED和UNALIGNED64宏。可以将未对齐的数据对齐。
- #pragma pack(show)
该命令来查看当前的对齐值,但是要注意的是,结果是以warning的形式输出的,所以要在VS的警告窗口中才看得见,如下
warning C4810: value of pragma pack(show) == 8
数据对齐规则:
一般来说,结构体的对齐规则是先按数据类型自身进行对齐,然后再按整个结构体进行对齐,对齐值必须是2的幂,比如1,2, 4, 8, 16。如果一个类型按n字节对齐,那么该类型的变量起始地址必须是n的倍数。比如int按四字节对齐,那么int类型的变量起始地址一定是4的倍数,比如0x0012ff60,0x0012ff48等
数据自身的对齐:
数据自身的对齐值通常就是数据类型所占的空间大小,比如int类型占四个字节,那么它的对齐值就是4
整个结构体的对齐:
整个结构体的对齐值一般是结构体中最大数据类型所占的空间,比如下面这个结构体的对齐值就是8,因为double类型占8个字节。
有了上面的基础,再看看一开始的两个例子
先看结构体Test
- struct Test
- {
- char c ;
- int i ;
- };
1 c是char类型,按1个字节对齐
2 i是int类型,按四个字节对齐,所以在c和i之间实际上空了三个字节。
整个结构体一共是1 + 3(补齐)+ 4 = 8字节。
再看Test1
- struct Test1
- {
- int i ;
- double d ;
- char c ;
- };
i是int类型,按4字节对齐
d是double类型,按8字节对齐,所以i和d之间空了4字节
c是char类型,按1字节对齐。
所以整个结构体是 4(i) + 4(补齐)+ 8(d) + 1(c) = 17字节,注意!还没完,整个结构体还没有对齐,因为结构体中空间最大的类型是double,所以整个结构体按8字节对齐,那么最终结果就是17 + 7(补齐) = 24字节。
书写结构体的建议
我们对Test1做一点改动
struct Test1
{
char c ;
int i ;
double d ;
};
这时Test1的大小就变成了16,而不是24了,节省了8个字节!可见结构体中成员的书写顺序对结构体大小的影响还是很大的,一个好的建议是,按照数据类型由小到大的顺序进行书写。
综上我们知道,数据对齐的结果是结构体中最大成员的整数倍。各个成员都按最大成员所占字节数对齐。判断一个结构是否是对齐的可以让数据的地址模数据的大小结果为0则该结构是数据对齐的。
《Windows核心编程系列》十二谈谈Windows内存体系结构的更多相关文章
- 《windows核心编程系列》二谈谈ANSI和Unicode字符集 .
http://blog.csdn.net/ithzhang/article/details/7916732转载请注明出处!! 第二章:字符和字符串处理 使用vc编程时项目-->属性-->常 ...
- 《Windows核心编程系列》十一谈谈Windows线程池
Windows线程池 上一篇博文我们介绍了IO完成端口.得知IO完成端口可以非常智能的分派线程.但是IO完成端口仅对等待它的线程进行分派,创建和销毁线程的工作仍然需要我们自己来做. 我们自己也可以创建 ...
- 《windows核心编程系列》一谈谈windows中的错误处理机制
错误处理 我们写的函数会用返回值表示程序执行的正确与否,使用void,就意味着程序一定不会出错.Bool类型标识true时为真,false时为假.其他类型根据需要可以定义成不同意义. Windows除 ...
- 《Windows核心编程系列》二十谈谈DLL高级技术
本篇文章将介绍DLL显式链接的过程和模块基地址重定位及模块绑定的技术. 第一种将DLL映射到进程地址空间的方式是直接在源代码中引用DLL中所包含的函数或是变量,DLL在程序运行后由加载程序隐式的载入, ...
- 《windows核心编程系列》二十一谈谈基址重定位和模块绑定
每个DLL和可执行文件都有一个首选基地址.它表示该模块被映射到进程地址空间时最佳的内存地址.在构建可执行文件时,默认情况下链接器会将它的首选基地址设为0x400000.对于DLL来说,链接器会将它的首 ...
- 《windows核心编程系列》十七谈谈dll
DLL全称dynamic linking library.即动态链接库.广泛应用与windows及其他系统中.因此对dll的深刻了解,对计算机软件开发专业人员来说非常重要. windows中所有API ...
- 《Windows核心编程系列》十三谈谈在应用程序中使用虚拟内存
在应用程序中使用虚拟内存 Windows提供了以下三种机制对内存进行操控: 一:虚拟内存.最适合来管理大型对象数据或大型结构数组. 二:内存映射文件.最适合用来管理大型数据流,以及在同一机 器上运行的 ...
- 《windows核心编程系列》二十二谈谈修改导入段拦截API。
一个模块的导入段包含一组DLL.为了让模块能够运行,这些DLL是必须的.导入段还包含一个符号表.它列出了该模块从各DLL中导入的符号.当模块调用这些导入符号的时候,系统实际上会调用转换函数,获得导入函 ...
- 《windows核心编程系列》七谈谈用户模式下的线程同步
用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...
- 《windows核心编程系列 》六谈谈线程调度、优先级和关联性
线程调度.优先级和关联性 每个线程都有一个CONTEXT结构,保存在线程内核对象中.大约每隔20ms windows就会查看所有当前存在的线程内核对象.并在可调度的线程内核对象中选择一个,将其保存在C ...
随机推荐
- zookeeper原理浅析(二)
参考:https://www.cnblogs.com/leocook/p/zk_1.html 代码:https://github.com/littlecarzz/zookeeper 1. 数据模型 1 ...
- Eclipse-Java代码规范和质量检查插件-Checkstyle
CheckStyle是SourceForge下的一个项目,提供了一个帮助JAVA开发人员遵守某些编码规范的工具.它能够自动化代码规范检查过程,从而使得开发人员从这项重要但枯燥的任务中解脱出来.它可以根 ...
- datasnap中间件如何控制长连接的客户端连接?
ActiveConnections: TClientDataSet; ... 有客户端连接上来的时候 procedure TForm8.DSServer1Connect(DSConnectEventO ...
- IntelliJ 中类似于Eclipse ctrl+o的是ctrl+F12
IntelliJ 中类似于Eclipse ctrl+o的是ctrl+F12 学习了:https://blog.csdn.net/sjzylc/article/details/47979815
- Linux集群的总结和思考
前言:在涉及到对外项目,经手许多小中型网站的架构,F5.LVS及Nginx都比较多,我想一种比较通俗易懂的语气跟大家说明下何谓负载均衡,何谓Linux集群,帮助大家走出这个误区,真正意义上来理解它们. ...
- Windows和linux双系统——改动默认启动顺序
电脑上装了Windows 7和Ubantu双系统,因为Linux系统用的次数比較少而且还是默认的启动项对此非常不能容忍,因此得改动Windows为默认的启动项. 因为电脑上的系统引导程序是GRUB,因 ...
- redux-saga 异步流
前言 React的作用View层次的前端框架,自然少不了很多中间件(Redux Middleware)做数据处理, 而redux-saga就是其中之一,目前这个中间件在网上的资料还是比较少,估计应用的 ...
- 在EasyUI的DataGrid中嵌入Combobox
在做项目时,须要在EasyUI的DataGrid中嵌入Combobox,花了好几天功夫,在大家的帮助下,最终看到了它的庐山真面: 核心代码例如以下: <html> <head> ...
- fedora下安装xdot和objgraph
前提:安装好了python 1.先下载xdot-0.6.tar.gz和objgraph-1.8.0-py27-none-any.whl,你也可以在官网上下载其他版本. 2.下载完后,解压. 3.打开终 ...
- 十分简洁的手机浏览器 lydiabox
没有地址栏,没有工具栏.web app无需下载.无需安装.无需更新,加入即用:再也不用记住网址.更不用输入网址--一款这样极简极方便的浏览器,你想要吗? 我们做了一个十分简洁的手机浏览器,这个浏览器也 ...