内核对象(VC_Win32)

文章来自:内核对象(VC_win32)


目录

内核对象概述
互斥对象
事件对象
可等待的计时器内核对象
信号量内核对象
内核对象状态速查表
保证实例的唯一性

 (本章节中例子都是用 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 的返回值表示为什么线程又能够继续执行了.

    • WAIT_OBJECT_0(对于 WaitForSingleObject 当参数 bWaitAll 为 FALSE 时候,则是 WAIT_OBJECT_0 到 WAIT_OBJECT_0 + dwCount-1 之间的值,这个值表示那个句柄被触发) 等待的对象被触发
    • WAIT_TIME 等待超时
    • WAIT_FAILED 传回错误句柄等错误时候的返回值,可调用 GetLastError 得到更详细的信息

WaitForMultipleObjects 与 WaitForMultipleObjects 相似.唯一不同之处在于它允许调用线程同时检查多个内核对象的触发状态.

代码样例

WaitForMultipleObjects 例程

bWaitAll 为 true

程序源码

View Code

运行结果

内核对象(VC_Win32)_第1张图片

若把 Fun2Proc 中的 SetEvent(g_hEvents[1]); 这句话注释掉,其结果便会如下所示:

内核对象(VC_Win32)_第2张图片

原因是在主线程结束睡眠前线程三一直卡在 WaitForMultipleObjects .所以等到主线程结束后,在终止进程前. Fun3Pro 在 if(WaitForMultipleObjects(2,g_hEvents,true,INFINITE) == WAIT_OBJECT_0) 后的语句都得不到执行机会.

bWaitAll 为 false

程序源码

View Code

运行结果

内核对象(VC_Win32)_第3张图片

若在 Fun1Proc 中的 Sleep(10); 这句话后面在添加两个 Sleep(10);,其结果便会如下所示:

原因是线程2的初始化先于线程1的初始化完成,(即线程1的SetEvent(g_hEvents[0]);语句先于线程2的SetEvent(g_hEvents[1]);语句先被执行)

互斥对象(mutex)

说明

  • 互斥对象(mutex)属于内核对象,它能够包确保线程拥有对单个资源的互斥访问权限,

组成

  • 一个使用数量
  • 一个线程 ID     ID 用于标识系统中那个线程当前拥有互斥对象
  • 计数器            指明该线程拥有互斥对象的次数

注意

  • 线程主动请求共享资源的使用所有权才有可能获得该所有权,调用 WaitForSingleObject 函数实现.在 WaitForSingleObject 等待请求互斥对象时候.系统要查看试图获取互斥对象的线程的 ID 是否与互斥对象中记录的线程 ID 相同.如果两个线程 ID 相同,即使互斥对象处于未通知状态,系统也允许该线程保持可调度状态.我们不认为该“异常”行为特性适用于系统中的任何地方的其他内核对象.每当线程成功地等待互斥对象时,该对象的递归计数器就递增.若要使递归计数器的值大于1,唯一的方法是线程多次等待相同的互斥对象,以便利用这个异常规则.但是切记如果线程多次成功地等待一个互斥对象,在互斥对象的递归计数器变成0 之前,该线程必须以同样的次数调用 ReleaseMutex 函数.当递归计数器到达0 时,该线程 ID 也被置为0,同时该对象变为已通知状态.
  • 一旦线程成功地等待到一个互斥对象,该线程就知道它已经拥有对受保护资源的独占访问权.试图访问该资源的任何其他线程(通过等待相同的互斥对象)均被置于等待状态中.当目前拥有对资源的访问权的线程不再需要它的访问权时,它必须调用 ReleaseMutex 函数来释放该互斥对象.当一个线程调用 ReleaseMutex 函数时,该函数要查看调用线程的 ID 是否与互斥对象中的线程 ID 相匹配.如果两个 ID 相匹配,递归计数器就会像前面介绍的那样递减.如果两个线程的 ID 不匹配,那么 ReleaseMutex 函数将不进行任何操作,而是将 FALSE(表示失败)返回给调用者.此时调用 GetLastError,将返回 ERROR_NOT_OWNER(试图释放不是调用者拥有的互斥对象).谁拥有互斥对象,谁才有权限释放互斥对象.
  • 在程序运行时,操作系统维护了线程的信息以及该线程相关的互斥对象信息,因此它知道那个线程终止了,如果某个线程得到其所有互斥对象的所有权,完成其线程代码的运行,但没有释放该互斥对象的所有权或被强行终止(使用 ExitThread、TerminateThread、ExitProcess 或 TerminateProcess 函数)退出之后,操作系统一旦发现该线程已经终止,把现在所占有的互斥对象废弃掉,然后自动把该线程拥有的所有互斥对象的线程 ID 设为0,并将其计数器归0,并且把互斥对象设置为有信号.最后系统要查看目前是否有任何线程正在等待该互斥对象.如果有,系统将“公平地”选定一个等待线程,将 ID 设置为选定的线程的 ID,并将递归计数器设置为 1,同时,选定的线程变为可调度线程.这与前面的情况相同,差别在于等待函数并不将通常的 WAIT_OBJECT_0 值返回给线程.相反,等待函数返回的是特殊值 WAIT_ABANDONED.

互斥对象的使用规则如下

  • 如果线程ID 是0(这是个无效ID),互斥对象不被任何线程所拥有,并且发出该互斥对象的通知信号.
  • 如果ID 是个非0 数字,那么一个线程就拥有互斥对象,并且不发出该互斥对象的通知信号.
  • 与所有其他内核对象不同,互斥对象在操作系统中拥有特殊的代码,允许它们违反正常的规则(后面将要介绍这个异常情况).

相关函数

  • CreateMutex(创建互斥对象)<如果为 fInitialOwner 参数传递TRUE,那么该对象的线程 ID 被设置为调用线程的 ID,递归计数器被设置为1.由于 ID 是个非0 数字,因此该互斥对象开始时不发出通知信号.>
  • OpenMutex(打开互斥对象)
  • ReleaseMutex(释放互斥对象)

互斥对象与关键代码段的比较

特性 互斥对象 关键代码段
运行速度
是否能够跨进程边界来使用
声明 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 或类似的函数)

