2.对物理内存的直接读写
在PC环境下,Windows是不允许用户态进程直接访问内存的,任何对内存的访问都会引起程序的异常。而在嵌入式设备中,需要直接对内存进行读写,以此来提高处理速度,此外,在ARM体系中,I/O被映射到高端的地址进行访问,只有读写物理地址,I/O的驱动才能高效地运行。Windows CE中有一些API提供了对物理内存的“直接”访问。不过,在访问之前,必须把物理内存映射到虚拟地址中,通过虚拟地址才能读写物理内存。
PHYSICAL_ADDRESS描述了Windows CE的物理内存结构体,Windows CE在ceddk.h中定义了PHYSICAL_ADDRESS,其定义如下:
n 在ceddk.h中
typedef LARGE_INTEGER PHYSICAL_ADDRESS, *PPHYSICAL_ADDRESS;
n 在winnt.h中
typedef union _LARGE_INTEGER{
struct{
DWORD LowPart;
LONG HighPart;
};
LONGLONG QuadPart;
} LARGE_INTEGER;
可见,Windows CE中用64位来代表物理地址,对于大多数32位的CPU而言,只需要把它的HighPart设置为0就可以了。
VirtualAlloc()函数是Windows CE中分配连续虚拟地址的API,VirtualCopy()函数将一段物理内存映射到虚拟地址。因此,在进程中访问物理地址,就像访问虚拟地址一样方便,当然,如何选择虚拟地址是需要研究的。
// 申请虚拟内存
LPVOID VirtualAlloc(
LPVOID lpAddress, // 希望的虚拟内存起始地址
DWORD dwSize, // 以字节为单位的大小
DWORD flAllocationType, // 申请类型,分为Reserve和Commit
DWORD flProtect // 访问权限
);
// 把物理内存绑定到虚拟地址空间
BOOL VirtualCopy(
LPVOID lpvDest, // 虚拟内存的目标地址
LPVOID lpvSrc, // 物理内存地址
DWORD cbSize, // 要绑定的大小
DWORD fdwProtect // 访问权限
);
VirtualAlloc对虚拟内存的申请分为两步,保留MEM_RESERVE和提交MEM_COMMIT。其中MEM_RESERVE只是在进程的虚拟地址空间内保留一段,并不分配实际的物理内存,因此保留的虚拟内存并不能被应用程序直接使用。MEM_COMMIT阶段才真正为虚拟内存分配物理内存。
下面的代码显示了如何使用VirtualAlloc和VirtualCopy来访问物理内存。因为VirtualCopy负责把一段物理内存和虚拟内存绑定,所以VirtualAlloc执行时只需要对内存保留,没有必要提交。
FpDriverGlobals =
(PDRIVER_GLOBALS) VirtualAlloc(
0,
DRIVER_GLOBALS_PHYSICAL_MEMORY_SIZE,
MEM_RESERVE,
PAGE_NOACCESS);
if (FpDriverGlobals == NULL) {
ERRORMSG(DRIVER_ERROR_MSG, (TEXT(" VirtualAlloc failed!\r\n")));
return;
}
else {
if (!VirtualCopy(
(PVOID)FpDriverGlobals,
(PVOID)(DRIVER_GLOBALS_PHYSICAL_MEMORY_START),
DRIVER_GLOBALS_PHYSICAL_MEMORY_SIZE,
(PAGE_READWRITE | PAGE_NOCACHE))) {
ERRORMSG(DRIVER_ERROR_MSG, (TEXT("VirtualCopy failed!\r\n")));
return;
}
}
CEDDK还提供了函数MmMapIoSpace,用来把一段物理内存直接映射到虚拟内存。用MmMapIoSpace申请的内存要用MmUnmapIoSpace释放,此函数的原型如下:
PVOID MmMapIoSpace(
PHYSICAL_ADDRESS PhysicalAddress, // 起始物理地址
ULONG NumberOfBytes, // 要映射的字节数
BOOLEAN CacheEnable // 是否缓存
);
VOID MmUnmapIoSpace(
PVOID BaseAddress, // MmMapIoSpace返回的起始虚拟地址
ULONG NumberOfBytes //
);
其实,MmMapIoSpace函数内部也是调用VirtualAlloc和VirtualCopy函数来实现物理地址到虚拟地址的映射的。MmMapIoSpace函数的原代码是公开的,可以从%_WINDOWS CEROOT%\PUBLIC\COMMON\OAK\DRIVERS\CEDDK\DDK_MAP\ddk_map.c得到。从MmMapIoSpace的实现中也可以看出VirtualAlloc和VirtualCopy的用法。
PVOID MmMapIoSpace (
IN PHYSICAL_ADDRESS PhysicalAddress,
IN ULONG NumberOfBytes,
IN BOOLEAN CacheEnable
)
{
PVOID pVirtualAddress; ULONGLONG SourcePhys;
ULONG SourceSize; BOOL bSuccess;
SourcePhys = PhysicalAddress.QuadPart & ~(PAGE_SIZE - 1);
SourceSize = NumberOfBytes + (PhysicalAddress.LowPart & (PAGE_SIZE - 1));
pVirtualAddress = VirtualAlloc(0, SourceSize, MEM_RESERVE, PAGE_NOACCESS);
if (pVirtualAddress != NULL)
{
bSuccess = VirtualCopy(
pVirtualAddress, (PVOID)(SourcePhys >> 8), SourceSize,
PAGE_PHYSICAL | PAGE_READWRITE | (CacheEnable ? 0 : PAGE_NOCACHE));
if (bSuccess) {
(ULONG)pVirtualAddress += PhysicalAddress.LowPart & (PAGE_SIZE - 1);
}
else {
VirtualFree(pVirtualAddress, 0, MEM_RELEASE);
pVirtualAddress = NULL;
}
}
return pVirtualAddress;
}
此外,Windows CE还供了AllocPhysMem函数和FreePhysMem函数,用来申请和释放一段连续的物理内存。函数可以保证申请的物理内存是连续的,如果函数成功,会返回虚拟内存的句柄和物理内存的起始地址。这对于DMA设备尤为有用。在这里就不详细介绍了,读者可以参考Windows CE的联机文档。
7.5.5 进程地址空间结构
系统中的32位虚拟地址提供了4GB的虚拟内存空间,对于嵌入式应用来说,内存一般很小,因而系统在使用内存方面作了些限制,以提供更高效能的存储空间管理。这些限制包括:大量的系统保留空间,实际上这些地址空间,通常不对应到任何的实体页面;系统处理程序数最多只有32个,每个处理程序的实际可使用内存空间受到限制(32MB);有固定的处理程序共享内存;有ROM地址的对应等。如图7-8所示。
Windows CE.NET把XIP DLL单独加载到Slot 1中,这样对于每个进程来说,它总的地址空间就大了一倍,也就是64MB。当这个进程得到CPU使用权时,它的整个地址空间被内核映射到Slot 0,也就是当前进程使用的地址空间,然后开始运行。图中给出的地址实际上是经过映射到Slot 0之后的结构。从图中可以看出,进程首先加载代码段,因为每个进程最低部64KB作为保留区域,所以代码段从0x0001 0000开始,内核为代码段分配足够的虚拟地址空间后,接着为只读数据和可读/可写数据分配空间,接着为资源数据分配空间,之后为默认堆和栈分配空间。非XIP DLL从进程最高地址向下开始加载。
7.5.6 堆和栈
堆是一段连续的较大的虚拟地址空间。应用程序在堆中可以动态地分配、释放所需大小的内存块。利用堆的优点是在一定范围内减小了内存碎块,而且开发者分配内存块前不必了解CPU的类型。因为不同的CPU分页大小不相同,每个内存页可能是1KB、4KB或更多。在堆内分配内存块可以是任意大小的,而直接分配内存就必须以内存页为单位。当应用程序启动时,内核在进程所在的地址空间中为进程分配一个默认192KB大小的虚拟地址空间,但是并不立刻提交物理内存。如果在运行时192KB不能满足需求,那么内核会在进程地址空间中重新查找一个足够大小的空闲的地址空间,然后复制原来堆的数据,最后释放原来的堆所占的地址空间。这是因为默认的堆的高地址处还有栈,所以必须重新分配。
栈也是一段连续的虚拟地址空间,和堆相比空间要小得多,它是专为函数使用的。当调用一个函数时(包括线程),内核会产生一个默认的栈,并且内核会立刻提交少量的物理内存(也可以禁止内核立刻提交物理内存)。栈的大小和CPU有关,一般为64KB,并且保留顶部2KB以防止溢出。可以修改栈的大小,具体修改方法在讲解线程的时候已经说过了,这里就不再重复了。一般不会修改栈的大小,如果在编译链接时修改大小,那么所有栈的大小都会改变,这不太合理。实际开发中最好不要在栈中分配很大、很多的内存块,如果分配的内存块超过了默认栈的限制,那么会引起访问非法并且内核会立刻终止进程。最好在进程的堆中分配大的内存块并且在函数返回前释放,或者在创建线程时指定栈的大小。
7.5.7 分页机制
Windows CE内核用分页虚拟内存机制来管理和分配程序内存。虚拟内存系统提供了连续的内存块。每个64KB的内存区域被分成多个1024B或4096B的页。所以应用程序不必进行实际的内存分配管理。对于少于64KB的内存请求,应用程序可以用系统为Windows CE程序提供的本地堆或创建分离的堆来满足应用程序的内存需要。内核也可以为每个新的进程或线程在栈上分配内存。
Windows CE操作系统使用KDataStruct数据结构来存放低地址2GB内的数据。代码样例7-9列出KdataStruct的整个数据结构代码如下:
在KDataStruct数据结构中,又利用PSECTION aSections[64]将低地址2GB分割成64个32MB大小的空间,称之为Section。Section再被分割成512个64KB大小的空间,称之为MemBlock,如程序代码4.2所示。MemBlock再被分割成数个页(Page)。如图7-9所示。页的大小(PAGE_SIZE)在不同的系统中略有不同。ARM4处理器的PAGE_SIZE为4096,ARM920的PAGE_SIZE为1024,MIPS及x86处理器的PAGE_SIZE则为4096。若以PAGE_SIZE = 4096,则MemBlock可被分割成16个页。程序代码7-10列出MemBlock整个数据结构,其中aPages[PAGES_PER_BLOCK]字段记录虚拟内存中每一个页所对应到的物理内存地址。代码如下:
#define BLOCK_MASK 0x1FF
typedef MEMBLOCK *SECTION[BLOCK_MASK+1];
//每一个SECTION指向512个BLOCK
typedef SECTION *PSECTION;
#define PAGE_SIZE 4096 /* page size */
#define PAGES_PER_BLOCK (0x10000 / PAGE_SIZE)
struct MemBlock {
ACCESSLOCK alk; /* 00: key code for this set of pages */
uchar cUses; * 04: # of page table entries sharing this leaf */
uchar flags; /* 05: mapping flags */
short ixBase; /* 06: first block in region */
short hPf; /* 08: handle to pager */
short cLocks; /* 0a: lock count */
ulong aPages[PAGES_PER_BLOCK];
}; /* MemBlock */
7.5.8 深入VirtualAlloc内部
VirtualAlloc是任何Microsoft Win32操作系统中最基础的内存分配调用函数。它在页级别分配内存。
VirtualAlloc调用分配内存的过程分为两个步骤。第一步,保留虚拟内存空间的区域。它只是防止一部分虚拟地址空间被用于其他用途。保留内存空间之后,就可以提交(commit)部分或整个区域,这个过程是指将实际物理内存映射到保留区域。VirtualAlloc函数用于保留内存空间和提交内存。下面显示了VirtualAlloc函数的原型。
LPVOID VirtualAlloc (LPVOID lpAddress, DWORD dwSize,
DWORD flAllocationType,
DWORD flProtect);
VirtualAlloc的第一个参数是要分配的内存区域的虚拟地址。如果该参数是NULL,则由系统确定从哪里分配内存区域,并以64KB或者32KB为边界。第二个参数是dwSize,它是要分配或保留的区域的大小。因为该参数是以字节而不是页为单位指定的,所以系统会将所请求的大小自动调整为页大小的整数倍。
flAllocationType参数指定分配的类型。可以指定以下标志的组合——MEM_COMMIT、MEM_AUTO_COMMIT和MEM_RESERVE。MEM_COMMIT标志用于分配程序使用的内存。MEM_RESERVE用于保留要随后提交的虚拟地址空间。保留页是无法访问的,直到通过指定区域并使用MEM_COMMIT标志进行了另一个VirtualAlloc调用为止。MEM_AUTO_COMMIT标志惟一用于Windows CE并且很好用,但它不是本文的主题。
因此,要使用VirtualAlloc来分配可使用的RAM,应用程序必须调用VirtualAlloc两次,一次保留内存空间,再一次则提交物理RAM,或者调用VirtualAlloc一次,这需要在flAllocationType参数中组合使用MEM_RESERVE和MEM_COMMIT标志。
组合保留和提交标志方式所使用的代码更少,并且更快、更简单。该技术通常用在Windows XP应用程序中,但用在Windows CE应用程序中不是很好。代码样例7-11演示了存在的问题。代码如下:
INT i;
PVOID pMem[512];
for (i = 0; i < 512; i++)
{
pMem[i] = VirtualAlloc (0, PAGE_SIZE, MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE);
}
代码样例7-11似乎是无害的。它分配了512块内存,每块内存的大小为1页。问题是:在Windows CE系统上,该代码总是会失败。原因在于Win32操作系统保留内存区域的方式。Windows CE 应用程序的问题是它们必须位于32MB虚拟内存空间的范围内。在整个应用程序内存空间中该空间的大小只有51264KB,并且它们中的一部分需要用作应用程序代码、本地堆、堆栈和应用程序所加载的每个DLL的区域。通常,在对VirtualAlloc进行大约470次调用之后上面的代码片段将失败。
上述问题的解决方案是首先保留足够用于整个分配的较大区域,然后在需要时提交RAM,如代码样例7-12所示。
INT i;
PVOID pBase, pMem[512];
pBase = VirtualAlloc (0, 512*PAGE_SIZE, MEM_RESERVE, PAGE_READWRITE);
for (i = 0; i < 512; i++) {
pMem[i] = VirtualAlloc (pBase + (i * PAGE_SIZE), PAGE_SIZE,
MEM_COMMIT, PAGE_READWRITE);
}
避免该问题的关键是知道这个情况。这只是Windows CE应用程序的地址空间中只有512个区域所带来的问题影响应用程序的很多地方中的一个。
分配大型内存块
Windows CE .NET应用程序的地址空间局限于32MB所引起的另一个问题是如何分配大型内存块。当应用程序的整个地址空间被限制在32MB以内时,如果应用程序需要一块8、16或32MB RAM用于具体用途,它怎样才能分配该内存?回答是应用首先用在Windows CE .NET早期版本中针对视频驱动程序的一个修复程序。有了它,如果Windows CE .NET检测到一个对VirtualAlloc的调用请求保留超过2MB的地址空间,该地址空间将不会保留在32MB的限制中。该内存块将保留在大型内存区域(Large Memory Area)中,大型内存区域位于全局内存空间中,正好在2GB系统保留空间的下面。
内存空间已经保留后,应用程序就可以通过调用VirtualAlloc来提交在保留空间内的具体页。这就允许应用程序或驱动程序使用大型内存块,即使它存在32MB大小的限制。代码样例7-13显示了分配64MB块后提交保留区域的一页的简单情形。代码如下:
PVOID ptrVirt, ptrMem;
ptrVirt = VirtualAlloc (0, 1024 * 1024 * 64, MEM_RESERVE,
PAGE_NOACCESS);
if (!ptrVirt) return 0;
ptrMem = VirtualAlloc ((PVOID)((int)ptrVirt+4096),
4096, MEM_COMMIT, PAGE_READWRITE);
if (!ptrMem) {
VirtualFree (ptr, 0, MEM_RELEASE);
return 0;
}
return ptrMem;
前面的代码还显示了直接处理虚拟内存API所具有的一个特性。这就是可以创建大型稀疏数组,而不会消耗大量