精华内容
下载资源
问答
  • Windows内存管理

    千次阅读 2014-04-05 23:46:50
    Windows内存管理 在驱动程序编写中,分配和管理内存不能使用熟知的Win32API函数,取而代之的是DDK提供的高效的内核函数。程序员必须小心地使用这些内存相关的内核函数。因为在内核模式下,操作系统不会检查内存使用...

    Windows内存管理

    在驱动程序编写中,分配和管理内存不能使用熟知的Win32API函数,取而代之的是DDK提供的高效的内核函数。程序员必须小心地使用这些内存相关的内核函数。因为在内核模式下,操作系统不会检查内存使用的合法性稍有不慎就可能导致操作系统的崩溃。另外,C语言和C++中大多数关于内存操作的运行时函数,大多在内核模式下是无法使用的。

    1、内存管理概念

    编写windows驱动之前,需要读者进一步理解windows操作系统是如何管理和使用内存的。

    1.1、物理内存的概念(Physical Memory Address

    PC上有三条总线,分别是数据总线,地址总线和控制总线。32位的CPU的寻址能力为4GB2^32)个字节。用户最多可以使用4GB的真实的物理内存。PC中会拥有很多设备,其中很多设备都提供了自己的设备内存。一个设备可以有好几块设备内存映射到物理内存上。

    1.2、虚拟内存地址概念(Virtual Memory Address

    虽然可以寻址4GB的内存,而在PC里往往没有如此多的真实物理内存。操作系统和硬件为使用者提供了虚拟内存的概念。Windows的所有程序包括RIng0层和Ring3层的程序可以操作的都是虚拟内存。之所以称为虚拟内存,是因为对它的所有操作,最终会变成一系列对真实物理内存的操作。

    虚拟内存转换为物理内存:在CPU中有一个重要的寄存器CR0,它是32位的寄存器,其中的一位PG位是复制告诉系统是否分页的。Windows在启动前会将它的PG位置1,即WIndows允许分页。DDK中有个宏PAGE_SIZE记录着分页大小,一般为4KB4GB的虚拟内存会被分割成1M4GB/4KB)个分页单元。

    其中,有一部分单元会和物理内存对应起来,即虚拟内存中第N个分页单元对应着物理内存第M个分页单元。这种对应不是一一对应,而是多对一的映射,多个虚拟内存页可以映射同一个物理内存页。还有一部分单元会被映射成磁盘上的文件,并标记为脏的。读取这段虚拟内存的时候,系统会发出一个异常,此时会触发异常的处理函数,异常处理函数会将这个页的磁盘文件读入内存,并标记设置为不脏。当让经常不读写的内存页,可以交换成文件,并将此页设置为脏。还有一部分单元什么也没有对应即空的。

    大部分的虚拟内存是没有被映射到物理内存上的。

     

    这样的设计基于以下两个原因:

    第一是虚拟的增加了内存的大小。不管PC是否有足够的4GB的物理内存,操作系统总会有4GB的虚拟内存。这就允许使用者申请更多的内存,当物理内存不够时,可以通过将不常用的虚拟内存页交换成文件,等需要的时候再去读取。

    第二是使不同进程的虚拟内存互不干扰,为了让系统可以同时运行不同的进程,windows操作系统让每个进程看到的虚拟内存都不同。这个方法就使得不同的进程会有不同的物理内存到虚拟内存的映射。

    1.3、用户模式地址和内核模式地址

    虚拟地址在0~0X7FFFFFFF范围内的虚拟内存,即低2GB的虚拟内存地址,被称为用户模式地址。而0X80000000~0XFFFFFFF范围内的虚拟内存,即高2GB的虚拟内存,被称为内核模式地址。WIndows规定运行在用户态Ring3层的程序只能访问用户模式地址,而运行在核心态Ring0层的程序,可以访问整个4GB的虚拟内存,即用户模式地址和内核模式地址。Windows的核心代码和Windows的驱动程序加载的位置都是在高2GB的内核地址里,所以一般的应用程序是不能访问到这新核心代码和重要数据的,这大大提高了系统的稳健性。同时,Windows操作系统在进程切换时,保存内核模式地址是完全相同的。也就是说,所有进程的内核地址映射完全一致,进程切换的时候,只改变用户模式地址的映射。

    1.4Windows驱动程序和进程的关系

    驱动程序可以看成是一个特殊的DLL文件被应用程序加载到虚拟内存中,只不过加载地址是内核模式地址,而不是用户模式地址。他能访问的只是这个进程的虚拟内存,而不能是其他进程的虚拟内存。需要指出的是,Windows驱动程序里的不同例程运行在不同的进程中。DriverEntry例程和AddDevice例程是运行在系统(System)进程中的。System进程是Windows中非常重要的进程,也是Windows第一个运行的进程。当需要加载的时候,System进程中会有一个线程将驱动程序加载到内核模式地址空间内,并调用DriverEntry例程。

    其他一些例程,例如IRP_MJ_READIRP_MJ_WRITE的派遣函数会运行与应用程序的“上下文”中。所谓运行在进程的“上下文”,指的是运行于某个进程的环境中,所能访问的虚拟地址是这个进程的虚拟地址。

    VOID DisplayItsProcessName()

    {

    //得到当前进程

    PEPROCESS pEProcess = PsGetCurrentProcess();

    //得到当前进程名称

    PTSTR ProcessName = (PTSTR)((ULONG)pEProcess+0x174);

    KdPrint(("%s\n",ProcessName));

    }

    1.5、分页与非分页内存

    前面介绍了虚拟内存页与物理内存页之间的关系,Windows规定有些虚拟内存页面是可以交换到文件中的,这类内存被称为分页内存。而有些虚拟内存页永远不会交换到文件中,这些内存被称为非分页内存。

    当程序的中断请求级在DISPATCH_LEVEL之上(包括DISPATCH_LEVEL)时,程序只能使用非分页内存,否则将导致蓝屏死机。

    如果将某个函数载入到分页内存中,我们需要在函数的实现中加入以下代码:

    #pragma PAGEDCODE

    VOID SomeFunction()

    {

    PAGED_CODE();

    //do something

    }

    PAGED_CODE()DDK提供的宏,他只在check版本中生效。它会检验这个函数是否运行低于DISOATCH_LEVEL的中断请求级,如果等于或者高于这个中断请求级,将产生一个断言。

    如果让函数加载到非分页内存中,需要在内存的实现中加入以下代码:

    #pragma LOCKEDCODE

    VOID SomeFunction()

    {

    //do something

    }

    还有一种特殊情况,就是某个例程需要在初始化的时候载入内存,然后就可以从内存中卸载掉。这种情况只出现在DriverEntry情况下。尤其是NT式的驱动,DriverEntry会很长,占据很大的空间,为了节省内存,需要及时地从内存中卸载掉。代码如下:

    #pragma INITCODE

    Extern C NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,IN PUNICODE_STRING RegistryPath)

    {

    //do something

    }

     

    1.6、分配内核内存

    Windows驱动程序使用的内存资源非常珍贵,分配内存时要尽量节约。和应用程序一样,局部变量是存放在栈空间中的。但栈空间不会像应用程序那么大,所以驱动程序不适合递归或者局部变量是大型结构体。如果需要大型结构体,请在堆中申请。

    堆中申请内存的函数有以下几个:

    PVOID 
      ExAllocatePool(
        IN POOL_TYPE  PoolType,
        IN SIZE_T  NumberOfBytes
        );

     

    PVOID 
      ExAllocatePoolWithTag(
        IN POOL_TYPE  PoolType,
        IN SIZE_T  NumberOfBytes,
        IN ULONG  Tag
        );

     

    PVOID 
      ExAllocatePoolWithQuota(
        IN POOL_TYPE  PoolType,
        IN SIZE_T  NumberOfBytes
        );

     

    PVOID 
      ExAllocatePoolWithQuotaTag(
        IN POOL_TYPE  PoolType,
        IN SIZE_T  NumberOfBytes,
        IN ULONG  Tag
        );

     

    PoolType是个枚举变量,如果此值为NonPagedPool,则分配非分页内存,如果此值为PagedPool,则分配内存为分页内存。

    NumberOfByte是分配内存的大小,最好是4的倍数

    返回值分配的内存地址,一定是内核模式地址,如果返回0表示分配失败

    以上四个函数功能类似,函数以WithQuota结尾的代表分配的时候按配额分配。函数以WithTag结尾的函数和ExAllocatePool功能类似,唯一不同的是多了一个Tag参数,系统在要求的内存外有额外地分配了4个字节的标签,在调试的时候,可以找出是否有标有这个标签的内存没有被释放。

    将分配的内存,进行回收的函数是ExFreePoolExFreePoolWithTag

    VOID 
      ExFreePool(
        IN PVOID  P
        );

     

    NTKERNELAPI
    VOID
      ExFreePoolWithTag(
        IN PVOID  P,
        IN ULONG  Tag 
        ); 

     

    2、在驱动中使用链表

    在驱动程序开发中,经常使用链表这种数据结构,DDK为用户提供两种链表的数据结构,简化了对链表的操作。

    链表中可以记录将整型,浮点,字符型或者程序员自定义的数据结构。链表通过指针将这些数据结构组成一条“链”,链中每个袁术对应着记录的数据。对于单向链表,每个元素中有一个Next指针指向下一个元素。对于双向链表,每个元素有两个指针,指向前驱元素BLINK和后继元素FLINK。本节以双向链表为例:

    2.1、链表结构

     

    2.2、链表初始化

    初次使用时需要初始化,主要将链表头的FLinkBLink两个指针都指向自己。这意味着链表头所代表的链是空链。使用InitializeListHead宏实现初始化。

    2.3、从首部插入链表

    对链表的插入有两种方式,一种是在链表的头部插入,一种是在链表的尾部插入。在头部插入链表使用语句InsertHeadList,用法:

    InsertHeadList(&head,&mydata->ListEntry);

    HeadLIST_ENTRY结构的链表头,mydata是用户定义的数据结构,而他的子域ListEntry是包含其中的LIST_ENTRY数据结构。

    2.4、从尾部插入链表

    在尾部插入链表使用语句InsertTailList(&head,&mydata->ListEntry);

    2.5、从链表删除

    a)、从链表头删除RemoveHeadList

    b)、从链表尾删除RemoveTailList

    //链表的操作

    VOID LinkListTest()

    {

    LIST_ENTRY linkListHead;

    //初始化链表

    InitializeListHead(&linkListHead);

    PMYDATASTRUCT pData;

    ULONG i=0;

    KdPrint(("Begin insert to link list"));

    //在链表中插入10个元素

    for(i=0;i<10;i++)

    {

    //分配分页内存

    pData = (PMYDATASTRUCT)ExAllocatePool(PagedPool,sizeof(MYDADASTRUCT));

    pData->number = i;

    //从头部插入链表

    InsertHeadList(&linkListHead,&pData->ListEntry);

    }

    //从链表中取出,并显示

    KdPrint(("Begin remove from link\n"));

    while(!IsListEmpty(&linkListHead))

    {

    //从尾部删除一个元素

    PLIST_ENTRY pEntry = RemoveTailList(&linkListHead);

    pData = CONTAAINING_RECORD(pEntry,MYDATASTRUCT,ListEntry);

    KdPrint(("%d\n",pData->number));

    ExFreePool(pData);

    }

    }

     

    3、Lookaside结构

    频繁申请和回收内存,会导致在内存上产生大量的内存“空洞”,从而导致最终无法申请内存。DDK为程序员提供了Lookaside结构来解决这个问题。

    3.1、频发申请内存的弊端

    频繁地申请内存,会导致一个问题,就是在内存中产生“空洞”,即内存碎片。如果系统中存在大量的内存碎片,即使内存中有大量的可用内存,也会导致申请内存失败。在操作系统空闲时,系统会整理内存碎片,将其合并。

    3.2、使用Lookaside

    如果驱动程序需要频繁地从内存中申请、回收固定大小的内存,DDK提供了一种机制来解决,即使用Lookaside对象。

    可以将Lookaside对象想象成一个内存容器。在初始的时候,它先向Windows申请了一块比较大的内存。以后程序员每次申请内存的时候,不是直接向Windows申请 内存,而是向Lookaside申请内存。Lookaside对象会智能地避免产生内存碎片。如果有Lookaside对象内部的内存不够用时,他会向操作系统申请更多的内存。当Lookaside对象内部有大量的未使用的内存时,他会自动让Windows回收一部分内存,总之,Lookaside是一个自动的内存分配容器。通过对Lookaside对象申请内存,效率要高于直接向windows申请内存。Lookaside一般会在以下情况使用:

    a)、程序员每次申请固定大小的内存。

    b)、申请和回收的操作十分频繁。

    初始化Lookaside对象:

    VOID 
      ExInitializeNPagedLookasideList(
        IN PNPAGED_LOOKASIDE_LIST  Lookaside,
        IN PALLOCATE_FUNCTION  Allocate  OPTIONAL,
        IN PFREE_FUNCTION  Free  OPTIONAL,
        IN ULONG  Flags,
        IN SIZE_T  Size,
        IN ULONG  Tag,
        IN USHORT  Depth
        );

     

    VOID 
      ExInitializePagedLookasideList(
        IN PPAGED_LOOKASIDE_LIST  Lookaside,
        IN PALLOCATE_FUNCTION  Allocate  OPTIONAL,
        IN PFREE_FUNCTION  Free  OPTIONAL,
        IN ULONG  Flags,
        IN SIZE_T  Size,
        IN ULONG  Tag,
        IN USHORT  Depth
        );

    这两个函数分别是对非分页和分页Lookaside对象进行初始化。初始化完成后,可以进行申请内存操作:

    PVOID 
      ExAllocateFromNPagedLookasideList(
        IN PNPAGED_LOOKASIDE_LIST  Lookaside
        );

     

    PVOID 
      ExAllocateFromPagedLookasideList(
        IN PPAGED_LOOKASIDE_LIST  Lookaside
        );

    这两个函数分别是对非分页和分页内存的申请

    VOID 
      ExFreeToNPagedLookasideList(
        IN PNPAGED_LOOKASIDE_LIST  Lookaside,
        IN PVOID  Entry
        );

     

    VOID 
      ExFreeToPagedLookasideList(
        IN PPAGED_LOOKASIDE_LIST  Lookaside,
        IN PVOID  Entry
        );

    这两个函数分别是对非分页内存和分页内存的回收。

    VOID 
      ExDeleteNPagedLookasideList(
        IN PNPAGED_LOOKASIDE_LIST  Lookaside
        );

     

    VOID 
      ExDeletePagedLookasideList(
        IN PPAGED_LOOKASIDE_LIST  Lookaside
        );

    以上两个函数分别是对非分页和分页Lookaside对象的删除

     

    4、运行时函数

    一般编译器厂商,在发布其编译器的同时,会将运行时函数一起发布给用户。运行时函数是程序运行的时候必不可少的,它有编译器提供。针对不同的操作系统,运行时函数的实现方法不同,但接口基本保持一致。例如:malloc函数就是典型的运行时函数,所有编译器厂商都必须提供这个函数,它在不同操作系统上的实现方法就不尽相同。

    4.1、内存间复制(非重叠)

    在驱动程序开发中,经常用到内存的复制。例如,将需要的内容,从缓冲区复制到显卡内存中。DDK为程序提供了以下函数:

    VOID 
      RtlCopyMemory(
        IN VOID UNALIGNED  *Destination,//表示要复制内存的目的地址
        IN CONST VOID UNALIGNED  *Source,//表示要复制内存的源地址
        IN SIZE_T  Length//表示要复制内存的长度,单位为字节
        );

     

    4.2、内存间复制(可重叠)

    RtlCopyMemory可以复制内存,但其内部没有考虑内存重叠的情况。RtlCopyMemory函数的内部实现方式是依靠memcpy函数实现的。不能保证重叠部分是否被复制。

    为了保证重叠部分也被正确复制,C99规定memmove函数完成这个任务。这个函数对两个内存是否重叠进行了判断,这个判断却牺牲了速度。DDK用宏对memmove进行了封装,名称变为RtlMoveMemory

    VOID 
      RtlMoveMemory(
        IN VOID UNALIGNED  *Destination,
        IN CONST VOID UNALIGNED  *Source,
        IN SIZE_T  Length
        );

     

    4.3、填充内存

    驱动程序开发中,还经常用到对某段内存区域用固定字节填充。DDK为程序员提供了函数RtlFillMemory

    VOID 
      RtlFillMemory(
        IN VOID UNALIGNED  *Destination,
        IN SIZE_T  Length,
        IN UCHAR  Fill
        );

     

    在驱动程序开发中,还经常要对某段内存填零,DDK提供了宏RtlZeroMemoryRtlZeroByte

    VOID 
      RtlZeroMemory(
        IN VOID UNALIGNED  *Destination,
        IN SIZE_T  Length
        );

    VOID 
      RtlZeroBytes( 
        PVOID  Destination, 
        SIZE_T  Length 
        );

    4.4、内存比较

    驱动程序开发中,还会用到比较两块内存是否一致。该函数是RtlCompareMemory

    SIZE_T 
      RtlCompareMemory(
        IN CONST VOID  *Source1,
        IN CONST VOID  *Source2,
        IN SIZE_T  Length
        );

     

    4.5、关于运行时函数使用的注意事项

    DDK提供的标准的运行时函数名都是RtlXX形式。其中,大部分是以宏的形式给出。

    5、使用C++特性分配内存

    C++语言中分配内存时,可以使用new操作符,回收内存时使用delete操作符。但是在驱动程序开发中,使用newdelete操作符,将得到错误的链接指示。

    6、其他

    6.1、数据类型

    C语言的数据类型和DDK中对应的数据类型

     

    DDK中又添加了一种64位的无符号长整型整数,范围0~2^64-1,用LONGLONG类型表示,后面加上i64

    使用如下:

    LONGLONG val = 100i64;

    这种64位整数支持加减乘除等运算。

    还有一种64位的表示方法LARGE_INTEGER数据结构。

    定义如下:

    Typedef union _LARGE_INTEGER{

    Struct{

    ULONG LowPart;

    ULONG HighPart

    };

    Struct{

    ULONG LowPart;

    ULONG HighPart

    }u;

    LONGLONG QuadPart

     

    }

    LARGE_INTEGER是个联合体,这种设计非常巧妙。联合体中的三个元素可以认为是LARGE_INTEGER的三个定义。可以认为是由两个部分组分。一个是低32位的整数LowPart,一个是高32位的整数HighPart

    LARGE_INTEGER LargeValue;

    LargeValue.LowPart=100;

    LargeValue.HighPart = 0;

    6.2、返回状态值

    DDK大部分函数返回类型是NTSTATUS类型,查看DDK.h文件,可以看到

    typedef LONG NTSTATUS;

    NTSTATUS的定义和LONG等价。为了函数的形式统一,所有的函数的返回值都是NTSTATUS类型。

     

     

    6.3、检查内存可用性

    在驱动程序开发中,对内存的操作要格外小心。如果某段内存是只读的,而驱动程序试图去写操作,会导致系统的崩溃。同样,当某段内存不可读的情况下,驱动程序试图去读,同样会导致系统的崩溃。

    DDK提供了两个函数帮助程序员在不知道某段内存是否可读写的情况下试探内存可读写性。ProbeForReadProbeForWrite

    VOID 
      ProbeForRead(
        IN CONST VOID  *Address,
        IN SIZE_T  Length,
        IN ULONG  Alignment
        );

     

    VOID 
      ProbeForWrite(
        IN CONST VOID  *Address,
        IN SIZE_T  Length,
        IN ULONG  Alignment
        );

    Address:需要被检查的内存的地址

    Length:需要被检查的内存的长度,单位是字节

    Alignment:描述该段内存是多少字节对齐的

     

    6.4、结构化异常处理(try-except块)

    结构化异常处理是微软编译器提供的独特处理机制,这种处理方式能在一定程度上出现错误的情况下,免于程序崩溃。为了说明结构化异常,有两个概念需要说明一下:

    a)、异常:异常的概念类似于中断的概念,当程序中某种错误触发一个异常,操作系统会寻找处理这个异常的处理函数。如果程序提供错误处理函数,则进入错误处理函数,如果没有提供,则有操作系统的默认错误处理函数处理。在内核模式下,操作系统默认处理错误的办法往往很简单,直接让系统蓝屏,并在蓝屏上简单描述出错信息,之后系统就进入死机状态。所以一般程序员需要自己设置异常处理函数。

    b)、回卷:程序执行到某个地方出现异常错误时,系统会寻找出错点是否处于一个try{}块中,并进入try块提供才异常处理程序代码。如果当前try块没有提供异常处理,则会向更外一层的try块,寻找异常处理代码,直到最外层try{}块也没有提供异常处理程序代码,则交个操作系统处理。

    这种向更外一层寻找异常处理的机制,被成为回卷。一般处理异常,是通过try-except块来处理的。

    6.5、结构化异常处理(try-finally块)

    结构化异常处理的另外一种使用方法就是利用try-finally块,强迫函数在退出前执行一段代码。

    6.6、使用宏需要注意的地方

    宏一般由多行组成,用“\”代表换行。

    6.7、断言

    在驱动程序开发中,还有一个技巧,就是使用“断言”,在驱动程序使用“断言”,一般是通过使用ASSERT宏。

    ASSERT(表达式);

    如果表达式返回FALSE,表示断言失败,会引发一个异常。

    7、小结

    本章围绕着驱动程序中的内存操作进行了介绍,在驱动程序开发中,首先要注意分页内存和非分页内存的使用。同时,还需要区分物理内存地址和虚拟内存地址这两个概念。

    在驱动程序开发中,还会经常使用单向链表和双向链表等数据结构,本章对这些数据结构的使用进行了介绍。另外,在驱动程序开发中,内存复制,内存搬移,内存填充和应用程序有所区别,要使用DDK提供专用的内核函数,而不能使用C语言提供的运行时函数。

     

    以上内容参考自张帆 史彩成等编著的《Windows 驱动开发技术详解》第五章

    展开全文
  • windows底层内存管理技术

    千次阅读 2009-10-16 09:27:00
    1.1. 物理地址在物理存储器上的内存地址,一般由内核管理,应用程序无法直接得到。1.2. 虚拟地址在进程私有空间中的地址,即应用程序指针所指向的地址值。1.3. 寻址空间进程所能够...2. windows内存结构2.1. 虚拟地址空

    1.1. 物理地址

    在物理存储器上的内存地址,一般由内核管理,应用程序无法直接得到。

    1.2. 虚拟地址

    在进程私有空间中的地址,即应用程序指针所指向的地址值。

    1.3. 寻址空间

    进程所能够范围的地址空间范围,跟指针的位数有关,指针的位数取决于cpu字长,32位指针的地址空间范围为4GB,64位指针的地址空间范围为1 6 E B。

    2. windows内存结构

    2.1. 虚拟地址空间的管理

    对于32位多任务的windows操作系统来说,每个进程都在自己的私有地址空间(虚拟地址空间)运行,因此当进程中的一个线程正在运行时,该线程可以访问只属于它的进程的内存。属于所有其他进程的内存则隐藏着,并且不能被正在运行的线程访问。

    在win2k中,属于内核的内存也是隐藏的,正在运行的线程无法访问。这意味着线程不能直接访问内核的数据。如果要想访问内核数据,则必须通过系统调用(系统win32 api)来操作,否则会引发一个内存错误异常。

    Win98中,属于操作系统的内存是不隐藏的,正在运行的线程可以访问。因此,正在运行的线程常常可以访问操作系统的数据,也可以破坏操作系统(从而有可能导致操作系统崩溃)。

    在Win98中,一个进程的线程不可能访问属于另一个进程的内存,与win2k相同。

    建议无论在win98还是win2k中,都采用系统调用来访问内核(操作系统内存)。

    2.2. 虚拟地址空间的划分

    虽然32位的应用程序理论上可以访问4GB的地址空间,但是真正可以使用的地址空间并没有那么多。

    每个进程的虚拟地址空间都要划分成各个分区。地址空间的分区是根据操作系统的基本实现方法来进行的。不同的Windows内核,其分区也略有不同。winxp的内存结构与win2k相同。

    进程的地址空间分区表

    分区

    32位Windows 2000(x86和Alpha处理器)

    32位Windows 2000(x86w/3GB用户方式)

    64位Windows 2000(Alpha和IA-64处理器)

    Windows 98

    N U L L指针分配的分区

    0 x 0 0 0 0 0 0 0 0 0 x 0 0 0 0 F F F F

    0 x 0 0 0 0 0 0 0 0 0 x 0 0 0 0 F F F F

    0x00000000 00000000 0x00000000 0000FFFF

    0 x 0 0 0 0 0 0 0 0 0 x 0 0 0 0 0 F F F

    DOS/16位Windows应用程序兼容分区

    0 x 0 0 0 0 0 1 0 0 0 0 x 0 0 3 F F F F F

    用户方式

    0 x 0 0 0 1 0 0 0 0 0 x 7 F F E F F F F

    0 x 0 0 0 1 0 0 0 0 0 x B F F E F F F F F

    0x00000000 00010000 0x000003FF FFFEFFFF

    0 x 0 0 4 0 0 0 0 0 0 x 7 F F F F F F F

    64-KB

    0 x 7 F F F 0 0 0 0

    0 x B F F F 0 0 0 0

    0 x 0 0 0 0 0 3 F F F F F F 0 0 0 0

    禁止进入

    0 x 7 F F F F F F F

    0 x B F F F F F F F

    0 x 0 0 0 0 0 3 F F F F F F F F F F

    共享的MMF分区

    0 x 8 0 0 0 0 0 0 0

    文件(MMF)内核方式

    0 x 8 0 0 0 0 0 0 0 0 0 x F F F F F F F F

    0 x C 0 0 0 0 0 0 0 0 x F F F F F F F F

    0x00000400 00000000 0xFFFFFFFFF FFFFFFF

    0 x B F F F F F F F 0 x C 0 0 0 0 0 0 0 0 x F F F F F F F F

    3 2位Windows 2000的内核与6 4位Windows 2000的内核拥有大体相同的分区,差别在于分区的大小和位置有所不同。另一方面,可以看到Windows 98下的分区有着很大的不同。

    NULL指针分配的分区:为了帮助程序员掌握N U L L指针的分配情况。如果你的进程中的线程试图读取该分区的地址空间的数据,或者将数据写入该分区的地址空间,那么C P U就会引发一个访问违规。保护这个分区是极其有用的,它可以帮助你发现N U L L指针的分配情况。一般的c/c++编译器都把NULL设置为0,落在这个分区中。

    MS-DOS/16Windows应用程序兼容分区(仅适用Win98):进程地址空间的这个4MB分区是Windows 98需要的,目的是维护MS - DOS应用程序与16位应用程序之间的兼容性。不应该试图从32位应用程序来读取该分区的数据,或者将数据写入该分区。在理想的情况下,如果进程中的线程访问该内存, CPU应该产生一个访问违规,但是由于技术上的原因, Microsoft无法保护这个4MB的地址空间。

    在Windows 2000中,16位MS-DOS与16位Windows应用程序是在它们自己的地址空间(其实是在虚拟机中)中运行的,32位应用程序不会对它们产生任何影响。

    16位DOS程序的虚拟机就是cmd,16位windows程序使用的是系统虚拟机。

    用户方式分区:这个分区是进程的私有(非共享)地址空间所在的地方。

    在Windows 2000中,所有的. e x e和DLL模块均加载这个分区。每个进程可以将这些D L L加载到该分区的不同地址中(不过这种可能性很小)。系统还可以在这个分区中映射该进程可以访问的所有内存映射文件。

    在Windows 98中,主要的Win32系统DLL(Kernel32.dll,AdvAPI32.dll,User32.dll和GDI32.dll)均加载共享内存映射文件分区中。. e x e和所有其他D L L模块则加载到这个用户方式分区中。多个进程的共享D L L均位于相同的虚拟地址中,但是其他DLL可以将这些D L L加载到用户方式分区的不同地址中(不过这种可能性不大)。另外,在Windows 98中,用户方式分区中决不会出现内存映射文件。

    在32位windows中,用户分区的最大寻址空间大约为2G,内核寻址空间为3G。M i crosof t允许x 8 6的Windows 2000 Advanced Server版本和Windows 2000 Data Center版本将用户方式分区扩大为3 G B,内核分区压缩为1G。若要使所有进程都能够使用3 G B用户方式分区和1 G B内核方式分区,必须将/ 3 G B开关附加到系统的BOOT. INI文件的有关项目中。

    在x86w/3GB和64位的windows中,若要使用2GB以上的用户空间,该应用程序必须使用/ LARGEADDRESSAWARE 链接开关来创建。

    64KB禁止进入的分区(适用于win2k):这个位于用户方式分区上面的64 KB分区是禁止进入的,访问该分区中的内存的任何企图均将导致访问违规。

    共享的MMF分区(适用于win98):存放系统DLL、进程共享数据和内存映射文件。

    内核方式分区:存放内核代码。用于线程调度、内存管理、文件系统支持、网络支持和所有设备驱动程序的代码全部在这个分区加载。驻留在这个分区中的一切均可被所有进程共享。

    在Windows 2000中,这些组件是完全受到保护的。如果你试图访问该分区中的内存地址,你的线程将会产生访问违规,导致系统向用户显示一个消息框,并关闭你的应用程序。

    在Windows 98中该分区中的数据是不受保护的。任何应用程序都可以从该分区读取数据,也可以写入数据,因此有可能破坏操作系统。

    2.3. 地址空间的区域

    当进程被创建并被赋予它的地址空间时,该可用地址空间的主体是空闲的,即未分配的。若要使用该地址空间的各个部分,必须通过调用VirtualAlloc函数来分配它里边的各个区域。对一个地址空间的区域进行分配的操作称为保留( reserving )。

    每当你保留地址空间的一个区域时,系统要确保该区域从一个分配粒度的边界开始。对于不同的CPU平台来说,分配粒度是各不相同的。几乎所有的CPU平台(x86、32位Alpha、64位Alpha和IA-64)都使用64 KB这个相同的分配粒度。

    当你保留地址空间的一个区域时,系统还要确保该区域的大小是系统的页面大小的倍数。页面是系统在管理内存时使用的一个内存单位。与分配粒度一样,不同的C P U,其页面大小也是不同的。x86使用的页面大小是4 KB,而A l p h a使用的页面大小则是8 KB。IA-64也使用8KB的页面。但是,如果测试显示使用更大的页面能够提高系统的总体性能,那么Microsoft可以切换到更大的页面(16KB或更大)。

    系统有时会直接代表进程保留一些区域,比如用来存放进程环境块PEB和线程环境块TEB。

    由于内核会做区域和页面管理,所以它给应用程序保留的区域边界可能不是64k边界。

    如果保留区域大小不是页面大小的整数倍,则会圆整到比它大的最近的页面倍数。比如,在x86平台上页面大小为4K,申请保留10k内存时,系统会保留12K内存给你。

    不再使用保留区域时,应该调用VirtualFree来释放。

    保留区域并不真正分配物理内存,只是占用进程的地址空间而已。

    如果要分配物理页面,必须通过调用VirtualAlloc函数来提交保留区域。

    2.4. 物理内存与页文件

    Windows虚拟内存是映射到磁盘上的页文件。页文件对应用程序透明。页面调度算法在内核中实现。

    虚拟内存的管理需要cpu和内核配合,cpu会判断内存页面是否在RAM中,否则会引发一个缺页中断通知操作系统内核,内核再进行页面调度,根据某种算法淘汰、调入和调出页面。

    clip_image002

    操作系统启动一个.exe文件时,把.exe文件本身作为一个页文件处理(内存映射文件),这样就大大减少了系统页文件的大小。

    把系统页文件分散到不同的磁盘分区中,这样可以提高读写效率。

    注意软盘上的应用程序是一次性映射到物理内存的,因为安装程序时经常需要更换软盘。

    2.5. 数据对齐

    数据对齐主要和cpu和编译器有关,跟操作系统关系不大。

    当CPU访问正确对齐的数据时,它的运行效率最高。当数据大小的数据模数的内存地址是0时,数据是对齐的。例如, W O R D值应该总是从被2除尽的地址开始,而D W O R D值应该总是从被4除尽的地址开始,如此等等。当C P U试图读取的数据值没有正确对齐时, CPU可以执行两种操作之一。即它可以产生一个异常条件,也可以执行多次对齐的内存访问,以便读取完整的未对齐数据值。

    数据对齐更深入的说明,请查看另一篇文档《深入研究字节对齐问题》。

    2.6. 内存管理的几种方法

    windows提供了3种进行内存管理的方法,它们是:

    • 虚拟内存,以页面为单位进行内存,最适合用来管理大型对象或结构数组。

    • 内存映射文件,最适合用来管理大型数据流(通常来自文件)以及在单个计算机上运行的多个进程之间共享数据。

    • 内存堆栈,最适合用来管理大量的小对象。

    malloc、new、allocator等内存管理是在应用程序的标准库中处理的,不属于操作系统内存管理的范围,故本文不做探讨,在其他文档中再做论述。

    3. 进程堆栈

    3.1. 简介

    堆栈可以用来分配许多较小的数据块。

    堆栈的优点是,可以不考虑分配粒度和页面边界之类的问题,集中精力处理手头的任务。堆栈的缺点是,分配和释放内存块的速度比其他机制要慢,并且无法直接控制物理存储器的提交和回收。

    从内部来讲,堆栈是保留的地址空间的一个区域。开始时,保留区域中的大多数页面没有被提交物理存储器。当从堆栈中进行越来越多的内存分配时,堆栈管理器将把更多的物理存储器提交给堆栈。物理存储器总是从系统的页文件中分配的,当释放堆栈中的内存块时,堆栈管理器将收回这些物理存储器。

    Microsoft并没有以文档的形式来规定堆栈释放和收回存储器时应该遵循的具体规则,Windows 98 与Windows 2000的规则是不同的。可以这样说,Windows 98 更加注重内存的使用,因此只要可能,它就收回堆栈。Windows 2000更加注重速度,因此它往往较长时间占用物理存储器,只有在一段时间后页面不再使用时,才将它返回给页文件。Microsoft常常进行适应性测试并运行各种不同的条件,以确定在大部分时间内最适合的规则。随着使用这些规则的应用程序和硬件的变更,这些规则也会有所变化。如果了解这些规则对你的应用程序非常关键,那么请不要使用堆栈。相反,可以使用虚拟内存函数(即VirtualAlloc和VirtualFree),这样,就能够控制这些规则。

    3.2. 默认堆栈

    当进程初始化时,系统在进程的地址空间中创建一个堆栈。该堆栈称为进程的默认堆栈。按照默认设置,该堆栈的地址空间区域的大小是1 MB。但是,系统可以扩大进程的默认堆栈,使它大于其默认值。当创建应用程序时,可以使用/ H E A P链接开关,改变堆栈的1 M B默认区域大小。/ H E A P链接开关的句法如下:/HEAP:reserve[,commit]

    单个进程可以同时拥有若干个堆栈。这些堆栈可以在进程的寿命期中创建和撤消。但是,默认堆栈是在进程开始执行之前创建的,并且在进程终止运行时自动被撤消。不能撤消进程的默认堆栈。

    可以通过调用GetProcessHeap函数获取你的进程默认堆栈的句柄。

    3.3. 辅助堆栈

    由于某种原因需要创建辅助堆栈:

    保护组件。

    更加有效地进行内存管理。

    更快的访问效率。

    减少线程同步的开销。

    迅速释放。

    3.3.1. 保护组件

    把不同组件放到不同的堆栈中,可以防止当一个组件的堆栈出错时影响另外一个组件。假设有两个组件,一个处理链表数据,一个处理二叉树数据,把它们放到不同的辅助堆栈中,当链表内的指针错误操作导致堆栈出错不会影响到二叉树的正确处理。

    3.3.2. 更有效的内存管理

    通过在堆栈中分配同样大小的对象,就可以更加有效地管理堆栈,这样可以避免内存碎片。

    如果每个堆栈只包含大小相同的对象,那么释放一个对象后,另一个对象就可以恰好放入被释放的对象空间中。

    3.3.3. 更快的访问效率

    如果把相同类型的数据连续放在同一个堆中,这样就可以大大减少cpu访问不同页面的次数,也可能大大减少访问虚拟内存页面的次数,因此会获得更佳的内存访问效率。

    3.3.4. 减少线程的开销

    多个线程访问进程的默认堆栈是串行操作的,要经常不停的同步互斥操作。如果某个线程的数据不需要与其他线程进行共享,则没有必要和其他线程竞争默认堆栈的访问权。此时创建线程自己的堆栈,可以减少不必要的加锁、解锁开销。

    3.3.5. 迅速释放

    将专用堆栈用于某些数据结构后,就可以释放整个堆栈,而不必显式释放堆栈中的每个内存块。比如把某个树的数据结构放到一个独立的堆栈中,释放这个树的数据结果就不用一个个节点的慢慢释放,直接撤销堆即可。如果这个树的数据比较大的话,效果会比较明显。

    3.4. 堆栈函数

    创建堆栈使用HeapCreate,从堆栈中分配内存HeapAlloc,改变堆栈内存大小HeapReAlloc,查询堆栈内存块大小HeapSize,释放堆栈内存块HeapFree,撤销堆栈HeapDestroy。

    HeapAlloc函数执行的操作:

    1) 遍历分配的和释放的内存块的链接表。

    2) 寻找一个空闲内存块的地址。

    3) 通过将空闲内存块标记为“已分配”并分配内存块。

    4) 将新内存块添加给内存块链接表。

    注意当你分配较大的内存块(大约1 MB或者更大)时,最好使用VirtualAlloc函数,应该避免使用堆栈函数。

    以上堆栈函数适用于win98和win2k。

    C++中的new/delete要调用malloc/free,而malloc/free最终要调用上面的堆栈函数。

    ToolHelp的各个函数可以用来枚举进程的各个堆栈和这些堆栈中分配的内存块。函数如下:Heap32First、Heap32Next、Heap32ListFirst和Heap32ListNext,适用于win98和win2k。

    以下堆栈函数只适用于win2k:GetProcessHeaps(获取进程多个堆栈的句柄)、HeapValidate(验证堆栈完整性)、HeapCompact(合并空闲地址块)、HeapLock/HeapUnlock(线程对堆栈加锁/解锁,如果在创建堆栈时未设置HEAP_NO_SERIALIZE,则在HeapAlloc和HeapFree时内部加锁)、HeapWalk(遍历堆栈,此时最好加锁,防止有其他线程分配或释放内存)。

    4. 线程堆栈

    4.1. windows 2000线程堆栈

    每当创建一个线程时,系统就会为线程的堆栈(每个线程有它自己的堆栈)保留一个堆栈空间区域,并将一些物理存储器提交给这个已保留的区域。

    按照默认设置,系统保留1 MB的地址空间并提交两个页面的内存。但是,这些默认值是可以修改的,方法是在你链接应用程序时设定Microsoft的链接程序的/STACK选项:/STACK:reserve[,commit]。

    当创建一个线程的堆栈时,系统将会保留一个链接程序的/ STACK开关指明的地址空间区域。但是,当调用CreateThread或_beginthreadex函数时,可以重设原先提交的内存数量。这两个函数都有一个参数,可以用来重载原先提交给堆栈的地址空间的内存数量。如果设定这个参数为0,那么系统将使用/ S TACK开关指明的已提交的堆栈大小值,即1 MB的保留区域,每次提交一个页面的内存。

    下图显示了在页面大小为4KB的计算机上的一个堆栈区域的样子(保留的起始地址是0x08000000) 。该堆栈区域和提交给它的所有物理存储器均拥有页面保护属性PAGE_READWRITE。

    clip_image004

    当保留了这个区域后,系统将物理存储器提交给区域的顶部的两个页面。在允许线程启动运行之前,系统将线程的堆栈指针寄存器设置为指向堆栈区域的最高页面的结尾处(一个非常接近0x08100000的地址)。这个页面就是线程开始使用它的堆栈的位置。从顶部向下的第二个页面称为保护页面。当线程调用更多的函数来扩展它的调用树状结构时,线程将需要更多的堆栈空间。

    可以看出栈是向下增长的。

    每当线程试图访问保护页面中的存储器时,系统就会得到关于这个情况的通知。作为响应,系统将提交紧靠保护页面下面的另一个存储器页面。然后,系统从当前保护页面中删除保护页面的保护标志,并将它赋予新提交的存储器页面。这种方法使得堆栈存储器只有在线程需要时才会增加。最终,如果线程的调用树继续扩展,堆栈区域就会变成下图所示的样子。

    clip_image006

    假定线程的调用树非常深,堆栈指针C P U寄存器指向堆栈内存地址0 x 0 8 0 0 3 0 0 4。这时,当线程调用另一个函数时,系统必须提交更多的物理存储器。但是,当系统将物理存储器提交给0 x 0 8 0 0 1 0 0 0地址上的页面时,系统执行的操作与它给堆栈的其他内存区域提交物理存储器时的操作并不完全一样。

    最底下的页面总是被保留的,从来不会被提交。

    完整的线程堆栈区域

    clip_image008

    当系统将物理存储器提交给0x08001000地址上的页面时,它必须再执行一个操作,即它要引发一个EXCEPTION_STACK_OVERFLOW 异常处理(在Wi nNT.h 文件中定义为0 x C00000FD)。通过使用结构化异常处理(SEH),你的程序将能得到关于这个异常处理条件的通知,并且能够实现适度恢复。

    如果在出现堆栈溢出异常条件之后,线程继续使用该堆栈,那么在0 x080010 0 0地址上的页面中的全部内存均将被使用,同时,该线程将试图访问从0 x 0 8 0 0 0 0 0 0开始的页面中的内存。当该线程试图访问这个保留的(未提交的)内存时,系统就会引发一个访问违规异常条件。如果在线程试图访问该堆栈时引发了这个访问违规异常条件,线程就会陷入很大的麻烦之中。这时,系统就会接管控制权,并终止进程的运行—不仅终止线程的运行,而切终止整个进程的运行。

    最后一个页面始终被保留着。这样做的目的是为了防止不小心改写进程使用的其他数据。

    4.2. windows 98线程堆栈

    在win98上,线程的堆栈前后都有一个64K的保护区块,可以防止线程堆栈的上溢和下溢,这是win98的一个不错的特色。

    堆栈下溢的示例:

    int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE,

    PSTR pszCmdLine, int nCmdShow)

    {

    char szBuf[100];

    szBuf[10000] = 0; // Stack underflow,注意栈是向下增长的(与地址相反)

    return(0);

    }

    当该函数的赋值语句执行时,便尝试访问线程堆栈结尾处之外的内存。当然,编译器和链接程序不会抓住上面代码中的错误,但是,如果应用程序是在Windows 98下运行,那么当该语句执行时,就会引发访问违规。这是Windows 98的一个出色特性,而Windows 2000是没有的。在Wi ndows2000中,可以在紧跟线程堆栈的后面建立另一个区域。如果出现这种情况,并且你试图访问你的堆栈外面的内存,那么你将会破坏与进程的另一个部分相关的内存,而系统将不会发现这个情况。

    4.3. c/c++运行库线程堆栈检查

    C / C + +运行期库包含一个堆栈检查函数。当编译源代码时,编译器将在必要时自动生成对该函数的调用。堆栈检查函数的作用是确保页面被适当地提交给线程的堆栈。

    示例代码:

    void SomeFunction()

    {

    int nValues[4000];

    // Do some processing with the array.

    nValues[0] = 0; // Some assignment

    }

    该函数至少需要16 000个字节(4000 x sizeof(int),每个整数是4个字节)的堆栈空间,以便放置整数数组。通常情况下,编译器生成的用于分配该堆栈空间的代码只是将C P U的堆栈指针递减16 000个字节。但是,在程序试图访问内存地址之前,系统并不将物理存储器分配给堆栈区域的这个较低区域。

    在使用4 KB或8 KB页面的系统上,这个局限性可能导致一个问题出现。如果初次访问堆栈是在低于保护页面的一个地址上进行的(如上面这个代码中的赋值行所示),那么线程将访问已经保留的内存并且引发访问违规。为了确保能够成功地编写上面所示的函数,编译器将插入对C运行期库的堆栈检查函数的调用。

    当编译程序时,编译器知道你针对的C P U系统的页面大小。x 8 6编译器知道页面大小是4K B,A l p h a编译器知道页面大小是8 KB。当编译器遇到程序中的每个函数时,它能确定该函数需要的堆栈空间的数量。如果该函数需要的堆栈空间大于目标系统的页面大小,编译器将自动插入对堆栈检查函数的调用。

    下面这个伪代码显示了堆栈检查函数执行什么操作。之所以称它是伪代码,是因为这个函数通常是由编译器供应商用汇编语言来实现的:

    // The C run-time library knows the page size for the target system.

    #ifdef _M_ALPHA

    #define PAGESIZE (8 * 1024) //8-KB page

    #else

    #define PAGESIZE (4 * 1024) //4-KB page

    #endif

    void StackCheck(int nBytesNeededFromStack)

    {

    //Get the stack pointer position.

    //At this point, the stack pointer has NOT been decremented

    //to account for the function's local variables.

    PBYTE pbStackPtr = (CPU's stack pointer);

    while(nBytesNeededFromStack >= PAGESIZE)

    {

    // Move down a page on the stack--should be a guard page.

    pbStackPtr -= PAGESIZE;

    // Access a byte on the guard page--forces new page to be

    // committed and guard page to move down a page.

    pbStackPtr[0] = 0;

    // Reduce the number of bytes needed from the stack.

    nBytesNeededFromStack -= PAGESIZE;

    }

    //Before returning, the StackCheck function sets the CPU's

    //stack pointer to the address below the function's

    //local variables.

    }

    5. 虚拟内存管理

    注意:我们这里说的虚拟内存指的是进程私有地址空间,而不是页文件(也有把页文件称为操作系统的虚拟内存)。

    用于管理虚拟内存的函数可以用来直接保留一个地址空间区域,将物理存储器(来自页文件)提交给该区域,并且可以设置你自己的保护属性。

    5.1. 获取系统内存信息

    系统内存信息,比如页面的大小,分配粒度大小、最小内存地址、最大内存地址等,都可以通过GetSystemInfo来获取。

    函数原型:VOID GetSystemInfo(LPSYSTEM_INFO psinf);

    5.2. 获取全局内存状态

    可以通过GlobalMemoryStatus来获取全局内存状态,比如整体物理内存大小、整体页文件大小、进程虚拟内存大小、进程可用虚拟内存大小等。

    函数原型:VOID GlobalMemoryStatus(LPMEMORYSTATUS pmst);

    5.3. 查询内存块的有关信息

    可以通过VirtualQuery/ VirtualQueryEx查询内存块的有关信息,如基地址、块大小,存储器类型和保护属性等。

    5.4. 保留和提交虚拟内存

    通过VirtualAlloc可以保留或提交一块虚拟内存空间。

    保留的基地址被圆整为64K的整数倍,保留的大小为cpu页面大小的整数倍。如果内存长期被保留不释放,建议从最高地址往下分配,这样可以把内存碎片放在用户空间的末尾,此时需要在分配类型上设置或参数MEM_TOP_DOWN。

    当保留一个区域后,必须将物理存储器提交给该区域,然后才能访问该区域中包含的内存地址。

    系统从它的页文件中将已提交的物理存储器分配给一个区域。物理存储器总是按页面边界和页面大小的块来提交的。

    若要提交物理存储器,必须再次调用VirtualAlloc函数。

    提交物理存储器时可以只提交部分区域,每次提交的页面保护属性页可以不同。提交的大小(单位为字节)会被操作系统圆整为页面大小的整数倍。

    把分配类型设置为MEM_RESERVE | MEM_COMMIT就可以保留并提交一块虚拟内存空间。

    5.5. 何时提交和回收虚拟内存

    对于大块不确定内存操作,可以先保留一个足够大的内存区域,在需要时再提交物理内存,这样可以节省大量的物理内存。

    提交方式有以下4种:

    1、总是提交。每次都调用VirtualAlloc提交物理内存,让操作系统来判断是否已经提交,这样可能会导致大量的无效调用,因为该页面很可能已经提交过。

    2、提交前查询。先调用VirtualQuery查询一下该内存块是否被提交,然后决定是否调用VirtualAlloc。此方法只是减少了VirtualAlloc调用次数,效率可能比第一种还低。

    3、跟踪提交页面。把已经提交的页面都记录起来,每次需要新内存时先看已经提交的页面是否有足够内存可用,否则调用VirtualAlloc提交物理内存。此方法效率较高,但是代码可能比较复杂。

    4、使用结构化异常处理(SEH)。但进程试图写一个未提交的保留页面时,系统会触发一个内存违规异常,在内存违规异常处理函数中提交物理内存,然后系统返回到异常触发点处继续执行指令,就好像什么都没有发生。此方法代码清晰,效率很高,推荐使用。

    使用VirtualFree可以回收全部的保留页面(包括提交和未提交的),也可以只回收部分物理页面。

    物理页面回收的3种方法:

    1、 对象大小为页面的整数倍。删除对象时直接回收相应的页面。

    2、 把每个页面放置固定数目的对象。当页面中所有的对象都删除时,回收该页面。

    3、 低优先级定时回收。定时检查每个页面中的所有对象是否都释放,如果是则回收该页面,这种做法的好处是比较通用,而且可以在进程比较空闲时执行,缺点是代码相对复杂。

    5.6. 改变页面保护属性和复位内存页面

    可以通过VirtualProtect来改变内存保护属性。例如,你编写了一个用于管理链接表的代码,将它的节点存放在一个保留区域中。可以设计一些函数,以便处理该链接表,这样,它们就可以在每个函数开始运行时将已提交内存的保护属性改为PAGE_READWRITE ,然后在每个函数终止运行时将保护属性重新改为PAGE_NOACCESS。

    通过这样的设置,就能够使链接表数据不受隐藏在程序中的其他错误的影响。如果进程中的任何其他代码存在一个迷失指针,试图访问你的链接表数据,那么就会引发访问违规。当试图寻找应用程序中难以发现的错误时,利用保护属性是极其有用的。

    可以通过在VirtualAlloc中设置MEM_RESET可以复位内存页面,这些页面会被操作系统设置为未修改页面,这样在下次缺页中断时就可以直接把页面文件的页面加载到这些未修改页面上,而不需要保存它们到页文件中去。

    5.7. 如何使用4G以上内存

    1)在64位的cpu上安装64位windows可以直接支持4G以上内存的访问。

    2)32位操作系统4G内存以上支持情况:

        WindowsNT4.0 Server与Enterprise版都属于32位服务器操作系统,支持最大内存都只有4G。

    Windows2000系列服务器版操作系统可支持容量最高的是数据中心版,可支持32G;高级服务器版只支持最高8G的内存容量;2000普通服务器版只支持最高4G的内存容量。

    Windows2003 Enterprise支持最高32G的内存。

    在32位cpu上访问4G以上内存,这是通过X86的PAE(Intel Physical Address Extension)实现的。而windows实现起来的话相当与把内存分页,页表12位,物理地址24 位,组合在一起就是2的36次方,也就是64GB。

    PAE需要处理器为Intel Pentium Pro以上。

    在cpu和操作系统支持的情况下,应用程序可以通过AWE来使用4G以上内存。

    6. 内存映射文件

    6.1. 基本概念

    与虚拟内存一样,内存映射文件可以用来保留一个地址空间的区域,并将物理存储器提交给该区域。它们之间的差别是,物理存储器来自一个已经位于磁盘上的文件,而不是系统的页文件。一旦该文件被映射,就可以访问它,就像整个文件已经加载内存一样。

    6.2. 用途

    内存映射文件可以用于3个不同的目的:

    • 系统使用内存映射文件,以便加载和执行.exe和DLL文件。这可以大大节省页文件空间和应用程序启动运行所需的时间。

    • 可以使用内存映射文件来访问磁盘上的数据文件。这使你可以不必对文件执行I/O操作,并且可以不必对文件内容进行缓存。

    • 可以使用内存映射文件,使同一台计算机上运行的多个进程能够相互之间共享数据。Windows确实提供了其他一些方法,以便在进程之间进行数据通信,但是这些方法都是使用内存映射文件来实现的,这使得内存映射文件成为单个计算机上的多个进程互相进行通信的最有效的方法。

    6.3. 可执行程序和DLL的内存映射

    可执行文件内存映射过程:

    1) 系统找出在调用CreateProcess时设定的.exe文件。如果找不到这个.exe文件,进程将无法创建,CreateProcesss将返回FALSE。

    2) 系统创建一个新进程内核对象。

    3) 系统为这个新进程创建一个私有地址空间。

    4) 系统保留一个足够大的地址空间区域,用于存放该.exe文件。该区域需要的位置在. e x e文件本身中设定。按照默认设置, .exe文件的基地址是0x00400000(这个地址可能不同于在6 4位Windows 2000上运行的6 4位应用程序的地址),但是,可以在创建应用程序的. exe文件时重载这个地址,方法是在链接应用程序时使用链接程序的/BASE选项。

    5) 系统注意到支持已保留区域的物理存储器是在磁盘上的.exe文件中,而不是在系统的页文件中。

    当.exe文件被映射到进程的地址空间中之后,系统将访问.exe文件的一个部分,该部分列出了包含.exe文件中的代码要调用的函数的DLL文件。然后,系统为每个DLL文件调用LoadLibrary函数,如果任何一个DLL需要更多的DLL,那么系统将调用LoadLibrary函数,以便加载这些DLL。

    DLL内存映射过程:

    1) 系统保留一个足够大的地址空间区域,用于存放该D L L文件。该区域需要的位置在D L L文件本身中设定。按照默认设置, Microsoft的Visual C++ 建立的DLL文件基地址是0 x 10000000(这个地址可能不同于在64位Windows 2000上运行的64位DLL的地址)但是,你可以在创建DLL文件时重载这个地址,方法是使用链接程序的/BASE选项。Windows提供的所有标准系统DLL都拥有不同的基地址,这样,如果加载到单个地址空间,它们就不会重叠。

    2) 如果系统无法在该DLL的首选基地址上保留一个区域,其原因可能是该区域已经被另一个DLL或.exe占用,也可能是因为该区域不够大,此时系统将设法寻找另一个地址空间的区域来保留该DLL。如果一个DLL无法加载到它的首选基地址,这将是非常不利的,原因有二。首先,如果系统没有再定位信息,它就无法加载该DLL(可以在DLL创建时,使用链接程序的/FIXED开关,从DLL中删除再定位信息,这能够使DLL变得比较小,但是这也意味着该DLL必须加载到它的首选地址中,否则它就根本无法加载)。第二,系统必须在DLL中执行某些再定位操作。在Windows 98中,系统可以在页面被转入RAM时执行再定位操作。在Windows 2000中,这些再定位操作需要由系统的页文件提供更多的存储器,它们也增加了加载DLL所需要的时间量。

    3) 系统会记录当前DLL是映射到磁盘文件还是系统的页文件中。

    当所有的.exe和DLL文件都被映射到进程的地址空间之后,系统就可以开始执行.exe文件的启动代码。当.exe文件被映射后,系统将负责所有的分页、缓冲和高速缓存的处理。例如,如果.exe文件中的代码使它跳到一个尚未加载到内存的指令地址,那么就会出现一个异常(缺页中断)。系统能够捕捉这个异常,并且自动将这页代码从该文件的映像加载到一个RAM页面。然后,系统将这个RAM页面映射到进程的地址空间中的相应位置,并且让线程继续运行,就像这页代码已经加载了一样。当然,这一切对应用程序透明。

    所有的.exe和DLL映射文件的内容被分割为不同的节。代码放在一个节中,全局变量放在另一个节中。各个节按照页面边界来对齐。通过调用Get SystemInfo函数,应用程序可以确定正在使用的页面的大小。在. e x e或D L L文件中,代码节通常位于数据数据节的前面。

    多个进程的.exe或DLL映射文件采用写时拷贝的方法共享RAM和页文件,这样可以避免修改全局变量时对不同进程的影响。

    6.4. 可执行程序或DLL的不同示例共享静态数据

    每个.exe或DLL文件的映像都由许多节组成。按照规定,每个标准节的名字均以圆点开头。例如,当编译你的程序时,编译器会将所有代码放入一个名叫.text的节中。该编译器还将所有未经初始化的数据放入一个.bss节,而已经初始化的所有数据则放入.data节中。

    .exe或D L L文件分节的属性

    属性

    含义

    READ

    该节中的字节可以读取

    WRITE

    该节中的字节可以写入

    EXECUTE

    该节中的字节可以执行

    SHARED

    该节中的字节可以被多个实例共享(本属性能够有效地关闭copy -on-write机制)

    编译器产生的标准节

    节名

    作用

    .bss

    未经初始化的数据

    .CRT

    C运行期只读数据

    .data

    已经初始化的数据

    .debug

    调试信息

    .didata

    延迟输入文件名表

    .edata

    输出文件名表

    .idata

    输入文件名表

    .rdata

    运行期只读数据

    .reloc

    重定位表信息

    .rsrc

    资源

    .text

    .exe或DLL文件的代码

    .tls

    线程的本地存储器

    .xdata

    异常处理表

    要想在.exe或dll不同的实例间共享变量,必须满足以下3个条件:

    1、 创建分节。如:

    #pragma data_seg("Shared")

    LONG g_lInstanceCount = 0;

    #pragma data_seg()

    2、 变量必须初始化,否则该变量就被放到其他分节中,达不到共享的目的。

    如:#pragma data_seg("Shared")

    LONG g_lInstanceCount;

    #pragma data_seg()

    3、 必须把该分节设置为共享属性RWS。

    可以在连接开关中设置CTION:Shared,RWS。

    还可以在代码中设置:#pragma comment(linker, "/SECTION:Shared,RWS")。

    如:

    #pragma data_seg("Shared")

    volatile LONG g_lApplicationInstances = 0;

    #pragma data_seg()

    #pragma comment(linker, "/Section:Shared,RWS")

    虽然可以创建共享节,但是,由于两个原因, Microsoft并不鼓励你使用共享节。第一,用这种方法共享内存有可能破坏系统的安全。第二,共享变量意味着一个应用程序中的错误可能影响另一个应用程序的运行,因为它没有办法防止某个应用程序将数据随机写入一个数据块。

    6.5. 内存映射数据文件

    操作系统可以将一个数据文件映射到进程的地址空间中。这样,对大量的数据进行操作是非常方便的。

    这种方法的最大优点是,系统能够为你管理所有的文件缓存操作。不必分配任何内存,或者将文件数据加载到内存,也不必将数据重新写入该文件,或者释放任何内存块。但是,内存映射文件仍然可能出现因为电源故障之类的进程中断而造成数据被破坏的问题。

    若要使用内存映射文件,必须执行下列操作步骤:

    1)使用CreateFile创建或打开一个文件内核对象,该对象用于标识磁盘上你想用作内存映射文件的文件。

    2) 使用CreateFileMapping创建一个文件映射内核对象,告诉系统该文件的大小和你打算如何访问该文件。

    3) 通过MapViewOfFile让系统将文件映射对象的全部或一部分映射到你的进程地址空间中。

    当完成对内存映射文件的使用时,必须执行下面这些步骤将它清除:

    1) 使用UnmapViewOfFile告诉系统从你的进程的地址空间中撤消文件映射内核对象的映像。

    2) 使用CloseHandle关闭文件映射内核对象。

    3) 使用CloseHandle关闭文件内核对象。

    注意读写权限分配规则:CreateFile≥CreateFileMapping≥MapViewOfFile。这个可以理解,后者都是基于前者进行操作的,不能超越基础权限,另外给程序员带来了一定的灵活性。

    另外,如果CreateFileMapping或MapViewOfFile设置了写时拷贝属性,则往映射的页面中写数据时,内核会在系统的页文件中创建新页面并把原始页面数据拷贝过来,然后把新创建的页面地址映射到进程的虚拟空间,并把新页面的属性被设置为读写属性,之后对数据的任何修改都是在私有页面上进行,对映射的数据文件没有任何影响。

    注意:设置了写时拷贝属性的页面,在撤销内存文件映射时系统会回收物理页面,所有的修改都会丢失。

    可以使用FlushViewOfFile强制系统把修改过的部分或全部页面数据写入数据文件,因为系统有自己的页面管理策略,可能不会马上把缓存数据写入数据文件。

    Windows 98不支持写时拷贝属性。

    6.6. 内存映射处理大文件

    首先映射一个文件的开头的视图。当完成对文件的第一个视图的访问时,可以取消它的映像,然后映射一个从文件中的一个更深的位移开始的新视图。必须重复这一操作,直到访问了整个文件。这使得大型内存映射文件的处理不太方便,但是,幸好大多数文件都比较小,因此不会出现这个问题。

    6.7. 内存映射与数据视图的相关性

    相同内存映射对象的不同视图,如果它们有部分重叠,则重叠部分在进程的虚拟地址空间上有多个拷贝,但都对应于相同的物理页面,不浪费物理内存。这种情况对于同一进程或不同进程之间都是如此。

    但是如果是相同文件的不同内存映射对象,则重叠部分的物理页面可能会重复加载,可能造成物理内存浪费。为什么说是可能,因为你虽然有重叠,但是如果你不访问重叠部分的文件页面,就不会加载到RAM。

    如果CreateFile时没有阻止其他进程对这个文件的写访问,则有可能导致内存映射中RAM页面内容和原始文件不一致。

    可以使用MapViewOfFileEx把文件内容映射到特定地址,只要地址是64k的整数倍。

    关闭视图只是释放虚拟内存地址,并不释放物理页面,只有内存映射文件对象的引用计数为0时才释放物理页面。

    6.8. 使用内存映射文件在多个进程间共享数据

    尽管windows有多种进程间通信机制,如:RPC、COM、OLE、DDE、窗口消息(尤其是WM_COPYDATA)、剪贴板、邮箱、管道和套接字等。但是,在同一个主机上还是内存映射文件的效率最高。

    内存映射可以使用普通磁盘文件,也可以使用系统页文件。如果只是共享和交换数据,使用磁盘文件很不方便,此时推荐使用系统页文件。

    使用系统页文件就不用创建文件了,只需要像通常那样调用CreateFileMapping函数,并且传递INVALID_HANDLE_VALUE作为hFile参数即可,其他用法与磁盘文件相同。

    与所有内核对象一样,可以使用3种方法与多个进程共享内存映射文件对象,这3种方法是句柄继承性、句柄命名和句柄复制。

    采用句柄对象命名的方法可读性较好,推荐使用。在一个进程中调用CreateFileMapping创建内存映射文件对象,在另一个进程中使用OpenFileMapping打开内存映射文件对象,然后建立视图,分别映射文件的相同区块到自己的进程空间中就可以实现数据共享。

    展开全文
  • 作为开发Windows驱动程序的程序员,需要比普通程序员更多了解Windows内部的内存管理机制,并在驱动程序中有效地使用内存。在驱动程序的编写中,分配和管理内存不能使用熟知的Win32 API函数,取而代之的是DDK提供的...
      
    

     作为开发Windows驱动程序的程序员,需要比普通程序员更多了解Windows内部的内存管理机制,并在驱动程序中有效地使用内存。在驱动程序的编写中,分配和管理内存不能使用熟知的Win32 API函数,取而代之的是DDK提供的高效内核函数。C语言和C++中大多数关于内存操作的运行时函数,大多在内核模式下是无法使用的。例如,C语言中的malloc函数和C++中的new操作符等。

     

    内存管理的概念:

     

    1.物理内存:

    PC上有三条总线,分别是数据总线,地址总线和控制总线。32位的CPU寻址能力是4GB。用户最多可以使用4GB的真实物理内存。PC中会拥有许多设备,其中很多设备都拥有自己的设备内存,这部分的设备内存会映射到PC机的物理内存上,读写这段物理地址其实会读写设备内存地址。

     

    2虚拟内存:

    .虽然可以寻址4GB的内存,而PC里往往没有如此多的真实物理内存。操作系统和硬件为使用者提供了虚拟内存的概念。虚拟内存和物理内存之间的转换暂不讨论。

     

    3.用户模式地址和内核模式地址:

    虚拟地址在0~0X7FFFFFFF范围内的虚拟内存,即低2GB的虚拟地址,被称为用户模式地址。而0X80000000~0XFFFFFFFF范围内的虚拟内存,即高2GB的虚拟内存,被称为内核模式地址。

     

    4.Windows驱动程序和进程的关系:

    驱动程序可以看成一个特殊的DLL文件被应用程序加载到虚拟内存中,只不过加载地址是内核模式地址,而不是用户模式地址。

     

    5.分页和非分页内存:

    Windows规定有些虚拟内存页面是可以交换到文件中的,这类内存成为分页内存。而有些虚拟内存页永远不会交换到文件中,这些内存被称为非分页内存。

    当程序的中断请求级在DISPATCH_LEVEL之上时,程序只能使用非分页内存,否则会导致蓝屏死机。

    在编译DDK提供的例程时,可以指定某个例程和某个全局变量是载入分页内存还是非分页内存,需要做如下定义:

    #define PAGEDCODE code_seg("PAGE")
    #define LOCKEDCODE code_seg()
    #define INITCODE code_seg("INIT")
    
    #define PAGEDDATA data_seg("PAGE")
    #define LOCKEDDATA data_seg()
    #define INITDATA data_seg("INIT")


    如果将某个函数在入到分页内存,我们需要在函数的实现中加入如下代码:

    #pragma INITCODE
    VOID SomeFunction()
    {
          PAGED_CODE();
          //do something
    }

    如果要让程序加载到非分页内存,需要在函数的实现中加入如下代码:

    #pragma LOCKEDCODE
    VOID SomeFunction()
    {
          //do something
    }

    还有一种特殊情况,就是某个例程初始化的时候载入内存,然后就可以从内存中卸载掉,例如DriverEntry

    #pragma INITCODE
    extern "C" NTSTATUS DriverEntry(
                               IN PDRIVER_OBJECT pDriverObject,
    			IN PUNICODE_STRING pRegistryPath
    						     )
    {
    
    }


     

    6.内存的分配:

    Windows驱动程序使用的内存资源非常珍贵,局部变量是在栈(stack)空间中,但是驱动程序的栈空间不会像应用程序那么大,所以不适合进行递归调用或者局部变量是大型的结构体,否则请在堆(Heap)中申请。

    堆中申请内存的函数有以下几个:

    PVOID 
    ExAllocatePool(
    		IN POOL_TYPE PoolType, 
    	    IN SIZE_T NumberOfBytes
    		);
    
    PVOID 
    ExAllocatePoolWithTag(
    		IN POOL_TYPE PoolType, 
    		IN SIZE_T NumberOfBytes, 
    		IN ULONG Tag 
    		);
    
    PVOID 
    ExAllocatePoolWithQuota( 
    		IN POOL_TYPE PoolType,
    		IN SIZE_T NumberOfBytes 
    		);
    
    PVOID 
    ExAllocatePoolWithQuotaTag(
    		IN POOL_TYPE PoolType,
    		IN SIZE_T NumberOfBytes,
    		IN ULONG Tag 
    		);


    其中有些重要的参数:

    PoolType: 是个枚举变量,如果此值为NonPagedPool,则分配非分页内存。如果次值为PagedPool,则分配内存分页内存。

    NumberOfBytes:是分配内存的大小,注意最好是4的倍数。

    以上四个函数功能类似,函数以WithQuota结尾的代表分配的时候按额分配。以WithTag结尾的函数和ExAllocatePool功能类似,唯一不同的是多了一个Tag参数,

    系统要求的内存外又额外地多分配4个字节的标签。在调试的时候,可以找出是否有标有这个标签的内存没有被释放。

     

    将分配的内存,进行回收的函数原型如下:

    VOID
    ExFreePool( 
    	IN PVOID P
    		 );
    
    NTKERNELAPI
    VOID
    ExFreePoolWithTag(
    	IN PVOID P,
    	IN ULONG Tag
      );


    参数P就是要释放的内存。

     

    在内存中使用链表:

    链表中可以记录整形,浮点型,字符型或者程序员自定义的数据结构。对于单链表,元素中有一个Next指针指向下一个元素。对于双链表,每个元素有两个指针:BLINK指向前一个元素,FLINK指向下一个元素。

     

    1.链表的结构:

    DDK提供了标准的双向链表。双向链表可以将链表形成一个环。以下是DDK提供的双向链表的数据结构:

    typedef struct _LIST_ENTRY {
       struct _LIST_ENTRY *Flink;
       struct _LIST_ENTRY *Blink;
    } LIST_ENTRY, *PLIST_ENTRY


    这个结构体只有指针没有数据。

     

    2.链表的初始化:

    每个双向链表都是以链表头作为链表的第一个元素。初次使用链表头需要进行初始化,主要将链表头的Flink和Blink两个指针都指向自己。初始化链表头用InitiallizeListHead宏实现。判断链表头是否为空,DDK提供了一个宏简化这种检查,这就是IsListEmpty。

    IsListEmpty(&head);


    程序员需要自己定义链表中每个元素的数据类型,并将LIST_ENTRY结构作为自定义结构的一个子域。例如:

    typedef struct _MYDATASTRUCT
    {
    	LIST_ENTRY ListEntry;
    	ULONG x;
    	ULONG y;
    }MYDATASTRUCT,*PMYDATASTRCUT;


    3.插入链表:

    对链表的插入有两种方式,一种是在链表的头部插入,一种是在链表的尾部插入。

    在头部插入链表使用语句InsertHeadList,用法如下:

    InsertHeadList(&head,&mydata->ListEntry);


    在尾部插入链表使用语句InsertTailList,用法如下:

    InsertTailList(&head,&mydata->ListEntry);

     

    4.链表的删除:

    和插入链表一样,删除链表也有两种方法。一种从头部删除,一种从尾部删除。分别对应RemoveHeadList和RemoveTailList函数。其使用方法如下:

    PLIST_ENTRY = RemoveHeadList(&head);
    
    PLIST_ENTRY = RemoveTailList(&head);


     

    下面代码完整演示向链表进行插入,删除等操作,其主要代码如下:

    typedef struct _MYDATASTRUCT
    {
    	ULONG number;
    	LIST_ENTRY ListEntry;
    }MYDATASTRUCT,*PMYDATASTRCUT;
    
    #pragma INITCODE
    VOID LinkListTest()
    {
    	LIST_ENTRY linkListHead;
    	//初始化链表
    	InitializeListHead(&linkListHead);
        
    	PMYDATASTRCUT pData;
    	ULONG i = 0;
    	//在链表中插入10个元素
    
        KdPrint(("Begain insert to link list"));
    	for (i = 0 ; i < 10 ; i++)
    	{
    		pData = (PMYDATASTRCUT)
    			     ExAllocatePool(PagedPool,sizeof(MYDATASTRUCT));
    		pData->number = i;
    		InsertHeadList(&linkListHead,&pData->ListEntry);
    	}
    	//从链表中取出,并显示
        KdPrint(("Begain remove from link list\n"));
    	while (!IsListEmpty(&linkListHead))
    	{
    		PLIST_ENTRY pEntry = RemoveTailList(&linkListHead);
    		pData = CONTAINING_RECORD(pEntry,
    			              MYDATASTRUCT,
    						  ListEntry);
    		KdPrint(("%d\n",pData->number));
    		ExFreePool(pData);
    	}
    }


    在DebugView中打印log信息:

     

    Lookaside结构:

    频繁申请和回收内存,会导致在内存上产生大量的内存“空洞”,从而导致最终无法申请内存。DDK为程序员提供了Lookaside结构来解决这个问题。

     

    1.使用Lookaside:

    Lookaside一般会在一下情况使用:

    (1)程序员每次申请固定大小的内存

    (2)申请和回收的操作十分频繁

    使用Lookaside对象,首先要初始化Lookaside对象,有以下两个函数可以使用:

    VOID
    ExInitializeNPagedLookasideList(
    			IN PNPAGED_LOOKASIDE_LIST Lookaside, 
    			IN PALLOCATE_FUNCTION Allocate, 
    			IN PFREE_FUNCTION Free,
    			IN ULONG Flags, 
    			IN SIZE_T Size,
    			IN ULONG Tag,
    			IN USHORT Depth 
    			);
    
    VOID
    ExInitializePagedLookasideList( 
    			IN PPAGED_LOOKASIDE_LIST Lookaside,
    			IN PALLOCATE_FUNCTION Allocate, 
    			IN PFREE_FUNCTION Free, 
    			IN ULONG Flags,
    			IN SIZE_T Size,
    			IN ULONG Tag,
    			IN USHORT Depth
    			);


    这两个函数分别对非分页和分页Lookaside对象进行初始化。

    在初始化完Lookaside对象后,可以进行申请内存的操作了,有以下两个函数:

    PVOID
    ExAllocateFromNPagedLookasideList(
    			IN PNPAGED_LOOKASIDE_LIST Lookaside 
    			      );
    
    PVOID
    ExAllocateFromPagedLookasideList(
    			IN PPAGED_LOOKASIDE_LIST Lookaside
    			     );


    这两个函数分别是对非分页和分页内存的申请。

    对Lookaside对象进行回收内存的操作,有以下两个函数:

    VOID
    ExFreeToNPagedLookasideList( 
    				IN PNPAGED_LOOKASIDE_LIST Lookaside,
    				IN PVOID Entry
    				);
    
    VOID
    ExFreeToPagedLookasideList( 
    				IN PPAGED_LOOKASIDE_LIST Lookaside,
    				IN PVOID Entry 
    				);

    这两个函数分别是对非分页和分页内存的回收。

    在使用完Lookaside对象后,需要删除Lookaside对象,有以下两个函数:

    VOID
    ExDeleteNPagedLookasideList(
    				IN PNPAGED_LOOKASIDE_LIST Lookaside
    				);
    
    VOID
    ExDeletePagedLookasideList( 
    				IN PPAGED_LOOKASIDE_LIST Lookaside
    				);


    下面代码完整展示Lookaside对象的使用:

    #pragma INITCODE
    VOID LookasideTest()
    {
    	//初始化Lookaside对象
    	PAGED_LOOKASIDE_LIST pageList;
    	ExInitializePagedLookasideList(&pageList,
    		                         NULL,NULL,0,
    								 sizeof(MYDATASTRUCT),
    								 '1234',
    								 0);
    #define ARRAY_NUMBER 50
    	PMYDATASTRUCT MyObjectArray[ARRAY_NUMBER];
    	//模拟频繁申请内存
    	for (int i = 0 ; i < ARRAY_NUMBER ; i++)
    	{
    		MyObjectArray[i] = 
    			(PMYDATASTRUCT)ExAllocateFromPagedLookasideList(&pageList);
    	}
    	//模拟频繁回收内存
    	for (i = 0 ; i < ARRAY_NUMBER ; i++)
    	{
    		ExFreeToPagedLookasideList(&pageList,MyObjectArray[i]);
    		MyObjectArray[i] = NULL;
    	}
    	//删除Lookaside对象
    	ExDeletePagedLookasideList(&pageList);
    }


    运行时函数:

     

    1.内存间复制(非重叠)

    在驱动程序开发中,经常用到内存的复制。DDK为程序员提供了以下函数:

    VOID
    RtlCopyMemory( 
    		IN VOID UNALIGNED *Destination, 
    		IN CONST VOID UNALIGNED *Source, 
    		IN SIZE_T Length
    		);

    Destination:表示要复制内存的目的地址

    Source:表示要复制内存的源地址

    Length:表示要复制内存的长度,单位是字节

     

    2.内存间的复制(可重叠)

    函数原型:

    VOID
    RtlMoveMemory( 
    		IN VOID UNALIGNED *Destination,
    		IN CONST VOID UNALIGNED *Source, 
    		IN SIZE_T Length
    		);

    Destination:表示要复制内存的目的地址

    Source:表示要复制内存的源地址

    Length:表示要复制内存的长度,单位是字节

     

    3.内存填充:

    驱动程序开发中,还经常用到对某段内存区域用固定字节填充。DDK为程序员提供了函数RtlFillMemory。它在IA32平台下也是个宏,实际是memset函数。

    VOID
    RtlFillMemory( 
    	   IN VOID UNALIGNED *Destination, 
    	   IN SIZE_T Length,
    	   IN UCHAR Fill
    	   );
    


    Destination:目的地址

    Length:长度

    Fill:需要填充的字节

     

    在驱动开发中,还经常需要对某段内存填零,DDK提供的宏是RtZeroBytes和RtZeroMemory。

    VOID
    RtlZeroMemory(
    	   IN VOID UNALIGNED *Destination, 
    	   in SIZE_T Length
    	   );


    Destination:目的地址

    Length:长度

     

    4.内存比较:

    驱动开发中,还会用到比较两块内存是否一致。该函数是RtlCompareMemory,其申明是:

    ULONG
    RtlEqualMemory(
          CONST VOID *Source1,
          CONST VOID *Source2
          SIZE_T Length
    	   );

    Source1:比较的第一个内存地址

    Source2:比较的第二个内存地址
    Length:比较的长度,单位为字节

     

    将这些运行时函数统一做一个实验,代码如下:

    #define BUFFER_SIZE 1024
    #pragma INITCODE
    VOID RtTest()
    {
    	PUCHAR pBuffer = (PUCHAR)ExAllocatePool(PagedPool,BUFFER_SIZE);
    	//用零填充内存
    	RtlZeroMemory(pBuffer,BUFFER_SIZE);
    
    	PUCHAR pBuffer2 = (PUCHAR)ExAllocatePool(PagedPool,BUFFER_SIZE);
    	//用固定字节填充内存
    	RtlFillMemory(pBuffer,BUFFER_SIZE,0xAA);
    
    	//内存拷贝
    	RtlCopyMemory(pBuffer,pBuffer2,BUFFER_SIZE);
    
    	//判断内存是否一致
    	ULONG ulRet = RtlCompareMemory(pBuffer,pBuffer2,BUFFER_SIZE);
    	if (ulRet == BUFFER_SIZE)
    	{
    		KdPrint(("The two blocks are same\n"));
    	}
    }


     

     

     

    (下一篇:Windows内核函数)


     

     

    展开全文
  • WINDOWS核心编程——Windows内存管理

    千次阅读 2017-07-18 20:53:07
    想要了解Windows内存体系结构首先要对系统的内存的分段分页和进程隔离机制要有所了解。系统为了对进程进行隔离,使得每个进程只能访问自己申请的内存而不能访问其他进程的内存资源,对每个进程的内存使用线性地址...

    想要了解Windows内存体系结构首先要对系统的内存的分段分页和进程隔离机制要有所了解。系统为了对进程进行隔离,使得每个进程只能访问自己申请的内存而不能访问其他进程的内存资源,对每个进程的内存使用线性地址编制,在通过内存的分页机制在进程需要访问物理内存时通过进程的页表找到世界的物理内存的地址通过系统读写内存中的数据。在早期总线(20位寻址1M)大于寄存器(16位寻址64k)的情况下为了表示更多的物理内存地址采用了分段技术,现在已经不需要分段技术了(32位的内表示4GB,64位内表示16EB)采用平坦模型。

    32位的系统支持4GB的内存,线性地址的各个区间有不同的作用:


    1.空指针赋值分区:用来给空指针赋值的,这个分区不可操作,操作就报错。

    2.用户模式分区:用户代码在这里跑,堆栈都在这里,用户可以随便用,一般出错都在这里。

    3.64kb禁入分区:不知道干什么用的,估计就是为了区隔内核模式跟用户模式的。

    4.内核模式分区:系统运行的空间,所有进程共用的,用户模式的的代码不能访问这部分代码,若要访问续的通过系统提供的API进入到内核态。

    windows的内存体系结构基于虚拟的线性的地址和分页机制。对于线性地址的分配也是以页为单位进行的,物理地址的管理更是以页为单位。我们可以调用函数从地址空间中预定一块内存,在实际使用的时候再从物理内存中调拨,相当于C语言中的声明与定义,当不再需要内存的时候可以还给系统,先将一块内存标记为可用的(标记线性空间中的地址空闲可用),当积攒够了一定的空闲内存是在取消提交(把物理内存归还给操作系统)。对于物理内存而言,在暂时不用或者内存紧张的情况下可以被交换到磁盘上的页交换文件中,在需要的时候(CPU缺页中断)再从也交换文件中载入到内存中,这样就提高了内存的使用效率。页交换文件的使用当然需要一定的代价,频繁的在磁盘与内存将交换页会导致系统性能下降(硬盘颠簸),一般而言采用增加内存的办法比提升CPU对系统的性能改善更大。对于程序的数据可以采用交换页的技术来扩展内存以提高物理内存的使用效率,对于一些相对于数据的内容多变而且大小不可预计的内存使用方式而言交换页确实能提高效率,但是对于可以预知整块内存大小且需要连续的空间而言如文件镜像,固定大小的数据文件等使用内存映射文件是效率更高的方式。分页内存机制调配内存的过程可以粗略的描述如下:


    系统在对内存访问的安全性方面做的不只是按区段来控制内存的访问,也可以对每一个内存页指定保护属性:


    我们将整个4GB的线性地址空间称为虚拟内存(地址称为逻辑地址),我们所有的内存操作只在逻辑地址上完成,系统会帮我们处理物理地址映射,缺页等所有的情况。系统的内存的状态也主要是通过虚拟内存的状态来表现的,主要通过如下接口获得内存的状态:

    //获取系统信息 64位系统要通过GetNativeSystemInfo
    void WINAPI GetSystemInfo(
        LPSYSTEM_INFO lpSystemInfo  
    );
    typedef struct _SYSTEM_INFO {  
      union {  
        DWORD  dwOemId;  
        struct {  
          WORD wProcessorArchitecture;  //处理器体系结构  
          WORD wReserved;  //保留
        } ;  
      } ;  
      DWORD     dwPageSize;   //分页大小
      LPVOID    lpMinimumApplicationAddress;  //进程最小寻址空间
      LPVOID    lpMaximumApplicationAddress; //进程最大寻址空间  
      DWORD_PTR dwActiveProcessorMask;  //处理器掩码; 0..31 表示不同的处理器
      DWORD     dwNumberOfProcessors;  //CPU数量  
      DWORD     dwProcessorType;  //处理器类型
      DWORD     dwAllocationGranularity;  //虚拟内存空间的粒度
      WORD      wProcessorLevel;  //处理器等级
      WORD      wProcessorRevision;  //处理器版本
    } SYSTEM_INFO;  
    
    //获取当前系统中关系内存使用情况
    BOOL WINAPI GlobalMemoryStatusEx(
        LPMEMORYSTATUSEX lpBuffer  
    );
    typedef struct _MEMORYSTATUSEX {  
      DWORD     dwLength;  // sizeof (MEMORYSTATUSEX)
      DWORD     dwMemoryLoad; //已使用内存数量  
      DWORDLONG ullTotalPhys;  //系统物理内存总量  
      DWORDLONG ullAvailPhys;  //空闲的物理内存  
      DWORDLONG ullTotalPageFile;//页交换文件大小  
      DWORDLONG ullAvailPageFile;//空闲的页交换空间  
      DWORDLONG ullTotalVirtual;  //进程可使用虚拟机地址空间大小  
      DWORDLONG ullAvailVirtual;  //空闲的虚拟地址空间大小  
      DWORDLONG ullAvailExtendedVirtual;  //ullAvailExtendedVirtual保留字段
    } MEMORYSTATUSEX, *LPMEMORYSTATUSEX  
    
    //获取当前进程的内存使用情况
    BOOL WINAPI GetProcessMemoryInfo(
        HANDLE Process, //进程句柄
        PPROCESS_MEMORY_COUNTERS ppsmemCounters, //返回内存使用情况的结构
        DWORD cb  //结构的大小
    ); 
    typedef struct _PROCESS_MEMORY_COUNTERS_EX {  
      DWORD  cb;  //结构的大小
      DWORD  PageFaultCount; //发生的页面错误  
      SIZE_T PeakWorkingSetSize;  //使用过的最大工作集  
      SIZE_T WorkingSetSize;      //目前的工作集  
      SIZE_T QuotaPeakPagedPoolUsage;//使用过的最大分页池大小  
      SIZE_T QuotaPagedPoolUsage;  //分页池大小  
      SIZE_T QuotaPeakNonPagedPoolUsage;//非分页池使用过的  
      SIZE_T QuotaNonPagedPoolUsage;  //非分页池大小  
      SIZE_T PagefileUsage; //页交换文件使用大小  
      SIZE_T PeakPagefileUsage; //历史页交换文件使用  
      SIZE_T PrivateUsage;  //进程运行过程中申请的内存大小  
    } PROCESS_MEMORY_COUNTERS_EX, *PPROCESS_MEMORY_COUNTERS_EX  
    
    //查询当前进程虚拟地址空间的某个地址所属的块信息
    SIZE_T WINAPI VirtualQuery(
        LPCVOID                   lpAddress, //查询内存的地址
        PMEMORY_BASIC_INFORMATION lpBuffer, //接收内存信息
        SIZE_T                    dwLength //结构的大小
    );
    //查询进程虚拟地址空间的某个地址所属的块信息
    DWORD VirtualQueryEx(
        HANDLE hProcess, //进程句柄
        LPCVOID lpAddress, //查询内存的地址
        PMEMORY_BASIC_INFORMATION lpBuffer, //接收内存信息
        DWORD dwLength //结构的大小
    );
    typedef struct _MEMORY_BASIC_INFORMATION {  
      PVOID  BaseAddress;  //区域基地址  
      PVOID  AllocationBase;//使用VirtualAlloc分配的基地址  
      DWORD  AllocationProtect; //保护属性  
      SIZE_T RegionSize;    //区域大小  
      DWORD  State;     //页属性  
      DWORD  Protect;  //区域属性  
      DWORD  Type;  //区域类型  
    } MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;  
    程序不能直接操作物理内存的,所有的数据都需要保存在线性的虚拟内存(逻辑地址)中。使用虚拟内存主要使用函数VirtualAlloc来预定和提交内存,使用VirtualFree来归还或取消提交内存。

    虚拟内存的操作以页为粒度,适合用来管理大型对象数组或大型结构数组。对于存在页交换文件的内存页若我们能确定整页的内存数据不会改变,或者放弃在内存中的改变,下回直接从页交换文件中重新载入,则称该内存页为可重设的,不需要被交换到页文件中,直接覆盖其中的内容,在需要的时候重新从也文件中载入。预定提交重设用的同一个函数说明如下:

    //预定虚拟内存和调拨物理内存,失败返回NULL,成功返回lpAddress的取整的值
    LPVOID VirtualAlloc{
         LPVOID lpAddress, // 要分配的内存区域的地址,按分配粒度向上取整,为NULL则由系统决定
         DWORD dwSize, // 分配的大小,分配粒度的整数倍
         DWORD flAllocationType, // 分配的类型
         DWORD flProtect // 该内存的初始保护属性
    };
    对函数VirtualAlloc中的类型和保护属性说明如下:


    VirtualAlloc的逆向操作为VirtualFree用于释放和清理虚拟内存:

    BOOL WINAPI VirtualFree(
        LPVOID lpAddress, //释放(取消预定或提交)的页的首地址
        SIZE_T dwSize,  //大小
        DWORD dwFreeType  //MEM_DECOMMIT 取消VirtualAlloc提交的页, MEM_RELEASE 释放指定页
        //当释放整个区域时 dwFreeType 设置为MEM_RELEASE,lpAddress设置为区域的起始地址,dwSize设置为0,
    );
    对于VirtualAlloc时指定的保护方式可以通过函数VirtualProtect来更改:

    BOOL VirtualProtect(
        LPVOID lpAddress, // 目标地址起始位置
        DWORD dwSize, // 大小
        DWORD flNewProtect, // 请求的保护方式
        PDWORD lpflOldProtect // 保存老的保护方式
    );
    为了允许一个32位进程分配和访问更多的物理内存,突破这一受限地址空间所能表达的内存范围,Windows提供了一组函数,称为地址窗口扩展(AWE , Address  Windowing  Extensions)。用到的不多可以稍微了解下。

    而更常见的在有限的地址空间中处理大数据量(大到4GB的地址空间无法容纳所有数据)是,我们通常采用内存映射文件的办法一段段的处理数据。所谓映射就是把一段逻辑地址与文件的一段内容一一对应起来(同一段地址可以多次对应不同的文件内容)。映射原理如下(图片摘自网络如有版权问题请联系删除):

    正是由于内存映射文件的这几个特性所以特别合适用来处理下列事情:

    1:系统使用内存映射文件来将exe或是dll文件本身作为后备存储器,而非系统页交换文件,这大大节省了系统页交换空间,由于不需要将exe或是dll文件加载到页系统交换文件,也提高了启动速度。由于是映射到各自的逻辑地址的所以每个进程保存自己的副本,所有的变量之间也互不共享,但是可以通过DLL的数据段在使用同一DLL的不同进程间共享变量。
    2:使用内存映射文件来将磁盘上的文件映射到进程的空间区域,使得开发人员操作文件就像操作内存数据一样,将对文件的操作交由操作系统来管理,简化了开发人员的工作。这是最常用的方式,使用方式如下:

    1.创建或打开一个文件内核对象
    HANDLE WINAPI CreateFile(
        LPCTSTR lpFileName,
        DWORD dwDesiredAccess,
        DWORD dwShareMode,
        LPSECURITY_ATTRIBUTES lpSecurityAttributes,
        DWORD dwCreationDisposition,
        DWORD dwFlagsAndAttributes,
        HANDLE hTemplateFile
    );
    2.创建一个文件映射内核对象
    HANDLE WINAPI CreateFileMapping(
        HANDLE hFile,  //文件句柄
        LPSECURITY_ATTRIBUTES lpAttributes, //安全属性
        DWORD flProtect, //保护属性
        DWORD dwMaximumSizeHigh, //文件映射的最大长度的高32位
        DWORD dwMaximumSizeLow, //文件映射的最大长度的低32位
        LPCTSTR lpName //内核文件命名
    );
    5.关闭文件对象
    CloseHandle(hFile);
    
    3.将文件映射对象映射到进程地址空间
    LPVOID WINAPI MapViewOfFile(
        HANDLE hFileMappingObject, //文件句柄
        DWORD dwDesiredAccess, //文件数据的访问方式要与CreateFileMapping()的保护属性相匹配
        DWORD dwFileOffsetHigh, //表示文件映射起始偏移的高32位
        DWORD dwFileOffsetLow, //表示文件映射起始偏移的低32位
        SIZE_T dwNumberOfBytesToMap //指定映射文件的字节数
    );
    
    6.关闭文件映射对象
    CloseHandle(hFileMapping);
    
    4.从进程的地址空间中撤消文件数据的映像
    BOOL UnmapViewOfFile(
        PVOID pvBaseAddress //pvBaseAddress由MapViewOfFile函数返回
    );
    
    //可以按以上顺序执行或者看情况执行4,5,6
    //对于修改过的数据的一部分或全部强制重新写入磁盘映像中
    BOOL FlushViewOfFile(
       PVOID pvAddress, //内存映射文件中的视图的一个字节的地址
       SIZE_T dwNumberOfBytesToFlush //想要刷新的字节数
    );
    对于一些参数的说明如下:

    使用fdwProtect 参数设定的部分保护属性

    dwDesiredAccess用于标识如何访问该数据



    3:windows提供了多种进程间通信的方法,但他们都是基于内存映射文件来实现的。

    对于进程间通信只要在不同进程中映射了同一个文件内容,当其中一个映射被改变时(就算还没有保存到磁盘上)其他进程自动会获取到改变。

    windows的进程除了直接向系统申请内存之外还可以使用运行时库提供的内存堆和栈,简单的有如下说明:

    http://blog.csdn.net/pokeyode/article/details/53303029

    http://blog.csdn.net/pokeyode/article/details/53336826
    虽然运行时库提供的堆足以满足我们的需要,但我们还是会基于一下原因来创建自己的堆(引用自):

    一:对数据保护。创建两个或多个独立的堆,每个堆保存不同的结构,对两个堆分别操作,可以使问题局部化。
    二:更有效的内存管理。创建额外的堆,管理同样大小的对象。这样在释放一个空间后可以刚好容纳另一个对象。
    三:内存访问局部化。将需要同时访问的数据放在相邻的区域,可以减少缺页中断的次数。
    四:避免线程同步开销。默认堆的访问是依次进行的。堆函数必须执行额外的代码来保证线程安全性。通过创建额外的堆可以避免同步开销。
    五:快速释放。我们可以直接释放整个堆而不需要手动的释放每个内存块。这不但极其方便,而且还可以更快的运行。
    要创建并管理自己的堆,需要使用以下接口,首先要创建堆:

    HANDLE HeapCreate(
        DWORD fdwOptions, //如何操作堆
        SIZE_T dwInitilialize, //一开始要调拨给堆的字节数向上取整到CPU页面大小的整数倍
        SIZE_T dwMaximumSize //堆所能增长到的最大大小,即预定的地址空间的最大大小。若为0,那么堆可增长到用尽所有的物理存储器为止。
    ); 

    fdwOptions表示对堆的操作该如何进行
    HEAP_NO_SERIALIZE标志使得多个线程可以同时访问一个堆,这使得堆中的数据可能会遭到破坏,因此应该避免使用。
    HEAP_GENERATE_EXCEPTIONS标志告诉系统,每当在堆中分配或者重新分配内存块失败的时候,抛出一个异常。
    HEAP_CREATE_ENABLE_EXECUTE标志告诉系统,我们想在堆中存放可执行代码。如果不设置这个标志,那么当我们试图在来自堆的内存块中执行代码时,系统会抛出EXCEPTION_ACCESS_VIOLATION异常。

    有了堆之后从堆中分配内存时要:

    1.遍历已分配的内存的链表和闲置内存的链表。
    2.找到一块足够大的闲置内存块。
    3.分配一块新的内存,将2找到的内存块标记为已分配。
    4.将新分配的内存块添加到已分配的链表中。

    调用函数来从堆中分配并在需要时调整内存大小:

    PVOID HeapAlloc(
        HANDLE hHeap, //堆句柄,表示要从哪个堆分配内存
        DWORD fdwFlags, //堆分配时的可选参数
        SIZE_T dwBytes //要分配堆的字节数
    );  
    PVOID HeapReAlloc(
        HANDLE hHeap, //堆句柄
        DWORD fdwFlags, //HeapAlloc的fdwFlags一样
        PVOID pvMem, //指定要调整大小的内存块
        SIZE_T dwBytes //指定内存块的新大小
    );  
    fdwFlags说明如下:

    HeapReAlloc的fdwFlags特别的有HEAP_REALLOC_IN_PLACE_ONLY 如果HeapReAlloc函数能在不移动内存块的前提下就能让它增大,那么函数会返回原来的内存块地址。另一方面,如果HeapReAlloc必须移动内存块的地址,那么函数将返回一个指向一块更大内存块的新地址。如果一个内存块是链表或者树的一部分,那么需要指定这个标志。因为链表或者树的其他节点可能包含指向当前节点的指针,把节点移动到堆中其他的地方会破坏链表或树的完整性。

    若成功则返回内存地址若失败则返回NULL,若指定了HEAP_GENERATE_EXCEPTIONS报异常:


    在不需要内存是把内存归还给堆:

    BOOL HeapFree( 
        HANDLE hHeap, //堆句柄
        DWORD fdwFlags,
        PVOID pvMem //指定要调整大小的内存块
    ); 
    
    在不需要堆时销毁堆

    BOOL HeapDestroy(HANDLE hHeap);  

    展开全文
  • Windows内存管理的几种方式和优缺点

    千次阅读 2016-07-27 11:34:03
    Windows内存管理方式主要分为:页式管理、段式管理和段页式管理。 页式管理的基本原理是将各进程的虚拟空间划分为若干个长度相等的页。把内存空间按页的大小划分为片或者页面,然后把页式虚拟地址与内存地址建立...
  • 前面的《Windows内存体系(3) – 内存映射文件》文章,对内存映射文件技术的原理进行了介绍,本篇文章着重介绍该技术的使用场景。 一、内存映射文件技术介绍 常用的有Win32 API的CreateFile()、WriteFile()、...
  • 本文基本上是windows via c/c++上的内容,笔记做得不错。。 本文背景: ...根本的原因是,没有清楚的理解操作系统的内存管理机制,本文企图通过简单的总结描述,结合实例来阐明这个机制。 本文目
  • 本文背景:在编程中,很多Windows或...本文目的:对Windows内存管理机制了解清楚,有效的利用C++内存函数管理和使用内存。本文内容:本文一共有六节,由于篇幅较多,故按节发表。其他章节请看本人博客的Windows内存管
  • Windows CE 内存管理

    千次阅读 热门讨论 2005-12-14 17:31:00
    内存管理 如果你在写Windows CE 程序中遇到的最重要的问题,那一定是内存问题。一个WinCE 系统可能只有4MB 的RAM,这相对于个人电脑来说是十分少的,因为个人电脑的标准配置已经到了128MB 甚至更多。事实上,运行...
  • 本文背景:在编程中,很多Windows...本文目的:对Windows内存管理机制了解清楚,有效的利用C++内存函数管理和使用内存。本文内容:本文一共有六节,由于篇幅较多,故按节发表。其他章节请看本人博客的Windows内存管理
  • 实验五 Windows XP 虚拟内存管理 一 实验目的 1) 了解存储器管理以及虚拟...三 背景知识 虚拟存储器技术是当代计算机中广泛采用的内存管理方案在 Windows XP 中合理的进行虚拟内存的设 置可以更有效的提高系统的工作效
  • 进程、线程、内存管理是一个内核最...在内存管理API方面,为了便于移植程序,Windows CE和其它Windows操作系统函数声明基本一致,这使一个在其它Windows下开发的程序员可以直接使用早就熟悉的API函数, 但是CE下内存管
  • C++内存管理技术内幕

    千次阅读 2015-11-22 09:19:04
    这几天在整理硬盘的资料,发现一个PDF,名字叫《C++内存管理技术内幕》,名字很霸气,于是顺着好奇心打开看看。花了一个多小时,终于看完,看完的感觉就是相见恨晚啊,写的如此之好,想看看这篇文章是谁写的,结果找...
  • 面试题总结之windows/linux内存管理

    千次阅读 2017-08-20 23:09:48
    前言请说说你对windows/linux内存管理的认识解答内存管理的必要性出现在早期的计算机系统当中,程序是直接运行在物理内存中,每一个程序都能直接访问物理地址。如果这个系统只运行一个程序的话,并且这个程序所需的...
  • 这是Linux内存管理的一个优秀特性,在这方面,区别于 Windows内存管理。主要特点是,无论物理内存有多大,Linux 都将其充份利用,将一些程序调用过的硬盘数据读入内存,利用内存读写的高速特性来提高Linux系统的...
  • 1.Windows内存分配与释放提供了缓存机制,由空闲链表方式提供对非分页内存管理,由位图方式提供分页内存管理同时提供相应的快查表,将最近释放的页面放入快查链表中,再次申请的时候可以快速的从快查表中取得数据. ...
  • 1. 进程地址空间   软件的进程运行于32位系统上,其寻址位也是32位,能表示的空间是2的32 =4G,范围从0x0000...例子:分配内存时,如果由于某种原因分配不成功,则返回空指针0x0000 0000;当用户继续使用比如改写数
  • Windows系统的内存管理

    千次阅读 2004-11-27 15:47:00
    关于内存管理 32位的Windows系统,每个进程都有4GB(Gigabytes)大小的虚拟内存空间可以寻址(因为32位指针可以使用从0x00000000 – 0xFFFFFFFF内的任何一个值),而64位的Windows系统,每个进程有8TB(Terabytes)大小...
  • Windows CE .NET 高级内存管理

    千次阅读 2004-07-19 00:00:00
    Windows CE .NET 技术文章。了解 Windows CE 如何管理内存。本文档包括专门讨论 Pocket PC 2002 开发和 DLL 的章节。点击此处阅读全文
  • Windows驱动开发基础(八)内存管理

    千次阅读 2014-08-25 21:19:21
    Windows驱动开发基础系列,转载请标明出处: 就32位的计算机来说,他有4G的真实的物理内存。但是这样是不够的,于是引入了虚拟内存的概念。使得每一个进程都有4G的虚拟内存。 虚拟内存实际上就是采用了一种映射...
  • Windows CE 内存管理(非常详细)

    千次阅读 2010-04-20 17:33:00
    一个Windows CE 应用程序有许多不同的内存分配方式。在内存食物链的底端是Virtualxxx 函数,它们直接保留,提交和释放(free)虚拟内存页。接下来的是堆(heap) API。堆是系统为应用程序保留的内存区域。堆有两种风味:...
  • Windows Embedded CE6.0高效的内存管理

    千次阅读 2010-06-02 10:59:00
    当然,Windows Embedded CE始终是一个先进的、抢先多任务和支持虚拟内存的操作系统,,在这段发展过程当中,微软在Windows embedded CE操作系统除内存管理外的各个方面都进行改进,但、Windows Embedded CE操作系统...
  • Windows内存管理机密+揭穿内存优化工具的骗局 Windows Vista最新的内存管理技术SuperFetch。在Webcast的最后,盆盆推荐了MarkRussinovich的一篇揭穿内存优化工具骗局的文章,并承诺会将它翻译成中文,并且加上盆盆...
  • Windows 任务管理器中的几个内存概念

    万次阅读 2016-07-30 18:34:43
    Windows 任务管理器中的几个内存概念
  • 这篇文章将带着大家来学习《Windows黑客编程技术详解》,其作者是甘迪文老师,推荐大家购买来学习。作者将采用实际编程和图文结合的方式进行分享,并且会进一步补充知识点。第三篇文章主要介绍木马病毒启动技术,...
  • 内存管理

    万次阅读 2015-03-12 17:09:40
    内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟的收获则是一遍一遍的检查代码和对C++的痛恨,但内存管理在C++中无处不在,内存泄漏几乎在每个C++...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 186,130
精华内容 74,452
关键字:

windows内存管理技术