执行流程

内核对象(VC_Win32)_第4张图片

代码样例 

样例链接

事件对象(Event)

说明

  • 事件对象(mutex)属于内核对象.
  • 自动重置事件能够包确保线程拥有对单个资源的互斥访问权限.而人工重置事件则无法确保线程拥有对单个资源的互斥访问权限.要实现线程同步时候人工重置事件是实现不了的,因为当人工事件对象得到通知时候,等待该事件对象的所有对象都变成可调用线程,并且一个事件得到该事件对象时候,该事件对象还是处于有信号状态的,必须手工调用 ResetEvent 才能取消事件的有信号状态,所有无法实现线程同步.但是人工事件却可用于让一个线程执行初始化工作,然后再触发其他线程,让它执行剩下的工作

组成

  • 一个使用数量
  • 事件类型
    • 人工重置事件   当人工重置的事件对象受到通知时,等待事件对象的所有线程均变成可调用的线程(当现场等待到该对象的所有权之后,需要调用ResetEvent函数手动的将该事件对象设置为无信号状态)
    • 自动重置事件   当自动重置的事件对象受到通知时,等待事件对象的线程中只有一个线程变成可调用线程(当现场等待到该对象的所有权之后,系统会自动将该对象设置为无信号状态)
  • 状态值     用于指明事件是处于通知还是为通知状态的布尔值

相关函数

  • CreateEvent (创建事件对象)
  • OpenEvent (打开事件对象)
  • SetEvent (设置事件对象变成触发状态)
  • ResetEvent (设置事件对象变成为触发状态)
  • PulseEvent (会先触发事件然后立刻恢复到未触发的状态)

执行流程

内核对象(VC_Win32)_第5张图片

代码样例

人工重置事件

程序源码(功能:线程2,线程3等待线程1初始化变量 g_x 后,输出 g_x 变量)

View Code

运行结果

自动重置事件

例子链接

可等待的计时器内核对象

说明

可等待计时器是这样一种内核对象,它们会在某个指定的时间触发,或每隔一段时间触发一次.要创建可等待的计时器,我们只需调用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 函数,将一个事件强加给线程池中的一个线程.

相关函数

  • CreateWaitableTimer (创建可等待计时器)
  • OpenWaitableTimer (打开可等待计时器)
  • SetWaitableTimer (设置计时器)
  • CancelWaitableTimer (取消可等待计时器)

执行流程

内核对象(VC_Win32)_第6张图片

代码样例

可等待计时器(非APC)

程序源码:

View Code

运行结果:

内核对象(VC_Win32)_第7张图片

