摘自何宗键老师《Windows CE嵌入式系统》相关章节
进程<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
与桌面版本的Windows不同,Windows CE最多只支持32个进程同时运行。每个进程占据32MB的虚拟地址空间,这也被称为一个Slot。
此外,Windows CE的进程也不支持环境变量的和当前目录。这两点对熟悉桌面Windows开发的开发者尤其要注意。如果MyApp.exe的同一目录中存在MyFile.txt,那么下面的代码都是不正确的。
_wfopen(L”%WINDOWS%\\MyFile.txt”, L”w”); // try to use environment variable
_wfopen(L” MyFile.txt”, L”w”); // try to use current directory
一种解决方案是,首先使用GetModuleFileName()函数得到当前执行文件所在目录,然后再把要打开的文件拼接到路径中得到完整的路径。参考代码如下:
TCHAR szBuf[MAX_PATH];
DWORD dwPathLen;
TCHAR pStr = NULL;
dwPathLen = GetModuleFileName(NULL, szBuf, MAX_PATH);
if (!dwPathLen) return -1;
while (szBuf[--dwPathLen] != ‘\\’);
szBuf[dwPathLen+1] = NULL;
_tcscat(szBuf, TEXT(“MyFile.txt”));
// 如果当前文件位于/Temp,则此时szBuf的内容为”/Temp/MyFile.txt”
线程
Windows CE中的核心态和用户态
在Windows CE中,核心态和用户态是两个比较令人迷惑的概念。当我们说驱动程序运行在用户态时,这里的用户态与桌面版本Windows中的用户态其实是两个不同的概念。在桌面版本的Windows中,核心态和用户态有着严格的划分,这通常还会与CPU硬件的状态有关(在X86上是Ring3,Ring0)。但是在Windows CE中,由于体系结构的不同,Windows CE中不存在这样的划分。
那么,在Windows CE中什么是核心态呢?在Windows CE的核心态下,线程可访问核心态的2GB虚拟地址空间(≥0x80000000)而不会导致访问违例(Access Violation)异常。这就是核心态与用户态的最主要的区别。
调度
Windows CE是一个抢占式多任务(Preemptive Multitasks)操作系统。调度程序使用基于优先级的时间片算法对线程进行调度。
表2.2 Windows CE的优先级映射示例
优先级 | 组件 |
0~19 | 高于驱动程序的实时 |
20 | 图形垂直回描(Vertical Retrace) |
99 | 电源管理唤醒线程 |
100~108 | USB OHCI UHCI、串口 |
109~129 | 红外、NDIS、触摸屏 |
130 | 内核无关传输层(KITL) |
131 | VMini |
132 | CxPort |
145 | PS2键盘 |
148 | IRComm |
150 | TAPI |
248 | 电源管理 |
249 | WaveDev、鼠标、PnP、Power |
250 | WaveAPI |
251 | 正常 |
252~255 | 应用程序 |
在线程获得处理器后,会执行特定的一段时间,然后重新调度,这段时间称做时间片大小(Quantum)。每个线程都有一个时间片大小,默认的时间片大小是100ms,OEM可在内核初始化的时候改变此值的大小。分配给线程的时间片大小可用CeGetThreadQuantum()函数获得,此函数以线程的句柄为参数,返回值是一个32位无符号整数,代表线程的时间片大小。
线程的状态可有以下几种。
● 运行(Running) 线程正在处理器上执行。
● 就绪(Ready) 线程可以执行,但是此刻没有占有处理器。如果就绪的线程被调度程序选中,则占有处理器就进入运行状态。
● 挂起(Suspended)创建线程时指定了CREATE_SUSPENDED参数或者调用SuspendThread函数都可导致线程挂起。挂起的线程不能占有处理器。每个线程都有一个挂起计数,SuspendThread函数用来增加挂起计数,ResumeThread函数使线程的挂起计数减1.当线程的挂起计数为0时,线程转入就绪态。
● 睡眠(Sleeping) 调用Sleep函数可使线程进入睡眠状态,处于睡眠状态的线程不能占有处理器。当睡眠时间结束后,线程转入就绪态。
● 阻塞(Blocked) 如果线程申请的共享资源暂时无法获得,那么线程就进入阻塞状态,处于阻塞状态的线程不能占有处理器。当共享资源可以获得后,线程转入就绪态。
● 终止(Terminated) 线程运行结束。
线程的几种状态之间的转换关系可由图2.8表示。
<?xml:namespace prefix = v ns = "urn:schemas-microsoft-com:vml" />
图2.8 线程的状态转换图
Windows CE的调度是严格按照优先级来进行的。例如,系统中有3个线程A,B和C。A拥有最高的优先级,B与C的优先级相同,都比A要低。在这样的情况下,如果A处于就绪态,那么调度系统总是选择A来执行,B和C不会得到执行的机会;如果A不在就绪态,调度系统会选择B或C来上台执行。B和C会采用时间片轮转算法(Round Robin,RR),也就是说,让B运行一段时间,然后让B下台再让C上台运行一段相同的时间,以此类推。由此我们可以看出,Windows CE的调度系统有这样的特点。
● 具有最高优先级的进程如果处于就绪状态,则总是会被调用系统选中执行;
● 如果系统中存在多个优先级相同的就绪进程,这些进程以时间片轮转算法调度;
● 如果线程的时间片大小被设置为0,那么它会一直占有处理器运行,直到线程结束或者进入阻塞、挂起及睡眠状态;
● 调度系统不提供对线程饥饿(Starvation)的自动检测。
Windows CE的调度系统总是会选择优先级最高的就绪线程去执行,这样一方面增加了系统的实时性,但是有时候也会出现一种比较奇怪的现象:系统中有一些资源是由多个线程共享的,如果具有高优先级的线程申请的资源正在被低优先级线程占有,那么此时高优先级的线程就阻塞在低优先级的线程上,反而使具有中优先级的线程先于高优先级的线程执行,这就是所谓的优先级反转(Priority Inversion)问题。优先级反转轻则导致系统的响应时间大大增加,重则导致系统的崩溃。因此,一般的实时操作系统中都会采取一些措施来解决优先级反转问题。
一般来说,操作系统解决调度中的优先级反转问题有两种方法:单级(Singal Level)和完全嵌套(Fully Nested)。
在完全嵌套方法中,操作系统将遍历系统中所有的阻塞线程,然后使每个阻塞的线程都上台执行,直到高优先级的线程可以运行为止。这种方法的优点是可以防止死锁。但遗憾的是,这也意味着在解决优先级反转时,操作系统要在阻塞的线程上执行复杂度为O(n)的搜索,这对于一个实时系统来说,可能会无故增加系统的响应时间。
为了支持硬实时,Windows CE从3.0版开始使用单级方法解决优先级反转。在这种方法中,操作系统只会激活导致高优先级线程阻塞的一个低优先级线程上台执行,直到释放共享资源。这种方法提高了算法的速度,但是可能贸然改变优先级会导致系统死锁。因此,保证系统不会因为优先级反转而死锁的任务就落到了开发者的头上。
图2.10是一个优先级反转及其解决方法的例子。图中,高优先级的线程1因为等待低优先级的线程3所占有的资源,因此造成优先级反转。系统检测到优先级反转后,把线程3的优先级提高到与线程1相同,这样原本低优先级的线程3就得以执行。当线程3释放共享资源,线程1得到资源后,系统再把线程3的优先级降低到以前的级别。这样,高优先级的线程1得以继续执行。这种方法也被称做优先级继承(Priority Inheritance)。
同步
Windows CE中提供了Mutex,Event和Semaphore三种内核机制来实现线程之间的同步。所有的这些同步对象都有两种状态:通知(Signaled)和未通知(Non-signaled)。未通知状态表示该同步对象被某一个或多个线程占有,不能被其他等待的线程占有。当某个同步对象的状态变为通知状态时,等到在它上面的阻塞线程会得到通知,并且转为就绪态,等到调度执行。这种方法可使线程锁步(Lockstep)执行,这也是同步的基本原理。
等到同步对象函数:
DWORD WaitForSingleObject(
HANDLE hHandle, //同步对象句柄
DWORD dwMillseconds //等待的ms数
);
DWORD WaitForMultipleObjects(
DWORD nCount, //lpHandles中的同步对象个数
CONST HANDLE* lpHandles, //同步对象句柄的数组
BOOL fWaitAll, //是否等待lpHandles中所有的内核对象
DWORD dwMillseconds //等待的ms数
);
Mutex
Mutex是Mutual Exclusion的缩写。正如其名,同时只有一个线程可以占有Mutex对象。其他的线程如果希望占有Mutex,则必须使用等待函数在该对象上等待,当占有Mutex的线程释放它后,其他线程才有机会获取Mutex。
如果某些共享资源同时只能被一个线程访问,那么使用Mutex对其进行同步是最好的选择。与Mutex相关的函数有以下两个:
//创建一个Mutex同步对象
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, //不支持,设置为NULL
BOOL bInitialOwner, //是否为创建该Mutex的线程所拥有
LPCTSTR lpName //可选,该Mutex的名称
);
//释放对Mutex的占有
BOOL ReleaseMutex(
HANDLE hMutex //Mutex的句柄
);
其中CreateMutex()函数用来创建一个Mutex对象。bInitialOwne参数指定该Mutex是否为创建的线程所占有,如果此参数为TRUE,那么该Mutex创建结束后,创建它的线程就获得了该Mutex的占有权。lpName是可选的Mutex的名称,如果此项不为空,那么系统在创建Mutex前会首先搜索系统中已经存在的Mutex。如果发现有同名的Mutex,则直接返回该Mutex;否则就创建一个新的同步对象。
下面的代码片段展示了两个线程如何使用Mutex对象对共享的全局变量i进行访问。每个线程在对共享变量进行访问前,都首先要使用等待函数得到Mutex对象,当对共享资源访问结束后,应当立即释放共享对象。
HANDLE hMutex;
INT i = 0;
//创建Mutex对象和线程
VOID Init()
{
hMutex = CreateMutex(NULL, FALSE, NULL);
CreateThread(NULL, NULL, ThreadProc1, NULL, NULL, NULL);
CreateThread(NULL, NULL, ThreadProc2, NULL, NULL, NULL);
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
WaitForSingleObject(hMutex, INFINITE); //获得Mutex对象
i ++; //访问共享资源
ReleaseMutex(hMutex); //释放同步对象
return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
WaitForSingleObject(hMutex, INFINITE); //获得Mutex对象
i --; //访问共享资源
ReleaseMutex(hMutex); //释放同步对象
return 0;
}
Semaphore
通常把Semaphore理解成是带有引用计数的Mutex。如果一个Semaphore带有计数n,这表示同时最多可以有n个线程占有该Semaphore。当n个线程占有了Semaphore后,其他希望占有该Semaphore的线程才会进入等待状态。
如果某些共享资源同时只能被固定数量的线程使用,那么应该选用Semaphore对它进行同步。与Semaphore相关的函数有以下两个:
//创建一个Semaphore对象
HANDLE CreateSemaphore(
LPSECURITY_ATTRUBUTES lpSemaphoreAttributes, //不支持,设为NULL
LONG lInitialCount, //初始计数
LONG lMaximumCount, //最大支持的线程数
LPCTSTR lpName //可选,Semaphore的名字
);
//释放Semaphore对象
BOOL ReleaseSemaphore(
HANDLE hSemaphore, //要释放的Semaphore句柄
LONG lReleaseCount, //释放的引用计数个数
LPLONG lpPreviousCount //返回释放前的Semaphore计数
);
使用CreateSemaphore()函数创建Semaphore时,可使用第3个参数lMaximumCount来指定最多可有多少个线程占有该Semaphore对象。同时,还可以使用第2个参数lInitialCount给Semaphore对象一个初始计数。当有线程使用ReleaseSemaphore()函数释放Semaphore对象时,该初始值增加lReleaseCount;当有线程使用等待函数占有Semaphore时,该初始值减1。因此,如果Semaphore的计数大于0,则Semaphore处于通知状态;如果计数小于0,则Semaphore处于未通知状态。
Event
如果一个线程需要通知其他线程某个时间发生了,那么可使用时间(Event)同步对象。前一个线程给事件发送一个通知信号,其他对此事件发生感兴趣的线程一般调用等待函数在事件上等待。如果没有线程发送事件的通知信号,那么其他等待的线程将一直阻塞。
事件是用的比较广泛的同步对象。在Windows CE的驱动程序中,中断处理线程就是使用事件同步对象来等待中断发生的。Windows CE中与事件相关的函数有如下几种:
//创建Event同步对象
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, //不支持,设为NULL
BOOL bManualReset, //是否需要人工重置
BOOL bInitialState, //初始状态,TRUE为通知状态
LPTSTR lpName //可选,Event的名字
);
//把Event同步对象设置为通知状态
BOOL SetEvent(
HANDLE hEvent; //Event句柄
);
//把Event同步对象设置为未通知状态
BOOL ResetEvent(
HANDLE hEvent;
);
//把Event变为通知状态,释放一些等待线程后,再把Event设置为未通知状态
BOOL PulseEvent(
HANDLE hEvent;
);
当使用CreateEvent()函数创建新的Event对象时,可使用bInitialState参数指定Event的初始状态。如果为TRUE,则新创建的Event最开始就是通知状态。同时,第2个参数bManualReset用来指定Event是否为人工重置。换言之,有两种不同类型的Event对象:一种是人工重置的Event,另一种是自动重置的Event。当人工重置的Event得到通知时,等待该Event的所有线程均变为就绪态,而Event还处于通知状态,直到调用ResetEvent()函数;当一个自动重置的Event得到通知时,等待该Event的线程中只有一个线程变为就绪态,然后Event自动变为未通知状态。PulseEvent()在实际开发中很少使用。
把上面Mutex的例子改为用Event来实现,代码如下:
HANDLE hEvent;
INT i = 0;
//创建一个自动重置,初始状态为通知的Event对象。创建线程
VOID Init()
{
hEvent = CreateEvent (NULL, FALSE, TRUE, NULL);
CreateThread(NULL, NULL, ThreadProc1, NULL, NULL, NULL);
CreateThread(NULL, NULL, ThreadProc2, NULL, NULL, NULL);
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
WaitForSingleObject(hEvent, INFINITE); //等待事件发生
i ++; //访问共享资源
SetEvent(hEvent); //让事件再次发生
return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
WaitForSingleObject(hEvent, INFINITE); //等待事件发生
i --; //访问共享资源
SetEvent(hEvent); //释放同步对象
return 0;
}
Critical Section
除此之外Windows CE还提供了两种用户态下的同步方法:Critical Section和互锁函数(Interlocked Functions)。这两种方法都没有相对应的Windows CE内核对象,因此它们不能跨进称,但优点是运行效率要远远比前面的几种同步对象高。
Critical Section是应用程序分配的一个数据结构。它用来把一段代码标记为临界区。临界区可保证其内部代码的访问是串行的。如果临界区并没有导致线程阻塞,那么Critical Section的效率非常高,因为代码不须进入操作系统内核;如果临界区导致了阻塞,临界区使用与Mutex相同的机制。因此,如果发生了很多阻塞,那么Critical Section的效率也不会很高。与Critical Section相关的函数有如下几种:
//初始化临界区
void InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
//开始标记代码为临界区
void EnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
//非阻塞版本的EnterCriticalSection,如果不能进入临界区,那么返回FALSE而不阻塞线程
void TryEnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
//离开临界区
void LeaveCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
//删除临界区
void DeleteCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
使用Critical Section时,一般可为每一个共享资源分配一个Critical Section变量。同时,应该把尽可能少的代码放在Critical Section中。下面是一个使用Critical Section的例子,功能与上面演示Mutex和Event的类似。
CRITICAL_SECTION cs;
INT i = 0;
//初始化临界区
VOID Init()
{
InitializeCriticalSection(&cs);
CreateThread(NULL, NULL, ThreadProc1, NULL, NULL, NULL);
CreateThread(NULL, NULL, ThreadProc2, NULL, NULL, NULL);
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
EnterCriticalSection(&cs); //进入临界区
i ++; //访问共享资源
LeaveCriticalSection(&cs); //离开临界区
return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
EnterCriticalSection(&cs); //进入临界区
i --; //访问共享资源
LeaveCriticalSection(&cs); //离开临界区
return 0;
}
Critical Section与关中断
很大嵌入式系统都有类似的Critical Section机制。在一些嵌入式系统中,Critical Section就是用关中断实现的,但是Windows CE中的Critical Section不是这样。像上面所述的一样,Windows CE的Critical Section只是一种特殊的Mutex。在Windows CE中,除了OAL之外的任何地方关中断都是不支持的。
互锁函数
互锁函数客队变量和指针进行原子的读/写操作。因为它们不需要额外的同步对象,所以有时这些互锁函数特别有用。Windows CE提供的互锁函数有:
InterlockedIncrement //把一个变量的值加1
InterlockedDecrement //把一个变量的值减1
InterlockedExchange //交换两个变量的值
InterlockedTestExchange //根据条件交换变量的值
InterlockedCompareExchange //根据比较原子交换
InterlockedCompareExchangePointer //根据比较原子交换指针
InterlockedExchangePointer //交换两个指针的值
InterlockedExchangeAdd //给某个变量增加某个特定值
进程间通信
Windows CE中实现了内存保护机制,这可防止一个进程偶尔或者恶意的访问另外一个进程的地址空间。但是,这些机制也不允许应用程序间有计划地共享数据。使用线程可以避开地址空间的隔离障碍,同一个进程中的线程运行在同一个地址空间内。然而,如果并发发生在不同的进程之间,那么操作系统就必须提供一种机制,可以把一个进程地址空间中的数据复制到另外一个进程的地址空间中,这也就是进程间通信的由来。
Windows CE提供了多种进程间通信的方式:
● 剪贴板(Clipboard) 可使用剪贴板函数在不同进程间复制数据。但是一般只适合图形界面的程序,而且通常剪贴板都是由用户操作完成的。
● COM/DCOM 通过COM组件的代理/存根方式进行进程间数据交换,但只能在调用接口函数时传送数据;通过DCOM可在不同主机间传送数据。
● 网络套接字(Socket) 通过计算机网络,可在相同主机或不同主机间交换数据。
● WM_COPYDATA消息 通过向进程发送WM_COPYDATA消息,将数据放在参数中给其他进程传送数据。只适合有窗口消息队列的进程。
文件映射
文件映射(File Mapping)又叫内存映射文件(Memory-Mapped File, MMF)。通过内存映射文件可在进程的共享虚拟地址空间内保留一个地址空间的区域,同时将文件所在的物理内存映射到此区域。这样,只须对虚拟内存进行读/写操作就能实现对文件的操作,而且文件何时被加载到物理内存、何时被换出完全不需要开发人员关心,这一切都由操作系统的内存管理来实现。在Windows CE中,内存映射文件总是会被映射到4GB虚拟地址空间的0x42000000~0x7FFFFFFF的进程共享区域中,因此,也就实现了多个进程之间的通信。
内存映射文件不是简单的文件I/O操作,实际用到了Windows CE的内存管理。所以,如果想对内存映射文件有更深刻的理解,那么必须对Windows CE的内存管理机制有清楚的认识。与内存映射文件相关的函数如下:
//创建一个文件,用于内存映射
HANDLE CreateFileForMapping(
LPCTSTR lpFileName, //要创建的文件名
DWORD dwDesiredAccess, //希望的权限
DWORD dwShareMode, //共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes, //不支持,设为NULL
DWORD dwCreationDisposition, //创建文件的方法
DWORD dwFlagsAndAttributes, //文件的属性
HANDLE hTemplateFile //不使用,被忽略
);
//创建内存映射文件对象
HANDLE CreateFileMapping(
HANDLE hFile, //文件的句柄
LPSECURITY_ATTRIBUTES lpSecurityAttributes, //不支持,设为NULL
DWORD flProtect, //保护方法
DWORD dwMaximumSizeHigh, //文件大小的高32位
DWORD dwMaximumSizeLow, //文件大小的低32位
LPCTSTR lpName //可选,内核对象名称
);
//把内存映射文件映射到进程共享地址空间
LPVOID MapViewOfFile(
HANDLE hFileMappingObject, //内存映射文件对象
DWORD dwDesiredAccess, //希望的权限
DWORD dwFileOffsetHigh, //起始映射地址偏移的低32位
DWORD dwFileOffsetLow, //起始映射地址偏移的高32位
DWORD dwNumberOfBytesToMap //要映射的字节数
);
//删除内存映射文件
BOOL UnmapViewOfFile(
LPVOID lpBaseAddress //内存映射文件的基地址
);
首先,通过CreateFile()函数来创建或打开一个文件,此文件标识了磁盘上将要用作内存映射文件的文件。在Windows CE中,系统还提供了CreateFileForMapping()函数,这是特殊版本的CreateFile(),此函数专门用于创建内存映射文件用的文件,它向内核地址空间提供了回调(回调函数说明放在文章最后),这保证了由CreateFileForMapping()创建的文件可被创建进程之外的其他进程使用。在Windows CE中如果要创建专门为内存映射用的文件,那么建议使用CreateFileForMapping()函数。
在用CreateFile()将文件映射在物理存储器的位置通告给操作系统后,只指定了映射文件的路径,映射的长度还未指定。为了指定文件映射对象需要多大的物理存储空间还须通过CreateFileMapping()函数来创建一个文件映射内核对象以告诉系统文件的尺寸以及访问文件的方式。
在创建了文件映射对象后,还必须为文件数据保留一个地址空间区域,并把文件数据作为映射到该区域的物理存储器进行提交。由MapViewOfFile()函数负责通过系统的管理而将文件映射对象的全部或部分映射到进程地址空间。此时,对内存映射文件的使用和处理同通常加载到内存中的文件数据的处理方式基本一样。
在完成对内存映射文件的使用后,还要通过一系列操作完成对其的清除和使用过资源的释放。这部分比较简单,可通过UnmapViewOfFile()完成从进程的地址空间撤销文件数据的映像,通过CloseHandle()关闭前面创建的文件映射对象和文件对象。
下面的代码片段展示了如何使用内存映射文件,打开程序的多个进程,从而可以看到运行结果。
//创建文件
hFile = CreateFileForMapping(
TEXT(“\\MAP.dat”),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
//创建内存映射文件
hMapFile = CreateFileMapping(
hFile,
NULL,
PAGE_READWRITE,
0,
0x10000,
TEXT(“MMFObj”)
);
//把内存映射文件映射到进程共享地址空间
hMMF = MapViewOfFile(
hMapFile,
FILE_MAP_ALL_ACCESS,
0,
0,
NULL
);
//读取内存映送和文件的内容
INT nVal = *((INT *)hMMF);
//写数据到内存映射文件
*((INT *)hMMF) = 34;
//清理资源
UnmapViewOfFile(hMMF);
CloseHandle(hMapFile);
CloseHandle(hFile);
点对点消息队列
点对点消息队列(Point-to-Point Message Queues)是常用的系统模块之间进行通信的方式。它很好地符合了操作系统原理中的“生产者——消费者”模型。消息队列通常是一个先进先出的队列结构,当一个进程把消息写入队列,需要此消息的其他进程,就可从队列中取得消息,从而达到进程间通信的目的。
在Windows CE的消息队列实现中,消息队列有如下特色:
● 消息可为任意的数据类型,事实上消息只是一个任意大小的内存缓冲区。这非常有利于在不同进程之间进行数据交换。
● 消息队列还可用来进行同步。这也是典型的“生产者——消费者”模型中的消费者等待问题。当队列中不存在消息时,如果消息使用线程继续从消息队列中读取消息,那么就会造成读取消息的线程在消息队列上阻塞,直到有新的消息进入队列,并被消息读取线程成功读取;当队列中的消息已满时,如果消息生产线程继续往消息队列中写入消息,那么就会造成写入消息的线程在消息队列上阻塞,直到有消息被取走,新的消息成功写入消息队列为止。
● 消息是没有优先级的,对于同一个队列,所有的消息都严格按照先进先出的方式进出。
● Windows CE中的消息队列是基于点对点操作的,它不能用来进行广播。
Windows CE提供了如下几个函数,用来对消息队列进行处理。
//创建一个新的消息队列,也根据名字打开现有的消息队列
HANDLE CreateMsgQueue(
LPCWSTR lpszName, //消息队列的名称
LPMSGQUEUEOPTIONS lpOptions //消息队列的配置选项
);
//打开一个现有的消息队列
HANDLE OpenMsgQueue(
HANDLE hSrcProc, //创建消息队列的进程句柄
HANDLE hMsgQ, //消息队列的句柄
LPMSGQUEUEOPTIONS lpOptions //消息队列的配置选项
);
//获得消息队列的信息
BOOL GetMsgQueueInfo(
HANDLE hMsgQ, //消息队列的句柄
LPMSGQUEUEINFO lpInfo //消息队列的配置选项
);
//从消息队列中读取消息
BOOL ReadMsgQueue(
HANDLE hMsgQ, //消息队列的句柄
LPVOID lpBuffer, //用户提供的接收消息的缓冲区
DWORD cbBufferSize, //缓冲区的大小
LPDWORD lpNumberOfBytesRead, //缓冲区实际使用的大小
DWORD dwTimeout, //在消息队列上等待超时的时间
DWORD* pdwFlags //消息的属性
);
//向消息队列中写入消息
BOOL WriteMsgQueue(
HANDLE hMsgQ, //消息队列的句柄
LPVOID lpBuffer, //存放消息的缓冲区
DWORD cbDataSize, //缓冲区的大小
DWORD dwTimeout, //在消息队列上等待超时的时间
DWORD dwFlags //消息的属性
);
//关闭消息队列
BOOL CloseMsgQueue(
HANDLE hMsgQ //消息队列的句柄
);
应用中,消息队列经常用来获得设备的装载/卸载通知。例如下面的代码展示了应用程序如何通过使用消息队列来获得文件系统挂载的通知。
DWORD NotificationsThread(LPVOID lpParameter)
{
GUID guid = STORE_MOUNT_GUID;
HANDLE hMsgQ;
DWORD numRead;
DWORD flags;
BYTE buf[sizeof(DEVDETAIL) + MAX_DEVCLASS_NAMELEN*sizeof(TCHAR)];
MSGQUEUEOPTIONS qoptions = {
sizeof(MSGQUEUEOPTIONS), MSGQUEUE_NOPRECOMMIT,
0,
sizeof(buf),
TRUE
};
//创建消息队列
hMsgQ = CreateMsgQueue(NULL, &qoptions);
if (hMsgQ == NULL)
{
//创建失败
return 0;
}
//注册设备等待通知
hNotifications = RequestDeviceNotifications(&guid, hMsgQ, TRUE);
if (hNotifications == NULL)
{
//注册失败
return 0;
}
//等待消息
HANDLE waitHandles[] = {XXXXXXX, hStopEvent};
while (WAIT_OBJECT_0 ==
WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE))
{
if (!ReadMsgQueue(hMsgQ, buf, sizeof(buf), &numRead, 0, &flags))
{
return 0;
}
//回调用户定义的文件系统挂载函数
OnMountStorage(*((const DEVDETAIL *)buf), flags);
}
//关闭消息队列
CloseMsgQueue(hMsgQ);
CloseHandle(hStopEvent);
return 1;
}
Callback Function
什么是回调函数?
首先做一个形象的比喻:
你有一个任务,但是有一部分你不会做,或者说不愿做,所以我来帮你做这部分,你做你其它的任务工作或者等着我的消息,但是当我完成的时候我要通知你我做好了,你可以用了,我怎么通知你呢?你给我一部手机,让我做完后给你打电话,我就打给你了,你拿到我的成果加到你的工作中,继续完成其它的工作.这就叫回叫,手机是我通知你的手段,它就是回叫函数,也叫回调函数.(来自:http://book.77169.org/data/web5505/20050228/20050228__2937934.html)
回调函数是应用程序提供给Windows系统DLL或其它DLL调用的函数,一般用于截获消息、获取系统信息或处理异步事件。应用程序把回调函数的地址指针告诉DLL,而DLL在适当的时候会调用该函数。回调函数必须遵守事先规定好的参数格式和传递方式,否则DLL一调用它就会引起程序或系统的崩溃。通常情况下,回调函数采用标准WindowsAPI的调用方式,即__stdcall,当然,DLL编制者可以自己定义调用方式,但客户程序也必须遵守相同的规定。在__stdcall方式下,函数的参数按从右到左的顺序压入堆栈,除了明确指明是指针或引用外,参数都按值传递,函数返回之前自己负责把参数从堆栈中弹出。
理解回调函数!
程序在调用一个函数(function)时(通常指api).相当于程序(program)呼叫(Call)了一个函数(function)关系表示如下:
call(调用)
program --------------------→ dll
程序在调用一个函数时,将自己的函数的地址作为参数传递给程序调用的函数时(那么这个自己的函数称回调函数).需要回调函数的 DLL 函数往往是一些必须重复执行某些操作的函数.关系表示如下:
call(调用)
program --------------------→ dll
↑ ¦
¦_______________________________¦
callback(回调)
当你调用的函数在传递返回值给回调函数时,你就可以利用回调函数来处理或完成一定的操作。至于如何定义自己的回调函数,跟具体使用的API函数有关,很多不同类别的回调函数有各种各样的参数,有关这些参数的描述一般在帮助中有说明回调函数的参数和返回值等.其实简单说回调函数就是你所写的函数满足一定条件后,被DLL调用!
也有这样的说法(比较容易理解):
回调函数就好像是一个中断处理函数,系统在符合你设定的条件时自动调用。为此,你需要做三件事:
1. 声明;
2. 定义;
3. 设置触发条件,就是在你的函数中把你的回调函数名称转化为地址作为一个参数,以便于DLL调用。
.NET Framework 开发人员指南
回调函数是托管应用程序中可帮助非托管 DLL 函数完成任务的代码。对回调函数的调用将从托管应用程序中,通过一个 DLL 函数,间接地传递给托管实现。在用平台调用调用的多种 DLL 函数中,有些函数要求正确地运行托管代码中的回调函数。
回调函数和实现
要从托管代码中调用大多数 DLL 函数,可创建该函数的托管定义,然后调用该函数。此过程比较直接。
要使用需要回调函数的 DLL 函数,则会有一些附加的步骤。首先,必须在文档中查阅该函数,确定该函数是否需要回调。接着,必须在托管应用程序中创建回调函数。最后,调用该 DLL 函数,并将指向回调函数的指针当作参数进行传递。右图总结了这些步骤。
回调函数非常适合在重复执行任务的情况下使用。另一个常见用途是与枚举函数(如 Win32 API 中的 EnumFontFamilies、EnumPrinters 和 EnumWindows)一起使用。EnumWindows 函数枚举计算机上的所有现有窗口,并调用回调函数以针对每个窗口执行任务.
文章评论(0条评论)
登录后参与讨论