Windows内存管理
来源:互联网 发布:博客app软件下载 编辑:程序博客网 时间:2024/06/08 10:24
因为看ogre的别人的日志偶然看到描述内存泄露的内容,发现自己也对于内存管理不是很清楚,遂写此文,防止以后遗忘。
内存管理——是指软件运行的时候对计算机内存资源进行分配和使用的技术,主要目的是如何高效、快速的分配,并且在适当时候释放和回收内存资源。
内存可以通过许多没接实现,例如磁带或者是硬盘,或者是小阵列容量的微芯片,从1950年代开始,计算机变得更复杂,甚至必须在一台机器同时执行多个进程。
两个内存:
物理内存——就是插在主板上的内存条,它是固定的,内存条的容量多大,物理内存就有多大(集成显卡系统除外),但是如果程序运行很多或者程序本身很大的话,就会导致大量的物理内存占用,甚至导致物理内存消耗殆尽。
虚拟内存——虚拟内存就是在硬盘上面划分一块页面文件,充当内存。当程序在运行的时候,有一部分资源还没有用上或者同时打开几个程序却只能操作其中一个程序时,系统没必要将程序所有的资源都塞在物理内存中,所以紫铜个暂时将这些不用的资源放在虚拟内存里面,等到需要的时候再调用。
三个地址:
物理地址(physicaladdress)——用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。内存并不是通过物理地址的方式来寻址的,(虽然可以直接把物理地址理解成插在机器上的内存本身,把内存看成一个从0直接一直到最大空间逐字节编号的大数组,然后把这个数组叫做物理地址),说它是“与地址总线相对应”还更贴切一些。
逻辑地址(logicaladdress)——是指由程序产生的与段相关的偏移地址部分。例如,你在进行c语言指针编程的过程中的&操作,取得的就是逻辑地址,它是相对于你当前进程数据段的地址,不和绝对物理地址相干。只有在Intel实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或者分页,cpu不进行自动地址转换),逻辑地址也就是在Intel保护模式下程序执行代码段限长内的偏移地址,(假定代码段、数据段如果完全一样)。应用程序员仅仅需要与逻辑地址打交道,而分段和分页是对程序员完全透明的,仅仅由系统编程人员涉及。应用程序员虽然自己可以直接操作内存,也只能在操作系统给你分配的那段内存来操作。
线性地址(linearaddress)/虚拟地址(virtual address)——与逻辑地址类似。它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址,那么线性地址就对应了硬件页式内存的转换前地址。
Ps:cpu工作的三个模式(从Intel开发80386以后的计算机都具有)
1)
8086/8088只能工作于实模式,80286及以上的微处理器可以工作于实模式、保护模式和虚拟8086模式。
实模式操作方式只允许微处理器寻址第一个1MB存储空间,存储器中第一个1MB存储单元成为实模式存储器或者常规内存,Dos操作系统要求微处理器工作于实模式,当80486微处理器工作于实地址模式时,存储器的管理方式与8086微处理器存储器的管理方式完全相同。
2)
保护模式通常是为了防止下列情况的发生:
a.
b.
c.
保护模式下的存储器寻址(80286及其以上的微处理器)允许访问第一个1MB及其以上的存储器内的数据和程序,寻址这个扩展的存储器段,需要更改用于实模式存储器寻址的段基址加上偏移地址的机制
在保护模式下,当寻址扩展内存里的数据和程序时,仍然使用偏移地址访问位于存储段内的信息。区别是,实模式下的段基址由段寄存器提供,而保护模式下的段寄存器里存放着一个选择符(selector),用于选择描述表内的一个描述符,描述符(descriptor)描述存储器段的位置、长度和访问权限。由于段基址加偏移地址仍然用于访问第一个1MB存储器内的数据,所以保护模式下的指令和实模式下的指令完全相同。
保护模式和实地址模式的不同之处在于存储地址空间的扩大(由1MB扩展到4GB),以及存储器管理机制的不同。
3)
在虚拟8086模式下,还可以用与实地址模式相同的形式应用段寄存器,而形成现行基地址。通过使用分页功能,就可以把虚拟8086模式下的1MB地址空间映像到80486微处理器的4GB物理空间中的任何位置。
三个内存的转换方式:
每个进程有4GB的虚拟地址空间,每个空间都做了如下划分:
a.
b.
c.
程序中都是使用4GB的虚拟地址,访问物理内存需要使用物理地址,物理地址是放在寻址总线上的地址,以字节(8位)为单位。
Cpu将一个虚拟内存空间中地址转换为物理地址,需要进行两步:
1. 将给定的一个逻辑地址(即段内偏移量)利用cpu的段式内存管理单元,转换成一个线性地址,
2. 然后利用其页式内存管理单元,转换为最后的物理地址。
页表\页目录概念
以下面的指令作为例子:
此时虚拟地址0将被发给MMU,MMU发现0属于页面0的范围内,如果页面0对应的页框号为1,那么物理地址就在4096-8191范围内,此时就会将4096发送到地址总线上。因为虚拟地址0的页内偏移也是0(页内偏移:在页面里的位置,比如1,页面偏移死1,4097的页面偏移也是1,这是因为一个页面大小为4K,用虚拟地址mod 4K就得到了页内偏移
上面的是虚拟地址到物理地址的映射的简单情况,但是在记录这些页面到页框的映射关系的时候(当然有些处理器是页框到页面的转化),在IA处理器上面使用的是页表,就是在物理内存里面有一块连续的空间,来记录这些页面到页框的映射关系,每一个页表项里面都有一部分去指向页框的起始地址,还有部分记录了这个页面的属性。可以通过页面号来做索引。页面号就是虚拟地址/4k得到的整数部分。如果只是单一的页表,也是有问题的,如果虚拟地址空间过大,那么页表所占的空间也会很大,这时候可以采用多级页表。IA32在采用4K页面的时候就使用了2级页表,IA64使用了四级。
使用了分页机制以后,4G的地址空间被分成了固定大小的页,每一页或者被映射到物理内存,或者被映射到硬盘上的交换文件中,或者没有映射任何东西,对于一般程序员来说,4G的地址空间,只有一小部分映射了物理内存,很大部分都是没有映射任何东西的。物理内存也被分页,来映射地址空间。对于32bit的win2k,页的大小是4k字节。CPU用来把虚拟地址转化成物理地址的信息存放在叫做页目录和页表的结构里。
物理内存分页,一个物理页的大小为4K字节,第0个物理页从物理地址0X00000000处开始,由于页的大小是4kb,就是0x1000字节,所以第一页从物理地址0X00001000处开始,第2页从物理地址0X00002000处开始。可以看到由于页的大小是4kb,所以需要32bit的地址中高20bit来寻址物理页。(4G/4k=2^20)
页目录:一个页目录大小为4K字节(2^20),放在一个物理页中.由1024个4字节的页目录组成(因为是win32),页目录中的每一项内容(每项4个字节)高20bit用来放一个页表(页表放在一个物理页中)的物理地址,低12bit放着一些标志。
页表:一个页表的大小为4K字节,放在一个物理页中,由1024个4字节的页表项组成,页表项的大小为4字节(32bit),所以页表中有1024个页表项,页表中每一项的内容(每项4个字节,32bit)高20bit用来放一个物理页的物理地址,低12bit放着一些标志。
对于x86系统,页目录的物理地址放在cpu的CR3寄存器中。
虚拟地址转换成物理地址
一个虚拟地址大小为4字节,其中包含找到物理地址的信息,虚拟地址分为3个部分:
1)31-22位(10位)是页目录中的索引;
2)21-12位(10位)是页表中的索引;
3)11-0位(12位)是页内偏移
转换过程:
首先通过CR3找到页目录所在的物理页——》根据虚拟地址中的31-22找到该页目录项——》通过该页目录项找到该虚拟地址对应的页表地址——》根据虚拟地址21-12找到物理页的物理地址——》根据虚拟地址的11-0位作为位偏移加上该物理页的地址就找到了该虚拟地址对应的物理地址。
CPU把虚拟地址转换成物理地址:一个虚拟地址,大小4个字节(32bit),包含着找到物理地址的信息,分为3个部分:第22位到第31位这10位(最高10位)是页目录中的索引,第12位到第21位这10位是页表中的索引,第0位到第11位这12位(低12位)是页内偏移。对于一个要转换成物理地址的虚拟地址,CPU首先根据CR3中的值,找到页目录所在的物理页。然后根据虚拟地址的第22位到第31位这10位(最高的10bit)的值作为索引,找到相应的页目录项(PDE,page director entry ),页表项中就有这个虚拟地址所对应物理页的物理地址。最后用虚拟地址的最低12位,也就是页内偏移,加上物理页的物理地址,就得到了该虚拟地址所对应的物理地址。
一个页目录有1024项,虚拟地址最高的10bit刚好可以索引1024项(2的10次方等于1024)。一个页表也有1024项,虚拟地址中间部分的10bit,刚好索引1024项。虚拟地址最低的10bit(2的12次方等于4096,作为页内偏移,刚好可以索引4kb,也就是一个物理页中的每个字节)。
每个进程都有自己的4G空间,从0X00000000~0XFFFFFFFF。通过每个进程自己的一套页目录和页表来实现。由于每个进程有自己的页目录和页表,所以每个进程的地址空间映射的物理内存是不一样的。两个进程的同一个虚拟地址处(如果都有物理内存映射)的值一班是不同的,因为ietamen往往对应不同的物理页。
4G地址空间中低2G,0X00000000-0X7FFFFFFF是用户地址空间,4G地址空间中高2G,即0X80000000-0XFFFFFFFF是系统地址空间。访问地址空间需要程序有ring()的权限。
Windows内存原理
Windows中,我们接触的一般都是线性地址,而其并非真实存在的,而真正的物理地址是利用一段N长的数组来定位的,这样能够完成保护模式。
虚拟内存:
虚拟内存的管理方式(通过一堆数据结构来实现内存管理)
在EPROCESS中有个数据结构如下:
内存空间:
typedef struct _MADDRESS_SPACE
{
}MADDRESS_SPACE , *PMADDRESS_SPACE ;
段结构节点(程序段的内存分配):
typedef struct _MEMORY_AREA
{
}MEMORY_AREA , *PMEMORY_AREA ;
这个节点内主要记录了已分配的虚拟内存空间,如果要申请虚拟内存空间就可以来此处创建一个节点,如果要删除空间同样只需要把对应节点删除,但是其中还要设计平衡二叉树的操作。我们在分配虚拟内存空间的时候系统就会找到这棵树,然后通过一定的算法便利此树,找到符合条件的内存间隙(未被分配的内存空间),创建个节点挂到这棵树上,返回起始地址就完成了。
物理内存:
物理内存的管理是基于一个数组的,win32下分页是4kb一页,假设我们物理内存有4GB,那么windows会将这4GB空间分页,分成4GB/4KB=1M页,那么每页(就是4KB)的物理空间都由一个叫PHYSICAL_PAGE的数据结构管理,这个数据结构可以看做是一个数组,这个数组有1M个元素,整好覆盖了4GB的物理地址。
物理内存的管理
在内核中有3个队列,这些队列内的元素就是PHYSICAL_PAGE结构:
A.
B.
C.
系统管理流程:
1)
2)
3)
映射
我们将转换过程分成下面几步看
1)
2)
3)
CPU段式内存管理,逻辑地址如何转换为线性地址
一个逻辑地址由两部分组成,段标识符:段内偏移量,。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号,后面3位包含一些硬件细节:
最后两位涉及权限检查
索引号——直接可以理解成数组下标,它是“段描述符”的索引,段描述符具体地址描述了一个段,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就代表了一个段的描述,每一个段描述符由8个字节组成:
上面的描述符虽然很复杂,但是我们需要利用一个数据结构来定义它,不过,在这里只需要关心Base字段,它描述了一个段开始位置的线性地址。
Intel设计的本意是,一些全局的段描述符,就放在“全局段描述表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述表(LDT)”中,在段选择符的T1字段中=0,表示用GDT,=1,表示用LDT。
GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。如下可看出:
首先,给定一个完整的逻辑地址[段选择符:段偏移地址]
1.
2.
3.
CPU页式内存管理
CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。从管理和效率的角度出发,线性地址被分为以固定长度为单位的组,成为页(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB位一个页来划分,这个页,作为一个线性地址就被划分为一个total_page[2^20]的大数组,共有2的20个次方个页,这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址一一对应的页的地址。
另一类“页“,我们称之为物理页,或者是页框,是分页段元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。
这里注意到,total_page数组有2^20个成员,每个成员是一个地址(32位机,一个地址也就是4字节),那么要单单表示这么一个数组,就要占去4MB的内存空间,为了节省空间,引入了一个二级管理模式的机器来组织分页单元:
如上图:
1.
2.
3.
1)从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
2)根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
3)根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
4)将页的起始地址与线性地址中最后12位相加,得到最终我们想要的内存块
页置换算法(主要是查看是否缺页或者调度)
Win32通过一个两层的表结构来实现地址映射,因为每个进程都拥有私有的4G的虚拟内存空间,相应的,每个进程都有自己的层次表结构来实现其地址映射。
如下图所示(注下图中的页目录项和页表项的大小应该是4个字节,而不是4kB):
具体的例子:
假设一个线程正在访问一个指针(Win32的指针指的就是虚拟地址)指向的数据,此指针指为0x2A8E317F,下图表示了这一个过程:
0x2A8E317F的二进制写法为0010101010_0011100011_000101111111,为了方便我们把它分为三个部分。
首先按照0010101010寻址,找到页目录项。因为一个页目录项为4KB,那么先将0010101010左移两位,001010101000(0x2A8),用此下标找到页目录项,然后根据此页目录项定位到下一层的某个页表。
然后按照0011100011寻址,在上一步找到页表中寻找页表项。寻址方法与上述方法类似。找到页表项后,就可以找到对应的物理内存页。
最后按照000101111111寻址,寻找页内偏移。
存储方式(分段/分页)
分段机制是必须的,分页机制是可选的,当不使用分页的时候线性地址将直接映射为物理地址,设立分页机制的目的主要是为了实现虚拟存储(分页机制在后面介绍)。先来介绍一下分段机制,以下文字是介绍如何由逻辑地址转换为线性地址。
分段机制在保护模式中是不能被绕过得,回到我们的seg:offset地址结构,在保护模式中seg有个新名字叫做“段选择子”(seg..selector)。段选择子、GDT、LDT构成了保护模式的存储结构,GDT、LDT分别叫做全局描述符表和局部描述符表,描述符表是一个线性表(数组),表中存放的是描述符。
“描述符”是保护模式中的一个新概念,它是一个8字节的数据结构,它的作用主要是描述一个段(还有其他作用以后再说),用描述表中记录的段基址加上逻辑地址(sel:offset)的offset转换成线性地址。描述符主要包括三部分:段基址(Base)、段限制(Limit)、段属性(Attr)。一个任务会涉及多个段,每个段需要一个描述符来描述,为了便于组织管理,80386及以后处理器把描述符组织成表,即描述符表。在保护模式中存在三种描述符表“全局描述符表”(GDT)、“局部描述符表”(LDT)和中断描述符表(IDT)(IDT在以后讨论)。
(1)全局描述符表GDT(GlobalDescriptor Table)在整个系统中,全局描述符表GDT只有一张,GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里,Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此积存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。GDTR中存放的是GDT在内存中的基地址和其表长界限。
(2)段选择子(Selector)由GDTR访问全局描述符表是通过“段选择子”(实模式下的段寄存器)来完成的,如图三①步。段选择子是一个16位的寄存器(同实模式下的段寄存器相同)
段选择子包括三部分:描述符索引(index)、TI、请求特权级(RPL)。他的index(描述符索引)部分表示所需要的段的描述符在描述符表的位置,由这个位置再根据在GDTR中存储的描述符表基址就可以找到相应的描述符(如图三①步)。然后用描述符表中的段基址加上逻辑地址(SEL:OFFSET)的OFFSET就可以转换成线性地址(如图三②步),段选择子中的TI值只有一位0或1,0代表选择子是在GDT选择,1代表选择子是在LDT选择。请求特权级(RPL)则代表选择子的特权级,共有4个特权级(0级、1级、2级、3级)。例如给出逻辑地址:21h:12345678h转换为线性地址
a. 选择子SEL=21h=0000000000100 0 01b 他代表的意思是:选择子的index=4即100b选择GDT中的第4个描述符;TI=0代表选择子是在GDT选择;左后的01b代表特权级RPL=1
b. OFFSET=12345678h若此时GDT第四个描述符中描述的段基址(Base)为11111111h,则线性地址=11111111h+12345678h=23456789h
(3)局部描述符表LDT(Local DescriptorTable)局部描述符表可以有若干张,每个任务可以有一张。我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。如图五
LDT和GDT从本质上说是相同的,只是LDT嵌套在GDT之中。LDTR记录局部描述符表的起始位置,与GDTR不同LDTR的内容是一个段选择子。由于LDT本身同样是一段内存,也是一个段,所以它也有个描述符描述它,这个描述符就存储在GDT中,对应这个表述符也会有一个选择子,LDTR装载的就是这样一个选择子。LDTR可以在程序中随时改变,通过使用lldt指令。如图五,如果装载的是Selector 2则LDTR指向的是表LDT2。举个例子:如果我们想在表LDT2中选择第三个描述符所描述的段的地址12345678h。
1. 首先需要装载LDTR使它指向LDT2 使用指令lldt将Select2装载到LDTR
2. 通过逻辑地址(SEL:OFFSET)访问时SEL的index=3代表选择第三个描述符;TI=1代表选择子是在LDT选择,此时LDTR指向的是LDT2,所以是在LDT2中选择,此时的SEL值为1Ch(二进制为11 100b)。OFFSET=12345678h。逻辑地址为1C:12345678h
3. 由SEL选择出描述符,由描述符中的基址(Base)加上OFFSET可得到线性地址,例如基址是11111111h,则线性地址=11111111h+12345678h=23456789h
4. 此时若再想访问LDT1中的第三个描述符,只要使用lldt指令将选择子Selector 1装入再执行2、3两步就可以了(因为此时LDTR又指向了LDT1)
由于每个进程都有自己的一套程序段、数据段、堆栈段,有了局部描述符表则可以将每个进程的程序段、数据段、堆栈段封装在一起,只要改变LDTR就可以实现对不同进程的段进行访问。
存储方式是保护模式的基础,学习他主要注意与实模式下的存储模式的对比,总的思想就是首先通过段选择子在描述符表中找到相应段的描述符,根据描述符中的段基址首先确定段的位置,再通过OFFSET加上段基址计算出线性地址。
段页存储管理
产生原因
分页式:等分内存,但是需要仔细设计页面的大小,灵活性很低
分段式:支持了用户内存观点,反映了程序结构,但是粒度一般很大,碎片问题很明显
段页式存储管理优点:
对于用户来讲
对于系统来讲,按页划分每一段
逻辑地址
管理
段表——记录了每一段的页表开始地址和页表长度;
页表——记录了逻辑页号与内存块号的对应关系,每一段有一个,一个程序可能有多个页表
空块管理:同页式管理
分配:同页式管理
硬件支持
1)
2)
3)
- 内存管理 - Windows内存管理
- Windows CE内存管理
- Windows CE 内存管理
- Windows内存管理
- Windows内存管理
- Windows CE 内存管理
- Windows内存管理
- Windows 内存管理 [转]
- Windows 内存管理
- Windows CE内存管理
- Windows CE 内存管理
- Windows内存管理
- Windows CE 内存管理
- Windows CE 内存管理
- Windows CE 内存管理
- Windows内存管理
- Windows 内存管理
- Windows CE 内存管理
- C++ STL set和multiset的使用
- Collada DOM 的使用--CreateSimple…
- cocos使用UI控件需要包含的头文件
- Ogre基本信息回顾
- Ogre监听类回顾
- Windows内存管理
- Ogre日志系统回顾
- Ogre异常处理回顾
- Ogre场景结构体系中的重要函数
- Ogre的材质
- hdu 1671-Phone List(字典树)
- Ogre粒子系统 以及 粒子脚本
- UML类图理解【转】
- Maven的依赖