可等待计时器(APC)

程序源码

View Code

运行结果

注意线程不应该等待定时器的句柄,也不应该以待命的方式等待定时器.如下面的代码:

HANDLEhTimer=CreateWaitableTimer(NULL,FALSE,NULL);
SetWaitableTimer(hTimer,...,TimerAPCRoutine,...);
WaitForSingleObjectEx(hTimer,INFINITE,TRUE);

不应该编写上面的代码,因为调用 WaitForSingleObjectEx 函数实际上是两次等待该定时器,一次是以待命方式等待,一次是等待内核对象句柄.当定时器变为已通知状态时,等待就成功了,线程被唤醒,这将使该线程摆脱待命状态,而 APC 例程则没有被调用.前面讲过,通常没有理由使用带有等待定时器的 APC 例程,因为你始终都可以等待定时器变为已通知状态,然后做你想要做的事情.

信号量内核对象

组成

  • 计数器            指明该线程拥有互斥对象的次数
  • 最大资源数量    标识信号量能够控制的资源的最大数量(带符号的 32 位值)
  • 当前资源数量    标识当前可以使用的资源的数量(带符号的 32 位值)

信号量的使用规则如下

  • 如果当前资源的数量大于 0,则发出信号量信号.
  • 如果当前资源数量是 0,则不发出信号量信号.
  • 系统决不允许当前资源的数量为负值.
  • 当前资源数量决不能大于最大资源数量.

相关函数

  • CreateSemaphore (创建信号量内核对象)
  • OpenSemaphore (打开信号量内核对象)
  • ReleaseSemaphore (释放信号量内核对象)

说明

  • 通过调用等待函数,传递负责保护资源的信号量的句柄,线程就能够获得对该资源的访问权.从内部来说,该等待函数要检查信号量的当前资源数量,如果它的值大于 0(信号量已经发出信号),那么计数器递减 1,调用线程保持可调度状态.信号量的出色之处在于它们能够以原子操作方式来执行测试和设置操作,这就是说,当向信号量申请一个资源时,操作系统就要检查是否有这个资源可供使用,同时将可用资源的数量递减,而不让另一个线程加以干扰.只有当资源数量递减后,系统才允许另一个线程申请对资源的访问权.
  • 通过调用 ReleaseSemaphore 函数,线程就能够对信号量的当前资源数量进行增加.该函数只是将 lReleaseCount 中的值添加给信号量的当前资源数量.通常情况下,为 lReleaseCount 参数传递 1,但是,不一定非要传递这个值.我常常传递 2 或更大的值.该函数也能够在它的 *plPreviousCount 中返回当前资源数量的原始值.实际上几乎没有应用程序关心这个值,因此可以传递 NULL,将它忽略.
  • 有时,有必要知道信号量的当前资源数量而不修改这个数量,但是没有一个函数可以用来查询信号量的当前资源数量的值.起先我认为调用ReleaseSemaphore 并为 lReleaseCount 参数传递 0,也许会在* plPreviousCount 中返回资源的实际数量.但是这样做是不行的,ReleaseSemaphore 用 0 填入这个长变量.接着,我试图传递一个非常大的数字,作为第二个参数,希望它不会影响当前资源数量,因为它将取代最大值.同样,ReleaseSemaphore用 0 填入 *plPrevious.可惜,如果不对它进行修改,就没有办法得到信号量的当前资源数量.

执行流程

内核对象(VC_Win32)_第8张图片

代码样例

程序源码

View Code

运行结果

内核对象(VC_Win32)_第9张图片

因为在本例程中,我们没有对信号进行增加,而创建时候初始化的可使用资源计数器为2.所以只有两个现象可以等待到信号量资源对象(即线程1和线程3),所以在主线程结束睡眠终止时候线程2都一直卡在 WaitForSingleObject(g_hSemaphore,INFINITE) 函数中.若我们稍加修改下上面的例子,源码如下:

View Code

结果显示如下

内核对象状态速查表

 

对象 何时处于未通知状态 何时处于已通知状态 成功等待的副作用
进程 当进程仍然活动时 当进程终止运行时(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) 将所有权赋予线程

 

保证实例的唯一性

保证运行实例单一性

 内核对象(VC_Win32)_第10张图片

代码样式

View Code

运行结果

内核对象(VC_Win32)_第11张图片


你可能感兴趣的:(内核对象(VC_Win32))