7.1 层次式架构
1.系统分层模型
操作系统(包括应用环境、操作环境等)一般具有分层的结构特征,典型的就是UNIX系统的同心环,最里面是硬件,从里向外依次是kernel、共享函数库、应用程序3个层次。Windows CE的分层结构比较复杂(如图7-1所示),这是根据它适合嵌入式应用的特点所设计的。从提供者的角度来看,图中上面4层主要由微软公司维护开发,下面两层主要由嵌入式产品开发商根据产品需要开发。当然,这个划分并不绝对,而是根据产品需求来决定。需要说明的是,这里并没有一个单独的应用层,因为在大多数情况下,从嵌入式产品发布的方式来看,使用者的应用产品是捆绑销售(bundle)的,所以它们本身也是构成应用整合层的一部份。
图7-1 分层结构示意图
从接口的角度来看,一般系统开发环境包括应用和系统两个接口,用以支持系统和应用程序开发,例如在Windows平台中,SDK代表了应用层接口,而DDK代表了系统接口,两个接口之间就是操作系统的实体。如图7-1所示,Windows CE操作系统在中间两层,应用支持库的上部和操作系统层的上部(主要是CoreDLL.dll,应用支持库中的很多接口也是透过DLL导出到系统中的)以及下部具有接口性质,它们构成了Windows CE的应用接口和系统接口。因而,从某种角度来看,OAL起到了系统接口的作用,而上面两层就是应用接口的使用者。
从层与层的互动来看,主要的互动发生在相邻两层之间,但上面三层具有一定的向下跨层存取能力。限制存取在一定程度上有助于功能的区隔,清楚的接口也使我们可以方便地置换掉某一层。
2.组件剖析
从宏观的角度来看,系统的软件组件有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,就是通过这组函数注册的。
7.2 内核模块
7.2.1 Windows CE中的中断体系结构
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中保留内存地址。
中断相关的API
OEMInterruptEnable函数的代码样例如下:
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时,内核会调用本函数。
7.2.2 Windows CE中的进程
由正文段(text)、用户数据段(user segment)以及系统数据段(system segment)共同组成一个进程执行环境,负责处理器、内存和外围等资源的分配和回收。进程是计算机系统资源的使用主体,是操作系统分配资源的基本单位。动态、独立、并行的Windows CE最多支持32个进程在系统中同时运行。
系统激活的时候,至少自动激活4个进程。第一个是NK.exe,用来提供操作系统中kernel的服务;第二个是FILESYS.EXE,用来提供相关文件系统的服务;第三个是GWES.EXE,用来提供对GUI系统的支持;第四个是DEVICE.EXE,用来加载和管理外围的驱动程序。它们占据虚拟地址的前4个slots,一个slot有32MB空间。
1.Windows CE进程简介
作为一先抢占式的、多任务的操作系统,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 |
为非实时应用保留 |
2.Windows CE进程结构组成
进程由正文段(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结构的各个域。
3.有关的函数简介(如表7-2所示)
表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 当一个已经被阻塞的线程的优先级增加时,被调度程序将立即判断该线程是否需要状态改变。而当一个已经被阻塞的线程的优先级减少时,丝毫不会影响线程当前的状态。
此函数返回实时线程的优先级。
int CeGetThreadPriority(
HANDLE hThread
);
参数功能解释如下。
n hThread 线程的句柄。如果成功,则返回线程的优先级,否则返回THREAD_
PRIORITY_ERROR_RETURN。
这个函数仅仅返回线程的原始优先级,如果由于优先级逆转等原因,使得线程的实际优先级和原始优先级不同时,此函数返回的值可能会与实际线程的优先级不一致。
4.有关的数据结构
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;
7.2.3 进程间通信
Windows CE处理程序间的通信(IPC,Interprocess Communication)可以使用临界区(critical section)、事件(event)、同步(synchronization)以及互斥器(mutex)。对于嵌入式系统来说,这些方法简单实用、并且不耗费系统成本。
1.临界区(critical section)
当多个线程都得到了同一对象的访问权限后,它们对对象的操作可能会导致数据的不一致。使用临界区对象,就能使某个对象或者某段代码避免被多个线程同时修改,导致脏数据的产生。临界区使得对象或者代码段只能由一个线程来操作,它们可以是一个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);
}
2.事件(event)
事件对象用来通知某个进程发生了特定的事件或者告诉这个进程该做什么事了。例如在触摸屏上采样到一个点后,驱动程序便会设置一个特定事件,系统就会触发和该事件相关联的进程,并告诉它触摸屏上有采样变动。
类似临界区对象,事件对象使用前也需要向系统要求资源句柄,在使用后则必须释放句柄,不同的是,系统由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))
{
// 错误处理
}
}
}
3.互斥器(mutex)
互斥器的拥有者只能有一个线/进程,互斥器是一个同步对象。当没有任何进程占用这个对象互斥器时,该互斥器处于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;
4.信号量(semaphore)
信号量和互斥器的基本原理是一样的,但是它允许特定数目的进程共享这个资源,获得和释放的函数分别是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;
7.3 文件系统概览
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 支持将外部设备的文件系统当作根文件系统来加载
7.4 GWES模块
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。
7.4.1 用户输入系统(User Input System)
用户输入系统分为消息队列、窗口管理器两大部分。消息队列Msgque是任何需要消息传递的地方所必须的部分,因为要实现用户输入就必须能够把输入的信息传给所需要的窗口。Wmbase组件的作用是建立窗口,为窗口提供窗口处理函数WndProc,并且给它发送消息。窗口管理器Winmgr负责把绘图操作的结果显示在屏幕上。
1.消息队列Msgque
消息队列机制是所有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函数获取一个全局的特殊消息格式。 |
2.输入管理
输入管理子系统负责处理前台窗口、活动窗口和焦点窗口之间的相互转换。
每一个用户进程有一个特定的窗口称为活动窗口。这是被特定进程拥有和启动的最高等级的窗口。活动窗口和它的子窗口可以是焦点窗口(具有输入焦点的窗口)。焦点窗口能够接收来自键盘的消息。系统中一个特定的进程或者消息队列称为前台进程,前台进程中的活动窗口是前台窗口。这些窗口主要可以通过以下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代表操作失败,窗口的状态没有改变。
7.4.2 图形设备接口GDI
设备描述表(DC,Device Context)描述了图形的输出模版。通过将使用的绘图工具(画笔、画刷等)对象选入设备描述表来完成绘图工具的选择。设备描述表是所有绘图工具的集合。
1.基本GDI对象
代码样例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;
} ;
2.图形基本操作
Windows CE GDI非常重要的结构特点是它不直接接触像素。所有的信息都将送至设备驱动程序,并由设备驱动程序最终完成像素点的输出。
3.位图
位图有许多不同的种类,可以通过调用函数CreateDIBSection、CreateBitmap或CreateCOMpatibleBitmap来建立位图,从而得到相对应的位图句柄。前两个函数是分配在系统内存中的。而CreateCOMpatibleBitmap根据设备驱动程序和硬设备的不同由设备驱动程序分配内存,同时设备驱动程序可以分配影像内存。影像内存是一个很有价值的资源,这是因为在它的小区域操作速度会大大增加。如果十分在意速度并希望能够快速地对位图进行操作,那么调用函数CreateCOMpatibleBitmap或许是最佳的选择。
在某些系统平台上,只有当信息源和目标文件同时存在于影像内存中时硬件加速卡才能起作用。因此将信息源和目标文件同时放在影像内存中会大大提高系统的影像性能。
4.字体
Windows CE拥有完整的True Type字体(由Microsoft和Apple公司共同研制的字型标准)处理体系。显示一个字形需要花费很长的时间,当调用某种商标字体中的一个字母时,系统需要从这种字体的字体文件中取出它的字形,缩放到需要的大小,最后将其显示出来。为了减少这个过程的时间,系统使用了“字形高速缓冲存储器”。当要建立一个特殊尺寸的字形时,系统需要权衡处理速度与占用高速缓存大小之间的矛盾以决定给这个操作分配多少高速缓存以及分配的时机。
5.GDI的组件(如图7-6所示)
图7-6 GDI组件
7.4.3 显示驱动程序接口
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色彩模式值 |
7.5 内存管理
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 存储器配置
7.5.1 易失性和非易失性存储器
易失性存储器即指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的运行效率得到很大提高。
7.5.2 物理内存与虚拟内存概述
从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函数因此主要用来将设备存储器映射到核心空间,这样中断服务器就能很方便地使用外设的存储器。
7.5.3 虚拟内存的管理和访问
虚拟内存内所使用的地址称为虚拟地址(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
7.5.4 物理内存的组织结构和访问
1.空闲物理内存的组织方式
在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()分别开启、关闭与加入可用内存链表的系统记录。
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 进程地址空间结构
|
图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的整个数据结构代码如下:
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 */
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所具有的一个特性。这就是可以创建大型稀疏数组,而不会消耗大量RAM。在上面的代码中,64MB保留区域不会消耗任何物理RAM。在该示例中,惟一被消耗的RAM是在第二次调用VirtualAlloc以提交页时使用的一个页(4096字节)。
7.5.9 动态链接库的加载分析
动态链接库(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可执行文件规范。程序最关心的是如何加载一个进程。当要加载一个进程时,首先要在新的进程上下文中创建一个线程,然后由这个线程从文件中读取可执行代码。接着它就陷入内核态,启动新的进程。
下面对源代码进行结构分析。
1.几个重要的数据结构
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;
2.函数功能分析
在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;
7.5.10 内存相关的数据结构
在系统的内存管理中有一些非常重要的数据结构,其中一部分在前文分析的过程中已经有所涉及,这里进行补充。
_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 定义了在用户态下能看到的虚拟地址空间。
n dwAvailVirtual 定义了用户态下可以被分配的虚拟内存,这些内存没有被保留,也没有被提交(内存的分配经历两步,先保留内存,然后再提交。)
_MEMORY_BASIC_INFORMATION这个数据结构包含了进程所使用的虚拟内存空间的信息,在VirtualQuery函数中将使用到这个结构。
typedef struct _MEMORY_BASIC_INFORMATION {
PVOID BaseAddress;
PVOID AllocationBase;
DWORD AllocationProtect;
DWORD RegionSize;
DWORD State;
DWORD Protect;
DWORD Type;
} MEMORY_BASIC_INFORMATION;
typedef MEMORY_BASIC_INFORMATION *PMEMORY_BASIC_INFORMATION;
域成员功能解释如下:
n BaseAddress域 指向页group的基准地址。
n AllocationBase域 制向由VirtualAlloc分配得到的页组的首地址,BaseAddress指向的页也被包含在内。
n AllocationProtect域 描述了内存区域被分配时候所携带的访问权限。表7-11列出了这些权限的掩码。这些权限掩码都可以和PAGE_GUARD、PAGE_NOCACHE一起使用。
n RegionSize域 定义了从base address开始的整个region的虚拟内存区域大小,以byte为单位。
表7-11 权限掩码的描述
|
值 |
描 述 |
|
PAGE_READONLY |
只读权限,允许进程向已经提交的页进行读。如果对只拥有此权限的页进行写操作,将引起访问权限冲突。如果系统将读权限和执行权限分开的话,那么对这样的页进行执行操作也是不允许的 |
|
PAGE_READWRITE |
对已经提交的页的读/写权限 |
|
PAGE_WRITECOPY |
已经提交页的写时复制的权限。关于写时复制,请参考进程章节 |
|
PAGE_EXECUTE |
已经提交页的执行权限。如果对这些页进行读或者写操作,将引起权限冲突 |
|
PAGE_EXECUTE_READ |
已经提交页的读和执行权限 |
|
GE_EXECUTE_READWRITE |
已经提交页的读/写/执行权限 |
|
PAGE_EXECUTE_WRITECOPY |
对于该地址空间的区域,不管执行什么操作,都不会引发访问违规。如果试图在该页面上的内存中进行写入操作,就会将它自己的私有页面(受页文件的支持)拷贝赋予该进程 |
|
PAGE_NOACCESS |
禁止任何对此区域页的访问,任何读/写/执行操作都将引起权限冲突异常。这种异常被称为general protection (GP) fault |
|
PAGE_NOCACHE |
对此页的读、写、执行操作将不通过硬件cache来执行。这点对于要直接访问内存的驱动程序是十分适合的(比如对显存的直接访问),但是对于普通的应用程序,可能会导致系统性能的下降 |
n State域 描述了区域中页的状态,表7-12列举了所有可能的状态。
表7-12 区域中页的状态
|
值 |
描 述 |
|
MEM_COMMIT |
表明该页已经有对应的物理页,物理页可能是内存中的,也可能位硬盘上 |
|
MEM_FREE |
此页没有映射到物理页,因此在AllocationBase,AllocationProtect,Protect,域中没有数据。进程对它的调用也将不能成功返回,只有alloc函数才能对它进行操作 |
|
MEM_RESERVE |
此页是被保留的,没有映射到任何物理页。保留页的操作权限中是没有数据的 |
n Protect域 定义了页的访问权限,权限列表可以参照AllocationProtect的说明。
n Type 域 定义了区域中页的类型,类型列表如表7-13所示。
表7-13 区域中页的类型
|
值 |
描 述 |
|
MEM_IMAGE |
页被映射到imagesection中 |
|
MEM_MAPPED |
页被映射到普通section中 |
|
MEM_PRIVATE |
此页是私有的,其他进程无法看到 |
7.6 Windows CE服务
对于如何操作Windows CE的服务,将在后面章节中描述。在此仅对服务做总体介绍。Win32系统服务是Windows CE对应用程序提供的接口,多数公用的系统管理函数,例如,产生进程等,都是由NK负责。在早期的CE版本中有一组基本的API函数管理集合,系统和用户可以通过这组函数,将自行定义的接口注册到系统服务应用程序代理中,因为系统稳定性的缘故,目前这些函数只能在系统内部使用,例如和设备管理相关的Win32 API,就是通过这组函数注册的。
Windows CE服务被看作一种驱动程序,它具有一般驱动程序的结构,但接口更加复杂,具有更强的容错能力,由Services.exe和Device.exe共同管理。从设备管理的角度来看,这种增强的驱动接口增加了系统的容错能力,使系统变得更加稳定。要使用这种特性,付出的代价就是Services.exe需要占用一个处理程序的Slot。
1.服务的自动加载
开启系统时,Services.exe根据注册表HKLM/Services key下的subkey激活服务。每个subkey均代表一个服务,其下的注册表value记录了该服务的配置信息,包括激活时的配置,Services.exe根据这些value决定如何激活这个服务。
2.程序激活服务
系统提供了一个用以激活未运行的服务的函数——ActivateService。它需要以服务名作为参数,这个函数使得Services.exe根据对应的HKLM/Services subkey下的value激活该服务。
3.注册服务
通过RegisterService来注册服务,它向Services.exe请求激活一个有参数的服务。这个函数类似于RegisterDevice,不同的是后者由Devices.exe控制运行。
4.停止服务
DeregisterService负责服务的注销和停止。
5.控制服务
服务(驱动)需要使用文件流接口来控制,它们的使用与对文件的操作并没有太大的差别,例如,可以用CreateFile来打开一个服务句柄。
7.7 Windows CE设备管理
Windows CE支持多种类型的设备驱动程序,主要包括:
Audio驱动
Battery驱动
Block驱动
Bluetooth HCI Transport驱动
设备电源管理
Direct3D设备驱动接口
DirectDraw 显示驱动
显示驱动
DVD-Video驱动
IEEE 1394驱动
键盘驱动
网络驱动
LED驱动
并口驱动
PC卡驱动
打印机驱动
串口驱动
Smart Card驱动
流接口驱动
触摸屏驱动
USB驱动
以上这些驱动以及实际设备由设备管理器负责管理。设备管理器以Device.exe的形式实现。设备管理器是运行在用户态的进程,在内核启动的早期阶段被启动。设备管理器完成以下任务:
n 检测用户将一个外部设备插入到系统中。例如插入USB设备等。
n 如果设备使用流接口驱动程序,则将流接口注册到系统中。
n 为插入的设备匹配驱动程序,这可以通过轮寻设备的PnP标识或者调用探测例程实现。
n 通过读写注册表来加载驱动程序。
n 在驱动不再需要的时候,卸载驱动。
大体流程如图7-10所示。
图7-10 设备管理流程图
7.8 Windows CE注册表
关于设备管理器所使用的注册表自举,将在驱动程序结构一章中详细分析,在此仅对Windows CE的注册表结构进行分析。
Windows CE和桌面环境系统一样使用注册表(Registry)来保存应用程序、驱动程序和用户的设定以及其他一些配置信息。Windows CE注册表也采用树形结构来管理配置信息。
Windows CE支持4个主键,如表7-14所示。
表7-14 主键的描述
|
键 名 |
描 述 |
|
HKEY_LOCAL_MACHINE |
硬件和驱动配置数据 |
|
HKEY_CURRENT_USER |
用户配置数据 |
|
HKEY_CLASSES_ROOT OLE |
和文件类型匹配配置数据 |
|
HKEY_USERS |
适用于所有用户的数据 |
由于嵌入式系统的特点,一些嵌入式设备是没有外存的。因此Windows CE的注册表提供了两种实现方式——基于RAM的注册表(RAM-Based Registry)和基于Hive的注册表(Hive-Based Registry)。可以选择在Windows CE中使用任何一种注册表,注册表类型对于用户和应用程序来说是透明的。
Windows CE 4.0之后版本的系统提供了基于Hive的注册表。基于Hive的注册表把注册表数据存放在文件系统的文件上,这种文件被称作Hive(蜂箱)。这就意味着不再需要在系统断电和启动时进行保存恢复注册表的操作。Hive是注册表中的一组键、子键和值。Hive在文件系统上表现为单个文件。
Windows CE中有3种Hive——引导HIVE、用户HIVE、系统HIVE。
引导HIVE ROM中的Boot.hv HKEY_LOCAL_MACHINE、HKEY_CLASSES_ROOT、HKEY_USERS中的所有数据,只在启动时使用。
系统Hive由OEM决定(通常是System.hv)HKEY_LOCAL_MACHINE、HKEY_CLASS-
ES_ROOT、HKEY_USERS中的所有数据,包含设备范围内不随着用户改变而改变的数据。
User hive User.hv是HKEY_CURRENT_USER下的所有数据,包含用户特有的设置,每个用户都有一个单独的User.hv。
基于Hive的注册表适用于对于有永久存储并且需要经常冷启动的设备。而且基于Hive的注册表把系统数据和用户数据分开存放,这就意味着基于Hive的注册表还提供多用户支持。对于每一个用户,可以提供不同的User.hv,当用户登录时加载相应的User.hv,从而达到多用户目的。
7.8.1 为设备选择注册表
在Windows CE系统中为设备提供了两种注册表——基于RAM的对象存储注册表和基于hive的注册表。对于应用程序来说,注册表的类型是不可见的。但是不同类型的注册表的永久性是不同的。它们给系统启动、系统内存管理也带来不同的影响。因此适当地选择一种注册表将对系统性能的提高有帮助。
基于RAM的注册表在Windows CE 1.0中就已经存在了。这也难怪,当时只有ROM,任何需要保存的信息都被存放在RAM中,注册表也不例外。基于RAM的注册表中的数据最终都以对象的形式存放其中。因此在这种系统中,一般只建议热启动,而非冷启动。在有非易失性可擦写存储器的设备中,OEM可以在冷启动前将注册表备份起来,然后进行冷启动。在启动完后再恢复注册表,但是这个操作过程是按照注册表中一项一项进行恢复的,要花去很长时间。基于RAM的注册表在经常热启动的设备上是非常高效的,而在经常冷启动的设备上,性能却让人失望。最适合的,便是有电池后备的个人电子消费类产品。
基于hive的注册表是一项新的功能。所有注册表的信息都被存储在文件中,这些文件也被称为hive文件,这种类型的文件可以用在任何文件系统中。而文件系统通常是存放在Flash上的,这样就允许OEM层在不启动RAM的情况下,始终能够访问注册表。基于hive的注册表同样也为用户提供了可自定义的hive文件,这样每个用户就可以有自己不同的配置。在一个多用户的系统中,通常会保留多个用户的hive文件。用户的hive文件可以在登录的时候被加载到文件系统中,在注销的时候被卸载。对于经常冷启动或者不怎么使用热启动方式的设备来说,基于hive的注册表是非常高效的。它最适合永久性的存储器和多用户环境下使用。
7.8.2 基于RAM的注册表
正如其名,基于RAM的注册表把整个注册表作为一个对象存储堆存放在RAM中。这意味着如果系统中断对RAM的电池供电,对注册表的所有改动都会丢失。也就是说,它们的有效性和RAM中数据的有效性是一致的。但是由于RAM读写速度的高效性,使用RAM注册表,对注册表的读写访问操作会变得快速高效。在旧版本的Windows CE中,大量应用了基于RAM的注册表。如果有外存且经常冷启动的设备采用基于RAM的注册表,则需要在系统断电的时候对注册表进行保存,等系统再次启动时对保存的注册表进行还原。
基于RAM的注册表使用默认的.fdf文件,这个文件在编译内核的过程中产生。在冷启动的时候,这个.fdf文件被打开并且读取,RAM注册表将按照其中的内容建立。在.fdf文件中,同一种数据(注册表项)可以有多个入口,但是只有文件最后一个入口中的值才有效。
如何使用基于RAM的注册表来保存注册表数据
基于RAM的注册表支持两种不同的存储-修复机制。这些机制使得OEM层可以在RAM断电后对注册表进行修复。另外,这两种修复机制还可以用来处理无效的数据配置。
第一种机制是建立在RegCopyFile和RegRestoreFile这两个API上的。RegCopyFile函数将整个注册表存放到文件中,RegRestoreFile函数将建立下次启动后用来替换原有注册表的新数据。要使用这个恢复功能,首先在设备断电前OEM层应当调用RegCopyFile函数将数据存储到一个永久性的文件中,然后在下一次冷启动的时候,OEM调用RegRestoreFile函数,从刚才的永久性文件中读出数据并填充到注册表中,然后再执行相当于热启动的一系列操作。这种注册表恢复机制是比较容易实现的,但是由于它要在冷启动时执行两次启动过程(如刚才所说恢复注册表后要再执行热启动),因此它的执行速度比较慢,况且恢复注册表项也是要花费一定时间的。因此这种方法最好只是用在错误处理的恢复功能上。
Windows CE提供了两个API用来保存和还原整个注册表,它们的原型如下:
BOOL RegCopyFile(
LPCWSTR lpszFile // 保存注册表信息的文件的名字
);
BOOL RegRestoreFile(
LPCWSTR lpszFile // 保存注册表信息的文件的名字
);
如果要保存和恢复注册表,应该在系统断电的时候调用RegCopyFile函数将整个注册表保存为外存上的一个文件。当系统重新启动时,再调用RegRestoreFile函数将文件全部从RAM中读出,然后再热启动系统,被保存的注册表才生效。因为只有热启动的情况下,系统才会去读取RegRestoreFile恢复在RAM里的注册表信息,将内容的变动映射到系统的管理中去。这种方法操作虽然相对简单。但是此方法的缺点是需要两次启动。因此效率相对比较低。
第二种恢复机制要求OEM对注册表的永久性作出保证。OEM可以在OAL层中实现WriteRegistryToOEM和ReadRegistryFromOEM函数。这些函数将在系统的注册表恢复中被自动调用。如果应用程序调用了RegFlushKey函数,系统将对WriteRegistryToOEM函数进行一系列的调用,将注册表中的数据导入到OEM中。然后由OEM将数据固化在存储器上。在启动时,系统又将自动地对ReadRegistryFromOEM函数进行一系列的调用,从存储地址中逐项读出数据并填写入注册表。然而文件系统调用,例如CreateFile、ReadFile、WriteFile,是无法在ReadRegistryFromOEM调用中操作文件数据的。但是它们可以在WriteRegistryToOEM中使用。因为,在注册表初始化之前,块设备驱动是无法对外部存储器进行访问的,但现在问题是在这种恢复机制下要初始化注册表需要先访问外部存储器。这就产生了一个鸡生蛋、蛋生鸡的问题。因此,如何实现ReadRegistryFromOEM函数是非常复杂的。开发人员需要绕过块设备驱动直接访问外部存储器。但是这种恢复机制又有一个好处,就是不像前一种机制那样需要双启动。这样在执行时间上有所减少。
下面是函数原型:
DWORD ReadRegistryFromOEM(
DWORD dwFlags, // 参数, REG_READ_BYTES_START表示读新的注册表
LPBYTE lpData, // 指向注册表的内存缓冲区,由操作系统分配
DWORD cbData // 缓冲区的大小
);
BOOL WriteRegistryToOEM(
DWORD dwFlags, // 参数,REG_WRITE_BYTES_START表示写新的注册表
LPBYTE lpData, // 指向注册表数据的缓冲区,由操作系统分配
DWORD cbData // 缓冲区的大小,0代表到达注册表尾部
);
Windows CE会在系统启动和关闭的时候调用这两个函数来保存和恢复注册表。此种方法虽然可以避免两次启动,但是ReadRegistryFromOEM函数的实现比较困难,因为在系统启动的时候,块设备驱动和文件系统的驱动都还没有加载,因此不能使用CreateFile、ReadFile这样的文件系统API来实现ReadRegistryFromOEM函数,只能使用一些更底层的操作来实现,当然这对习惯硬件编程的程序员也许还能接受。
下列各对函数是不能混合使用的:
n RegCopyFile和RegRestoreFile
n WriteRegistryToOEM和ReadRegistryFromOEM
此外,由函数RegCopyFile和WriteRegistryToOEM产生的注册表存储文件格式也是不一样的。
选择使用RAM注册表的恢复机制,需要首先理解RAM注册表的初始化过程。下面分析此初始化的过程。
RAM注册表初始化的过程如下:
① 系统初始化内核。
② 内核加载Filesys.exe文件系统模块。
③ Filesys.exe文件系统模块初始化RAM文件系统。
④ Filesys.exe文件系统模块初始化数据库子系统。
⑤ Filesys.exe文件系统模块初始化注册表,如果在RAM中寻找到一个注册表,那么直接跳转到第7步。
⑥ 如果在第5步中没有找到任何注册表,那么Filesys.exe将从ROM文件系统中调用Windows、Default.fdf文件,以重建注册表。
⑦ Filesys.exe检查ReadRegistryFromOEM函数是否被实现。如果没有实现,那么第6步中建立的注册表被保留。
在RAM注册表初始化的时候,不能访问任何通过设备驱动才能访问的外存设备,比如PC卡,因为这个时候Device.exe还没有被内核加载。
开发人员需要做的如下:
① 在Filesys.exe初始化文件系统后,从OAL中导出ReadRegistryFromOEM和WriteRegistryToOEM接口。
② 如果OAL不能访问,那么使用RegCopyFile和RegRestoreFile接口以便从RAM初始化注册表。
③ 当RegRestoreFile执行成功后,重启目标系统(热启动)来使设置有效。
如果要使用户不用手动地重启目标系统,可以在HKEY_LOCAL_MACHINE/Init中添加一个注册表键。在这个键中,首先恢复注册表,然后在设备管理器被加载之前重新启动。在设备管理器被加载之前重新启动是为了防止显示器上有任何信息输出,因此在整个注册表初始化或者恢复的过程中,显示器都将没有任何信息显示,这样用户也不会感觉到两次启动的存在了。
7.8.3 基于Hive的注册表
Hive系统包含了不属于任何用户信息的系统设置。Hive系统由OEM层调用。通常hive文件为System.hv,但是它的存储位置可能随平台而变化。
在默认情况下,系统和各个用户的hive文件被分割存放在两个位置。当系统启动的时候,系统读取ROM中的hive文件。任何对注册表的改动都将被写入文件系统的hive中。冷启动后,在对注册表修改之前,文件系统中的hive都是空的。
HIVE注册表初始化的过程如下。
将注册表存放在外存上,势必使系统的启动顺序复杂起来。例如,如果注册表存放在PC卡中,那么注册表的加载必然要求设备驱动的支持。而使用设备管理器,又需要注册表的支持。为了解决这个类似死锁的问题,可以先加载一个最小版本的注册表(引导HIVE),在这个hive注册表中只包含了最小范围的注册信息,它们保证了访问hive文件所必要的设备驱动的加载。引导HIVE被存放在ROM中,因而它不会发生改动。任何对它的改动都将被另外处理(将在后续章节中讲解)。
系统中设备驱动的加载分为两个阶段。在第一阶段中,一个很小范围的驱动被加载,这个范围在引导HIVE中明确表述。这些驱动最主要的功能就是访问外存上的文件系统,即加载文件系统模块,因为引导HIVE是一个文件,必然需要对文件系统的支持。当注册表被完全加载后,第二阶段的驱动加载就开始了。但是在第一阶段中已经被加载的驱动应当做上标记,以防止在第二阶段中再次被加载。此外,由于引导HIVE位于ROM中,因此第一阶段加载的设备,它们的工作模式等都不会发生改变。如果这些设备需要改变自己的配置,那么必须等到第二阶段的驱动加载开始后才能进行。
在文件系统模块加载之后,Filesys.exe再通过启动boot registry hive加载设备管理模块(Device.exe),然后等待system registry加载。设备管理模块加载后,它加载一组boot registry hive中指定的驱动(这组驱动用来初始化system registry所在设备的文件系统),然后等待system registry加载。当system registry完成加载后,它被从设备中读出。这时候,注册表就完成了设置,应用程序可以启动运行了。
然而在这个时候,第二阶段的驱动加载开始了。同样,设备管理模块由于已经在运行,系统会触发一个System/BootPhase2的事件,这个事件促使设备管理模块重新进行设备枚举。
图7-11从MSDN中截取的,它有效地说明了整个加载过程。
图7-11 注册表的加载过程
表7-15所示描述了图7-11所示中的各个阶段。
表7-15 注册表加载过程各阶段描述
|
阶 段 |
描 述 |
|
A |
内核启动Filesys.exe |
|
B |
Filesys.exe加载boot registry hive |
|
C |
Filesys.exe启动设备 |
|
D |
设备管理模块按照boot registry hive启动相关的设备驱动 |
|
E |
块设备和注册表所在设备的文件系统被初始化 |
|
F |
Filesys.exe加载系统真正的注册表 |
|
G |
Filesys.exe完成任务,内核初始化系统注册表 |
|
H |
内核设置注册表完毕,并且通知文件管理模块 |
|
I |
Filesys.exe启动应用程序 |
|
J |
Filesys.exe发出BootPhase2事件 |
1.如何使用基于Hive的注册表
以hive为基础的文件可使用于任何文件系统。建立Hive注册表的第一步是将Hive注册表的目录入口添加到设备配置中去。建立Hive注册表最大的任务包括准备适当的注册表设置。这些注册表设置信息将用来指示设备的启动顺序。
可以在注册表文件Platform.reg中检查下列键值。
[HKEY_LOCAL_MACHINE/init/BootVars]
"SystemHive"="<your 系统HIVE location>"
"Start DevMgr"=dword:<your value>
值SystemHive是系统hive文件的文件名和路径。在这里不需要填写hive文件要存放的文件系统的名称,因为系统将根据其他注册表的键值来判断文件系统类型。可以用下列方法来填写:“System.hv”或者“Documents and Settings/System.hv”。
值Start DevMgr是Boolean值,它指出是否应该启动设备管理器,只有启动设备管理器才能访问必要的外部设备。0代表不启动设备管理器,1代表启动设备管理器。只有在一种情况下才可以不启动设备管理器,hive文件是存放在对象存储空间的。但是通常为了防止RAM中的数据丢失,hive文件被存放在除RAM以外的设备中,这样设备管理器势必要在对hive文件操作前启动。
在多用户的情况下,HKEY_LOCAL_MACHINE/init/BootVars键下的DefaultUser值定义了默认加载的用户。系统启动时将加载DefaultUser所指定的用户的profile文件,如果这个用户是已经存在的,则加载;如果是新的用户,则系统为其创建新的profile文件。
OEM可以这样来设置DefaultUser:
[HKEY_LOCAL_MACHINE/init/BootVars]
"DefaultUser"="<username>"
使用基于Hive的注册表允许保存注册表的信息而不必修改启动或者关闭的顺序。要实现基于Hive的注册表,需要完成以下步骤:
① 保证将包含系统HIVE的文件系统驱动程序的注册表信息和存储设备的驱动程序的注册表信息被包含在“; HIVE BOOT SECTION”和“; END HIVE BOOT SECTION”之间,在这两个语句之间的注册表数据全部属于引导HIVE。假如将系统HIVE文件system.hv存放在硬盘上,并采用FAT文件系统,那么就要将[HKEY_LOCAL_MACHINE/System/ StorageManager/FATFS]和[HKEY_LOCAL_MACHINE/System/StorageManager/Profiles/HDProfile]移动到“; HIVE BOOT SECTION”下。
② 为设备驱动加载第一阶段中的驱动设置如下的位标记:
[HKEY_LOCAL_MACHINE/Drivers/...]
"Flags"=dword:1000
这个标志是一个位掩码,它可以和其他已经存在的“Flags”或运算。这是个或操作标记,通知了设备管理器在第二阶段加载驱动过程中不要加载本驱动程序。
③ 将MountFlags值设置成DWORD:2。这个标志表示这个存储设备包含系统HIVE文件。在包含系统HIVE的存储设备的驱动程序的注册表信息中加入如下标志:
[HKEY_LOCAL_MACHINE/System/StorageManager/Profiles/<ProfileName>/<FileSystemName>]
"MountFlags"=dword:2
④ 最后,应当决断是否使用注册表刷新机制。
任何对hive注册表的修改都将定期地或者在软关机前刷新到存储器文件中。在突然断电的情况下,数据仍然丢失。可以使用RegFlushKey来实现这个功能。
2.如何使用基于Hive的注册表来保存注册表数据
使用基于Hive的注册表就是为了使OEM从对注册表的信息保留处理中解脱出来,在启动和关机时不必要像RAM注册表那样消耗大量的时间和资源保存注册表的修改。
当应用程序调用RegFlushKey接口时,系统会将所有对注册表的改动刷新到存储器的hive文件中。可以选择刷新系统hive或者用户hive,这是由传入的参数决定的。以下是这个函数的原型:
LONG RegFlushKey(
HKEY hKey
);
其中参数hKey的含义为指向当前打开的注册表键值或者是以下任意一个:
HKEY_CLASSES_ROOT
HKEY_CURRENT_USER
HKEY_LOCAL_MACHINE
HKEY_USERS
在使用hive注册表的情况下,关机时一般使用主动刷新注册表机制,在这种机制下,每当调用RegCloseKey后,RegFlushKey 都被执行一遍来更新注册表的内容。
下面的注册表键显示了如何启用主动注册表刷新功能:
[HKEY_LOCAL_MACHINE/init/BootVars]
"RegistryFlags"=dword:1
将值RegistryFlags设置成1代表起用主动刷新功能。如果这个值被设置成0或者缺省,那么注册表的刷新机制是不确定的。
在系统启动的过程中,RegistryFlags值是只读属性。直到下一次重启动的时候才被再次读取。因此在系统运行过程中,无法强制转换注册表的刷新机制。
7.9 电源管理
电源管理是管理设备电源并且改进操作系统电源效率的一个组件。它提供了对设备的电源管理,在应用程序和驱动程序不支持电源管理的情况下,它仍然能够工作。使用电源管理后,可以让设备在休眠、等待、空闲等时间段上降低电源消耗,而非用切断电源的手段来保存易失性存储器中的数据。
同时,电源管理也提供了以下的功能:
n 提供了设备可以自己管理电源供应的框架。
n 提供了一种机制,将设备的电源管理和系统的电源状态分割开。
n 提供了一个模块,可以从全局的角度查看系统环境、电源状态和设备供电状态。用户可以从整个系统的角度出发,修改电源管理模块,实施适合自己应用特点的电源管理策略。
n 电源管理模块是以动态连接库的形式存在于系统中的,称为Pm.dll,它和Device.exe直接相连。
在设备自举的时候,设备的驱动程序应该通过系统提供的接口通知操作系统该设备的电源管理能力。在自举完成后,操作系统就会按照设备提供的电源管理功能来向设备发出电源消耗改变请求。在自己管理电源的情况下,设备应该通过DevicePowerNotify接口请求系统改变它们的电源消耗状态。因此所谓自己设备管理电源,是指设备知道自己该如何去消耗电源,但是更改状态还需要通过操作系统。
在驱动中,可以通过DeviceIoControl服务例程来处理系统发来的电源请求,这大大方便了驱动程序的编写。
在Windows CE.net的设备电源管理接口分为应用程序和驱动程序两大部分,也就是说只要驱动程序提供支持,在应用程序中一样可以控制设备的电源使用状态。
7.9.1 电源状态等级
电源管理模块提供了一系列设备电源状态,设备如果要自我管理电源就需要支持这些状态中的一部分,其中有几个状态是必须被支持的。在设备自举的时候,驱动程序应当将其支持的电源状态通知操作系统。
电源管理是在系统电源管理的框架下对设备的电源进行管理的。系统电源状态是在注册表中被定义的。当设备要求的电源状态和系统能提供的电源状态不一致时,系统将选择功耗大于设备请求电源状态的前一个电源状态。
有些应用程序可能要求对设备的管理必须在某一特定的电源状态上。例如,在播放声音的过程中,声卡可能会要求它的解码器工作在高电源供应状态。在这种情况下,应用程序可以通过使用SetPowerRequirement和ReleasePowerRequirement API来要求系统对其电源请求做出响应。
以下是Windows CE提供的电源选项,在C:/WINCE420/PUBLIC/COMMON/SDK/INC/
pm.h文件中:
#define POWER_STATE(f) ((f) & 0xFFFF0000) // power state mask
#define POWER_STATE_ON (DWORD)(0x00010000) // on state
#define POWER_STATE_OFF (DWORD)(0x00020000) // no power, full off
#define POWER_STATE_CRITICAL (DWORD)(0x00040000) // critical off
#define POWER_STATE_BOOT (DWORD)(0x00080000) // boot state
#define POWER_STATE_IDLE (DWORD)(0x00100000) // idle state
#define POWER_STATE_SUSPEND (DWORD)(0x00200000) // suspend state
#define POWER_STATE_RESET (DWORD)(0x00800000) // reset state
7.9.2 支持电源管理的驱动程序
为了建立一个能够对设备进行电源管理的驱动程序,用户必须首先建立一个支持non-COM-related设备接口的驱动程序。non-COM-related设备接口标明这个设备是电源可管理的。
可以用以下方式来建立这种接口:
n 可以在注册表中,用激活设备的注册表链的IClass值定义接口。
n 可以使用驱动程序的init函数,在注册表中的Active registry key里设置IClass值。
n 可以使用ActivateDeviceEx的参数REGINI设置IClass值。
n 可以在驱动中显式地调用AdvertiseInterface函数。
可以通过调用RequestPowerNotifications函数来注册驱动中电源管理的回调函数并且将句柄传递给电源管理回调专用的消息队列。通常情况下,当一个驱动程序声明为支持电源管理时,驱动只需要在DeviceIoControl中实现电源的管理即可。
电源管理模块通过IOCTL代码来和驱动通信。IOCTL是Windows驱动程序员非常熟悉的概念。
7.9.3 驱动程序电源管理API
电源管理模块使用两种机制来和支持电源管理的驱动程序进行通信——自上而下的调用和自下而上的调用:电源管理模块可以自上而下地要求设备驱动更新电源状态,返回当前电源状态等;同样,当设备驱动探测到设备需要转换电源状态时,就会自下而上地请求系统予以确认。自上而下的通信方式是通过前一节所述的IOCTLs机制来实现的。自下而上的通信方式是通过调用DevicePowerNotify,然后执行回调函数来完成的。
1.Device Control IOCTL
表7-16所示是Windows CE提供的IOCTL 。
表7-16 IOCTL描述
|
功 能 |
描 述 |
|
IOCTL_POWER_CAPABILITIES |
代表电源管理模块请求设备驱动返回设备支持的电源状态 |
|
IOCTL_POWER_SET |
请求驱动刷新设备的电源状态 |
续表
|
功 能 |
描 述 |
|
IOCTL_POWER_QUERY |
电源管理模块询问设备是否准备好进行电源状态转换 |
|
IOCTL_POWER_GET |
请求驱动返回当前设备的电源状态 |
|
IOCTL_REGISTER_POWER_RELATIONSHIP |
要求父设备注册所有它所控制的设备 |
可以通过处理IOCTL_POWER_QUERY时的延迟来等待设备做好转换前的准备。
2.Driver-to-Power Manager API
电源管理模块提供了如表7-17所示的函数,使驱动程序能向模块发出请求。
表7-17 电源管理模块函数
|
功 能 |
描 述 |
|
DevicePowerNotify |
驱动请求电源管理模块向其发出更新电源状态的命令 |
|
RegisterPowerRelationship |
当总线驱动或者代理电源管理器要截获驱动与电源管理模块之间的IOCTL命令字时,应当调用此函数。也就是说,该设备的电源管理被代理了 |
|
ReleasePowerRelationship |
结束前一函数设置的电源管理代理关系 |
7.9.4 电源状态的转变
电源管理模块在以下情况才是设备更改电源状态:
n 当产生一个OEM-specific事件,它的执行需要更改当前设备的电源状态。
n 应用程序调用SetSystemPowerState。
所谓OEM-specific事件,可以包括像从AC供电转换为电池供电的信号、电池能量低信号等。OEM应当将电源管理模块配置成能够识别设备电源状态,准确判断电源状态的转换时间。Platform Builder自带的电源管理模块的源程序中,仅仅支持从AC供电转换为电池供电的信号的处理。
7.9.5 应用程序电源管理API
应用程序也能参与到设备电源管理的活动中。Windows CE同样提供了一组接口,如表7-18所示。
表7-18 电源管理接口
|
功 能 |
描 述 |
|
GetDevicePower |
返回设备电源状态 |
|
GetSystemPowerState |
返回系统当前有效电源状态 |
续表
|
功 能 |
描 述 |
|
GetSystemPowerStatusEx2 |
获得电池信息 |
|
ReleasePowerRequirement |
撤消SetPowerRequirement设置的特定设备的电源状态要求 |
|
RequestPowerNotifications |
通过这个函数,应用程序和驱动程序可以注册电源的通知事件 |
|
SetDevicePower |
设置特定设备的电源状态 |
|
SetPowerRequirement |
通知电源管理模块,该应用程序需要将设备设置在特殊的电源状态下。这样,特定设备的电源状态就不会随系统电源状态的改变而改变 |
|
SetSystemPowerState |
将电源状态设置到指定级别。根据StateFlags 参数标志,程序或者立即设置系统的电源状态,或者等待系统和各设备的允许 |
|
StopPowerNotifications |
允许应用程序或者驱动程序撤消拦截来自电源管理模块的电源管理回调信息 |
在这里要说明的是,并不建议程序员在程序中频繁使用SetDevicePower函数,因为这将打乱并且覆盖系统提供的电源管理算法。在电源管理模块工作的时候,尤其是在转换系统或者设备的电源状态时,应用程序也应该尽可能少地调用SetSystemPowerState。一般情况下,只建议GetSystemPowerState、SetPowerRequirement、ReleasePowerRequirement使用在应用程序中,理由很简单,它们不会和系统以及驱动的电源管理相冲突。
设置系统电源状态
在某些应用的场合下,应用程序可能需要改变系统的电源状态。假设应用程序并不知道系统支持哪几种电源状态,而且也不知道它们可以利用的系统电源状态的种类。前文已经说明,并不希望程序员直接调用SetSystemPowerState来设置系统的电源状态,在这样的情况下,应用程序可以使用bitmask参数和这个函数一起调用。这个bitmask中的位掩码代表了系统电源的各种状态。电源管理模块会将这个掩码安全地映射到系统的电源状态。比如,一个应用程序设置了掩码中POWER_STATE_SUSPEND位,电源管理模块会将这个位解释成Suspend或者SuspendCradle状态。
电源管理模块也许会阻止应用程序将系统设置到某种电源状态。比如,当设备仅仅依靠电池供电时,应用程序无论如何请求转换成AC供电都是徒劳的。
表7-19所示是bitmask中各位代表的系统电源状态。
表7-19 bitmask代表的电源状态
|
位 号 |
描 述 |
|
POWER_STATE_ON |
最高的性能级别,并且耗电量也最高 |
|
POWER_STATE_OFF |
所有设备都被关闭。这是冷启动前的准备 |
|
POWER_STATE_CRITICAL |
电池电量低,这将执行系统基本的维护工作,将DRAM设置为自动刷新模式,然后将电源集中供应给设备 |
续表
|
位 号 |
描 述 |
|
POWER_STATE_BOOT |
系统在启动过程中 |
|
POWER_STATE_IDLE |
没有设备处于运行状态中,并且没有用户的信息输入。这时所有设备应该进入低电源消耗状态。这个状态可以用来替代挂起电源模式 |
|
POWER_STATE_RESET |
将缓冲文件内容全部写出到文件,关闭设备,重新启动 |
|
POWER_STATE_SUSPEND |
将系统悬挂,这将使CPU进入休眠状态 |
本文深入介绍了Windows CE的层次式架构,特别关注内核模块,包括中断处理机制和进程结构。中断体系结构由硬件中断、内核ISR、OAL ISR和IST组成,中断服务模型围绕这5部分展开。此外,还探讨了Windows CE的进程,包括进程组成、进程简介和进程结构。最后,讨论了注册表管理,特别是基于Hive的注册表的使用和保存注册表数据的方法。

828

被折叠的 条评论
为什么被折叠?



