系列文章目录
[笔记]Windows核心编程《一》错误处理、字符编码
[笔记]Windows核心编程《二》内核对象
[笔记]Windows核心编程《三》进程
[笔记]Windows核心编程《四》作业
[笔记]快乐的LInux命令行《五》什么是shell
[笔记]Windows核心编程《五》线程基础
[笔记]Windows核心编程《六》线程调度、优先级和关联性
[笔记]Windows核心编程《七》用户模式下的线程同步
[笔记]Windows核心编程《八》用内核对象进行线程同步
[笔记]Windows核心编程《九》同步设备I/O和异步设备I/O
[笔记]Windows核心编程《十一》Windows线程池
[笔记]Windows核心编程《十二》纤程
[笔记]Windows核心编程《十三》windows内存体系结构
[笔记]Windows核心编程《十四》探索虚拟内存
[笔记]Windows核心编程《十五》在应用程序中使用虚拟内存
[笔记]Windows核心编程《十六》线程栈
[笔记]Windows核心编程《十七》内存映射文件
[笔记]Windows核心编程《十八》堆栈
[笔记]Windows核心编程《十九》DLL基础
[笔记]Windows核心编程《二十》DLL的高级操作技术
[笔记]Windows核心编程《二十一》线程本地存储器TLS
[笔记]Windows核心编程《二十二》注入DLL和拦截API
[笔记]Windows核心编程《二十三》结构化异常处理
相关:
参考1
参考2
参考3
参考4
管理内核对象及句柄方式
文章目录
系列文章目录
前言
用户模式下的同步机制
内核模式下的同步机制
等待函数
WatiForSingleObject
WaitForMultipleOBjecs
例
等待成功所引起的副作用
事件内核对象
CreateEvent函数
CreatEventEx创建对象
可等待的计时器内核对象
信号量内核对象
互斥量内核对象
线程同步对象速查表
其他的线程同步函数
前言
用户模式下的同步机制
用户模式下的同步机制的特点就是速度快,但也有些局限性,
例如,
- 对Interlocked系列函数只能戳一个值进行操作,它们从不会把线程切换到等待状态。
- 只能对同一个进程的线程进行同步
内核模式下的同步机制
在创建或清除内核对象时调用线程必须从用户态切换到内核模式。这种切换非常耗时。 x86平台一个空的系统调用大概会占用200cpu周期1,这还不包括执行被调用函数在内核模式下的实例代码。
造成内核对象对用户模式下的同步机制慢几个数量级的原因,是伴随调度新线程而来的刷新高速缓存以及错过高速缓存(即未命中),而造成的成百上千个CPU周期。
对线程同步来说,内核对象只有触发,和非触发的状态。进程内核对象创建时总是处于未触发状态,当进程终止时,操作系统会自动使进程内核对象变为触发状态。当进程对象内核对象被触发后,它将永远保持这种状态,再也不会变回到未触发状态。
等待函数
WatiForSingleObject
该函数可以使一个线程进入等待状态,直到它所等待的内核对象被触发为止。
DWORD WatiForSingleObject( HANDLE hObject, DWORD dwMilliseconds );
hObject:标识要等待的内核对象。
dwMilliseconds:指定主调线程最多愿意花多长时间来等待对象被触发。
WaitForMultipleOBjecs
WaitForMultipleOBjecs函数与WaitForSingleObject相类似。唯一的不同是他允许线程同时等待多个内核对象。
DWORD WaitForMultipleOBjecs( DWORD dwCount, CONST HANDLE*pbObject, BOOL bWaitAll, DWORD dwMilliseconds );
dwCount:指定等待内核对象的数量。
pbObject: 指向一个存储所有需要等待内核对象的数组。
bWaitAll:指定是否等待所有内核对象都触发。当其为true时,只有当所有内核对象都变成触发态时函数返回。否则,只要有一个内核对象变成触发态等待函数就返回。
dwMillisecond:是指定等待时间与WaitForSingleObject相同。
bWaitAll:为true和false时函数返回值意义不同。
当为true时,函数返回时所有对象都被触发。这与WaitForSingleObject的返回值意义相同。
当为false时,返回值将会标识那个对象被触发从而导致等待函数返回。
例
HANDLE h[3]; h[0]=hProcess1; h[1=hProcess2; h[2]=hProcess3; DWORD dw=WaitForMultipleOBjecs(3,h,false,5000); switch(dw) { case WAIT_OBJEC_0://第一个对象被触发。 break; case WAIT_OBJEC_0+1://第二个对象被触发。 break; case WAIT_OBJEC_0+2://第三个对象被触发。 break; case WAIT_TIMEOUT://超时 break; case WAIT_FAILED://句柄无效。 break; }
等待成功所引起的副作用
对一些内核对象来说,成功地调用WaitForSingleObject或WaitForMultipleObject事实上会改变对象的状态。一个成功的调用指的是函数发现对象已经被触发了,然后返回WAIT_OBJECT_0的一个相对值。如果对象的状态发生了改变,则称之为等待成功所引起的副作用。
事件内核对象
事件内核对象是比较简单的一种,它内部有一个用来表示是自动重置事件还是手动重置事件的布尔值,以及一个用来表示事件有没有被触发的布尔值。
有两种类型的事件对象:手动重置事件和自动重置事件,两者区别:
当一个手动重置事件被触发时,正在等待该事件的所有线程都会变成可调度状态。
当一个自动重置事件被触发时,只会有一个正在等待该事件的线程变成可调度状态,如果有多个正在等待该事件的线程,则不确定是哪一个线程会变成可调度状态。
把事件变成触发状态,使用SetEvent(); 把事件变成未触发状态,使用ResetEvent()。
事件最通常的用途是:让一个线程执行初始化工作,然后再触发另一个线程,让它执行剩余的工作。
CreateEvent函数
HANDLE WINAPI CreateEvent( __in_opt LPSECURITY_ATTRIBUTES lpEventAttributes, __in BOOL bManualReset, __in BOOL bInitialState, __in_opt LPCTSTR lpName );
CreatEventEx创建对象
HANDLE WINAPI CreateEventEx( __in_opt LPSECURITY_ATTRIBUTES lpEventAttributes, __in_opt LPCTSTR lpName, __in DWORD dwFlags, __in DWORD dwDesiredAccess );
可等待的计时器内核对象
可等待的计时器是这样一种内核对象,会在某个指定的时间触发,或- 每隔一段时间触发一次。
创建可等待的计时器,调用CreateWaitableTimer函数。
OpenWaitableTimer函数用来获得一个已经存在的可等待计时器。在创建的时候,可等待的计时器对象总是处于未触发状态。当我们想要触发计时器的时候,必须调用SetWaitableTimer函数。
HANDLE WINAPI CreateWaitableTimer( _In_opt_ LPSECURITY_ATTRIBUTES lpTimerAttributes, _In_ BOOL bManualReset, //TRUE手动,FALSE自动 _In_opt_ LPCTSTR lpTimerName );
bManualReset:表示要创建的是一个手动重置计时器还是自动重置计时器。当手动计时重置计时器被触发的时候,正在等待该计时器的所有线程编程可调度状态;自动计时器被触发的时候,只有一个正在等待该计时器的线程会变成可调度状态。
信号量内核对象
信号量内核对象用来对资源进行计数。与其他所有内核对象相同,它们也包含一个使用计数,但它们还包含另外两个32位值:一个最大资源计数和一个当前资源计数(可被使用资源数目)。最大资源计数表示信号量可以控制的最大资源数量,当前资源计数表示信号量当前可用资源的数量。
信号量的规则如下:
a、如果当前资源计数大于0,那么信号量处于触发状态;
b、如果当前资源计数等于0,那么信号量处于未触发状态;
c、系统绝对不会让当前资源计数变为负数;
d当前资源计数绝对不会大雨最大资源计数。
不能把信号量对象的使用计数和它的当前资源计数混为一谈。
CreateSemaphore和CreateSemaphoreEx创建,OpenSemaphore打开。
信号量的最大的优势在于它们会以原子方式来执行这些测试和设置操作,也就是说,当我们向信号量请求一个资源的时候,操作系统会检查资源是否可用,并将可用资源的数量递减,整个过程不会被别的线程打断。只有当资源计数递减完成之后,系统才会允许另一个线程请求对资源的访问。
线程通过调用ReleaseSemaphore来递增信号量的当前资源计数。
互斥量内核对象
互斥量(mutex)内核对象用来确保一个线程独占对一个资源的访问。这也是互斥量名字的由来。互斥量对象保护一个使用计数、线程ID以及一个递归计数。互斥量与关键段的行为完全相同。但是,互斥量是内核对象,而关键段是用户模式下的同步对象。(除非对资源的争夺非常激烈,这种情况下关键段的线程将不得不进入内核模式等待)这意味着互斥量比关键段慢。但这同时意味着不同进程中的线程可以访问同一个互斥量,还意味着线程可以在等待对资源的访问权时指定一个最长等待时间。
线程ID用来标识当前占用这个互斥量的是系统中的哪个线程,递归计数表示这个线程占用该互斥量的次数。互斥量一般用来对多个资源访问的同一块内存进行保护。
互斥量的规则:
a、如果线程ID为0(无效线程ID),那么该互斥量不为任何线程锁占用,它处于触发状态;
b、如果线程ID为非零值,那么有一个线程已经占用了该互斥量,它处于未触发状态;
c、与所有其他内核对象不同,操作系统对互斥量进行了特殊处理,允许它们违反一些常规的规则。
CreateMutex和CreateMutexEx用来创建,OpenMutex用来得到一个已经存在的互斥量的句柄,该句柄与当前进程想关联。
在用来触发普通内核对象和撤销触发普通内核对象的规则中,有一条不适用于互斥量。假设线程试图等待一个未触发的互斥量对象。在这种情况下,线程通常会进入等待状态。但是,系统会检查想要获得互斥量的线程的线程ID与互斥量对象内部记录的线程ID是否相同。如果线程ID一致,那么系统会让线程保持可调度状态——即使该互斥量尚未触发。对系统中的任何其他内核对象来说,都找不到这种异常的举动。每次线程成功地等待了一个互斥量,互斥量对象的递归计数会递增。使递归计数大于1的唯一途径是利用这个例外,让线程多次等待同一个互斥量。
互斥量与所有其他内核对象不同,这是因为它们具有“线程所有权”的概念。除了互斥量,没有任何一个会记住自己是哪个线程等待成功的。互斥量的这种线程所有权的概念,也是它具有特殊规则的原因,这使它即使在未触发的状态下,也能为线程所获得。
这个例外不仅适用于获得互斥量的线程,而且适用于试图释放互斥量的线程。当线程调用ReleaseMutex的时候,会缉拿吃线程的线程ID与互斥量内部保存的线程ID是否一致。如果ID一致,则递归计数会递减。如果ID不一致,则不执行任何操作,返回FALSE。
如果占用互斥量的线程在释放互斥量之前终止(使用ExitThread,TerminateThread,ExitProcess或TerminateProcess),那么系统会认为互斥量被遗弃(abandoned)——由于占用互斥量的线程已经终止,因此再也无法释放它。
系统会记录所有的互斥量和线程内核对象,因此它确切地知道互斥量何时被遗弃。当互斥量被遗弃的时候,系统会自动将互斥量对象的线程ID设为0,将它的递归计数设为0.然后再检查有没有其它线程正在等待该互斥量。被调度的线程的等待函数返回的是一个特殊的值WAIT_ABANDONED。这个特殊的返回值只适用于互斥量。
线程同步对象速查表
其他的线程同步函数
异步设备I/O(asynchronous device I/O)允许线程开始读取操作或写入操作,但不必等待读取操作或写入操作完成。
设备对象是可同步的内核对象,这意味着可以调用WaitForSingleObject,并传入文件句柄、套接字、通信端口,等等。
WaitForInputIdle函数将自己挂起,会等待由hProcess标志的进程,知道创建应用程序第一个窗口的线程中没有待处理的输入为止。这个函数对父进程来说比较有用。
MsgWaitForMultipleObjects或MsgWaitForMultipleObjectsEx使得线程等待需要自己处理的消息。与WaitForMultipleObjects函数类似,不同之处在于,不仅内核对象被触发的时候调用线程会变成可调度状态,而且当窗口消息需要被派送到一个由调用线程创建的窗口时,它们也会变成可调度状态。
创建窗口的线程和执行与用户界面相关的任务的线程不应该使用WaitForMultipleObjects,而应该使用MsgWaitForMultipleObjectsEx。这是因为前者会妨碍线程对用户在用户界面上的操作进行响应。
WaitForDebugEvent函数,可以用来等待调试事件发生。
SignalObjectAndWait函数会通过一个原子操作来触发一个内核对象并等待另一个内核对象。触发的内核对象必须是一个互斥量、信号量或事件。等待的可以是任何一种内核对象。
SignalObjectAndWait受欢迎有两个原因:一个是节省时间,只用进入内核一次,否则先release一次,然后又要wait一次;另一个是如果没有该函数,一个线程就无法知道另一个线程合适处于等待状态。
使用等待链遍历API来检测死锁,Vista提供一组新的等待链遍历(Wait Chain Traversal,WCT)API。