前言
前一篇文章学习了信号量(Semaphore),信号量的引出是为了解决事件(Event)解决不了的同步问题,例如生产者消费者问题。本篇将要学习的互斥体(Mutant),同样是为了解决信号量与事件解决不了的事情而诞生的。
为什么要有互斥体
等待对象被遗弃问题
互斥体(Mutant)与事件(Event)和信号量(Semaphore)一样,都可以用来进行线程的同步控制。但需要指出的是,这几个对象都是内核对象,这就意味着,通过这些对象可以进行跨进程的线程同步控制,比如:
观察上图,X和Y分别属于不同进程中的线程,它们都要用到等待对象Z,正常情况下,通过事件或者信号量便可以完成同步控制。但是在极端情况下,如果B进程的Y线程还没有来得及调用修改SignalState的函数(例如SetEvent)就挂掉了,那么等待对象Z将被遗弃,这也就意味着X线程将永远等下去!在等待对象被遗弃的情况下,事件或信号量将无法继续维持线程的同步,而互斥体可以解决此类问题。
重入问题
事件只允许同一时间有一个线程进入临界区,信号量允许同一时间有多个线程进入临界区。考虑一种特殊情况
上图中,由于代码设计的问题,在WaitForSingleObject等待A对象后,内部代码又调用了WaitForMultipleObjects等待A对象。这种情况被称作重入,当调用一次WaitForSingleObject后,A对象的SignalState变为了0,此时已经没有信号了,当调用WaitForMultipleObjects时,由于被等待对象A是没有信号的,因此代码会永远困在该函数内部。这种情况叫做死锁。事件和信号量面对重入问题都显得无能为力,而互斥体可以解决重入问题,避免死锁的情况发生。
互斥体的创建与信号设置
前面提到,互斥体(Mutant)可以解决信号量(Semaphore)和事件(Event)解决不了的问题,这里就从结构,用法等方面开始了解互斥体。
互斥体结构
首先来看互斥体结构,它所包含的字段较多
1 | _KMUTANT |
- Type:互斥体的Type值为2。
- SignalState:互斥体的信号通过两个函数进行设置:
- 互斥体创建函数CreateMutex。
- 互斥体信号设置函数ReleaseMutex。
- MutantListEntry:在KThread+0x10处有一个MutantListHead字段,指向链表头,链表圈着所有该线程拥有的互斥体对象,这个MutantListEntry就是挂在链表的位置(例如Kapc的ApcListEntry其结构就是挂在APC链表的位置,因此通过APC链表取的Kapc首地址时需要,减去ApcListEntry在结构体中的偏移)
- OwnerThread:正在拥有互斥体的线程;由CreateMutex函数第二个参数决定。
- Abandoned:是否已经被放弃不用。
- ApcDisable:是否禁用内核APC。
CreateMutex函数
CreateMutex是用来创建互斥体的函数,函数原型如下,其中第二个参数比较重要。
1 | HANDLE CreateMutex( |
创建互斥体,就是对互斥体结构进行初始化,大致如下:
1 | 初始化Mutant结构体: |
该函数最重要的是第二个参数bInitialOwner,它对互斥体结构中的两个字段都有影响:
- SignalState:
- bInitialOwner=TRUE:SignalState <- 0。
- bInitialOwner=FALSE:SignalState <- 1。
- OwnerThread:
- bInitialOwner=TRUE:OwnerThread存的当前线程(创建互斥体的这个线程)的地址。同时,将自己0x10处(MutantListEntry)的地址挂到当前线程MutantListHead(KThread+0x10)指向的链表上。
- bInitialOwner=FALSE:OwnerThread值为NULL。
ReleaseMutex函数
ReleaseMutex函数用于设置互斥体的信号(类似事件的SetEvent,信号量的ReleaseSemaphore)
1 | BOOL WINAPI ReleaseMutex(HANDLE hMutex); |
正常调用时:Mutant._DISPATCHER_HEADER.SignalState++。
如果SignalState=1,说明其它进程可以使用这个互斥体了,此时互斥体会从线程链表(KThread+0x10,MutantListHead指向的那个链表)中移除。这句话不好理解,需要结合代码分析,它可以用来解决 “等待对象被遗弃问题” 。
如何解决重入问题
前面提到,重入问题是事件(Event)与信号量(Semaphore)解决不了的,只有互斥体(Mutant)可以解决,这里就简单分析一下解决重入问题的过程。
OwnerThread的作用
回顾互斥体结构_KMUTANT,有一个字段OwnerThread,它就是解决重入的关键。
若一个互斥体被创建时,它的OwnerThread字段不为空,创建它的线程即为互斥体的所属线程。此时,初始化的互斥体SignalState字段被设置为0,也就是没有信号,这个时候别的线程是没法使用这个互斥体的。但是创建它的线程仍然可以使用。并且可以重复使用0x8000000次,这也是为什么互斥体可以重入的原因,因为创建它的线程可以在没有信号的情况下使用互斥体。至于为何创建它的线程在互斥体没有信号的情况下也可以使用,来看下面对KeWaitForSingleObject的分析。
KeWaitForSingleObject分析
这个函数可以说是同步系列的核心函数了,事件,信号量,互斥体都会调用它。
- 首先定位到紫色方框部分,这里是开头,会先判断等待对象类型,若是互斥体,就接着执行;若是信号量或者事件,就跳转。
- 然后到橙色方框这里,会根据SignalState的值判断是否有信号,如果有信号,就跳转。
- 如果没有信号,会先到红色方框这里,判断当前线程与互斥体所属线程是否相同。如果相同,就会跳到和有信号时一样的地方。也就是说,即使没有信号,互斥体也可以被它的所属线程使用。这样,对于拥有互斥体的线程,就可以重入该互斥体。那么究竟可以重入多少次呢?接着分析。
- 跳转后,会先判断SignalState的值是否与0x80000000相等,也就是说,只要不等于0x80000000,就可以继续执行,执行到蓝色方框的时候,会给SignalState的值减去1。这里要分两种情况:
- SignalState有信号:说明此时SignalState值大于0,任一线程可以等待这个互斥体,且没有重入。
- SignalState无信号: 此时的SignalState值为小于等于0,执行到这一步,说明当前线程一定拥有该互斥体,此时SignalState仍会减1,最多可以减少至0x80000000。
结论:由于互斥体进入激活条件有2个(SignalState有信号,线程拥有该互斥体),满足任何一种情况,均可进入激活状态, 从而解决了重入的问题。
如何解决等待对象被遗弃问题
接下来是另一个问题,关于如何解决等待对象被遗弃的问题,就像文章开篇提到的情况,当一个线程还没有设置等待对象的信号(例如SetEvent),那么另一个对象就只能一直等待下去了。来看看互斥体是如何解决这个问题的。
互斥体定义了MutantListEntry和Abandoned两个字段,在处理等待对象遗弃的情况时会用到。
当一个线程异常 “死亡” 时,系统会调用内核函数MmUnloadSystemImage
处理后事,它会根据 “死亡线程” 0x10位置指向的链表头,找到它所拥有的所有互斥体。将这些互斥体Abandoned字段值设置为1,并对它们调用KeReleaseMutant(X, Y, Abandon, Z)
函数(ReleaseMutant的内核函数)。
KeReleaseMutant函数的逻辑如下:
前面提到,正常情况下(Abandon=FALSE),该函数仅对SignalState作加1的操作。如果出现,互斥体所属线程突然死亡的情况(Abandon=TRUE),该函数会将SignalState直接设置为1,并且将互斥体所属线程设置为NULL,同时把自己从死亡线程的互斥体链表中移除。这样互斥体便可再为其它线程所使用,从而解决了等待对象被遗弃问题。
关于禁用内核APC
前面的介绍里,互斥体仍有一个字段未被解释清楚,就是最后这个ApcDisable(KMUTANT+0x1d)
互斥体分为2种,一种是用户用的,另一种是内核用的,区别如下:
- 用户互斥体
- 结构名:Mutant(在3环被创建)
- 对应内核函数:NtCreateMutant
- ApcDisable:0
- 内核互斥体
- 结构名:Mutex(在0环被创建)
- 对应内核函数:NtCreateMutex
- ApcDisable:1
用户互斥体与内核互斥体结构名不同,但是结构体相同,主要的区别在于ApcDisable字段。用户互斥体是允许内核APC执行的,但是内核互斥体是不允许内核APC进行执行的。另一个细节是,互斥体只是一个内核对象,它是如何限制线程的执行(内核APC的执行)的呢?
在KeWaitForSingleObject中有如上的代码,可以根据ApcDisable的值修改KThread.KernelApcDisable(正在使用互斥体的线程),若ApcDisable值为0,则KernelApcDisable的值不会发生改变。若ApcDisable值为1,则KernelApcDisable的值将会减1,此时KernelApcDisable将会是一个不为0的值,内核APC将会被禁用(根据内核APC执行过程,若KernelApcDisable的值不为0,内核APC将会被禁用)。
参考资料
参考教程:
- 海哥中级预习班教程
参考链接:
- https://blog.csdn.net/weixin_42052102/article/details/83473993 (CSDN-My classmates学习笔记)
- https://blog.csdn.net/qq_41988448/article/details/104895544 (CSDN-lzyddf学习笔记)