操作系统(包括应用环境、操作环境等)一般具有分层的结构特征,典型的就是UNIX系统的同心环,最里面是硬件,从里向外依次是kernel、共享函数库、应用程序3个层次。Windows CE的分层结构比较复杂(如图7-1所示),这是根据它适合嵌入式应用的特点所设计的。从提供者的角度来看,图中上面4层主要由微软公司维护开发,下面两层主要由嵌入式产品开发商根据产品需要开发。当然,这个划分并不绝对,而是根据产品需求来决定。需要说明的是,这里并没有一个单独的应用层,因为在大多数情况下,从嵌入式产品发布的方式来看,使用者的应用产品是捆绑销售(bundle)的,所以它们本身也是构成应用整合层的一部份。
图7-1 分层结构示意图
从接口的角度来看,一般系统开发环境包括应用和系统两个接口,用以支持系统和应用程序开发,例如在Windows平台中,SDK代表了应用层接口,而DDK代表了系统接口,两个接口之间就是操作系统的实体。如图7-1所示,Windows CE操作系统在中间两层,应用支持库的上部和操作系统层的上部(主要是CoreDLL.dll,应用支持库中的很多接口也是透过DLL导出到系统中的)以及下部具有接口性质,它们构成了Windows CE的应用接口和系统接口。因而,从某种角度来看,OAL起到了系统接口的作用,而上面两层就是应用接口的使用者。
从层与层的互动来看,主要的互动发生在相邻两层之间,但上面三层具有一定的向下跨层存取能力。限制存取在一定程度上有助于功能的区隔,清楚的接口也使我们可以方便地置换掉某一层。
从宏观的角度来看,系统的软件组件有CoreDLL、NK、设备管理模块、数据储存模块、GWES模块、通信模块、OAL模块以及两个比较特殊的部分——驱动程序模块和Win32系统服务模块,这两个模块和其他的模块在划分上有一些重叠。
n NK
NK透过nk.exe在系统中运行,它是Windows CE操作系统的真正核心,主要包含以下6类:功能处理器进程、内存管理、异常处理、系统内的通信机制、为其他部分提供核心应用程序例程(routine)、为系统范围内侦错提供的支持。
NK.exe的程序代码非常精简,始终以较高的优先级和处理器特权级别(privilege mode)执行。除了中断处理例程,系统内其他的进程不能中断kernel程序,并且在虚拟储存管理模式下,kernel程序是不被允许换出(swap out)的,它被存放在系统储存空间从0xC2000000起始的位置。NK的程序代码位于[CEROOT]/PRIVATE/WINDOWS CEOS/COREOS/NK目录下。
n CoreDLL
CoreDLL在系统中具有举足轻重的地位,它分隔了应用程序和操作系统的其他模块,是使系统稳定的保护性屏障。它提供了两类功能:第一类是外部应用程序系统功能的代理;第二类则提供了类似字符串处理、随机数生成、时间计算等基本支持函数。前者是主要的功能,它负责系统API的管理和安装应用程序,系统应用程序的核心软件中断过程就发生在这里。这个模块是透过CoreDLL.dll运行的,它是第一段被加载的系统共享程序代码。CoreDLL的程序代码位于[CEROOT]/PRIVATE/WINDOWS CEOS/COREOS/CORE目录下。
n 设备管理模块
这是Windows CE的设备管理核心,透过Device.exe来执行。它提供系统范围内基本的设备列表管理、随插即用管理、电源管理、I/O资源管理,并提供了设备驱动程序运作的基本机制。这一部分的程序代码位于[CEROOT]/PRIVATE/WINDOWS CEOS/COREOS/DEVICE目录下。
n 数据存储模块 Windows CE体系结构
数据存储模块主要是提供系统基本的数据存储能力,其中包括对象存储以及文件系统,这些功能主要是透过filesys.exe来执行。数据存储模块并没有开放全部的原始程序代码,主要开放的原始程序代码部分位于[CEROOT]/PRIVATE/WINDOWS® CEOS/
COREOS/FSD目录以及[CEROOT]/PRIVATE/WINDOWS CEOS/COREOS/STORAGE目
录下。
n GWES模块
Windows CE通过这个模块提供的图形接口提供了几个主要的功能:基本的绘图引擎、窗口管理、接口的事件机制等。这个模块运行时为gwes.exe。这个模块的主要程序代码位于[CEROOT]/PRIVATE/WINDOWS CEOS/COREOS/GWE目录下,与数据存储一样,它的原始程序代码也只开放了一部分。
n 通信模块
在整个Windows CE系统中,网络通信模块是最为独立的部分,它通过一系列的动态链接库来运作,这一部分因为牵涉到较多的Windows平台公用特性,所以原始程序代码开放的程度也最低。也正因为这个原因,本书并没有这一部分的详细分析。对于通信模块的结构,可以找到的程序代码包括TAPI的一个实例、NDIS的一个实例版本(pcx500),它们位于[CEROOT]/PRIVATE/WINDOWS CEOS/COMM目录下。
n OAL模块
这个模块没有确定的形态,主要包括和硬件相关的若干功能,例如处理器的专用支持程序代码、总线控制器的驱动、系统引导程序、系统初始化程序等。一般来说OAL不具有可移植性。
n 驱动程序模块
驱动程序模块实际上并不是一个单独的软件实体,而是一个由驱动程序实体构成的集合,它包括很多组件,执行也比较复杂。它实际上是多个其他模块的底层,例如网络通信模块的下层就是驱动层,NDIS实际上可以看作一个具体的类别驱动程序。此外它是分散在系统中的,有大量的驱动程序分布在OAL中,而系统服务和协议也可以看作是驱动程序,它们由不同的模块管理,例如services.exe和gwes.exe。
n Win32系统服务模块
Win32系统服务是Windows CE对应用程序提供的接口,多数公用的系统管理函数,如产生进程等,都是由NK负责。模块中的黑色虚线框,它表示系统实际运行时,此模块某一部分内容被包含在nk.exe中了。这个模块也是有特殊性的,类似驱动模块,它实际上也包含多个模块的上层对外功能接口。在早期的CE版本中有一组基本的API函数管理集合,系统和用户可以通过这组函数,将自行定义的接口注册到系统服务应用程序代理中,因为系统稳定性的缘故,目前这些函数只能在系统内部使用,例如和设备管理相关的Win32 API,就是通过这组函数注册的。
Windows CE的中断体系由硬件中断、内核中断服务例程、OAL中断服务例程和IST程序组成。整个中断服务模型就是围绕着这5部分展开的。
图7-2是所示Windows CE中的中断体系结构的模型。
图7-2 中断体系结构模型
图7-2描述了中断过程中的主要转换过程,纵轴代表系统的抽象层次,横轴代表中断发生及处理的时间顺序。
系统最低层是硬件及其中断控制器。外围设备向处理器的中断信号线上发送中断信号,这时就产生了中断。
第二层是内核ISR。外设产生中断后,处理器立即转入中断服务向量表,一般也就是操作系统的中断服务向量表,然后搜索并执行内核阶段中断服务程序。由于外设的中断可能不仅是一个,有可能是多个中断而且有优先级,因此,内核级的中断服务程序主要做的就是禁止处理器上的所有具有相同和较低优先级的中断在本中断处理时发生中断。之后便进入到OAL的ISR例程。
第三层是OAL的ISR例程.这层由外设的生产商通过驱动实现其设备的特定功能,往往这部分程序会将集成在板级支持包BSP中。此后,OAL ISR既可以直接处理中断,也可以使用NKCallIntChain遍历已安装的ISR列表。主ISR或任何已安装的ISR随后执行任意工作,并且为该设备返回名为SYSINTR的映像中断。这里所说的处理中断,并未完成中断的全部功能,仅仅是完成了以下的简单功能。
A执行最小的中断处理,最小的中断处理指能够检验、答复产生中断的硬件,而把更多的处理工作留给IST。
B当ISR完成时返回中断ID(中断ID大部分是预定义的)。这个中断ID用来给IST标识中断,并且在IST返回后,通过这个ID通知内核此中断处理已经完成.可以释放资源,重新等待该中断的发生。
当然,如时钟这样非常简单的中断处理,可以不需要IST,如果不需要进一步执行IST,那么就返回SYSINTR_NOP。
最上层是驱动程序或应用程序的中断服务线程IST.在OAL ISR对中断进行简单处理后,IST就要具体地处理中断了,这包括从外设取回数据,通过特定协议和设备握手,将处理结果向GUI或者其他文件输出。在IST处理完后,必须返回中断ID,通知内核处理完毕。
对于有些应用场合,ISR可能并不是每次都希望通知IST进行处理.比如,每次发生中断时,ISR仅仅把设备的数据取回,存在缓冲区中,缓冲区未满时,ISR返回SYSINTR_NOP,等到缓冲区满了后,再返回SYSINTR,唤醒IST处理,这样就需要在两者间传递数据。
首先,在Config.bib文件中为ISR保留物理内存空间。Config.bib文件中已经为串口驱动保留了内存空间,大家可以将此作为例子,自己进行修改。在ISR中使用保留的内存空间。由于ISR运行在物理地址模式,因此ISR完成后启动IST前,还需要调用MMMapIoSpace函数,这个函数调用了VirtualAlloc和VirtualCopy函数,把特定的物理地址映像到虚拟地址中去。或者也可以自己显式地调用VirtualAlloc和VirtualCopy函数来实现映像功能。
另外,在可安装ISR中,由于内存可以被自由地动态分配,因此不需要在Config.bib中保留内存地址。
OEMInterruptEnable
BOOL OEMInterruptEnable(DWORD idInt,…)
{
BOOL bRet = TRUE;
switch(idInt)
{
case SYSINTR_POWER:
POWER_OFF_INT_CLR(1);
POWER_OFF_INT_MASK(1);
Break;
return bRet;
}
}
OEMInterruptEnable函数由内核调用,建立ISR和驱动程序的IST之间的映射关系。这个函数应当完成任何必须的操作来使设备有能力产生中断,同时驱动程序能接收并且处理中断。通常可能包括的操作有设置硬件优先级、使能端口和复位设备(以使设备能够产生中断)。
当设备驱动程序调用InterruptInitialize内核函数后,内核将调用OEMInterruptEnable。在本函数被执行的过程中,系统不应当发生抢占现象。OEMInterruptDisable的代码样例如下:
OEMInterruptDisable
BOOL OEMInterruptDisable(DWORD idInt)
{
…
switch(idInt)
{
case SYSINTR_POWER:
POWER_OFF_INT_MASK(0);
Break;
…
return bRet;
}
}
在OAL中必须实现一个能处理可安装驱动的函数例程。这个函数可以被用来卸载驱动程序。当需要卸载驱动程序并且已调用InterruptDisable时,内核会相应调用OEMInterruptDisable函数。在本函数被执行的过程中,系统不应该发生抢占的现象。本函数的调用参数指定了要关闭的中断号。函数OEMInterruptDone代码样例如下:
OEMInterruptDone
BOOL OEMInterruptDone(DWORD idInt)
{
…
switch(idInt)
{
case SYSINTR_POWER:
POWER_OFF_RISING_EDGE;
POWER_OFF_INT_MASK(1);
Break;
…
return bRet;
}
}
在系统处理中断的过程中,会将中断屏蔽。因此必须在中断处理完毕之后,调用相应的函数(OEMInterruptDone)清除中断屏蔽掩码。
当驱动程序调用InterruptDone时,内核会调用本函数。
由正文段(text)、用户数据段(user segment)以及系统数据段(system segment)共同组成一个进程执行环境,负责处理器、内存和外围等资源的分配和回收。进程是计算机系统资源的使用主体,是操作系统分配资源的基本单位。动态、独立、并行的Windows CE最多支持32个进程在系统中同时运行。
系统激活的时候,至少自动激活4个进程。第一个是NK.exe,用来提供操作系统中kernel的服务;第二个是FILESYS.EXE,用来提供相关文件系统的服务;第三个是GWES.EXE,用来提供对GUI系统的支持;第四个是DEVICE.EXE,用来加载和管理外围的驱动程序。它们占据虚拟地址的前4个slots,一个slot有32MB空间。
作为一先抢占式的、多任务的操作系统,Windows CE在系统之内最高支持32个正在运行的进程。每个进程由若干个线程组成,其中有一个为主线程。每一线程代表进程的一个独立的部分。进程也能创建未指定的数字的附加的线程。只有可用的系统资源才能限制实际的附加的线程数量。
从Windows CE 3.0开始,系统提供了256个优先级,0为最高优先级,255是最低的优先级。Windows CE 2.12中定义的8种优先级分别映射到了248~255。优先级255~248是为用户态下的应用程序线程所准备的。其余优先级更高的级别(247~0)则是为其他实时要求比较高的线程,如驱动、实时程序、内核线程所准备的。为了防止随机应用把系统的性能降级,OEM可能限定所有247点和零点之间的优先水平只能被应用在OEM指定应用程序中。
由于兼容旧的版本,GetThreadPriority and SetThreadPriority这两个旧的函数依然可以使用,但是建议程序员不要使用这两个函数,因为它们对应的是旧版本的8种优先级。在Windows CE.net中可以使用CeSetThreadPriority和CeGetThreadPriority来替代他们。CeSetThreadPriority用来设置线程的优先级,而CeGetThreadPriority从系统获取线程的优先级别。
Windows CE.net 中的优先级别大致分为如表7-1所示的4组。
表7-1 Windows CE.net中的优先级分组
Levels |
Description |
0到96 |
为实时要求比驱动强的实时程序保留 |
97 through 152 |
为基本的Windows CE驱动程序保留 |
153 through 247 |
为实时要求比驱动弱的实时程序保留 |
248 through 255 |
为非实时应用保留 |
进程由正文段(text)、用户数据段(user segment)以及系统数据段(system segment)共同组成,负责处理器、内存和外围等资源的分配和回收。进程是计算机系统资源的使用主体,是操作系统分配资源的基本单位。为了方便管理,在Windows CE中把进程当作对象(HANDLE hProc)。下面将简单介绍代表进程的数据结构的主要部分:
struct Process {
BYTE procnum; /* 00: ID of this process [ie: it's slot number] */
BYTE DbgActive; /* 01: ID of process currently DebugActiveProcess'ing this process */
BYTE bChainDebug; /* 02: Did the creator want to debug child processes? */
BYTE bTrustLevel; /* 03: level of trust of this exe */
#define OFFSET_TRUSTLVL 3 // offset of the bTrustLevel member in Process structure
LPPROXY pProxList; /* 04: list of proxies to threads blocked on this process */
HANDLE hProc; /* 08: handle for this process, needed only for SC_GetProcFromPtr */
DWORD dwVMBase; /* 0C: base of process's memory section, or 0 if not in use */
PTHREAD pTh; /* 10: first thread in this process */
ACCESSKEY aky; /* 14: default address space key for process's threads */
LPVOID BasePtr; /* 18: Base pointer of exe load */
HANDLE hDbgrThrd; /* 1C: handle of thread debugging this process, if any */
LPWSTR lpszProcName; /* 20: name of process */
DWORD tlsLowUsed; /* 24: TLS in use bitmask (first 32 slots) */
DWORD tlsHighUsed; /* 28: TLS in use bitmask (second 32 slots) */
PEXCEPTION_ROUTINE pfnEH; /* 2C: process exception handler */
LPDBGPARAM ZonePtr; /* 30: Debug zone pointer */
PTHREAD pMainTh; /* 34 primary thread in this process*/
PMODULE pmodResource; /* 38: module that contains the resources */
LPName pStdNames[3]; /* 3C: Pointer to names for stdio */
LPCWSTR pcmdline; /* 48: Pointer to command line */
DWORD dwDyingThreads; /* 4C: number of pending dying threads */
openexe_t oe; /* 50: Pointer to executable file handle */
e32_lite e32; /* structure containing exe header */
o32_lite *o32_ptr; /* o32 array pointer for exe */
LPVOID pExtPdata; /* extend pdata */
BYTE bPrio; /* highest priority of all threads of the process */
BYTE fNoDebug; /* this process cannot be debugged */
WORD wPad; /* padding */
PGPOOL_Q pgqueue; /* list of the page owned by the process */
#if HARDWARE_PT_PER_PROC
ulong pPTBL[HARDWARE_PT_PER_PROC]; /* hardware page tables */
#endif
};
n procnum:BYTE类别,目前进程的识别号码(ID),用来辨识不同的进程。
n pProxList:存放proxy的队列,LPPROXY结构的链接。
n hProc:这是此进程的句柄,在调用SC_GetProcFromPtr时使用。
n dwVMBase:DWORD类别,记录进程在内存所占区域中的基地址。
n pTh:一个进程可能拥有多个线程(详见线程介绍部分),pTh表示当前进程中的第一个线程。
n BasePtr:LPVOID类别,指向加载.EXE可执行文件的基底指针。
n lpszProcName:LPWSTR类别,记录进程的名称。
n PfnEH:进程例外处理器,PEXCEPTION_ROUTINE类别。
n pMainTh:此进程所拥有的主线程,当主线程结束后,进程也随之结束。
n pmodResource:PMODULE类别,MODULE结构在NK/INC/kernel.h中所定义。包含资源的模块指针,其中的资源可以被目前的进程用到。
n oe:openexe_t类别。指向可执行档句柄的指针。
以下结构由CreateProcess函数在创建进程和其主线程时填充的。
typedef struct _PROCESS_INFORMATION {
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
} PROCESS_INFORMATION;
成员功能介绍如下。
n hProcess 返回代表新进程的句柄。这个句柄在所有引用这个进程对象的函数中代表本进程。
n hThread 返回代表主线程的句柄。这个句柄在所有引用这个线程对象的函数中代表本线程。
n dwProcessId 返回全局的进程标识符,用来代表本进程。这个ID从进程创建一直到进程死亡都是有效的。
n dwThreadId 返回全局的线程标识符,用来代表本线程。这个ID从线程创建一直到线程死亡都是有效的。
这个数据结构被填充后,将用来填充Process结构的各个域。
表7-2 函数介绍
函 数 名 |
功 能 描 述 |
CeGetCallerTrust |
这个函数能返回进程的权限 |
CeGetThreadPriority |
这个函数能返回进程的优先级 |
CeGetThreadQuantum |
这个函数能返回线程占用处理器的时间量 |
CeSetThreadPriority |
这个函数设置了线程的优先级 |
CeSetThreadQuantum |
设置线程可以拥有的处理器占用时间 |
CeZeroPointer |
这个函数将一个被映射指向一个进程的指针转换成空指针 |
续表
函 数 名 |
功 能 描 述 |
CreateProcess |
创建新的进程 |
CreateThread |
在进程的上下文空间内,创建线程 |
ExitProcess |
结束进程以及它所有的线程 |
ExitThread |
结束一个线程 |
FreeLibraryAndExitThread |
将线程调用的DLL的引用计数减一,然后结束这个线程 |
GetCommandLine |
返回当前进程的command-line |
GetCurrentProcess |
返回当前进程的句柄 |
GetCurrentProcessId |
返回当前进程的ID |
GetCurrentThread |
返回当前线程的句柄 |
GetCurrentThreadId |
返回当前线程的ID |
GetExitCodeProcess |
返回特定进程结束时的状态(错误号) |
GetExitCodeThread |
返回特定进程结束时的状态(错误号) |
GetThreadPriority |
返回线程的优先级 |
GetThreadTimes |
获取和特定线程相关的时间片信息 |
IsProcessorFeaturePresent |
获取系统中支持的x86处理器的特性 |
OpenProcess |
打开已经存在的进程,返回进程对象 |
ResumeThread |
减少线程的悬挂计数,这样在任务调度时,就能使线程继续执行 |
SetThreadPriority |
这功能为指定的线程设置了优先级 |
Sleep |
这功能将当前的线程延缓执行一定间隔 |
SuspendThread |
挂起指定的线程 |
TerminateProcess |
终止进程和它所有的线程 |
TerminateThread |
终止指定的线程 |
ThreadProc |
指定了线程启动时要执行的用户态的函数 |
TlsAlloc |
为线程分配thread local storage (TLS) index |
TlsFree |
释放一个(TLS) index,以便重新分配 |
下面我们来具体看下其中几个对进程和线程比较重要的函数。
(1)CreateProcess
这个函数用来创建新的进程,运行新的程序。它创建了新的进程和它的主线程。新的进程将加载可执行文件。下面是函数的原型:
BOOL CreateProcess(
LPCWSTR lpszImageName,
LPCWSTR lpszCmdLine,
LPSECURITY_ATTRIBUTES lpsaProcess,
LPSECURITY_ATTRIBUTES lpsaThread,
BOOL fInheritHandles,
DWORD fdwCreate,
LPVOID lpvEnvironment,
LPWSTR lpszCurDir,
LPSTARTUPINFOW lpsiStartInfo,
LPPROCESS_INFORMATION lppiProcInfo
);
参数解释如下。
n lpszImageName 以null结尾的指针,指向可执行的模块。该字符串可以指定完全路径和文件名,或者相对路径。
n lpszCmdLine 以null结尾的指针,指向可执行的命令行。系统把一种无效的字符添加到命令行,修修串,以表明哪一文件正在被使用。
n lpszCmdLine可以为NULL.这样的话,系统会把lpszImageName中的字符串当作命令行。
如果lpszImageName和lpszCmdLine都非空,则* lpszImageName指定了执行模块的名称,而* lpszCmdLine代表了命令行。
如果文件名没有后缀的话,那么默认后缀为.EXE。
n lpsaProcess 保留,必须设置为NULL.
n LpsaThread 保留,必须设置为NULL.
n fInheritHandles 保留,必须设置为NULL.
n fdwCreate 规定附加的标记对进程的创建中的优先级控制。这些标记可以如7-3中的任意组合。
表7-3 进程创建中的优先级控制
值 |
描 述 |
CREATE_DEFAULT_ERROR_MODE |
暂时保留 |
CREATE_NEW_CONSOLE |
新的进程有自己的控制台,而非从父进程继承 |
CREATE_NEW_PROCESS_GROUP |
暂时保留 |
CREATE_SEPARATE_WOW_VDM |
暂时保留 |
CREATE_SHARED_WOW_VDM |
暂时保留 |
CREATE_SUSPENDED |
进程的主线程在被创建后立即进入挂起状态,直到ResumeThread被调用 |
CREATE_UNICODE_ENVIRONMENT |
暂时保留 |
DEBUG_PROCESS |
如果这个标志被设置,那么调用的进程将被视为被创建进程的调试进程。被创建进程的子进程也同样被调试。被调试进程产生的任何事件都将被传递到调试进程,只有调用CreateProcess函数的线程才可以调用WaitForDe- |
续表
值 |
描 述 |
DEBUG_ONLY_THIS_PROCESS |
功能和DEBUG_PROCESS标志一样,但是被调试进程的子进程不在调试范围内 |
DETACHED_PROCESS |
暂时保留 |
n lpvEnvironment 暂时保留不使用设置成NULL。
n lpszCurDir 暂时保留不使用设置成NULL。
n lpsiStartInfo 暂时保留不使用设置成NULL。
n lppiProcInfo 指向PROCESS_INFORMATION结构,这个数据结构用来接收新进程的标识信息。
函数返回值:非0代表成功,0代表失败。如果要得到详细的错误代码,可以调用GetLast-
Error函数。
(2)CreateThread
这个函数在调用进程的进程上下文中创建线程。
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
参数解释如下。
n lpThreadAttributes 暂时保留,必须设置成NULL.
n dwStackSize 一般情况下被忽略,除非STACK_SIZE_PARAM_IS_A_RESERVATION标志被使用,这时,dwStackSize参数指定了为要创建的线程保留的虚拟内存区域。当被忽略时,线程的堆大小是默认的。
n lpStartAddress 指定了应用程序中在线程被启动时要执行的函数。
n lpParameter 指向传递给线程的32位长的参数指针。
n dwCreationFlags 设置了线程被创建后的状态。这些状态可以是表7-4所示项目的组合:
表7-4 线程创建后的状态表
值 |
描 述 |
CREATE_SUSPENDED |
线程被创建后将立即被挂起,直到ResumeThread函数被调用使被创建的线程获得CPU的占用权。当此标志没有被设置时,线程在被创建后可以立即执行 |
STACK_SIZE_PARAM_IS_A_RESERVATION |
如果此标志被设置,参数dwStackSize则代表线程可以拥有的最大内存数量 |
n lpThreadId 32位长的指针,指向的空间中保存了线程的ID。
如果这个参数被设置成NULL。线程的ID将不被返回。返回值非空返回代表了某个线程的ID,NULL代表失败。
(3)CeSetThreadPriority
这个函数设置了实时线程的优先级。函数原型为:
BOOL CeSetThreadPriority(
HANDLE hThread,
int nPriority
);
参数解释如下。
n hThread 一个线程的句柄。
n nPriority 为线程设置的优先级。可以从0到255,其中0是最高优先级别。返回值为TRUE代表设置成功;FALSE代表失败。
n Remarks 当一个已经被阻塞的线程的优先级增加时,被调度程序将立即判断该线程是否需要状态改变。而当一个已经被阻塞的线程的优先级减少时,丝毫不会影响线程当前的状态。
(4)CeGetThreadPriority
此函数返回实时线程的优先级。
int CeGetThreadPriority(
HANDLE hThread
);
参数功能解释如下。
n hThread 线程的句柄。如果成功,则返回线程的优先级,否则返回THREAD_
PRIORITY_ERROR_RETURN。
这个函数仅仅返回线程的原始优先级,如果由于优先级逆转等原因,使得线程的实际优先级和原始优先级不同时,此函数返回的值可能会与实际线程的优先级不一致。
schedule.h中有关于线程时间的数据结构如下。
typedef struct THREADTIME {
struct THREADTIME *pnext;
HANDLE hTh;
FILETIME CreationTime; /*建立时间*/
FILETIME ExitTime; /*结束时间*/
FILETIME KernelTime; /*kernel-mode时间*/
FILETIME UserTime; /*user-mode时间*/
} THREADTIME, *LPTHREADTIME;
有关临界区的数据结构如下。
typedef struct CRIT {
LPCRITICAL_SECTION lpcs; /* Pointer to a critical_section structure */
LPPROXY pProxList;
LPPROXY pProxHash[PRIORITY_LEVELS_HASHSIZE];
LPCRIT pPrev; /* previous event in list */
BYTE bListed; /* Is this on someone's owner list */
BYTE bListedPrio;
BYTE iOwnerProc; /* Index of the owner process */
BYTE bPad;
struct CRIT * pPrevOwned; /* Prev crit/mutex (for prio inversion) */
struct CRIT * pNextOwned;
struct CRIT * pUpOwned; /* Next crit/mutex section owned (for prio inversion)
*/
struct CRIT * pDownOwned;
LPCRIT pNext; /* Next CRIT in list */
} CRIT;
有关可执行线程队列的数据结构如下。
typedef struct
{
PTHREAD pRunnable; /* 可执行线程的队列*/
PTHREAD pth; /* 目前正在执行的线程*/
PTHREAD pHashThread[PRIORITY_LEVELS_HASHSIZE];
} RunList_t;
pRunnable 是指向可执行队列的指针,这个队列是由双向链表(doubly-linked list)组成的。PRIORITY_LEVELS_HASHSIZE的值是256,所以,pHashTable共有256个entry,每一个entry指向一个优先权等级大小不同的线程队列。
有关线程等待队列的数据结构如下所示。
typedef struct sleeper_t {
PTHREAD pth; /*指向睡眠队列的指针*/
WORD wCount2;
WORD wDirection;
DWORD dwWakeupTime; /* 下一个线程最长要被唤醒的时间 */
} sleeper_t;
结束线程(void RemoveThread(RTs *pRTs)函数)的相关结构如下所示。
typedef struct RTs {
PTHREAD pHelper;
DWORD dwBase, dwLen; //如果要释放堆栈,将用到此信息
DWORD dwOrigBase; //如果在fiber堆栈中,则需要释放的初始基址信息
PPROCESS pProc; //如果要释放进程,将用到此信息
LPTHRDDBG pThrdDbg; //如果要释放一个侦错结构,将用到此信息
HANDLE hThread; //如果要释放一个句柄或线程时间,将用到此信息
PTHREAD pThread; //如果要释放一个线程数据结构,将用到此信息
CLEANEVENT *lpce1;
CLEANEVENT *lpce2;
CLEANEVENT *lpce3;
LPDWORD pdwDying;
} RTs;
Windows CE处理程序间的通信(IPC,Interprocess Communication)可以使用临界区(critical section)、事件(event)、同步(synchronization)以及互斥器(mutex)。对于嵌入式系统来说,这些方法简单实用、并且不耗费系统成本。
当多个线程都得到了同一对象的访问权限后,它们对对象的操作可能会导致数据的不一致。使用临界区对象,就能使某个对象或者某段代码避免被多个线程同时修改,导致脏数据的产生。临界区使得对象或者代码段只能由一个线程来操作,它们可以是一个DLL或者是一个进程空间中的线程。
这是系统内部最基本的互斥方式,经常用于重要系统资料的修改。一般情况下,临界区对象用于保证一段程序代码执行的原子性。临界区对象的使用被限制在某一个处理程序的上下文中,它不能被多个处理进程同时使用。
使用InitializeCriticalSection函数可以向系统取得一个临界区对象,当使用完毕之后,需要调用DeleteCriticalSection释放该资源。一段临界程序代码执行时,相关的临界区对象句柄必须是有效的,而这段程序代码需要以EnterCriticalSection或者TryEnterCriticalSection函数开始,以LeaveCriticalSection结束。这样,系统可以保证当多个进程试图同时进入同一段临界程序代码时,只有一个能获得临界区的进入权限。
在进入临界区之前,线程应当调用EnterCriticalSection函数或者TryEnterCriticalSection函数来获取临界区的进入权限。如果在它之后,有另外一个进程调用了这两个函数,也无法得到临界区的进入权限,直到前一线程调用LeaveCriticalSection函数来释放临界区的进入权限。
在使用临界区之前,必须先申明CRITICAL_SECTION类型的结构体。在其他操作临界区对象的函数中需要有一个指向此结构的指针,因此这个结构体的定义必须在所有对其操作函数可见的范围之内。然后在InitializeCriticalSection中初始化这个结构体并且返回指向被初始化完的结构体的指针。
接着,可以使用EnterCriticalSection或者TryEnterCriticalSection获取临界区的占用权限。要释放这种权限,应该调用LeaveCriticalSection函数。当使用完临界区后,可以调用DeleteCriticalSection释放临界区对象。
下面是使用临界区的各种API的函数原形,需要注意的是,它们都需要指向LPCRITICAL_
SECTION结构的指针。
void InitializeCriticalSection (LPCRITICAL_SECTION lpCriticalSection);
void EnterCriticalSection (LPCRITICAL_SECTION lpCriticalSection);
void LeaveCriticalSection (LPCRITICAL_SECTION lpCriticalSection);
void DeleteCriticalSection (LPCRITICAL_SECTION lpCriticalSection);
下面的程序显示了如何在一个线程中初始化、进入、离开临界区。这段程序使用了异常捕获机制try-finally来确保线程在调用LeaveCriticalSection后一定会释放临界区。
void helloworld()
{
//获取进入临界区的权限
EnterCriticalSection (&sampleCriticalSection);
//这将使得调用线程被阻塞,直到它能获取这样的权限。
__try {
//在这里可以实现对资源的操作
} __except (EXCEPTION_EXEUTE_HANDLER) {
// 异常处理
}
// 解除临界区的进入权限
LeaveCriticalSection (&sampleCriticalSection);
}
事件对象用来通知某个进程发生了特定的事件或者告诉这个进程该做什么事了。例如在触摸屏上采样到一个点后,驱动程序便会设置一个特定事件,系统就会触发和该事件相关联的进程,并告诉它触摸屏上有采样变动。
类似临界区对象,事件对象使用前也需要向系统要求资源句柄,在使用后则必须释放句柄,不同的是,系统由SetEvent或者PulseEvent设置一个事件,使用ResetEvent可以还原这个事件对象的状态。
以下是CreateEvent函数原型:
HANDLE CreateEvent (NULL, BOOL bManualReset, BOOL bInitialState,
LPTSTR lpName);
可以使用lpName参数来指定事件对象的名称,也可以让事件对象处于未命名状态。bManualReset参数设置了事件对象复位的方式,是自动复位还是需要显式地调用相关操作函数复位。使用bInitialState参数来指定事件对象在被创建后处于signaled或者nonsignaled状态。后者的情况下,需要先调用ResetEvent然后再使用事件。
命名事件可以被其他进程共享,其他进程的线程可以通过在CreateEvent中指定事件名,来获取已经存在的事件的句柄。所有命名的同步对象都存储在同一个队列中。要判别CreateEvent函数究竟是创建了新的对象还是打开了已经存在的对象,可以在调用CreateEvent后立即调用GetLastError函数,如果GetLastError返回了ERROR_ALREADY_
EXISTS值,那么CreateEvent打开了一个已经存在的事件。也就是说,当CreateEvent函数中的事件名参数和已经存在的某事件一致时,函数返回已经存在的时间的句柄。当使用这个机制后,调用进程不应该立即去获得这个事件的所有权,因为它可能被别的进程占用了。
要想触发事件,应当使用SetEvent或者PulseEvent函数。SetEvent函数在发出事件消息后并不将事件复位,而PulseEvent在发出事件消息后会将事件自动复位。
如果事件能够自动复位,那么程序员就只需要调用SetEvent即可。如果要手动复位事件对象的话,应该调用ResetEvent函数。
如果要关闭事件对象,可以调用CloseHandle函数。对于一个有名事件,Windows CE为它维护了一个引用计数器,对于每个调用CreateEvent的进程,都应该调用CloseHandle。
下面的程序实现了写线程和读线程对受保护资源的访问。主程序声明了一个对象,并且是不允许自动复位的。其中,当一个线程要占用资源时,通过调用WaitForSingleObject来等待事件的发生。写线程在进入时,会手动地复位事件。代码样例7-5如下:
VOID ReadThreadFunction (LPVOID lpParam); //函数申明
#define NUMTHREADS 4
HANDLE hGlobalWriteEvent;
HANDLE hReadEvents[NUMTHREADS];
void CreateEventsAndThreads (void)
{
HANDLE hThread; // 新建线程
DWORD dwThreadID; // 新建线程ID
int i;
//读事件的建立
hReadEvents = CreateEvent (NULL, // 没有安全标志
FALSE, // 允许自动复位
TRUE, // 初始状态为signaled
NULL); // 未命名对象
if (hReadEvents == NULL)
{
// 创建事件失败处理
}
//创建读线程
hThread = CreateThread (
NULL, // Windows CE中没有安全标志s
0, // 在Windows CE只能为0
(LPTHREAD_START_ROUTINE) ReadThreadFunction, //指定要
//启动的应用程序函数
&hReadEvents, // 传入事件对象的引用作为参数
0, // 线程立即创建
&dwThreadID); // 返回线程ID值(在此对我们没有关系)
if (hThread == NULL)
{
//线程创建失败后的处理程序
}
} //创建事件和读线程的函数结束
/*
* 在下面的程序中,在写线程向竞争资源写入之前,先调用WaitForSingleObject
* 函数。如果这时候有读线程在访问竞争资源的话,写线程将被阻塞。当受到事件通知
* 表示读线程完成操作的时候,函数WaitForSingleObject返回,这时候,写线程可以
* 安全的对竞争资源进行操作。写完之后,再调用SetEvent来通知读线程对竞争资源
* 进行读操作。
*/
int WriteToBuffer (void)//写线程
{
DWORD dwWaitResult;
int i;
//等待读信号,即读进程已经完成
while(1)
{
dwWaitResult = WaitForSingleObject (hReadEvents, INFINITE);
if (WAIT_OBJECT_0 != dwWaitResult)
{
// 错误处理
}
// 在这里已经可以对对象资源进行安全的写操作了。
// 写操作完成,触发读事件,通知读线程启动
if (!SetEvent (hReadEvents))
{
// 错误处理
}
}
return 1;
} // End of WriteToBuffer example code
/*
* 在下面的代码中,当读线程要读竞争资源中的数据前,调用WaitForSingleObject
* 函数来等待事件的发生,如果这时候写线程正好在竞争资源中进行操作,则读线程被
* 阻塞。 直到写线程发出SetEvent后,函数才返回,线程执行读操作。读操作完之后,
* 再次调用SetEvent通知写线程进行操作。
*/
VOID ReadThreadFunction (LPVOID lpParam)
{
DWORD dwWaitResult;
HANDLE hEvent;
BOOL bStayInLoop;
hEvent = * (HANDLE *) lpParam; //从createthread传入的参数,即读事件
bStayInLoop = TRUE;
while (bStayInLoop)//循环读
{
// 等待读事件
WaitForSingleObject (hEvent, INFINITE);
if (WAIT_OBJECT_0 != dwWaitResult)
{
// 错误处理
}
// 这里可以读对象资源中的内容
//...
// 读完成,触发事件
if (!SetEvent (hEvent))
{
// 错误处理
}
}
}
互斥器的拥有者只能有一个线/进程,互斥器是一个同步对象。当没有任何进程占用这个对象互斥器时,该互斥器处于non-signaled状态,一旦任何线/进程占用了这个互斥器,它就进入signaled状态。当使用CreateMutex申请资源成功时,进程就拥有了该资源的所有权,别的进程申请同样的资源就会进入等待队列,直到这个进程调用ReleaseMutex函数,释放资源为止。如果进程退出的时候没有调用ReleaseMutex,则这个资源会自动释放,等待队列上的第一个进程就会得到这个互斥器的所有权。
线程可以通过调用CreateMutex函数来创建互斥器对象。创建完毕后,线/进程立即申请对互斥器的占用权并且为互斥器命名。其他进程的线程也可以通过在CreateMutex中设置互斥器名称的方式来获取一个已经存在的互斥器的句柄,这一点和有名事件的机制非常相似。如果互斥器对象已经存在,在CreateMutex后立即调用GetLastError将返回ERROR_ALREADY_
EXISTS。
一旦得到了互斥器的句柄,线程就能通过调用wait函数来尝试获取互斥器对象的占用权。如果互斥器对象正被别的线程占用,wait函数将阻塞调用线程,直到其他线程释放这个互斥器对象。wait函数的返回值可以是以下两种情况:函数由于其他某种原因未完成操作而返回;操作成功,互斥器对象已经进入signaled状态。
一旦线程占用了一个互斥器对象,它就可以反复地调用这个对象而不会被阻塞。这样就能防止线程等待自己已经占用的互斥器对象受阻塞而引起的死锁。同样,线程每次占用了多少次互斥器,也即成功调用了多少次CreateMutex,在释放互斥器时,也应该相应地调用多少次ReleaseMutex。
在线程没有释放互斥器直接退出时,互斥器将自动释放。这样,等待这个互斥器对象队列中的下一个线程就可以获取这个互斥器的占用权限,但是wait函数返回的仍然将是互斥器被撤消的代表值。在这样的情况下,互斥器就已经失去原来的作用,因此为安全起见,任何被互斥器保护的对象都应该被视为处于未知状态。
代码样例7-6所示的程序显示了如何调用CreateMutex来创建一个有名的互斥器对象。
void NamedMutexExample (void)
{
HANDLE hMutex;
TCHAR szMsg[100];
hMutex = CreateMutex (
NULL, // No security descriptor
FALSE, // Mutex object not owned
TEXT("NameOfMutexObject")); // Object name
if (NULL == hMutex)
{
// Your code to deal with the error goes here.
// Here is one example of what might be done.
wsprintf (szMsg, TEXT("CreateMutex error: %d."), GetLastError ());
MessageBox (NULL, szMsg, TEXT("Error"), MB_OK);
}
else
{
// Not an error -- deal with success
if ( ERROR_ALREADY_EXISTS == GetLastError () )
MessageBox (NULL, TEXT("CreateMutex opened existing mutex."),
TEXT("Results"), MB_OK);
else
MessageBox (NULL, TEXT("CreateMutex created new mutex."),
TEXT("Results"), MB_OK);
}
} // End of NamedMutexExample code
下面是互斥器的数据结构:
typedef struct MUTEX {
HANDLE hNext; /*指向列表中下一个互斥器*/
LPPROXY pProxList;
LPPROXY pProxHash[PRIORITY_LEVELS_HASHSIZE];
HANDLE hPrev; /*指向列表中前一个互斥器*/
BYTE bListed;
BYTE bListedPrio;
WORD LockCount; /*当前lock count */
struct MUTEX *pPrevOwned;
struct MUTEX *pNextOwned;
struct MUTEX *pUpOwned;
struct MUTEX *pDownOwned;
struct Thread *pOwner; /* 宿主线程*/
Name *name; /* 指向事件的名称*/
} MUTEX;
信号量和互斥器的基本原理是一样的,但是它允许特定数目的进程共享这个资源,获得和释放的函数分别是Createsemaphore和Releasesemaphore。
信号量是通过维护引用计数器来实现进程间同步的同步对象。它的计数器计数范围从0到某个设定的最大值。当对象的计数超过0的时候,对象就处于signaled状态。信号量可以被视为资源计数控制器,它可以将对资源的访问线程数控制在某个范围内。
可以使用Createsemaphore函数来创建一个有名或者无名的信号量对象。返回的信号量句柄可以被用在任何需要使用信号量的函数中。
可以使用Releasesemaphore函数来减少信号量的引用计数。信号量的计数器不能为负值,也不能超过Createsemaphore函数lInitialCount参数设定的最大值。
最典型的,应用程序使用信号量来控制某一资源的访问线程数。在线程要获取资源之前,它必须在wait函数中设置信号量的句柄。当wait函数返回后,信号量的计数器就减1,线程就可以自由地对资源进行操作。当线程完成对资源的操作后,它将调用Releasesemaphore释放信号量,这时信号量的计数器会增加1。
Releasesemaphore不仅仅能实现释放信号量的功能,同样还可以在函数初始化的时候起到保护资源的作用。例如,应用程序希望某一资源在其初始化过程中不被其他线程修改,则它在初始化时候调用Createsemaphore并将引用计数器设置为0,这样没有任何其他的线程能获得这个信号量。等到这个应用程序完成初始化的时候,再调用Releasesemaphore,这样就能使得信号量的引用计数器加1,也可以持续调用这个函数,直到引用计数器的值达到最大值,从而线程能争占这个信号量。
信号量可以跨进程使用。多个进程都可以拥有同一个信号量的句柄。同样是使用Createsemaphore函数并且指定信号量的名称,就可以获取已经存在的信号量的占用权限。
使用CloseHandle函数就能结束对一个信号量的引用,当信号量的最后一个引用被释放后,信号量对象就将被撤消。
下面是信号量的数据结构:
typedef struct semaphore {
HANDLE hNext; /* 指向下一个链表中的信号量*/
LPPROXY pProxList;
LPPROXY pProxHash[PRIORITY_LEVELS_HASHSIZE];
HANDLE hPrev; /*指向前一个链表中的信号量*/
LONG lCount; /*当前的引用值*/
LONG lMaxCount; /*最大引用计数值*/
LONG lPending; /* Pending count */
Name *name; /*内存区定义了信号量的名称*/
} semaphore;
Windows CE提供了三种类型的文件系统——RAM-based文件系统、ROM-based文件系统以及用于支持ATA(Advanced Technology Attachment)设备和SRAM卡等外围储存设备的FAT文件系统。前两种文件系统属于Windows CE的内建文件系统,后者属于可安装性文件系统。另外,嵌入式系统的开发人员也可以编写自己的文件系统,并在系统中注册使用。Windows CE提供了platform-independent API,不论是何种储存设备,所有对文件系统的存取都是通过Win32 API完成。
Windows CE.NET的文件系统是非常灵活的模块,它可以兼容用户自己开发的文件系统、文件过滤器以及其他的各种块设备文件系统。所有文件系统以及和文件操作相关的API都由FileSys.exe进程提供。这个进程模块实现了对象存储和存储管理器的功能。在Windows CE .NET中所有文件和文件管理系统都存在于一个以“/”开始的名字空间下。所有文件都位于从根文件开始的一棵树中,并且以特定的路径惟一标识。这点和Windows桌面系统非常相似。在Windows CE中,设备被mount到根文件下的文件夹中。这样,当存储卡这样的设备添加到系统中时,就会自动映射。
FileSys.exe由以下几个组件构成:
n ROM文件系统
n 存储管理器
n 对象存储
对象存储是一种由FileSys.exe控制的内存堆。对象存储包括了RAM中的系统注册表、RAM文件系统和属性数据库。这3个组件都是FileSys.exe模块可选择的支持功能。RAM文件系统和属性数据库是完全可以选择的功能,在一些系统上可能根本找不到它们的影子。而注册表虽然是可以选择的,但是大多数情况下许多应用程序和系统服务以及驱动程序都需要注册表的支持。因此注册表一般都留驻在对象存储的区域中。在Windows CE .NET中,注册表可以以文件的形式留驻在外部存储器设备上。
RAM文件系统通常和统一的文件系统的根目录相连接。ROM文件系统和“/Windows”文件夹相连接。这意味着所有ROM中的文件都能通过“/Windows”文件夹以只读文件的形式进行访问。
存储管理器是Windows CE .NET新增加的模块,它负责管理系统中的存储设备以及使用这些设备所必须的文件系统。存储管理器有4项主要任务需要处理:
n 存储器驱动:这是物理存储器的驱动程序,通常被称为块设备驱动。
n 分区驱动:这层驱动提供了在物理设备上划分多个存储区的功能。Windows CE .NET允许存储设备被划分成多个存储区,并且可以用不同的文件格式来格式化。本层驱动的主要任务是将分区地址转换成设备的物理地址,然后将请求传递给存储器驱动。
n 文件驱动:这层驱动将数据以文件和文件夹的形式组织起来。在这里可以实现FATFS(包括FAT32支持),将组织完的数据传递给分区驱动。
n 文件过滤器驱动:文件过滤驱动程序是为了特定的文件处理要求而设立的(例如加密、压缩数据等),这和Windows桌面系统中的概念相一致。在处理完数据后,再将数据传递给文件驱动程序。
图7-3所示是Windows CE .NET中的驱动层次的概要图。
图7-3 驱动层次的概要图
对象存储和ROM文件系统中没有前面所述的4种驱动。目前为止,微软公司仍然没有提供对这两个文件系统的过滤驱动机制。下面将对存储管理器做更深入地分析。
如图7-4所示,并不是所有的文件系统驱动都使用了物理设备,即便使用了物理设备,也不一定会使用分区驱动。比如网络重定向器,负责提供网络资源,在它的下层就是使用了WinSock接口来通过网络和远程服务器通信,这个重定向器的底部并没有具体的物理设备。
系统是如何加载这些层次的驱动程序呢?当操作系统启动的时候,NK.exe进程直接从ROM文件系统加载FileSys.exe。接着FileSys.exe按照ROM文件系统中的默认注册表文件初始化系统注册表。
然后FileSys.exe进程会从注册表的入口读取数据,启动各种应用级进程。第一个被启动的程序通常就是Device.exe——设备管理模块。Device.exe从注册表的HKEY_LOCAL_MAC-
HINE/Driver/BuiltIn键中加载各种设备。通常在这个键下罗列了集成在系统内的设备,比如硬盘等。这样,块设备驱动就将被加载。块设备驱动引用了一个特殊的类描述标识——BLOCK_DRIVER_GUID{A4E7EDDA-E575-4252-9D6B-4195D48BB865}.
图7-4 存储管理器
图7-5 层次接口的构成 |
存储管理器为特定设备读取分区驱动的信息,并且加载合适的驱动。一旦分区驱动被加载后,存储管理器会要求分区驱动枚举设备上的分区,并且判定各个分区上的文件系统类型。分区驱动会从主启动记录Master Boot Record(MBR)中读取有关分区和文件系统的信息,并且返回给存储管理器。存储管理器用这些信息来为每个分区加载文件系统驱动,并将这些文件系统驱动添加到全局的文件系统之下。在这样的框架之下,即便是网络重定向器或者是DVD ROMs设备,都将以同样的步骤被探测和加载。
下面仔细分析文件系统驱动和文件系统驱动管理器(File System Driver Manager,FSDMGR)的重要性。FSDMGR是存储管理器的一部分,负责向提供文件系统驱动提供服务。文件系统驱动并不需要知道传入的数据是从分区而来,还是从设备存储区直接进来的。也就是说,分区对文件系统驱动是透明不可见的。FSDMGR为文件系统提供了向上和向下的接口。图7-5所示为这个层次接口的构成。
存储管理器调用文件系统驱动(FSD),然后FSD使用SDMGR_APIs接口从设备获取数据。如果存储设备是个类似硬盘的设备,那么设备将通过FSDMGR_API接口和文件系统驱动进行通信。
现在回过头来再继续看驱动加载的顺序。Profile位于注册表中的HKEY_LOCAL_MAC-
HINE/System/StorageManager/Profiles位置。
每个profile项都是一个在基准profile项下的子项。例如,当使用硬盘时,profile中就会有项目HKEY_LOCAL_MACHINE/System/StorageManager/Profiles/Hard Disk。Profile中所有的信息都存储在value中,表7-5所示为这些value的说明。
表7-5 Value值的说明
值 |
类 型 |
描 述 |
Folder |
REG_SZ |
文件夹名,这和在图形界面中看到的/目录下的文件名一致。但是,当有多种实例时,在文件名后会追加一个整数。比如,Storage Card、Storage Card2等 |
FileSystem |
REG_SZ |
设备使用的默认的文件系统,一般这个项下的值不会被使用,因为分区驱动会确定各个分区上的文件系统 |
PartitionDriver |
REG_SZ |
当默认的文件系统不匹配时,分区中应该使用的文件系统。如果这个值是空,系统将不会加载分区文件系统。如果没有提供这个项,则系统不会加载分区驱动 |
AutoFormat |
REG_DWORD |
如果磁盘没有被格式化,则自动格式化 |
AutoPart |
REG_DWORD |
如果磁盘没有分区,则自动将其分区,其中一个分区将占用磁盘绝大多数的空间 |
AutoMount |
REG_DWORD |
当存储设备驱动被加载时,自动挂载文件系统 |
Name |
REG_SZ |
在控制面板中显示的profile名 |
MountFlags |
REG_DWORD |
设置如何挂载文件系统的标志 |
MountFlags项需要注意,表7-6所示为这个项各种值的含义。
表7-6 MountFlags项各种值的含义
标 记 |
描 述 |
标 记 |
描 述 |
1 |
隐藏文件系统 |
4 |
加载到文件系统的根("/") |
2 |
可能含有hive注册表 |
8 |
隐藏ROM文件系统 |
如果将文件系统设置成隐藏,那么任何分区文件系统的自举都不会发现这个文件系统,同样在用户界面中也不会有相应的图标。在文件系统操作的API中,例如FindFirstFile等也不会返回被隐藏的文件系统的入口点。但是,即便FindFirstFile无法识别被隐藏的文件系统,如果用户知道文件系统的名称,则同样可以自由地使用被隐藏的文件系统。下面的代码探测了是否存储管理器正在使用一个文件系统。
BOOL IsStorageManagerUsingFs()
{
DWORD attr = GetFileAttributes( _T("//StoreMgr") );
if( (attr != -1) && (attr & FILE_ATTRIBUTE_TEMPORARY) )
return TRUE;
return FALSE;
}
当用户希望把布置的文件系统连接到全局文件系统,并且作为根文件系统的话,将使用MountFlags的下两个位。通常情况下,全局文件系统的根可以是RAM文件系统。在电池供电的设备中,RAM文件系统通常都被指定为根,因为电池供电设备中电源并不真正关闭,RAM中始终有电力支持,因此数据不会丢失。但是如果在一些AC供电设备的环境下,电源可能是真正会被关闭的,因此不能使用RAM文件系统作为全局文件系统的根。出于对这种情况的考虑,需要使用外部文件系统作为全局文件系统的根,例如使用Flash上的文件系统。
如果profile中任何项目的值没有被提供,存储管理器会使用HKEY_LOCAL_MACHINE/
System/StorageManager/Profiles键下的默认值来代替。这些默认设置可以在COMMON.REG中修改,当然这是为所有平台提供的默认值,一般建议程序员在PLATFORM.REG或者PROJECT.REG中修改。表7-7列举了COMMON.REG中的一些默认值。
表7-7 Common.reg中的一些默认值
项 |
默 认 值 |
Folder |
LOC_STORE_DEFAULT_FOLDER |
FileSystem |
FATFS |
PartitionDriver |
Mspart.dll |
AutoFormat |
0 |
AutoPart |
0 |
AutoMount |
1 |
MountFlags |
0 |
综上所述,Windows CE的文件系统结构是复杂但又灵活的,它提供了以下支持:
n 多个块设备
n 每个块设备上的多分区
n 每个分区上不同的文件系统
n 支持将外部设备的文件系统当作根文件系统来加载
Windows CE整合Microsoft Win32 API、用户接口及GDI(Graphics Device Interface)的函数库,建构了GWES(Graphics、Windowing和Events Subsystem)模块(GWES.exe)。GWES是介于用户、应用程序及操作系统间的一组接口,它支持了所有用户控制应用程序的Windows CE用户接口,包括窗口、对话框、控件、选单以及资源。GWES也提供了用户关于位图、carets、光标、文字及图标的相关信息,即使是缺乏图形用户接口的那些Windows CE平台仍使用GWES基本的窗口与信息功能及电源管理函数。
Windows CE将Win32 API的用户界面(User32)和图形设备接口(GDI32)合成一个新的模块gwes.exe,称为GWE(Graphics Window Manager Event Manager)子系统。
Windows CE GWE子系统中的USER部分包含3大模块:
n 用户输入系统(User Input System)
n 事件管理器(Event Manager)
n 窗口管理器(Window Manager)
关于GWE的源程序位于[CEROOT]/Private/Winceos/Coreos/GWE目录,而且GWE子系统中只有GDI部分公开了少量的原始程序代码。
基于Windows CE的程序设计要使用消息循环。消息循环是在Windows应用程序中的一种循环。在Windows CE中,独立的菜单栏和工具栏将占据太多的空间,所以Windows CE将菜单栏和工具栏结合成一个新的控件,称为命令条。以前的菜单被放在非用户区而工具栏放在用户区,现在菜单和其他控件一样被放在命令条中,并且由命令条实际操作菜单控件的功能。对话框管理器位于窗口管理器的上层。它负责接收系统传送过来的消息,并且把它们发送到相对应的窗口,直到系统表明所有的消息都发送完毕,消息循环才结束。包含在WinMain函数中,处理消息循环的函数是WndProc。
用户输入系统分为消息队列、窗口管理器两大部分。消息队列Msgque是任何需要消息传递的地方所必须的部分,因为要实现用户输入就必须能够把输入的信息传给所需要的窗口。Wmbase组件的作用是建立窗口,为窗口提供窗口处理函数WndProc,并且给它发送消息。窗口管理器Winmgr负责把绘图操作的结果显示在屏幕上。
消息队列机制是所有windows程序员所熟悉的,它将应用程序图形界面的事件发送到消息队列中,然后通过分发器将这些消息发到目的窗口。
在WinMain()函数中经常看见这样的消息循环:
while (GetMessage((&msg...)){
TranslateMessage(&msg);
DispatchMessage(&msg);
每个用户进程都对应了一个消息队列。
在windows体系结构中,发送消息主要用到两个函数——SendMessage和PostMessage。这两个函数有一点区别。当SendMessage返回时,表明目的窗口已经对事件做出了响应。而PostMessage仅仅是将消息传递到队列中,然后就立即返回。下面仔细分析这两个函数。
(1)PostMessage
这个函数将消息放入创建目的窗口的线程的消息队列中,然后它就立即返回,而不等待线程处理消息。消息队列中的消息可以用GetMessage或者PeekMessage函数来获得。下面是PostMessage的函数原型。
BOOL PostMessage(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam
);
参数说明如下。
n hWnd 指向要接受消息的窗口的进程。有两种值是有特别意义的。如表7-8描述。
表7-8 hWnd值的描述
值 |
描 述 |
HWND_BROADCAST |
这个消息将作为广播发送到所有顶级的窗口。这些窗口包括了被disabled的窗口、不可见的窗口和弹出的窗口。但是,这些消息不会被传送到它们的子窗口 |
NULL |
当dwThreadId设置成当前线程的ID后,这个函数就能像PostThreadMessage一样发挥向线程发送消息的功能 |
n Msg 描述了要被传递的消息。
n wParam 描述了额外的和消息一起传送参数。
n lParam 描述了额外的和消息一起传送参数。
n 返回值 非0代表操作成功,0代表出错。
n Remarks 如果应用程序要使用HWND_BROADCAST来广播,则使用RegisterWin-
dowMessage函数来获取一个特殊的消息,这样在传递时会显的更安全。
(2)SendMessage
这个函数将一个特定的消息传递到目的窗口。SendMessage调用目标窗口的处理进程,并且一直等到进程完成对消息的处理后再返回。PostMessage函数则相反,在将消息插入消息队列后就立即返回。下面是函数原型:
LRESULT SendMessage(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam
);
参数说明如下。
n hWnd 指向目标窗口的句柄,该窗口的处理进程将接受消息。如果这个参数被设置成HWND_BROADCAST,则消息将被广播到系统中所有顶级的窗口。
n Msg 描述了要发送的消息。
n wParam 和消息一起传送的参数。
n lParam 和消息一起传送的参数。返回值代表了消息的处理结果。
注意 |
和PostMessage一样,如果要采取广播行为,应该首先使用RegisterWindowMessage函数获取一个全局的特殊消息格式。 |
输入管理子系统负责处理前台窗口、活动窗口和焦点窗口之间的相互转换。
每一个用户进程有一个特定的窗口称为活动窗口。这是被特定进程拥有和启动的最高等级的窗口。活动窗口和它的子窗口可以是焦点窗口(具有输入焦点的窗口)。焦点窗口能够接收来自键盘的消息。系统中一个特定的进程或者消息队列称为前台进程,前台进程中的活动窗口是前台窗口。这些窗口主要可以通过以下3个API进行转换——SetActiveWindow、SetFocus、SetForegroundWindow。
(1)SetActiveWindow
这个函数将使用线程所拥有的最上层的窗口成为活动窗口,原型如下。
HWND SetActiveWindow(
HWND hWnd
);
参数说明如下。
n hWnd 要激活的窗口的句柄。如果返回该窗口的句柄,则代表设置成功。返回NULL代表操作失败。
注意 |
SetActiveWindow虽然会激活窗口,但是如果应用程序在后台执行,那么本操作即使返回成功,窗口也不会被激活。此外,被激活的窗口会自动地跳到界面的最前端。 |
关于线程的活动窗口状态,如果被激活窗口是由调用本函数的线程所创建的,那么调用线程的活动窗口状态将被设置成hWnd,调用线程的活动窗口状态将被设置为NULL。
SetForegroundWindow函数不但激活窗口,而且会把窗口强制推到用户界面的最前端。因此,为安全起见,应用程序只有在要显示重大错误的时候才可以调用SetForegroundWindow函数。
(2)SetFocus
这个函数将键盘的焦点设置到某一窗口上。此函数成功返回之后,所有的键盘输入都被定向到目标窗口。函数原型如下:
HWND SetFocus(
HWND hWnd
);
参数说明如下:
n hWnd 目标窗口句柄。如果返回目标窗口的句柄,则代表操作成功。如果返回NULL,则代表失败。
n Remarks SetFocus函数向要失去焦点的窗口发送WM_KILLFOCUS消息,向要获得焦点的窗口发送WM_SETFOCUS消息。
如果窗口被激活,但是没有获得焦点,那么任何键盘事件都将触发WM_SYSCHAR、WM_SYSKEYDOWN或者WM_SYSKEYUP消息。如果键被按下的同时,ALT键也同时被按下的话,那么lParam参数的第30位将被设置成1,否则消息参数中这个位为0。
(3)SetForegroundWindow
这个函数将创建窗口的线程推向前台,并且激活窗口。函数原型如下。
BOOL SetForegroundWindow(
HWND hWnd
);
参数说明如下:
n hWnd 窗口句柄。返回值非0代表窗口被推向前台。0代表操作失败,窗口的状态没有改变。
设备描述表(DC,Device Context)描述了图形的输出模版。通过将使用的绘图工具(画笔、画刷等)对象选入设备描述表来完成绘图工具的选择。设备描述表是所有绘图工具的集合。
代码样例7-7是Windows CE中对GDI对象的基本描述数据结构。代码如下:
/ /摘自[CEROOT]/Private/Winceos/Coreos/GWE/MGDI/inc/GDIOBJbase.hpp
class GDIOBJ
{
public:
static HTABLE* m_pHTable; // 句柄表
INT 16 m_nCount; // 引用计数
UINT 16 m_nIndex; // 句柄表索引
GDIOBJ ( void ) ;
~ GDIOBJ ( void ) ;
ULONG Increment(void);
ULONG Decrement(void);
void Rem0oveFromHandleTable(void);
BOOL IsStockObject(void);
virtual BOOL DeleteObject(void);
virtual int GetObject(int CntBytesBuffer, void* pObject) = 0;
virtual DWORD GetObjectType(void ) = 0;
virtual GDIOBJ* SelectObject(DC*) = 0;
} ;
Windows CE GDI非常重要的结构特点是它不直接接触像素。所有的信息都将送至设备驱动程序,并由设备驱动程序最终完成像素点的输出。
位图有许多不同的种类,可以通过调用函数CreateDIBSection、CreateBitmap或CreateCOMpatibleBitmap来建立位图,从而得到相对应的位图句柄。前两个函数是分配在系统内存中的。而CreateCOMpatibleBitmap根据设备驱动程序和硬设备的不同由设备驱动程序分配内存,同时设备驱动程序可以分配影像内存。影像内存是一个很有价值的资源,这是因为在它的小区域操作速度会大大增加。如果十分在意速度并希望能够快速地对位图进行操作,那么调用函数CreateCOMpatibleBitmap或许是最佳的选择。
在某些系统平台上,只有当信息源和目标文件同时存在于影像内存中时硬件加速卡才能起作用。因此将信息源和目标文件同时放在影像内存中会大大提高系统的影像性能。
Windows CE拥有完整的True Type字体(由Microsoft和Apple公司共同研制的字型标准)处理体系。显示一个字形需要花费很长的时间,当调用某种商标字体中的一个字母时,系统需要从这种字体的字体文件中取出它的字形,缩放到需要的大小,最后将其显示出来。为了减少这个过程的时间,系统使用了“字形高速缓冲存储器”。当要建立一个特殊尺寸的字形时,系统需要权衡处理速度与占用高速缓存大小之间的矛盾以决定给这个操作分配多少高速缓存以及分配的时机。
图7-6 GDI组件
Windows CE的显示驱动程序接口DDI(Display Driver Interface)是Microsoft WindowsNT/2000/XP DDI的子集,Windows CE仅使用Windows NT DDI中最基本的图形引擎函数和驱动程序函数。
Windows CE的显示驱动程序被编译成动态链接库(.DLL)而不是函数库(.LIB)。除了DDI函数,还须有一套C++的图形基本操作引擎GPE(Graphics Primitive Engine)类别。它使得显示驱动程序对硬件加速更容易。
GPE类别的程序代码位于[CEROOT]/Private/Winceos/Coreos/GWE/MGDI/gpe/gpe.cpp中。表7-9是DDI的API接口:
表7-9 DDI的API接口描述
函 数 名 称 |
用 途 |
DrvAnyBlt |
传送字节(支持缩放及透明化) |
DrvBitBlt |
传送一般字节(支持剪切及屏蔽) |
DrvContrastControl |
允许软件对显示硬件的对比度进行调整 |
DrvCopyBits |
将由GDI产生的打印段发送到打印驱动程序 |
DrvCreateDeviceBitmap |
建立和处理位图 |
DrvDeleteDeviceBitmap |
删除位图 |
DrvDisableDriver |
通知驱动程序GDI不再需要它,并准备卸载它 |
DrvDisablePDEV |
通知驱动程序GDI不再需要一个特殊的打印设备或显示设备 |
DrvDisableSurface |
通知驱动程序GDI不再需要一个特殊的打印设备或显示设备 |
DrvEnableDriver |
为其他DDI函数返回函数指标并连接到DDI |
DrvEnablePDEV |
为GDI返回一个描述物理显示设备的PDEV |
DrvEnableSurface |
为GDI返回一个描述物理显示设备的PDEV |
DrvEndDoc(NULL) |
发送结束一个打印工作的消息 |
DrvEscape |
获得一个在装置驱动程序接口中无效的设备的信息 |
DrvFillPath |
用画刷填充路径 |
DrvGetMasks |
为显示设备的目前模式获取彩色屏蔽 |
DrvGetModes |
列出显示设备支持的色彩模式 |
DrvMovePointer |
移动光标以保证不被GDI干扰 |
DrvPaint |
用画刷绘出一个指定区 |
DrvPowerHandler |
发出通、断电消息 |
DrvRealizeBrush |
建立由GDI指定参数的画刷 |
DrvRealizeColor |
把一个RGB模式颜色映像成设备支持的最接近的色彩模式值 |
续表
函 数 名 称 |
用 途 |
DrvSetPalette |
设置显示设备的调色板 |
DrvSetPointerShape |
设置新的光标形状并更新显示 |
DrvStartDoc(NULL) |
发送开始一个打印工作的消息 |
DrvStartPage(NULL) |
发送开始打印新的一页的消息 |
DrvStrokePath |
描绘路径 |
DrvTransparentBlt |
传送字节(支持透明化) |
DrvUnrealizeColor |
把一个显示设备色彩模式的颜色值映像成RGB色彩模式值 |
Windows CE提供了灵活的存储器存取机制,使系统中不同类型的应用程序可以充分的使用系统提供的RAM、ROM以及Flash Memory,并选择性地有效利用处理器提供的虚拟存储器、存储器保护等功能。存储器管理可以划分为3部分。
实体页面管理:主要负责追踪系统中实体存储器的使用情況,为换页程序选取可用的实体页面,释放不使用的实体页面等。
虚拟存储器管理:主要管理系统的存储器位址对应页面的换进换出等。
堆管理:主要管理处理程序空间內部的动态存储器释放与回收,以支持程序的动态数据结构。
系统中的32位虚拟位址提供了4GB的虚拟存储器空间,对于嵌入式应用来说,存储器一般很小,因而系统在使用存储器方面作了些限制,以提供更高效能的储存空间管理。这些限制包括:大量的系统保留空间,实际上这些位址空间通常不对应到任何的实体页面;系统处理程序数最多只有32个,每个处理程序的实际可使用存储器空间受到限制(32MB);有固定的处理程序共用存储器;有ROM位址的对应等。
Windows CE使用如图7-7所示的存储器配置(memory layout)。
由于限定了处理程序空间的大小和处理程序的数目,内存配置(memory layout)变得相对简单,系统提供33个Slot供这些处理程序使用,其中,Slot 0是全域的共享内存,底部的一些Slot由重要的系统处理程序使用,kernel部分的静态虚拟地址(Static Virtual Addresses)专门用来对应ROM、外围设备等资源。NK Slot存放NK.EXE,NK是在config.bib中定义的一段RAM区域,专用于保存内核镜像解压出来的所有文件。Windows CE将NK看作是ROM,NK.EXE是一个kernel mode的处理程序,包括了操作系统kernel的程序代码。对物理内存相对比较紧缺的系统来说,对处理程序空间和处理程序数目的限制,将有效缓解内存系统的压力。例如,某个应用系统限制6个处理程序,那么系统对应中将永远不会用到第7个Slot,系统作地址对应,将不考虑其他部分。更详细的信息请参看第4章的内容。
图7-7 存储器配置
易失性存储器即指RAM,非易失性存储器指ROM、Flash等。
RAM(Random Access Memory)用于内核、所有应用程序运行时使用,早期的Windows CE产品只有RAM和ROM,存储器,而ROM存储器只能一次写入,因此在产品出售后,外在的数据就再也不能写入ROM。由于无法使用非易失性存储器,系统即便关闭电源,其实后备电池始终在为RAM提供电力来保存系统配置信息、用户产生的文件等,一旦后备电池耗尽,这些数据将丢失。为了适应这样的硬件结构,Windows CE制定了两种文件系统——RAM文件系统和ROM文件系统。ROM的一次写入性和小容量,不能满足用户上升的容量需求。在后期,出现了Flash这样可重复擦写的非易失性存储器,其存储器容量一般都在32MB以上,这样既能使内核添加更多的功能(编译出的内核更大),又可以使用户更多的信息真正意义上被永久保留下来。由于当时在PC上Flash Disk一般采用FAT文件系统,因此在Windows CE上很快也引入了这个概念。
Windows CE一共有3种文件系统——基本RAM的文件系统;基本ROM(Read Only Memory)的文件系统和FAT文件系统。
在Windows CE系统中,RAM被分为程序空间(program memory)和对象空间(对象存储)。RAM文件系统专用于对象存储(object storage)。对象存储则类似一般计算机系统中的硬盘,用来储存应用程序及文件。对象存储空间中存放的数据可分为3大类——文件系统(file system)、注册信息(registry)和Windows CE数据库(Windows CE database)。程序空间和对象空间的大小是可以调整的。RAM文件系统的优点是支持文件压缩、数据读写较快。Windows CE.NET把除了NK以外的RAM分为对象存储(对象存储)区域和应用程序内存(program memory)区域,并且默认各使用一半RAM。对象存储中可以存储的对象类型有文件、目录、数据库、记录、数据库卷。默认在对象存储中存储的对象全部是压缩的。由于前述的历史原因,系统关闭后,RAM中仍然有电力供应,因此对象存储可以被认为是永久存储器。随着Flash的产生,对象存储存在的必要性也发生了改变。
系统程序执行有两种模式——RAM执行模式和ROM执行模式。一般的系统程序执行采用RAM执行模式,操作系统会先把程序从ROM中取出,加载到RAM中执行。RAM执行模式因为牵涉到程序拷贝,有执行效率差及占用RAM内存空间等缺点。为此,Windows® CE提供XIP(Execute in Place),也就是ROM执行模式,使得储存在ROM中的系统程序,可以在指定的ROM地址中直接执行,而不用加载到RAM中。
在非易失性存储器的文件系统中,分为ROM和Flash的文件系统。ROM和Flash系统有一个共同的特性,支持随机访问。由于这个特性,就产生了本地执行(XIP,executed in place)。XIP执行利用了操作系统的虚拟地址和物理地址对应的机制,将一部分进程空间的虚拟地址映射到ROM而不是RAM,这样原本执行的是RAM中的程序,到了特定地址处(可能要加载ROM中的DLL或者其他文件的时候),只要pc指针跳转到相应的ROM空间,而不必将ROM中的数据加载到RAM中再执行。这样节省了RAM的空间开销,节省了复制数据的时间开销,但是由于ROM的读写速度过慢,因此反而会加大程序执行的时间开销。因此这种方法可以被认为是时间换空间的典型例子,对存储器容量小,实时性要求不高的系统来说比较适合。
对于目前应用比较广泛的Flash中的文件系统,由于其读写速度较ROM快,因此在启动内核时考虑使用复制并解压的机制,即在启动时,将内核镜像文件复制到或者解压缩到RAM的特定空间中,也就是nk.exe。是否需要解压缩。要看Flash中的内核是否是压缩的格式。Windows CE与Linux不同的地方是,Linux在自解压的时候是自动探测内存区域的,而Windows CE则指定使用NK slot。在Flash的文件系统中,也使用XIP技术。当执行一个应用程序时,Windows CE内核将这个应用程序需要的系统DLL(在NK中保存)加载到Slot 1(地址范围0x0200 0000~0x03FF FFFF,在Windows CE.NET中Slot 1专供XIP DLL使用)。Slot 1是一段虚拟地址,当CPU执行DLL的代码时,CPU会根据地址映射关系到NK中寻找实际的代码执行,因为NK是一段实际的物理内存,I/O速度非常快,所以相对于在ROM中执行,DLL的运行效率得到很大提高。
从Microsoft Windows CE .NET操作系统角度看,内存有两种组织方式——物理内存和被映射到物理内存的虚拟内存。
物理内存可以是需要被OS访问的实际的RAM或者其他快速存储设备。物理内存是由操作系统规定机制的物理地址来组织和管理的。当MMU处理器的MMU启动后,操作系统无法再通过物理地址来访问物理内存了。在某些CPU平台上,像MIPS和SHx,Windows CE只能直接访问第一个GB的物理内存,并且只有前512MB的物理内存才可以被缓存。在x86和ARM处理器上,可以使用OEMAddressTable来定义物理内存的范围。
被映射的虚拟地址定义了物理内存和虚拟内存之间的映射关系,这种映射可以使用在用户态进程与内核线程中。在Windows CE中,有两种虚实地址的映射方式——静态映射和动态映射。静态映射向操作系统提供了一张映射表,定义虚拟空间和实际空间的映射关系。这种映射在启动时建立之后就不会发生更改。在x86和ARM平台上OEMAddressTable就是静态映射表的实现。在MIPS和SHx平台上,由处理器自动实现这种映射。静态映射可以在启动后使用CreateStaticMapping或者NKCreateStaticMapping函数被强制更改,但是建议读者不要执行这样的操作。由这两个函数建立的静态映射只有内核才能使用。
动态映射就是指在需要时被建立,在不需要使用时又可以解除的映射关系。这种映射只能由用户态的进程使用VirtualCopy建立或者更改。
使用VirtualCopy函数并且在参数中加入PAGE_PHYSICAL就可以在用户态下将物理地址转换为和特定平台机制相关的虚拟地址。这个函数通过首先调用VirtualAlloc来为要被映射的物理地址分配虚拟地址空间。接着,进程调用VirtualCopy将虚拟地址和物理地址绑定,通过使用虚拟地址的方式使这段物理地址能被进程访问。如果是驱动程序的话,它将直接使用VirtualCopy将设备的内存直接映射到用户态进程的地址空间中。同样的物理内存块,可以被不同的进程映射。
函数CreateStaticMapping和VirtualCopy的共同点在于都将虚拟地址空间和物理地址空间建立映射关系。它们的区别在于前者映射后的静态虚拟地址只能由核心态下的进程访问,也就是说,是映射到高2GB的虚拟空间的(关于用户态虚拟空间和核心态虚拟空间请参考下一节)。CreateStaticMapping函数因此主要用来将设备存储器映射到核心空间,这样中断服务器就能很方便地使用外设的存储器。
虚拟内存内所使用的地址称为虚拟地址(virtual address)。由于MMU的存在,虚拟内存是操作系统内核使用的寻址方式。一般32位的处理器能寻址到4GB空间(232B=4GB)空间,即使在PC环境中也已经远远超出物理内存的实际大小,更何况一般只有64MB内存的嵌入式设备。一般市场上出售的开发板的物理内存为32~64MB。另外,虚拟内存允许不同程序共享相同的文件和程序代码。
Windows CE把这4GB虚拟地址空间主要划分为两部分,从0x8000 0000的2GB以上为内核区,0x8000 0000以下的2GB为用户态进程使用的空间。这与armLinux有所不同,armLinux将整个虚拟空间划分成前3GB和第4GB,第4GB空间才是内核区。
表7-10详细描述了Windows CE的虚拟空间划分。
表7-10 虚拟空间划分
地 址 范 围 |
用 途 |
0x0000 0000~0x41FF FFFF |
由所有应用程序使用。共33个Slot,每个Slot占32MB。Slot 0由当前进程使用(内核代码中用pCurProc来表示)。Slot 1由XIP DLL使用,其他Slot用于其他进程使用,每个进程占用一个槽 |
续表
地 址 范 围 |
用 途 |
0x4200 0000~0x7FFF FFFF |
由所有应用程序共享的区域。32MB地址空间有时不能够满足一些进程的需求;那么进程可以使用这个范围的地址空间,在这个区域中应用程序可以分配大的地址空间等 |
0xA000 0000~0xBFFF FFFF |
内核重复定义0x8000 0000~0x9FFF FFFF之间定义的物理地址映射空间。但是在这范围映射的虚拟地址空间不能够用于缓冲 |
0xC000 0000~0xC1FF FFFF |
内核保留空间 |
0xC200 0000~0xC3FF FFFF |
内核程序nk.exe使用的地址空间 |
0xC400 0000~0xDFFF FFFF |
这个范围为用户定义的静态虚拟地址空间,但这个地址空间只能用于非缓冲使用。 |
0xE000 0000~0xFFFF FFFF |
内核使用的虚拟地址。当内核需要大的虚拟地址空间时,会在这个范围内分配 |
OEMAddressTable详细定义了虚拟地址和物理地址的映射关系。OEMAddressTable本身并不是文件,它只是存在于其他文件中描述虚拟地址和实际物理地址的映射关系的数据。OEMAddressTable在文件%_WINDOWS CEROOT%/Public/Common/Oak/Csp/i486/Oal/
OEMInit.asm中。其中的每个表项都定义了映射关系中的虚拟地址,启始的物理地址以及映射的区域大小,以MB为单位。
第一个映射表项目必须描述RAM的映射方式,必须以0x80000000为虚拟地址的启始地址。每个表项都要遵循以下的格式:
Virtual Address, Physical Address, Size.
在x86平台上,size必须为4MB的倍数;而在ARM平台上必须为1MB的倍数。最后一个表项的入口必须为0。
有效的虚拟内存映射关系只从0x80000000~0x9fffffff。如前表中所描述,0xA0000000~0xBFFFFFFF是0x80000000~0x9FFFFFFF虚拟内存的副本,区别在于前者不能被缓冲。
表中没有定义的,并且用户没有显式调用CreateStaticMapping函数建立的映射,不能被ISR使用。
代码样例7-8是x86平台上OEMAddressTable的实例。代码如下。
public _OEMAddressTable
_OEMAddressTable:
;
; OEMAddressTable defines the mapping between Physical and Virtual Address
; MUST be in a READONLY Section
; First Entry MUST be RAM, mapping from 0x80000000 -> 0x00000000
; each entry is of the format ( VA, PA, cbSize )
; cbSize must be multiple of 4M
; last entry must be (0, 0, 0)
; must have at least one non-zero entry
; RAM 0x80000000 -> 0x00000000, size 64M
dd 80000000h, 0 04000000h
; FLASH and other memory, if any
; dd FlashVA, FlashPA, FlashSize
; Last entry, all zeros
dd 0 0 0
在Windows CE中,对物理内存管理的源代码都放在以下这个文件中:[CEROOT]/PRIVATE/
WINDOWS CEOS/COREOS/NK/KERNEL/physmem.c
可用内存链表记录目前系统中尚可使用的内存,可由LogPtr->pKList找到链表。可用内存链表采用简单的双向链表形式。
在程序中,LogPtr->pKList的原型如下:
fslog_t *LogPtr;
typedef struct fslog_t {
DWORD version; // version of this structure, must stay as first DWORD
DWORD magic1; // LOG_MAGIC if memory tables valid
DWORD magic2; // LOG_MAGIC if heap initialized
union {
struct { // SystemHeap contains this data
mem_t fsmemblk[MAX_MEMORY_SECTIONS]; // Memory blocks to use for file system
LPBYTE pFSList;
LPBYTE pKList;
};
struct { // All other heaps contain this data
CEGUID guid;
DWORD dwRestoreFlags;
DWORD dwRestoreStart;
DWORD dwRestoreSize;
fshash_t ROMSignature; // Hives only: signature of the hive in ROM
};
};
fshash_t pwhash; // hashed password (stored in SystemHeap or system hive)
DWORD virtbase; // VirtBase when last booted
DWORD entries; // number of entries in recovery log
DWORD hDbaseRoot; // handle to first dbase, else INVALID_HDB
DWORD hReg; // Handle to registry header, else INVALID_HREG
DWORD dwReplInfo; // Persistent replication information
DWORD flags; // file system flags. High 24 bits are dbase LCID
fslogentry_t log[]; // log entries
} fslog_t;
下面的程序代码是插入可用内存链表的程序代码:
void LinkPhysPage(LPBYTE pMem)
{
KCALLPROFON(32);
*(LPBYTE *)((DWORD)pMem + 0x20000000) = LogPtr->pKList;
*(LPBYTE *)((DWORD)pMem + 0x20000004) = 0;
if (LogPtr->pKList)
*(LPBYTE *)((DWORD)LogPtr->pKList + 0x20000004) = pMem;
LogPtr->pKList = pMem;
PageFreeCount++;
LogPhysicalPages(PageFreeCount);
KCALLPROFOFF(32);
}
其中,KCALLPROFON()和KCALLPROFOFF()分别开启、关闭与加入可用内存链表的系统记录。
在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-8 进程地址空间结构 |
Windows CE.NET把XIP DLL单独加载到Slot 1中,这样对于每个进程来说,它总的地址空间就大了一倍,也就是64MB。当这个进程得到CPU使用权时,它的整个地址空间被内核映射到Slot 0,也就是当前进程使用的地址空间,然后开始运行。图中给出的地址实际上是经过映射到Slot 0之后的结构。从图中可以看出,进程首先加载代码段,因为每个进程最低部64KB作为保留区域,所以代码段从0x0001 0000开始,内核为代码段分配足够的虚拟地址空间后,接着为只读数据和可读/可写数据分配空间,接着为资源数据分配空间,之后为默认堆和栈分配空间。非XIP DLL从进程最高地址向下开始加载。
堆是一段连续的较大的虚拟地址空间。应用程序在堆中可以动态地分配、释放所需大小的内存块。利用堆的优点是在一定范围内减小了内存碎块,而且开发者分配内存块前不必了解CPU的类型。因为不同的CPU分页大小不相同,每个内存页可能是1KB、4KB或更多。在堆内分配内存块可以是任意大小的,而直接分配内存就必须以内存页为单位。当应用程序启动时,内核在进程所在的地址空间中为进程分配一个默认192KB大小的虚拟地址空间,但是并不立刻提交物理内存。如果在运行时192KB不能满足需求,那么内核会在进程地址空间中重新查找一个足够大小的空闲的地址空间,然后复制原来堆的数据,最后释放原来的堆所占的地址空间。这是因为默认的堆的高地址处还有栈,所以必须重新分配。
栈也是一段连续的虚拟地址空间,和堆相比空间要小得多,它是专为函数使用的。当调用一个函数时(包括线程),内核会产生一个默认的栈,并且内核会立刻提交少量的物理内存(也可以禁止内核立刻提交物理内存)。栈的大小和CPU有关,一般为64KB,并且保留顶部2KB以防止溢出。可以修改栈的大小,具体修改方法在讲解线程的时候已经说过了,这里就不再重复了。一般不会修改栈的大小,如果在编译链接时修改大小,那么所有栈的大小都会改变,这不太合理。实际开发中最好不要在栈中分配很大、很多的内存块,如果分配的内存块超过了默认栈的限制,那么会引起访问非法并且内核会立刻终止进程。最好在进程的堆中分配大的内存块并且在函数返回前释放,或者在创建线程时指定栈的大小。
Windows CE内核用分页虚拟内存机制来管理和分配程序内存。虚拟内存系统提供了连续的内存块。每个64KB的内存区域被分成多个1024B或4096B的页。所以应用程序不必进行实际的内存分配管理。对于少于64KB的内存请求,应用程序可以用系统为Windows CE程序提供的本地堆或创建分离的堆来满足应用程序的内存需要。内核也可以为每个新的进程或线程在栈上分配内存。
Windows CE操作系统使用KDataStruct数据结构来存放低地址2GB内的数据。代码样例7-9列出KdataStruct的整个数据结构代码如下:
struct KDataStruct {
LPDWORD lpvTls; /* 0x000 Current thread local storage pointer */
HANDLE ahSys[NUM_SYS_HANDLES];/* 0x004 If this moves, change kapi.h */
char bResched; /* 0x084 reschedule flag */
char cNest; /* 0x085 kernel exception nesting */
char bPowerOff; /* 0x086 TRUE during "power off" processing */
char bProfileOn; /* 0x087 TRUE if profiling enabled */
ulong ptDesc; /* 0x088 Page Table Descriptor */
ulong rsvd2; /* 0x08c was DiffMSec */
PPROCESS pCurPrc; /* 0x090 ptr to current PROCESS struct */
PTHREAD pCurThd; /* 0x094 ptr to current THREAD struct */
DWORD dwKCRes; /* 0x098 */
ulong handleBase; /* 0x09c handle table base address */
PSECTION aSections[64]; /* 0x0a0 section table for virutal memory */
LPEVENT alpeIntrEvents[SYSINTR_MAX_DEVICES]; /* 0x1a0 */
LPVOID alpvIntrData[SYSINTR_MAX_DEVICES]; /* 0x220 */
ulong pAPIReturn; /* 0x2a0 direct API return address for kernel mode */
uchar *pMap; /* 0x2a4 ptr to MemoryMap array */
DWORD dwInDebugger; /* 0x2a8 !0 when in debugger */
PTHREAD pCurFPUOwner; /* 0x2ac current FPU owner */
PPROCESS pCpuASIDPrc; /* 0x2b0 current ASID proc */
long alPad[19]; /* 0x2b4 - padding */
DWORD aInfo[32]; /* 0x300 - misc. kernel info */
/* 0x380 - interlocked api code */
/* 0x400 - end */
}; /* 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]字段记录虚拟内存中每一个页所对应到的物理内存地址。代码如下:
图7-9 Kdata Struct数据结构
#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 */
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所具有的一个特性。这就是可以创建大型稀疏数组,而不会消耗大量RAM。在上面的代码中,64MB保留区域不会消耗任何物理RAM。在该示例中,惟一被消耗的RAM是在第二次调用VirtualAlloc以提交页时使用的一个页(4096字节)。
动态链接库(DLL)一直以来都是Windows的重要基础,Windows CE也不例外,大部分驱动是以动态链接库的形式加载的,因为在嵌入式操作系统中,一般不允许死机,因此不能让驱动以内核态的权限运行,否则一旦出错,将使系统无法恢复到正常运行状态,因此DLL的重要性对Windows CE来说丝毫不亚于对桌面系统。本节的内容主要是分析loader.c中的程序代码,它负责加载EXE和DLL。Windows CE.net的DLL加载代码都集中在[CEROOT]/
PRIVATE/WINDOWS CEOS/COREOS/NK/KERNEL/loader.c文件下。
这个文件中的代码符合Win32可执行文件规范。程序最关心的是如何加载一个进程。当要加载一个进程时,首先要在新的进程上下文中创建一个线程,然后由这个线程从文件中读取可执行代码。接着它就陷入内核态,启动新的进程。
下面对源代码进行结构分析。
struct KDataStruct *PtrKData;//如前文所述,是代表低地址2GB内的数据结构。
FREEINFO FreeInfo[MAX_MEMORY_SECTIONS];//系统空闲内存区域
typedef struct FreeInfo {
PHYSICAL_ADDRESS paStart; /* start of available region */
PHYSICAL_ADDRESS paEnd; /* end of region (last byte + 1) */
PHYSICAL_ADDRESS paRealEnd;
PBYTE pUseMap; /* ptr to page usage count map */
} FREEINFO; /* FreeInfo */
MEMORYINFO MemoryInfo;
typedef struct MemoryInfo {
PVOID pKData; /* start of kernel's data */
PVOID pKEnd; /* end of kernel's data & bss */
uint cFi; /* # of entries in free memory array */
FREEINFO *pFi; /* Pointer to cFi FREEINFO entries */
} MEMORYINFO; /* MemoryInfo */
typedef struct Module {
LPVOID lpSelf; /* Self pointer for validation */
PMODULE pMod; /* Next module in chain */
LPWSTR lpszModName; /* Module name */
DWORD inuse; /* Bit vector of use */
DWORD calledfunc; /* Called entry but not exit */
WORD refcnt[MAX_PROCESSES]; /* Reference count per process*/
LPVOID BasePtr; /* Base pointer of dll load (not 0 based) */
DWORD DbgFlags; /* Debug flags */
LPDBGPARAM ZonePtr; /* Debug zone pointer */
ulong startip; /* 0 based entrypoint */
openexe_t oe; /* Pointer to executable file handle */
e32_lite e32; /* E32 header */
o32_lite *o32_ptr; /* O32 chain ptr */
DWORD dwNoNotify; /* 1 bit per process, set if notifications disabled */
WORD wFlags;
BYTE bTrustLevel;
BYTE bPadding;
PMODULE pmodResource; /* module that contains the resources */
DWORD rwLow; /* base address of RW section for ROM DLL */
DWORD rwHigh; /* high address RW section for ROM DLL */
PGPOOL_Q pgqueue; /* list of the page owned by the module */
} Module;
typedef struct openexe_t {
union {
int hppfs; // ppfs handle
HANDLE hf; // 对象存储 handle
TOCentry *tocptr; // rom entry pointer
};
BYTE filetype;
BYTE bIsOID;
WORD pagemode;
union {
DWORD offset;
DWORD dwExtRomAttrib;
};
union {
Name *lpName;
CEOID ceOid;
};
} openexe_t;
在loader.c文件中,有以下几个系统调用,它们都调用LoadOneLibraryW,可见这个函数是加载DLL关键环节。下面是对这个函数的说明。
n SC_LoadLibraryW
n SC_LoadLibraryExW
n SC_LoadDriver
n SC_LoadKernelLibrary
n SC_LoadIntChainHandler// This function is called by a driver to install an ISR to handle a particular interrupt.
下面是这个关键函数在loader.c中的实现。
//------------------------------------------------------------------------------
// Load a library (DLL)
//------------------------------------------------------------------------------
PMODULE
LoadOneLibraryW(LPCWSTR lpszFileName, DWORD fLbFlags,WORD wFlags)
{
/*
参数lpszFileName
指向DLL名字的字符串,该名字确定了模块(module)的文件名称,而与它存储在模块库中的名字无关。函数库模块以LIBRARY为关键词,在模块定义文件(.def)中定义。
参数wFlags指定加载模块时所要处理的工作。其值可以为0、DONT_RESOLVE_DLL_
REFERENCES、LOAD_LIBRARY_AS_DATAFILE或者是LOAD_LIBRARY_IN_KERNEL。如果使用DONT_RESOLVE_DLL_REFERENCE这个旗标,而且可执行模块本身是一个DLL,则系统并不调用DllMain来初始化和结束处理程序和进程。此外,一个DLL可能会引入包含在另一个DLL中的函数,而系统映像一个DLL时也会自动加载,当这个旗标被设置之后,系统就不再自动加载额外的DLL。
*/
PMODULE pMod = 0, pModRes;//代表一个模块
/*这是记录DLL信息的重要数据结构之一,每一个程序对应一个module,但一个module
* 可以对应多个处理程序。在系统中维持一条已加载模块的链表,它是一个单向链表。
*/
LPCWSTR dllname;
DWORD dwErr = 0;
DWORD prevRefCnt = 0;
if ((DWORD)lpszFileName & 0x1)
{
wFlags |= LOAD_LIBRARY_IN_KERNEL;
(DWORD)lpszFileName &= ~0x1;
}
DEBUGMSG (ZONE_LOADER1, (L"LoadOneLibraryPart2: %s, %d, 0x%x/n", lpszFileName, fLbFlags, wFlags));
//在此并没有调用LoadOneLibraryPart2,仅仅是打印出调试信息而已。
if (wFlags & LOAD_LIBRARY_AS_DATAFILE)
{
wFlags |= DONT_RESOLVE_DLL_REFERENCES;
}
if (wFlags & LOAD_LIBRARY_IN_KERNEL)
/*在此判别在加载DLL时候需要做些什么事情,是在kernel空间加载还是其他*/
{
if (pCurProc->bTrustLevel != KERN_TRUST_FULL)
/* 这里的pCurProc的原型为:
* #define pCurProc (KData.pCurPrc);
* #define KData (ArmHigh->kdata)
* KDataStruct->pCurPrc
* 综合来看也就是ArmHigh->KDataStruct->pCurPrc,也就是当前运行的进程,即要加载DLL的进程。
* ptr to current PROCESS struct 03: level of trust of this exe
*/
{
KSetLastError(pCurThread,(DWORD)NTE_BAD_SIGNATURE);
return 0;
}
fLbFlags |= LLIB_NO_PAGING | LLIB_NO_MUI;/*设置内核加载标志*/
}
dllname = lpszFileName+strlenW(lpszFileName);
while ((dllname != lpszFileName) && (*(dllname-1) != (WCHAR)'//') && (*(dllname-1) != (WCHAR)'/'))
{
dllname--;
}
/*以上对DLL名字进行了处理*/
DEBUGMSG(ZONE_LOADER1,(TEXT("LoadOneLibrary %s (%s)/r/n"),lpszFileName,dllname));
pMod = FindModByName(dllname, wFlags);
/*在module chain中寻找准备要加载的module*/
if (pMod) /*Module被找到,也就是说以前被安装过的*/
{
/*以前的判别主要是判断已经被安装的module是否能被debug,
* 如果已经有调试器和这个module绑定的话,就算加载失败,返回NULL。
*/
if ((FT_OBJSTORE != pMod->oe.filetype) && !ChkDebug (&pMod->oe))
{
KSetLastError(pCurThread,(DWORD)NTE_BAD_SIGNATURE);
return 0;
}
if ((pCurProc->bTrustLevel == KERN_TRUST_FULL) && (pMod->bTrustLevel == KERN_TRUST_RUN))
{
/*判断当前进程的执行权限和要加载的module本身的权限*/
KSetLastError(pCurThread,(DWORD)NTE_BAD_SIGNATURE);
return 0;
}
if (pMod->wFlags != wFlags)
{
KSetLastError(pCurThread,ERROR_BAD_EXE_FORMAT);
return 0;
}
/*到这里我们已经找到module,并且它也符合各项加载要求,
* 因此将它从Proc0复制到pProc指向的区域中,
* 同时还要增加module的引用计数,如果失败,则解除
*/
if (!(prevRefCnt = IncRefCount (pMod)) && !(wFlags & (LOAD_LIBRARY_IN_KERNEL|LOAD_LIBRARY_AS_DATAFILE)) && !CopyRegions (pMod))
{
DecRefCount (pMod);
dwErr = ERROR_OUTOFMEMORY;
}
else
/*已经成功复制了要加载并且已经存在的dll
*在复制完以后,还要做些其他设置
*/
{
if ((fLbFlags & LLIB_NO_PAGING) && (pMod->oe.pagemode == PM_FULLPAGING))
/*如果是内核加载,则不允许页交换,但是module的权限又是可以页交换的,那么就需要修改*/
{
o32_lite *optr = pMod->o32_ptr;
int i;
pMod->oe.pagemode = PM_NOPAGEOUT;
/*
如果正在使用内存池的页,则需要把页交还
*/
EnterCriticalSection (&PagerCS);/*进入临界区*/
if (INVALID_PG_INDEX != pMod->pgqueue.idxHead)
{
UnloadMod (pMod);
FreeAllPagesFromQ (&pMod->pgqueue);
/*
释放
*/
}
LeaveCriticalSection (&PagerCS);
DEBUGMSG(ZONE_LOADER1,(TEXT("LoadOneLibraryPart2 - change from pageable to not paged/r/n")));
/*
将所有的页换入,并且锁住不允许再交换
*/
for (i = 0; i < pMod->e32.e32_objcnt; i ++, optr ++)
{//o32_lite *optr = pMod->o32_ptr;
DEBUGMSG(ZONE_LOADER1,(TEXT("LoadOneLibraryPart2 - paging in %8.8lx->%8.8lx, (%a)/r/n"),
ZeroPtr (optr->o32_realaddr), ZeroPtr (optr->o32_realaddr) + optr->o32_vsize,
(optr->o32_flags & IMAGE_SCN_MEM_WRITE)? "WRITE" : "READ"));
if (!DoLockPages ((LPVOID)ZeroPtr (optr->o32_realaddr), optr->o32_vsize, NULL,
LOCKFLAG_QUERY_ONLY | ((optr->o32_flags & IMAGE_SCN_MEM_WRITE)? LOCKFLAG_WRITE : LOCKFLAG_READ)))
{
dwErr = ERROR_OUTOFMEMORY;
pMod->oe.pagemode = PM_FULLPAGING;
break;
}
}
}
if (dwErr)
{
if (!prevRefCnt)
UnCopyRegions (pMod);
/*
出错误的话,就把刚才的工作撤消
*/
DecRefCount (pMod);
}
else if (pMod->pmodResource)
/*
如果module占用资源,将资源的引用计数增加。
*/
{
IncRefCount (pMod->pmodResource);
}
}
}
else
{
//
// Module instance没有在链表中被找到,也就是此module以前没有被安装过*/
//
if (!(pMod = AllocMem(HEAP_MODULE)))
/*
从内核的堆上分配内存
*/
{
KSetLastError(pCurThread, ERROR_OUTOFMEMORY);
return NULL;
/*
不成功则返回
*/
}
/*初始化module结构*/
if ((dwErr = InitModule (pMod, (LPWSTR) lpszFileName, (LPWSTR) dllname, fLbFlags, wFlags))
|| (!(wFlags & LOAD_LIBRARY_IN_KERNEL) && !IsModCodeAddr (pMod->BasePtr) && /
!IsInResouceSection (pMod->BasePtr) && !CopyRegions (pMod)))
{
if (!dwErr)
dwErr = ERROR_OUTOFMEMORY;
}
else
/*
初始化成功
*/
{
/*
增加引用计数
*/
IncRefCount (pMod);
/*将pMod插入到chain的头部*/
EnterCriticalSection(&ModListcs);
pMod->pMod = pModList;
pModList = pMod;
LeaveCriticalSection(&ModListcs);
// can't load MUI before this since we might have to query resource section of current module.
if (!(fLbFlags & LLIB_NO_MUI))
{
pMod->pmodResource = LoadMUI (pMod, pMod->BasePtr, &pMod->e32);
}
}
}
/*
后面是些收尾的工作。
*/
return pMod;
在系统的内存管理中有一些非常重要的数据结构,其中一部分在前文分析的过程中已经有所涉及,这里进行补充。
_MEMORYSTATUS这个数据结构记录了当前系统内存的使用情况。这个结构在函数GlobalMemoryStatus中被使用到。
typedef struct _MEMORYSTATUS {
DWORD dwLength;
DWORD dwMemoryLoad;
DWORD dwTotalPhys;
DWORD dwAvailPhys;
DWORD dwTotalPageFile;
DWORD dwAvailPageFile;
DWORD dwTotalVirtual;
DWORD dwAvailVirtual;
} MEMORYSTATUS, *LPMEMORYSTATUS;
域成员功能解释如下:
n dwLength 定义了整个MEMORYSTATUS数据结构的大小,函数在调用这个结构时候,先将这个域填充为sizeof(MEMORYSTATUS),然后再传递给GlobalMemory-
Status。
n dwMemoryLoad 定义了当前内存使用的百分比,0代表没有内存被占用,100代表内存全部分配完毕。
n dwTotalPhys 定义了物理内存的大小,以byte为单位。
n dwAvailPhys 定义了可以被分配的物理内存的大小,以byte为单位。
n dwTotalPageFile 定义了能被存放在页文件上的数据总量,以byte为单位。
n dwAvailPageFile 定义了在页文件中能被使用的数据量,以byte为单位。
n dwTotalVirtual 定义了在用户态下能看到的虚拟地址空间。