内核对象概述
互斥对象
事件对象
可等待的计时器内核对象
信号量内核对象
内核对象状态速查表
保证实例的唯一性
(本章节中例子都是用 VS2010 编译调试的)
何为内核对象
内核对象为一个数据结构且只能被内核访问,因此应用程序无法在内存中找到这些数据结构并直接改变它们的内容.Microsoft 规定了这个限制条件,目的是为了确保内核对象结构保持状态的一致.所以 Microsoft 能自由的添加、删除和修改这些结构中的成员.同时不干扰任何程序正常运行.
Windows 提供了一组函数,来对这些结构进行操作.始终可以使用这组函数来访问这些内核对象.当调用一个用于创建内核对象的函数时,该函数就返回一个用于标识该对象的句柄.该句柄可以被视为一个不透明值,你的进程中的任何线程都可以使用这个值.
使用计数器
内核对象的所有者是操作系统,而非进程.换句话说,如果进程调用了一个函数来创建内核对象,然后进程终止运行,则内核对象不一定被撤消.在大多数情况下,这个内核对象将被撤消,但是如果另一个进程正在使用你的进程创建的内核对象,那么其他线程在停止使用该内核之前,它是不会被销毁的.总之,内核对象的生命周期可能长于创建它的那个进程.
操作系统内核知道有多少个进程正在使用一个特定的内核对象,因为每个对象包含一个使用计数.使用计数是所有内核对象类型都有的一个成员变量.当一个内核对象刚刚被创建时,它的使用计数被置为1.后当另一个进程获得对现有内核对象的访问后,使用计数就递增.进程终止运行时,操作系统内核将自动递减此进程仍然打开的所有内核对象的使用计数器.若内核对象的使用计数降为0,操作系统内核就销毁该内核对象.这样可以确保不存在没有任何进程引用的内核对象.
改变句柄的标志
有时会遇到这样一种情况,父进程创建一个内核对象,以便检索可继承的句柄,然后生成两个子进程。父进程只想要一个子进程来继承内核对象的句柄。换句话说,有时可能想要控制哪个子进程来继承内核对象的句柄。若要改变内核对象句柄的继承标志,可以调用 SetHandleInformation 函数
进程内核对象句柄表
当一个进程被初始化时,系统要为它分配一个句柄表.该句柄表只用于内核对象,不用于用户对象或GDI对象.句柄表的详细结构和管理方法并没有具体的资料说明.
索引 内核对象 内存块的指针访问屏蔽(标志位的DWORD) 标志(标志位的DWORD)
1 0x???????? 0x???????? 0x????????
2 0x???????? 0x???????? 0x????????
... ... ... ...
创建一个内核对象
当进程初次被初始化时,它的句柄表是空的.然后,当进程中的线程调用创建内核对象的函数时,该对象分配一块内存,并初始化其对象.然后内核对进程的句柄表进行扫描,找出一个空项.并获得其索引,然后对其进行初始化.(即设置其内核对象内存块指针,访问掩码,标志)
用于创建内核对象的任何函数都会返回一个与进程相关的句柄,这个句柄可由同一个进程中的所有线程使用.系统用索引来表示内核对象的信息保存在进程句柄表中的具体位置,要得到实际的索引值,句柄值应该除以4(或右移两位,以忽略 Windows 操作系统内部使用的最后两位),所以,在调试应用程序时,查看内核对象句柄的实际值时,会看到4,8之类的很小的值,记住,句柄的含义尚未公开,将来可能发生变化.
由于句柄值实际是作为进程句柄表的索引来使用的,所以这些句柄是与当前这个进程相关的,无法供其他进程使用,如果我们真的在其他进程中使用它,那么实际引用的只是那个进程的句柄表中位于同一个索引的内核对象 -- 只是索引值相同而已,我们根本不知道它会指向什么对象.
关闭内核对象
无论什么方式创建内核对象,我们都需要调用 ClosseHandle 向系统表明我们已经结束使用对象.就在 CloseHandle 函数返回前,它会清除进程句柄表中对应的记录项 -- 这个句柄现在对我们的进程来说是无效的,不要在试图利用它.换句话说,一旦调用 CloseHandle, 我们的进程就不能访问那个内核对象.
当进程终止运行,操作系统会确保此进程所使用的所有资源都被释放(即不调用 CloseHandle 也不会在进程结束后造成泄漏) --- 这是可以保证的!对于内核对象,操作系统执行的是以下操:进程终止时,系统自动扫描该进程的句柄表.如果这个表中任何有效的记录项(即进程终止前没有关闭的对象),操作系统会为我们关闭这些对象句柄.
但当进程终止运行,系统能保证一切都被正确清除.这适合所有内核对象,资源(包括 GDI 对象在内)以及内存块.
触发/非触发状态
内核对象的某个时刻只能处于一种状态,要么是处于触发状态,要么是处于未触发状态.(进程内核对象在创建的时候总是处于未触发状态.当进程终止的时候,操作系统会自动使进程内核对象变为触发状态.当进程内核对象被触发后,它将永远保持这种状态,再也回不到为触发状态)
在进程内核对象的内部有一个布尔变量,当系统创建内核对象的时候会把这个变量的值初始化为FALSE(未触发).当进程终止的时候,操作系统会自动把相应的内核对象中的这个布尔值设置为 TRUE ,表示该对象已经被触发
下面内核对象既可以处于触发状态,也可以处于未触发状态:
等待函数
等待函数 WaitForSingleObject 的返回值表示为什么线程又能够继续执行了.
WaitForMultipleObjects 与 WaitForMultipleObjects 相似.唯一不同之处在于它允许调用线程同时检查多个内核对象的触发状态.
代码样例
WaitForMultipleObjects 例程
bWaitAll 为 true
程序源码
运行结果
若把 Fun2Proc 中的 SetEvent(g_hEvents[1]); 这句话注释掉,其结果便会如下所示:
原因是在主线程结束睡眠前线程三一直卡在 WaitForMultipleObjects .所以等到主线程结束后,在终止进程前. Fun3Pro 在 if(WaitForMultipleObjects(2,g_hEvents,true,INFINITE) == WAIT_OBJECT_0) 后的语句都得不到执行机会.
bWaitAll 为 false
程序源码
运行结果
若在 Fun1Proc 中的 Sleep(10); 这句话后面在添加两个 Sleep(10);,其结果便会如下所示:
原因是线程2的初始化先于线程1的初始化完成,(即线程1的SetEvent(g_hEvents[0]);语句先于线程2的SetEvent(g_hEvents[1]);语句先被执行)
说明
组成
注意
互斥对象的使用规则如下
相关函数
互斥对象与关键代码段的比较
特性 | 互斥对象 | 关键代码段 |
运行速度 | 慢 | 快 |
是否能够跨进程边界来使用 | 是 | 否 |
声明 | HANDLE hmtx; | CRITICAL_SECTION cs; |
初始化 | hmtx=CreateMutex(NULL,FALSE,NULL); | InitializeCriticalSection(&es); |
清除 | CloseHandle(hmtx); | DeleteCriticalSection(&cs); |
无限等待 | WaitForSingleObject(hmtx,INFINITE); | EntercriticalSection(&cs); |
0等待 | WaitForSingleObject(hmtx,0); | EntercriticalSection(&cs); |
任意等待 | WaitForSingleObject(hmtx,dwMilliseconds); | 不能 |
释放 | ReleaseMutex(hmtx); | LeaveCriticalSection(&cs); |
是否能够等待其他内核对象 | 是(使用 WaitForSingleObject 或类似的函数) | 否 |
执行流程
代码样例
样例链接
说明
组成
相关函数
执行流程
代码样例
人工重置事件
程序源码(功能:线程2,线程3等待线程1初始化变量 g_x 后,输出 g_x 变量)
运行结果
自动重置事件
例子链接
说明
可等待计时器是这样一种内核对象,它们会在某个指定的时间触发,或每隔一段时间触发一次.要创建可等待的计时器,我们只需调用CreateWaitableTimer 函数.也可调用 OpenWaitableTimer 函数来得到一个已存在的可等待计时器的句柄,该句柄与当前进程相关联.在创建可等待计时器对象,它总是处于未触发状态.当我们想要触发计时器的时候必须调用 SetWaitableTimer 函数.
定时器常常用于通信协议中.例如,如果客户机向服务器发出一个请求,而服务器没有在规定的时间内作出响应,那么客户机就会认为无法使用服务器.目前,客户机通常要同时与许多服务器进行通信.如果你为每个请求创建一个定时器内核对象,那么系统的运行性能就会受到影响.可以设想,对于大多数应用程序来说,可以创建单个定时器对象,并根据需要修改定时器报时的时间.
定时器报时时间的管理方法和定时器时间的重新设定是非常麻烦的,只有很少的应用程序采用这种方法.但是在新的线程共享函数中有一个新函数,称为CreateTimerQueueTimer,它能够为你处理所有的操作.如果你发现自己创建和管理了若干个定时器对象,那么应该观察一下这个函数,以减少应用程序的开销.
与定时器的比较
凡是称职的 Windows 编程员都会立即将等待定时器与用户定时器(用 SetTimer 函数进行设置)进行比较.它们之间的最大差别是,用户定时器需要在应用程序中设置许多附加的用户界面结构,这使定时器变得资源更加密集.另外,等待定时器属于内核对象,这意味着它们可以供多个线程共享,并且是安全的.
用户定时器能够生成 WM_TIMER 消息,这些消息将返回给调用 SetTimer (用于回调定时器)的线程和创建窗口(用于基于窗口的定时器)的线程.因此,当用户定时器报时的时候,只有一个线程得到通知.另一方面,多个线程可以在等待定时器上进行等待,如果定时器是个人工重置的定时器,则可以调度若干个线程.
如果要执行与用户界面相关的事件,以便对定时器作出响应,那么使用用户定时器来组织代码结构可能更加容易些,因为使用等待定时器时,线程必须既要等待各种消息,又要等待内核对象(如果要改变代码的结构,可以使用 WaitForMultipleObjects 函数).最后,运用等待定时器,当到了规定时间的时候,更有可能得到通知.WM_TIMER 消息始终属于最低优先级的消息,当线程的队列中没有其他消息时,才检索该消息.等待定时器的处理方法与其他内核对象没有什么差别,如果定时器发出报时信息,而你的线程正在等待之中,那么你的线程就会醒来.
异步过程调用
Microsoft 还允许计时器把一个异步过程调用(asynchronous procedure call,APC)放到 SetWaitableTimer 的调用线程队列中.通常,当我们调用 SetWaitableTimer 的时候,会给 pfnCompletionRoutine 和 pvArgToCompletionRoutine 两个参数传 NULL.当 SetWaitableTimer 看到这两个参数为 NULL 的时候,它知道时间一到应该触发计时器对象.但是,如果希望时间一到就让计时器把一个 APC 添加到队列中去,就必须实现一个计时器 APC 函数,并把函数地址传入,其 函数原型链接.
可以将该函数命名为 TimerAPCProc ,不过可以根据需要给它赋予任何一个名字.该函数可以在定时器报时的时候由调用 SetWaitableTimer 函数的同一个线程来调用,但是只有在调用线程处于待命状态下才能调用.换句话说,该线程必须正在 SleepEx,WaitForSngleObjectEx(WaitForSingleObject),WaitForMultipleObjectsEx(WaitForMultipleObjects),MsgWaitForMultipleObjectsEx 等函数的调用中等待.如果该线程不在这些函数中的某个函数中等待,系统将不给定时器 APC 例程排队.这可以防止线程的 APC 队列中塞满定时器 APC 通知,这会浪费系统中的大量内存.
当定时器报时的时候,如果你的线程处于待命的等待状态中,系统就使你的线程调用回调例程.回调例程的第一个参数的值与你传递给 SetWaitableTimer 函数的 pvArgToCompletionRoutine 参数的值是相同的.你可以将某些上下文信息(通常是你定义的某个结构的指针)传递给 TimerAPCProc.剩余的两个参数 dwTimerLowValue 和 dwTimerHighValue 用于指明定时器何时报时.
并且只有当所有的 APC 项都已经处理之后,待命的函数才会返回.因此,必须确保定时器再次变为已通知状态之前,TimerAPCProc 函数完成它的运行,这样,APC 项的排队速度就不会比它被处理的速度快.
虽然定时器能够给 APC 项进行排队是很好的,但是目前编写的大多数应用程序并不使用 APC,它们使用 I/O 完成端口机制.过去,我自己的线程池中(由一个 I/O 完成端口负责管理)有一个线程,它按照特定的定时器间隔醒来.但是,等待定时器没有提供这个方法.为了做到这一点,我创建了一个线程,它的唯一工作是设置而后等待一个等待定时器.当定时器变为已通知状态时,线程就调用 PostQueuedCompletionStatus 函数,将一个事件强加给线程池中的一个线程.
相关函数
执行流程
代码样例
可等待计时器(非APC)
程序源码:
运行结果:
可等待计时器(APC)
程序源码
运行结果
注意线程不应该等待定时器的句柄,也不应该以待命的方式等待定时器.如下面的代码:
HANDLEhTimer=CreateWaitableTimer(NULL,FALSE,NULL);
SetWaitableTimer(hTimer,...,TimerAPCRoutine,...);
WaitForSingleObjectEx(hTimer,INFINITE,TRUE);
不应该编写上面的代码,因为调用 WaitForSingleObjectEx 函数实际上是两次等待该定时器,一次是以待命方式等待,一次是等待内核对象句柄.当定时器变为已通知状态时,等待就成功了,线程被唤醒,这将使该线程摆脱待命状态,而 APC 例程则没有被调用.前面讲过,通常没有理由使用带有等待定时器的 APC 例程,因为你始终都可以等待定时器变为已通知状态,然后做你想要做的事情.
组成
信号量的使用规则如下
相关函数
说明
执行流程
代码样例
程序源码
运行结果
因为在本例程中,我们没有对信号进行增加,而创建时候初始化的可使用资源计数器为2.所以只有两个现象可以等待到信号量资源对象(即线程1和线程3),所以在主线程结束睡眠终止时候线程2都一直卡在 WaitForSingleObject(g_hSemaphore,INFINITE) 函数中.若我们稍加修改下上面的例子,源码如下:
结果显示如下
对象 | 何时处于未通知状态 | 何时处于已通知状态 | 成功等待的副作用 |
进程 | 当进程仍然活动时 | 当进程终止运行时(ExitProcess,TerminateProcess) | 无 |
线程 | 当线程仍然活动时 | 当线程终止运行时(ExitThread,TerminateThread) | 无 |
作业 | 当作业的时间尚未结束时 | 当作业的时间已经结束时 | 无 |
文件 | 当 I/O 请求正在处理时 | 当 I/O 请求处理完毕时 | 无 |
控制台输入 | 不存在任何输入 | 当存在输入时 | 无 |
文件修改通知 | 没有任何文件被修改 | 当文件系统发现修改时 | 重置通知 |
自动重置事件 | ResetEvent,PulseEvent 或等待成功 | 当调用 SetEvent/PulseEvent 时 | 重置事件 |
人工重置事件 | ResetEvent 或 PulseEvent | 当调用 SetEvent/PulseEvent 时 | 无 |
自动重置等待定时器 | CancelWaitableTimer 或等待成功 | 当时间到时(SetWaitableTimer) | 重置定时器 |
人工重置等待定时器 | CancelWaitableTimer | 当时间到时(SetWaitableTimer) | 无 |
信标 | 等待成功 | 当数量>0时(ReleaseSemaphore) | 数量递减1 |
互斥对象 | 等待成功 | 当未被线程拥有时(ReleaseMutex 互斥对象) | 将所有权赋予线程 |
关键代码段(用户方式) | 等待成功(EntercriticalSection/TryEnterCriticalSection) | 当未被线程拥有时(LeaveCriticalSection) | 将所有权赋予线程 |
保证运行实例单一性
代码样式
运行结果