[7] Windows内核情景分析---线程同步
基于同步对象的等待、唤醒机制:
一个线程可以等待一个对象或多个对象而进入等待状态(也叫睡眠状态),另一个线程可以触发那个等待对象,唤醒在那个对象上等待的所有线程。
一个线程可以等待一个对象或多个对象,而一个对象也可以同时被N个线程等待。这样,线程与等待对象之间是多对多的关系。他们之间的等待关系由一个队列和一个‘等待块’来控制,等待块就是线程与等待目标对象之间的纽带。
WaitForSingleObject可以等待那些“可等待对象”,哪些对象是‘可等待’的呢?进程、线程、作业、文件对象、IO完成端口、可等待定时器、互斥、事件、信号量等,这些都是‘可等待’对象,可用于WaitForSingleObject等函数。
‘可等待’对象又分为‘可直接等待对象’和‘可间接等待对象’
互斥、事件、信号量、进程、线程这些对象由于内部结构中的自第一个字段是DISPATCHER_HEADER结构(可以看成是继承了DISPATCHER_HEADER),因此是可直接等待的。而文件对象不带这个结构,但文件对象内部有一个事件对象,因此,文件对象是‘可间接等待对象’。
比如:信号量就是一种可直接等待对象,它的结构如下:
Struct KSEMAPHORE
{
DISPATCHER_HEADER Header;//公共头
LONG Limit;//最大信号量个数
}
Struct DISPATCHER_HEADER
{
…
LONG SignalState;//信号状态量(>0表示有信号,<=0表示无信号)
LIST_ENTRY WaitListHead;//等待块队列
…
}
WaitForSingleObject内部最终调用下面的系统服务
NTSTATUS
NtWaitForSingleObject(IN HANDLE ObjectHandle,//直接或间接可等待对象的句柄
IN BOOLEAN Alertable,//表示本次等待操作是否可被吵醒(即被强制唤醒)
IN PLARGE_INTEGER TimeOut OPTIONAL)//超时
{
PVOID Object, WaitableObject;
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
LARGE_INTEGER SafeTimeOut;
NTSTATUS Status;
if ((TimeOut) && (PreviousMode != KernelMode))
{
_SEH2_TRY
{
SafeTimeOut = ProbeForReadLargeInteger(TimeOut);
TimeOut = &SafeTimeOut;
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
Status = ObReferenceObjectByHandle(ObjectHandle,SYNCHRONIZE,NULL,PreviousMode,
&Object,NULL);
if (NT_SUCCESS(Status))
{
//得到那个对象的‘可直接等待对象’DefaultObject
WaitableObject = OBJECT_TO_OBJECT_HEADER(Object)->Type->DefaultObject;
if (IsPointerOffset(WaitableObject))//if DefaultObject是个偏移,不是指针
{
//加上偏移值,获得内部的‘可直接等待对象’
WaitableObject = (PVOID)((ULONG_PTR)Object + (ULONG_PTR)WaitableObject);
}
_SEH2_TRY
{
Status = KeWaitForSingleObject(WaitableObject,//这个函数只能等待‘直接等待对象’
UserRequest,PreviousMode,Alertable,TimeOut);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
ObDereferenceObject(Object);
}
return Status;
}
#define IsPointerOffset(Ptr) ((LONG)(Ptr) >= 0)
如上,每个对象的对象类型都有一个默认的可直接等待对象,要么直接指向对象,要么是个偏移值。
如果是个偏移值,那么DefaultObject值的最高位为0,否则为1。
NtWaitForSingleObject可以等待直接的、间接的可等待对象,然而KeWaitForSingleObject只能等待真正的可直接等待对象,所以,必须将间接可等待对象转换为直接可等待对象,而每个对象的可直接等待对象记录在其对象类型的DefaultObject字段中。
下面这个函数是重点。这个函数很不好理解,我也是认真看了好几遍才有所明白,这个函数本身逻辑量比较大,函数也较长。重要的是对唤醒原因的理解。(把WaitStatus理解为唤醒原因就好了)
NTSTATUS //返回值表示本次睡眠的唤醒原因
//注意下面的函数只能在DISPATCH_LEVEL以下调用,否则蓝屏。(除非Timeout!=NULL && *Timeout==0)
KeWaitForSingleObject(IN PVOID Object,//要等待的‘可直接等待对象’
IN KWAIT_REASON WaitReason,//线程上次被切出原因
IN KPROCESSOR_MODE WaitMode,//表示这是来自用户模式/内核模式的等待请求
IN BOOLEAN Alertable,//表示本次等待操作是否可以被强制唤醒
IN PLARGE_INTEGER Timeout OPTIONAL)//超时值
{
PKTHREAD Thread = KeGetCurrentThread();
PKMUTANT CurrentObject = (PKMUTANT)Object;//其实意思是(DISPATCHER_HEADER*)Object
PKWAIT_BLOCK WaitBlock = &Thread->WaitBlock[0];
//内置的第4个等待块固定用于定时器(TIMER_WAIT_BLOCK是索引3)
PKWAIT_BLOCK TimerBlock = &Thread->WaitBlock[TIMER_WAIT_BLOCK];
PKTIMER Timer = &Thread->Timer;//复用这个超时定时器
NTSTATUS WaitStatus;//其实表示‘上次唤醒原因’
BOOLEAN Swappable;//内核栈是否可换到外存
LARGE_INTEGER DueTime, NewDueTime, InterruptTime;
PLARGE_INTEGER OriginalDueTime = Timeout;
ULONG Hand = 0;
//首次等待时需从WaitStart处开始,加锁,提升irql至DISPATCH_LEVEL
if (!Thread->WaitNext) goto WaitStart;
Thread->WaitNext = FALSE;//复位
KxSingleThreadWait();//这是一个宏,主要用来构造当前线程的等待块链表
for (;;)//首次循环、以后每次临时唤醒切回来时的入口
{
Thread->Preempted = FALSE;//表示因等待而主动放弃的cpu,不是被抢占
//if进入等待态前的原irql是APC_LEVEL,每次切回来时会自动在KiSwapContextInternal执行掉所有内核APC。if进入等待态前的原irql是APC_LEVEL,则用下面的语句手动执行掉所有内核APC。总之,确保每次切回来时,总是会先执行掉所有内核APC
if ((Thread->ApcState.KernelApcPending) && !(Thread->SpecialApcDisable) &&
(Thread->WaitIrql == PASSIVE_LEVEL))
{
KiReleaseDispatcherLock(Thread->WaitIrql);//执行掉所有Pending中的内核APC
}
//每轮循环中(即每次切回来时),测试是否条件成熟,可以退出睡眠。可以退出的3种条件为:
1、 所等待的对象有信号了
2、 可被强制唤醒了
3、 超时了
Else
{
//先检测所等待的对象是否有信号了,不过互斥对象需要特殊判断。
if (CurrentObject->Header.Type == MutantObject)
{
//if等待的互斥对象有信号了,或者当前线程本身就是该互斥对象的拥有者(一个线程可以反复多次等待同一个互斥对象的)
if ((CurrentObject->Header.SignalState > 0) ||
(Thread == CurrentObject->OwnerThread))
{
if (CurrentObject->Header.SignalState != (LONG)MINLONG)
{
KiSatisfyMutantWait(CurrentObject, Thread);
WaitStatus = Thread->WaitStatus;//唤醒原因
goto DontWait;//退出函数
}
Else 抛出异常…
}
}
//普通的等待对象只要DISPATCHER_HEADER头部中的SignalState > 0就表示有信号了
else if (CurrentObject->Header.SignalState > 0)
{
KiSatisfyNonMutantWait(CurrentObject);//递减信号状态量计数
WaitStatus = STATUS_WAIT_0;//唤醒原因为‘真唤醒’
goto DontWait; //退出函数
}
//若所等等待的对象没有信号,就检查当前状态是否可被强制唤醒
WaitStatus = KiCheckAlertability(Thread, Alertable, WaitMode);
if (WaitStatus != STATUS_WAIT_0)//if 可以强制唤醒了,就break退出循环,退出函数
break;
//如果无法强制唤醒,就再检测是否已经等待超时了
if (Timeout)//if用户提供了一个超时值,就检测超时情况
{
InterruptTime.QuadPart = KeQueryInterruptTime();
if ((ULONGLONG)InterruptTime.QuadPart >= Timer->DueTime.QuadPart)
{
WaitStatus = STATUS_TIMEOUT;//唤醒原因为‘超时’
goto DontWait; //退出函数
}
Timer->Header.Inserted = TRUE;
}
//如果所有条件都不满足,就需要进入睡眠(首轮检测)或继续进入睡眠(每次临时唤醒切回来时的检查)
//将等待块挂入目标等待对象内部的等待块链表中
InsertTailList(&CurrentObject->Header.WaitListHead,&WaitBlock->WaitListEntry);
if (Thread->Queue) KiActivateWaiterQueue(Thread->Queue);
Thread->State = Waiting;//将线程标记为等待状态(即睡眠状态)
KiAddThreadToWaitList(Thread, Swappable);//加入到当前cpu的等待线程链表中
if (Timeout)
KxInsertTimer(Timer, Hand);
else
KiReleaseDispatcherLockFromDpcLevel();
KiSetThreadSwapBusy(Thread);//标记线程正在进行切换
WaitStatus = KiSwapThread(Thread, KeGetCurrentPrcb());//关键。切出线程,进入睡眠
-------------------------------华丽的分割线--------------------------------------
//上面函数的返回值表示线程唤醒切回来的原因(大体分为三种)【临时、强制、真唤醒】
1、 临时唤醒。指被其他线程发来的内核APC临时唤醒,要求执行紧急APC任务
2、 强制唤醒。指被其他线程发来的强制唤醒要求唤醒/被发来的用户APC强制唤醒
3、 真唤醒。指所等待的对象真的有信号了而被唤醒
if (WaitStatus != STATUS_KERNEL_APC)//if唤醒原因不是临时唤醒,就退出函数了
return WaitStatus;
//下面的代码,是当被临时唤醒回来后,需要继续执行的。临时唤醒回来后,需要重新计算剩余超时值、执行内核APC,继续进入下轮循环,测试是否可以退出睡眠。
if (Timeout)//重新计算剩余超时值
Timeout = KiRecalculateDueTime(OriginalDueTime,&DueTime,&NewDueTime);
}
WaitStart://首次开始进入等待时,从这儿开始,提升irql
Thread->WaitIrql = KeRaiseIrqlToSynchLevel();
KxSingleThreadWait();//构造好线程的等待块链表
KiAcquireDispatcherLockAtDpcLevel();
}
KiReleaseDispatcherLock(Thread->WaitIrql);//break方式退出到这儿
return WaitStatus;
DontWait:
KiReleaseDispatcherLockFromDpcLevel();
KiAdjustQuantumThread(Thread);//调整时间片
return WaitStatus;//返回本次睡眠的唤醒原因
}
如上,这个函数是一个循环测试,概念上有点类似忙式等待。首先,第一轮循环,从WaitStart处开始,提升irql(目的是防止被切换),然后回到for循环开头处,测试条件。如果所要等待的对象本来就有信号,那么第一轮循环时,就不用进入睡眠了,直接退出函数。否则,就切出线程,让出cpu,构造好线程的等待块链表,并将等待块(此处意指线程自身)挂入目标等待对象的等待块队列中(可理解为等待者线程队列)。然后,当以后条件成熟,唤醒回来时,检测唤醒原因。若是被临时唤醒的,就继续进入下轮循环测试等待条件,否则即可退出函数,退出睡眠了。
下面的等待块结构是线程的等待机制核心,它即用来挂入线程的等待块链表,也用来挂入等待对象的队列中。当挂入前者时,等待块就可理解为一个等待对象,当挂入后者时,就可理解为一个等待者线程。
typedef struct _KWAIT_BLOCK
{
LIST_ENTRY WaitListEntry;//用来挂入目标对象的等待者线程队列
struct _KTHREAD *Thread;//所属线程
PVOID Object;//要等待的目标对象
struct _KWAIT_BLOCK *NextWaitBlock;//下一个等待块(用来挂入线程的等待块链表)
USHORT WaitKey;//本等待块是所属线程的第几个等待对象
UCHAR WaitType;//WaitAll/WaitAny
volatile UCHAR BlockState;
} KWAIT_BLOCK, *PKWAIT_BLOCK, *PRKWAIT_BLOCK;
上面的函数中牵涉到一个重要宏和几个子函数,我们看。
#define KxSingleThreadWait()
Thread->WaitBlockList = WaitBlock;//等待块链表
WaitBlock->WaitKey = 0;//即等待块的索引
WaitBlock->Object = Object;//要等待的目标对象
WaitBlock->WaitType = WaitAny; //只等待一个对象的话就固定是WaitAny
Thread->WaitStatus = 0; //复位唤醒原因
if (Timeout) //if给定了超时值
{
KxSetTimerForThreadWait(Timer, *Timeout, &Hand);//设置好定时器对象
DueTime.QuadPart = Timer->DueTime.QuadPart;
WaitBlock->NextWaitBlock = TimerBlock; //定时器对象等待块也挂入链表中
TimerBlock->NextWaitBlock = WaitBlock;//单循环链表
Timer->Header.WaitListHead.Flink = &TimerBlock->WaitListEntry;
Timer->Header.WaitListHead.Blink = &TimerBlock->WaitListEntry;
}
else
{
WaitBlock->NextWaitBlock = WaitBlock; //单循环链表
}
Thread->Alertable = Alertable;//线程睡眠模式,是否可被提醒(即强制唤醒)
Thread->WaitMode = WaitMode;//来自用户还是内核模式的等待请求
Thread->WaitReason = WaitReason; //上次线程切换原因
Thread->WaitListEntry.Flink = NULL;
Swappable = KiCheckThreadStackSwap(Thread, WaitMode);//检测本线程的内核栈是否可以换到外存
Thread->WaitTime = KeTickCount.LowPart;//记录上次切出时间
如上,这个宏的主要功能就是用来构造好该线程的等待块链表,以及一些其它乱七八糟的工作,上面的函数KiCheckThreadStackSwap用来检测本线程的内核栈是否可以置换到外存
BOOLEAN KiCheckThreadStackSwap(IN PKTHREAD Thread,IN KPROCESSOR_MODE WaitMode)
{
if ((WaitMode == UserMode) && (Thread->EnableStackSwap) &&
(Thread->Priority >= (LOW_REALTIME_PRIORITY + 9)))
{
return TRUE;
}
else
{
return FALSE;
}
}
如上,超级实时类的线程在处理来自用户模式的等待请求时,内核栈可以置换到外存。
下面的函数用来在线程被临时唤醒后,测试线程的状态是否可被强制唤醒。
NTSTATUS
KiCheckAlertability(IN PKTHREAD Thread,
IN BOOLEAN Alertable,//本次睡眠操作是否支持强制唤醒
IN KPROCESSOR_MODE WaitMode)//来自哪个模式的等待请求
{
if (Alertable)//if本次睡眠操作支持强制唤醒(即可被强制唤醒)
{
if (Thread->Alerted[WaitMode])//if 真收到了来自那个模式的强制唤醒要求
{
Thread->Alerted[WaitMode] = FALSE;//复位
return STATUS_ALERTED;//唤醒原因设为强制唤醒
}
//若没收到其他线程发来对应模式的强制唤醒要求,但收到了用户APC
else if ((WaitMode != KernelMode) &&
(!IsListEmpty(&Thread->ApcState.ApcListHead[UserMode])))
{
Thread->ApcState.UserApcPending = TRUE;
return STATUS_USER_APC;//标记为被用户APC给强制唤醒了
}
//只要收到了内核模式的强制唤醒要求,就可被强制唤醒(不必模式匹配)
else if (Thread->Alerted[KernelMode])
{
Thread->Alerted[KernelMode] = FALSE;
return STATUS_ALERTED; //唤醒原因设为强制唤醒
}
}
//即使本次睡眠不支持强制唤醒,但其他线程发来的用户APC仍可强制唤醒本线程
else if ((WaitMode != KernelMode) && (Thread->ApcState.UserApcPending))
{
return STATUS_USER_APC; //标记为被用户APC给强制唤醒了
}
return STATUS_WAIT_0;
}
如上:当线程的本次睡眠支持强制唤醒的情况下,可被对应模式或内核模式的强制唤醒要求给唤醒,即使不支持强制唤醒,也会被其他线程发来的用户APC给强制搞醒。
关于模式匹配记住下面一点。来自内核模式的强制唤醒要求可以强制唤醒来自内核模式、用户模式的等待请求,而来自用户模式的强制唤醒要求只能强制唤醒来自用户模式的等待请求。这就好比:内核空间的程序可以访问内核空间的代码和数据,而用户空间的程序只能访问用户空间的代码和数据。这样记就容易了。
线程的睡眠其工作之一就是线程切换。线程一被切出了,要么进入就绪态,要么进入等待态。因等待对象而引起的线程切换,将使线程处于等待态。等待态与就绪态的本质区别就是处于就绪态的线程直接挂入就绪队列,随时等候被调度运行,处于等待态的线程,则要将自己挂入cpu的等待队列,挂入各个目标等待
对象的等待线程队列,然后一直等待别的线程触发等待对象,唤醒自己,重新进入就绪态或运行态。
#define KiAddThreadToWaitList(Thread, Swappable)
{
if (Swappable) //为什么要满足这个条件,我也搞不清
InsertTailList(&KeGetCurrentPrcb()->WaitListHead, &Thread->WaitListEntry);
}
当一个线程处于睡眠态时,最典型的唤醒原因就是所等待的对象有了信号。当一个等待对象有了信号时,系统(指别的线程)会调用下面的函数尝试唤醒在该等待对象上等待的所有线程。
VOID FASTCALL
KiWaitTest(IN PVOID ObjectPointer,//目标等待对象
IN KPRIORITY Increment)//唤醒线程后的优先级增量(以便尽快得到调度运行)
{
PLIST_ENTRY WaitEntry, WaitList;
PKWAIT_BLOCK WaitBlock;
PKTHREAD WaitThread;
PKMUTANT FirstObject = ObjectPointer;
NTSTATUS WaitStatus;
WaitList = &FirstObject->Header.WaitListHead;
WaitEntry = WaitList->Flink;
//遍历等待者线程队列,尝试唤醒所有线程,直到信号状态量分配完毕
while ((FirstObject->Header.SignalState > 0) && (WaitEntry != WaitList))
{
WaitBlock = CONTAINING_RECORD(WaitEntry, KWAIT_BLOCK, WaitListEntry);
WaitThread = WaitBlock->Thread;
WaitStatus = STATUS_KERNEL_APC;//模拟给那个等待者线程发送内核apc而临时唤醒它
if (WaitBlock->WaitType == WaitAny)//WaitAnt类型的话,肯定满足分配了
{
WaitStatus = (NTSTATUS)WaitBlock->WaitKey;//唤醒原因改为‘真唤醒’,此处即索引
KiSatisfyObjectWait(FirstObject, WaitThread);//分配资源,递减对象的信号状态量计数
}
KiUnwaitThread(WaitThread, WaitStatus, Increment);//关键函数
WaitEntry = WaitList->Flink;//下一个线程
}
}
如上,这个函数会在每一个等待对象变成有信号时,尝试唤醒所有在该对象上等待的所有线程。注意,若队列中的某个等待块类型是WaitAny时,那么,那个等待者线程必然真的满足了等待条件,所以需要将唤醒原因改为‘真唤醒’类型。反之,若那个线程的等待类型是WaitAll,也即它还要等待其他对象,那么,就模拟给它发送内核apc的方式(其实没发送),临时唤醒它,进入下轮循环,继续测试它所等待的其他对象,这一点务必要注意。
下面的宏用于当满足分配条件时,分配信号状态量给指定线程。
#define KiSatisfyObjectWait(Object, Thread)
{
if ((Object)->Header.Type == MutantObject) //互斥对象要特殊处理
{
(Object)->Header.SignalState--;//递减信号状态量计数(此处为拥有计数)
if ((Object)->Header.SignalState == 0)//if拥有计数==0
{
(Object)->OwnerThread = Thread;
Thread->KernelApcDisable = Thread->KernelApcDisable - (Object)->ApcDisable;
if ((Object)->Abandoned)//如果该互斥对象是因为原拥有者线程意外终止了而让出的
{
(Object)->Abandoned = FALSE;//复位
Thread->WaitStatus = STATUS_ABANDONED;//唤醒原因
}
//插入本线程获得的所有互斥对象链表中
InsertHeadList(Thread->MutantListHead.Blink, &(Object)->MutantListEntry);
}
}
//如果是‘自动复位’型事件,触发对象后,又立马复位它的状态
else if (((Object)->Header.Type & TIMER_OR_EVENT_TYPE) == EventSynchronizationObject)
(Object)->Header.SignalState = 0;
else if ((Object)->Header.Type == SemaphoreObject)
(Object)->Header.SignalState--;//递减信号状态量计数(此处为真的信号量计数)
}
熟悉Win32 多线程编程的朋友想必不用解释这段代码了吧。
只是要提醒一下,不管是什么同步对象,其内部的SignalState表示信号状态量计数,当该值<=0时表示无信号,>0时表示有信号。其实这个字段本用于信号量的,不过所有的同步对象都可以看做是SignalState只有0和1两种情况的‘特殊信号量’。
下面的这个函数可以说是线程的等待唤醒机制的核心,其功能用来唤醒指定线程
VOID FASTCALL
KiUnwaitThread(IN PKTHREAD Thread,//目标线程
IN LONG_PTR WaitStatus,//唤醒原因
IN KPRIORITY Increment)//唤醒后的优先级增量,以便唤醒后尽快得到调度运行
{
KiUnlinkThread(Thread, WaitStatus);//将所有等待块脱链
Thread->AdjustIncrement = (SCHAR)Increment;//上次优先级调整增量
Thread->AdjustReason = AdjustUnwait;//调整原因
KiReadyThread(Thread);//‘就绪化’指定线程(也即转入就绪队列或者置为抢占者线程)
}
任意一个线程都可以调用这个函数唤醒目标线程。唤醒原因大体分为:临时唤醒、强制唤醒、真唤醒
每当插入一个apc的时候,将调用下面的函数
NTSTATUS KiInsertQueueApc(…)
{
…….
if (Thread != KeGetCurrentThread())
{
if (ApcMode == KernelMode)//若要给其他线程插入一个内核apc
{
Thread->ApcState.KernelApcPending = TRUE;
if (Thread->State == Running)
{
RequestInterrupt = TRUE;
}
else if ((Thread->State == Waiting) && (Thread->WaitIrql == PASSIVE_LEVEL) &&
!(Thread->SpecialApcDisable) && (!(Apc->NormalRoutine) ||
(!(Thread->KernelApcDisable)&&!(Thread->ApcState.KernelApcInProgress))))
{
Status = STATUS_KERNEL_APC;
KiUnwaitThread(Thread, Status, PriorityBoost);//临时唤醒目标线程
}
…….
}
else if ((Thread->State == Waiting) && (Thread->WaitMode == UserMode) &&
((Thread->Alertable) || (Thread->ApcState.UserApcPending)))
{
Thread->ApcState.UserApcPending = TRUE;
Status = STATUS_USER_APC;
KiUnwaitThread(Thread, Status, PriorityBoost);//插入用户APC,强制唤醒目标线程
}
……
}
看到没,插入内核apc的时候可能会临时唤醒目标线程,插入用户apc的时候可能会强制唤醒目标线程。
VOID FASTCALL
KiUnlinkThread(IN PKTHREAD Thread, IN NTSTATUS WaitStatus)
{
PKWAIT_BLOCK WaitBlock;
PKTIMER Timer;
//关键。唤醒原因记录在线程结构的这个字段中,KiSwapContext返回的就是这个字段值
Thread->WaitStatus |= WaitStatus;
WaitBlock = Thread->WaitBlockList;
//让各个等待块脱离各自等待对象的队列
do
{
RemoveEntryList(&WaitBlock->WaitListEntry);
WaitBlock = WaitBlock->NextWaitBlock;
} while (WaitBlock != Thread->WaitBlockList);
//脱离cpu的等待线程队列
if (Thread->WaitListEntry.Flink) RemoveEntryList(&Thread->WaitListEntry);
Timer = &Thread->Timer;
if (Timer->Header.Inserted) KxRemoveTreeTimer(Timer);
if (Thread->Queue) Thread->Queue->CurrentCount++;
}
注意,上面的这个函数只是脱离各个等待对象的等待块队列。线程自己的等待块队列还在。
现在看一下KeWaitForSingleObject那个函数内部调用的KiSwapThread。注意是KiSwapThread不是KiSwapContext。
LONG FASTCALL
KiSwapThread(IN PKTHREAD CurrentThread,IN PKPRCB Prcb)
{
BOOLEAN ApcState = FALSE;
KIRQL WaitIrql;//上次切出时的irql
LONG_PTR WaitStatus;//上次唤醒原因
PKTHREAD NextThread;
KiAcquirePrcbLock(Prcb);
NextThread = Prcb->NextThread;//当前的抢占者线程
if (NextThread)
{
Prcb->NextThread = NULL;
Prcb->CurrentThread = NextThread;
NextThread->State = Running;
}
else
{
NextThread = KiSelectReadyThread(0, Prcb);//调度处一个
if (NextThread)
{
Prcb->CurrentThread = NextThread;
NextThread->State = Running;
}
else
{
InterlockedOr((PLONG)&KiIdleSummary, Prcb->SetMember);
NextThread = Prcb->IdleThread;//使用空转线程
Prcb->CurrentThread = NextThread;
NextThread->State = Running;
}
}
KiReleasePrcbLock(Prcb);
WaitIrql = CurrentThread->WaitIrql;//记录上次切出时的irql
MiSyncForContextSwitch(NextThread);
ApcState = KiSwapContext(CurrentThread, NextThread);
----------------------------------------华丽的分割线---------------------------------------
if (ApcState)//切回来后,例行执行内核apc
{
KeLowerIrql(APC_LEVEL);
KiDeliverApc(KernelMode, NULL, NULL);
ASSERT(WaitIrql == PASSIVE_LEVEL);
}
KeLowerIrql(WaitIrql);
WaitStatus = CurrentThread->WaitStatus;//关键。返回该线程上次唤醒的原因
return WaitStatus;
}
相信到此为止,大家理解了线程如何等待单个对象的机制了吧?一个线程可以同时等待对个对象,我们看。
NTSTATUS
NTAPI
KeWaitForMultipleObjects(IN ULONG Count,//数组元素个数
IN PVOID Object[],//等待对象数组
IN WAIT_TYPE WaitType,//WaitAll/AitAny
IN KWAIT_REASON WaitReason,//上次切换原因
IN KPROCESSOR_MODE WaitMode,//来自用户模式/内核模式的等待请求
IN BOOLEAN Alertable,//本次等待是否可提醒(指是否可被强制唤醒)
IN PLARGE_INTEGER Timeout OPTIONAL,//超时
OUT PKWAIT_BLOCK WaitBlockArray OPTIONAL)//等待块数组
{
PKMUTANT CurrentObject;
PKWAIT_BLOCK WaitBlock;
PKTHREAD Thread = KeGetCurrentThread();
PKWAIT_BLOCK TimerBlock = &Thread->WaitBlock[TIMER_WAIT_BLOCK];//定时器等待块是固定的
PKTIMER Timer = &Thread->Timer;
NTSTATUS WaitStatus = STATUS_SUCCESS;
BOOLEAN Swappable;
PLARGE_INTEGER OriginalDueTime = Timeout;
LARGE_INTEGER DueTime, NewDueTime, InterruptTime;
ULONG Index, Hand = 0;
if (!WaitBlockArray)//没提供就使用内置的等待块数组(3个等待块+一个定时器等待块)
WaitBlockArray = &Thread->WaitBlock[0];
if (!Thread->WaitNext) goto WaitStart;//首轮循环从WaitStart处开始
Thread->WaitNext = FALSE;
KxMultiThreadWait();//关键。与等待单个对象时使用的宏不相同
for (;;)
{
Thread->Preempted = FALSE;
if ((Thread->ApcState.KernelApcPending) && !(Thread->SpecialApcDisable) &&
(Thread->WaitIrql < APC_LEVEL))
{
KiReleaseDispatcherLock(Thread->WaitIrql);
}
else
{
Index = 0;
if (WaitType == WaitAny)
{
do
{
CurrentObject = (PKMUTANT)Object[Index];
if (CurrentObject->Header.Type == MutantObject)
{
if ((CurrentObject->Header.SignalState > 0) ||
(Thread == CurrentObject->OwnerThread))
{
if (CurrentObject->Header.SignalState != (LONG)MINLONG)
{
KiSatisfyMutantWait(CurrentObject, Thread);
WaitStatus = Thread->WaitStatus | Index;
goto DontWait;//只要满足一个就退出函数
}
else
{
KiReleaseDispatcherLock(Thread->WaitIrql);
ExRaiseStatus(STATUS_MUTANT_LIMIT_EXCEEDED);
}
}
}
else if (CurrentObject->Header.SignalState > 0)
{
KiSatisfyNonMutantWait(CurrentObject);
WaitStatus = Index;
goto DontWait; //只要满足一个就退出函数
}
Index++;
} while (Index < Count);
}
Else //WaitAll
{
do
{
CurrentObject = (PKMUTANT)Object[Index];
if (CurrentObject->Header.Type == MutantObject)
{
if ((Thread == CurrentObject->OwnerThread) &&
(CurrentObject->Header.SignalState == (LONG)MINLONG))
{
KiReleaseDispatcherLock(Thread->WaitIrql);
ExRaiseStatus(STATUS_MUTANT_LIMIT_EXCEEDED);
}
else if ((CurrentObject->Header.SignalState <= 0) &&
(Thread != CurrentObject->OwnerThread))
{
break;//只要任有一个对象不满足,就要继续进入等待状态
}
}
else if (CurrentObject->Header.SignalState <= 0)
{
break; //只要任有一个对象不满足,就要继续进入等待状态
}
Index++;
} while (Index < Count);
if (Index == Count)//if 所有对象都有信号了
{
WaitBlock = WaitBlockArray;
do
{
CurrentObject = (PKMUTANT)WaitBlock->Object;
KiSatisfyObjectWait(CurrentObject, Thread);
WaitBlock = WaitBlock->NextWaitBlock;
} while(WaitBlock != WaitBlockArray);
WaitStatus = Thread->WaitStatus;//唤醒原因为‘真唤醒’类型
goto DontWait;
}
}
WaitStatus = KiCheckAlertability(Thread, Alertable, WaitMode);
if (WaitStatus != STATUS_WAIT_0) break;
if (Timeout)
{
InterruptTime.QuadPart = KeQueryInterruptTime();
if ((ULONGLONG)InterruptTime.QuadPart >= Timer->DueTime.QuadPart)
{
WaitStatus = STATUS_TIMEOUT;
goto DontWait;
}
Timer->Header.Inserted = TRUE;
WaitBlock->NextWaitBlock = TimerBlock;
}
WaitBlock = WaitBlockArray;
do
{
CurrentObject = WaitBlock->Object;
InsertTailList(&CurrentObject->Header.WaitListHead,&WaitBlock->WaitListEntry);
WaitBlock = WaitBlock->NextWaitBlock;
} while (WaitBlock != WaitBlockArray);
if (Thread->Queue) KiActivateWaiterQueue(Thread->Queue);
Thread->State = Waiting;
KiAddThreadToWaitList(Thread, Swappable);
KiSetThreadSwapBusy(Thread);
if (Timeout)
KxInsertTimer(Timer, Hand);
else
KiReleaseDispatcherLockFromDpcLevel();
WaitStatus = KiSwapThread(Thread, KeGetCurrentPrcb());
----------------------------------------华丽的分割线---------------------------------------
//if 唤醒原因不是临时唤醒,直接退出整个函数,退出睡眠状态
if (WaitStatus != STATUS_KERNEL_APC) return WaitStatus;
//否则,若是临时唤醒回来的,则进入下一轮循环,继续去测试等待对象的信号情况
if (Timeout)
Timeout = KiRecalculateDueTime(OriginalDueTime,&DueTime,&NewDueTime);
}
WaitStart:
Thread->WaitIrql = KeRaiseIrqlToSynchLevel();
KxMultiThreadWait();
KiAcquireDispatcherLockAtDpcLevel();
}
KiReleaseDispatcherLock(Thread->WaitIrql);
return WaitStatus;
DontWait:
KiReleaseDispatcherLockFromDpcLevel();
KiAdjustQuantumThread(Thread);
return WaitStatus;
}
这段代码我想不用多解释了吧。唯一需要注意的是使用了不同的宏:
#define KxMultiThreadWait()
Thread->WaitBlockList = WaitBlockArray;
Index = 0;
//构造好本线程的等待块链表
do
{
WaitBlock = &WaitBlockArray[Index];
WaitBlock->Object = Object[Index];
WaitBlock->WaitKey = (USHORT)Index;//关键
WaitBlock->WaitType = WaitType;//所有等待块的等待类型都相同
WaitBlock->Thread = Thread;
WaitBlock->NextWaitBlock = &WaitBlockArray[Index + 1];
Index++;
} while (Index < Count);
WaitBlock->NextWaitBlock = WaitBlockArray; //单循环链表
Thread->WaitStatus = STATUS_WAIT_0;
if (Timeout)
{
TimerBlock->NextWaitBlock = WaitBlockArray;
KxSetTimerForThreadWait(Timer, *Timeout, &Hand);
DueTime.QuadPart = Timer->DueTime.QuadPart;
InitializeListHead(&Timer->Header.WaitListHead);
}
Thread->Alertable = Alertable;//是否可被强制唤醒
Thread->WaitMode = WaitMode;//来自用户模式/内核模式的等待请求
Thread->WaitReason = WaitReason;//上次被切原因
Thread->WaitListEntry.Flink = NULL;
Swappable = KiCheckThreadStackSwap(Thread, WaitMode);
Thread->WaitTime = KeTickCount.LowPart;//记录上次被切时间
弄懂了线程的等待唤醒机制后,下面我们看各种具体等待对象(又叫同步对象)的原理:
同步对象:(互斥、事件、信号量、自旋锁)
信号量的原理:.
typedef struct _KSEMAPHORE {
DISPATCHER_HEADER Header;//公共头部
LONG Limit;//信号量的最大信号个数
} KSEMAPHORE, *PKSEMAPHORE;
NTSTATUS
NtCreateSemaphore(OUT PHANDLE SemaphoreHandle,//返回信号量对象的句柄
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,//信号量的名称及其他属性
IN LONG InitialCount,//信号量的初始信号个数
IN LONG MaximumCount)//支持的最大信号个数
{
PKSEMAPHORE Semaphore;
HANDLE hSemaphore;
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
NTSTATUS Status;
if (PreviousMode != KernelMode)
{
_SEH2_TRY
{
ProbeForWriteHandle(SemaphoreHandle);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
if ((MaximumCount <= 0) || (InitialCount < 0) || (InitialCount > MaximumCount))
return STATUS_INVALID_PARAMETER;
//信号量也是一种内核对象
Status = ObCreateObject(PreviousMode,ExSemaphoreObjectType,ObjectAttributes,PreviousMode,
NULL,sizeof(KSEMAPHORE),0,0, (PVOID*)&Semaphore);
if (NT_SUCCESS(Status))
{
//初始化对象结构
KeInitializeSemaphore(Semaphore,InitialCount,MaximumCount);
//插入对象目录和句柄表
Status = ObInsertObject((PVOID)Semaphore,NULL,DesiredAccess,0,NULL,&hSemaphore);
if (NT_SUCCESS(Status))
{
_SEH2_TRY
{
*SemaphoreHandle = hSemaphore;
}
_SEH2_EXCEPT(ExSystemExceptionFilter())
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
}
}
return Status;
}
关键看下面的函数:
VOID
KeInitializeSemaphore(IN PKSEMAPHORE Semaphore,IN LONG Count,IN LONG Limit)
{
KeInitializeDispatcherHeader(&Semaphore->Header,SemaphoreObject,
sizeof(KSEMAPHORE) / sizeof(ULONG),Count);
Semaphore->Limit = Limit;
}
#define KeInitializeDispatcherHeader(Header, t, s, State) \
{ \
(Header)->Type = t; \
(Header)->Absolute = 0; \
(Header)->Size = s; \
(Header)->Inserted = 0; \
(Header)->SignalState = State;//初始信号个数 \
InitializeListHead(&((Header)->WaitListHead));//初始化等待线程队列 \
}
信号量对象可用于WaitFor系列函数中。没得到一个信号量,SignalState就递减。
当SignalState减到0时,就需要等待其他线程释放信号量。
NTSTATUS
NtReleaseSemaphore(IN HANDLE SemaphoreHandle,
IN LONG ReleaseCount,//一次可以释放多个信号量,这将一次唤醒多个线程
OUT PLONG PreviousCount OPTIONAL)//返回之前的信号个数
{
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
PKSEMAPHORE Semaphore;
NTSTATUS Status;
if ((PreviousCount) && (PreviousMode != KernelMode))
{
_SEH2_TRY
{
ProbeForWriteLong(PreviousCount);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
if (ReleaseCount <= 0)
return STATUS_INVALID_PARAMETER;
Status = ObReferenceObjectByHandle(SemaphoreHandle,SEMAPHORE_MODIFY_STATE,
ExSemaphoreObjectType,PreviousMode,
(PVOID*)&Semaphore,NULL);
if (NT_SUCCESS(Status))
{
_SEH2_TRY
{
//实质函数
LONG PrevCount = KeReleaseSemaphore(Semaphore,IO_NO_INCREMENT,
ReleaseCount,FALSE);
if (PreviousCount) *PreviousCount = PrevCount;
}
_SEH2_EXCEPT(ExSystemExceptionFilter())
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
ObDereferenceObject(Semaphore);
}
return Status;
}
LONG
KeReleaseSemaphore(IN PKSEMAPHORE Semaphore,
IN KPRIORITY Increment,//优先级增量
IN LONG Adjustment,// 一次可以释放多个信号量,这将一次唤醒多个线程
IN BOOLEAN Wait)
{
LONG InitialState, State;
KIRQL OldIrql;
PKTHREAD CurrentThread;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
OldIrql = KiAcquireDispatcherLock();
InitialState = Semaphore->Header.SignalState;
State = InitialState + Adjustment;//一次增加N个信号
if ((Semaphore->Limit < State) || Adjustment< 0))
{
KiReleaseDispatcherLock(OldIrql);
ExRaiseStatus(STATUS_SEMAPHORE_LIMIT_EXCEEDED);
}
Semaphore->Header.SignalState = State;
//没释放一次信号量,就会尝试唤醒在该信号量对象上等待的所有线程
if (!(InitialState) && !(IsListEmpty(&Semaphore->Header.WaitListHead)))
KiWaitTest(&Semaphore->Header, Increment);
if (Wait == FALSE)
KiReleaseDispatcherLock(OldIrql);
else
{
CurrentThread = KeGetCurrentThread();
CurrentThread->WaitNext = TRUE;
CurrentThread->WaitIrql = OldIrql;
}
return InitialState;
}
如上,如此简单而已。只是需要注意信号量对象一有信号,就会调用KiWaitTest
互斥对象的原理:
互斥对象有点特殊,他有一个‘拥有者线程’,并且同一时刻,最多只能让一个线程拥有。
typedef struct _KMUTANT {
DISPATCHER_HEADER Header;//公共头
LIST_ENTRY MutantListEntry;//用来挂入拥有者线程的互斥对象链表
struct _KTHREAD *RESTRICTED_POINTER OwnerThread;//关键。当前拥有者线程
BOOLEAN Abandoned;//是否被拥有者线程因意外终止而抛弃了
BOOL ApcDisable;//表示获得互斥体,进入临界区前,是否需要禁用内核APC(用于用户模式的Mutex)
} KMUTANT, *PKMUTANT, KMUTEX, *PKMUTEX;
VOID
KeInitializeMutant(IN PKMUTANT Mutant,
IN BOOLEAN InitialOwner)//指当前线程是否初始为该互斥对象的拥有者
{
PKTHREAD CurrentThread;
KIRQL OldIrql;
if (InitialOwner)
{
CurrentThread = KeGetCurrentThread();
Mutant->OwnerThread = CurrentThread;//看
OldIrql = KiAcquireDispatcherLock();
//挂入本线程当前拥有的互斥对象链表
InsertTailList(&CurrentThread->MutantListHead,&Mutant->MutantListEntry);
KiReleaseDispatcherLock(OldIrql);
}
else
Mutant->OwnerThread = NULL;
KeInitializeDispatcherHeader(&Mutant->Header,MutantObject,
sizeof(KMUTANT) / sizeof(ULONG),
InitialOwner ? 0: 1);//初始对外信号个数为0或者1
Mutant->Abandoned = FALSE;
Mutant->ApcDisable = 0;
}
因为互斥对象有一个拥有者线程,所以互斥对象是可以重入的,也即一个线程可以反复多次调用,等待同一个互斥对象,如:
WaitForSingleObject(mutex1); //获得互斥对象,同时递增拥有计数
其他代码;
WaitForSingleObject(mutex1);//递增拥有计数
其他代码;
ReleaseMutex(mutex1); //释放拥有计数
其他代码;
ReleaseMurex(mutex1);// 释放拥有计数
NTSTATUS
NtReleaseMutant(IN HANDLE MutantHandle,
IN PLONG PreviousCount OPTIONAL)
{
PKMUTANT Mutant;
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
NTSTATUS Status;
if ((PreviousCount) && (PreviousMode != KernelMode))
{
_SEH2_TRY
{
ProbeForWriteLong(PreviousCount);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
Status = ObReferenceObjectByHandle(MutantHandle,MUTANT_QUERY_STATE,ExMutantObjectType,
PreviousMode, (PVOID*)&Mutant,NULL);
if (NT_SUCCESS(Status))
{
_SEH2_TRY
{
//释放互斥对象,释放一次本线程对它的一个拥有计数
LONG Prev = KeReleaseMutant(Mutant,MUTANT_INCREMENT,FALSE,FALSE);
if (PreviousCount) *PreviousCount = Prev;
}
_SEH2_EXCEPT(ExSystemExceptionFilter())
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
ObDereferenceObject(Mutant);
}
return Status;
}
LONG
KeReleaseMutant(IN PKMUTANT Mutant,
IN KPRIORITY Increment,
IN BOOLEAN Abandon,//是否是因为线程意外终止而抛弃的
IN BOOLEAN Wait)
{
KIRQL OldIrql;
LONG PreviousState;
PKTHREAD CurrentThread = KeGetCurrentThread();
BOOLEAN EnableApc = FALSE;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
OldIrql = KiAcquireDispatcherLock();
PreviousState = Mutant->Header.SignalState;//先前的拥有计数
if (Abandon == FALSE)//最典型
{
if (Mutant->OwnerThread != CurrentThread)
{
KiReleaseDispatcherLock(OldIrql);
ExRaiseStatus(Mutant->Abandoned ? STATUS_ABANDONED :STATUS_MUTANT_NOT_OWNED);
}
Mutant->Header.SignalState++;//递增对外(指对其他线程)的信号个数
}
else
{
Mutant->Header.SignalState = 1;//复位成1,表示直接对外有信号
Mutant->Abandoned = TRUE;//标记为被拥有者线程意外抛弃了
}
if (Mutant->Header.SignalState == 1)//如果该互斥对象变得对其他线程可用了
{
if (PreviousState <= 0)//若是正常释放的互斥对象(非意外终止原因)
{
RemoveEntryList(&Mutant->MutantListEntry);//本线程不再拥有该互斥对象
EnableApc = Mutant->ApcDisable;
}
Mutant->OwnerThread = NULL; //不再有拥有者线程了
if (!IsListEmpty(&Mutant->Header.WaitListHead))
KiWaitTest(&Mutant->Header, Increment);//关键。尝试唤醒其他线程
}
if (Wait == FALSE)
KiReleaseDispatcherLock(OldIrql);
else
{
CurrentThread->WaitNext = TRUE;
CurrentThread->WaitIrql = OldIrql;
}
if (EnableApc) KeLeaveCriticalRegion();
return PreviousState;
}
互斥对象的原理就这样,除了普通的互斥体,内核实际上还提供了一种快速互斥体,那张互斥体不再可重入。
互斥对象之所以设计成可重入的,可嵌套的,是有其原因的,有其好处的。比如如果不能重入,那么在内层的WaiFor操作就会一直阻塞,等待外层的WaitFor操作释放互斥对象,这样就会死锁。
事件对象的原理:
事件分为两种事件:
1、 自动复位型,又叫SynchronizationEvent
2、 手动复位型事件,又叫NotificationEvent
前者类型由于事件有信号号,马上又自动复位,所以一次只能唤醒一个线程,后者类型则类似广播通知,一次可以唤醒等待该事件的所有线程。
LONG
KeSetEvent(IN PKEVENT Event,
IN KPRIORITY Increment,
IN BOOLEAN Wait)
{
KIRQL OldIrql;
LONG PreviousState;
PKTHREAD Thread;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
if ((Event->Header.Type == EventNotificationObject) && (Event->Header.SignalState == 1) &&
!(Wait))
{
return TRUE;
}
OldIrql = KiAcquireDispatcherLock();
PreviousState = Event->Header.SignalState;
Event->Header.SignalState = 1;//置为有信号状态
if (!(PreviousState) && !(IsListEmpty(&Event->Header.WaitListHead)))//如果变得有信号
{
if (Event->Header.Type == EventNotificationObject) //唤醒所有正在等待的线程
KxUnwaitThread(&Event->Header, Increment);
Else //唤醒队列中的第一个等待类型为WaitAny的线程
KxUnwaitThreadForEvent(Event, Increment);
}
if (!Wait)
KiReleaseDispatcherLock(OldIrql);
else
{
Thread = KeGetCurrentThread();
Thread->WaitNext = TRUE;
Thread->WaitIrql = OldIrql;
}
return PreviousState;
}
手动复位型事件事件需要手动调用下面的函数复位事件。
LONG KeResetEvent(IN PKEVENT Event)
{
KIRQL OldIrql;
LONG PreviousState;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
OldIrql = KiAcquireDispatcherLock();
PreviousState = Event->Header.SignalState;
Event->Header.SignalState = 0;//复位成无信号状态
KiReleaseDispatcherLock(OldIrql);
return PreviousState;
}
互斥、事件、信号量的原理都这么简单。在此不多说了。
自旋锁的原理:
#define KeAcquireSpinLock(SpinLock,OldIrql)
*(OldIrql) = KeAcquireSpinLockRaiseToDpc(SpinLock)
ReactOS的实现原理与Windows的有出入,在此引用另外一位作者的分析成果:
(来源地址:http://bbs.pediy.com/showthread.php?t=74502)
--------------------------------------------------------------------------------------------
自旋锁的结构:
KSPIN_LOCK SpinLock;
KSPIN_LOCK实际是一个操作系统相关的无符号整数,32位系统上是32位的unsigned long,64位系统则定义为unsigned __int64。
在初始化时,其值被设置为0,为空闲状态。
参见WRK:
FORCEINLINE
VOID KeInitializeSpinLock ( __out PKSPIN_LOCK SpinLock )
{
*SpinLock = 0;
}
关于自旋锁的两个基本操作:获取和释放
VOID
KeAcquireSpinLock(
IN PKSPIN_LOCK SpinLock,
OUT PKIRQL OldIrql
);
VOID
KeReleaseSpinLock(
IN PKSPIN_LOCK SpinLock,
IN KIRQL NewIrql
);
获取时做了哪些工作呢?
Ntddk.h中是这样定义的:
#define KeAcquireSpinLock(SpinLock, OldIrql) *(OldIrql) = KeAcquireSpinLockRaiseToDpc(SpinLock)
很明显,核心的操作对象是SpinLock,同时也与IRQL有关。
再翻翻WRK,找到KeAcquireSpinLockRaiseToDpc的定义:
__forceinline
KIRQL
KeAcquireSpinLockRaiseToDpc (__inout PKSPIN_LOCK SpinLock)
{
KIRQL OldIrql;
OldIrql = KfRaiseIrql(DISPATCH_LEVEL); // 先将irql提升到DISPATCH_LEVEL
KxAcquireSpinLock(SpinLock);//正题
return OldIrql;
}
首先会提升IRQL到DISPATCH_LEVEL,然后调用KxAcquireSpinLock()。(若当前IRQL就是DISPATCH_LEVEL,那么就调用KeAcquireSpinLockAtDpcLevel,省去提升IRQL一步)。因为线程调度也是发生在DISPATCH_LEVEL,所以提升IRQL之后当前处理器上就不会发生线程切换。单处理器时,当前只能有一个线程被执行,而这个线程提升IRQL至DISPATCH_LEVEL之后又不会因为调度被切换出去,自然也可以实现我们想要的互斥“效果”,其实只操作IRQL即可,无需SpinLock。实际上单核系统的内核文件ntosknl.exe中导出的KxAcquireSpinLock函数都只有一句话,就是return。什么都不用做。
而多处理器呢?提升IRQL只会影响到当前处理器,保证当前处理器的当前线程不被切换,那还得考虑其它处理器啊,继续看 KxAcquireSpinLock()函数吧
__forceinline
VOID KxAcquireSpinLock (__inout PKSPIN_LOCK SpinLock)
{
if (InterlockedBitTestAndSet((DWORD *)SpinLock, 0))/表示if自旋锁忙碌,就调用下面的函数等待变成空闲
KxWaitForSpinLockAndAcquire(SpinLock); //只有声明没有定义的函数,应该是做了测试,等待的工作
}
InterlockedBitTestAndSet()函数如下,返回值表示自旋锁是否忙碌(即已被占用)
BOOLEAN InterlockedBitTestAndSet (IN LONG *Base, IN LONG Bit)
{
__asm {
mov eax, Bit
mov ecx, Base
lock bts [ecx], eax //关键检测最低位
setc al //关键,若*SpinLock最低位为1,那么表示自旋锁被占用,函数将返回1
};
}
关键就在bts指令,是一个进行位测试并置位的指令。这里在进行关键的操作时有lock前缀,保证了多处理器安全。InterLockedXXX函数都有这个特点。显然,KxAcquireSpinLock()函数先测试锁的状态。若锁空闲,则*SpinLock为0,那么InterlockedBitTestAndSet()将返回0,并使*SpinLock置位,不再为0。这样KxAcquireSpinLock()就成功得到了锁,并设置锁为占用状态(*SpinLock不为0),函数返回。若锁已被占用呢?InterlockedBitTestAndSet()将返回1,此时将调用KxWaitForSpinLockAndAcquire()等待并获取这个锁。这表明,SPIN_LOCK为0则锁空闲,非0则已被占有。
由于WRK中仅有KxWaitForSpinLockAndAcquire()的声明而无定义,我们只能从名字猜测其做了什么。在WRK中看到了这两个函数:
__forceinline
BOOLEAN KxTryToAcquireSpinLock (__inout PKSPIN_LOCK SpinLock )
{
if (*(volatile DWORD*)SpinLock == 0) //if 锁空闲,最低位置1,标记为占用状态
return !InterlockedBitTestAndSet((DWORD*)SpinLock, 0);//返回获取成功
else
return FALSE; //返回获取失败
}
从名字看应该是试图获取自旋锁,先判断锁是否被占有。若空闲,则设置其为占用状态,这就成功地抢占了。若已被占用,则获取自旋锁失败
下面这个函数则是仅测试自旋锁的状态:
__forceinline
BOOLEAN KeTestSpinLock (__in PKSPIN_LOCK SpinLock) //测试锁是否处于空闲状态
{
KeMemoryBarrierWithoutFence();//
if (*SpinLock != 0)
return FALSE;
else
return TRUE;
}
好,看了获取部分,再看看释放锁的时候做了什么。
__forceinline
VOID
KeReleaseSpinLock (
__inout PKSPIN_LOCK SpinLock,
__in KIRQL OldIrql
)
{
KxReleaseSpinLock(SpinLock);//先释放锁
KeLowerIrql(OldIrql);//恢复至原IRQL
return;
}
继续看KxReleaseSpinLock()
__forceinline
VOID KxReleaseSpinLock (__inout PKSPIN_LOCK SpinLock)
{
InterlockedAnd64((LONG64 *)SpinLock, 0);//释放时进行与操作设置其为0
}
实际的系统是:
void __fastcall KiAcquireSpinLock(int _ECX)
{
while ( 1 )
{
__asm { lock bts dword ptr [ecx], 0 } //不停检测最低位是否为0
if ( !_CF )//if 为0,表示变成空闲了,就退出循环
break;
while ( *(_DWORD *)_ECX & 1 ) //while循环一直等到变成空闲
{}
}
}
--------------------------------------------------------------------------------------------
总结下,自旋锁的实现:
1、在单核系统中就是将cpu的irql提到DISPATCH_LEVEL,防止线程切换,从而达到同步的目的
2、在多核系统中,先将当前cpu提到DISPATCH_LEVEL,防止当前cpu上发生线程切换。然后不停的使用lock前缀指令,锁定内存总线,忙式测试自旋锁的最低位是否标记处于了空闲状态。
由此可以看出,在多cpu系统中,自旋锁可以用来实现多个cpu之间的同步。原理就是lock总线。
自旋锁的用途之一:内联钩子的多cpu同步问题。
先讨论单cpu上的同步问题:
内联钩子一般修改了函数的前5B为 jmp xxxxxxxx 指令,共5个字节。由于内联函数在hook的过程中,其他线程可能正在执行这个函数,因此必须确保同步,否则,极易引发蓝屏崩溃。为什么呢?巧就巧在hook修改的是5个字节,若是<=4B的话,由于32位系统是以4B为单位访问内存单元的,4B区域的修改必定能用一条简单的MOV类似指令完成修改,就不会存在同步问题。而现在要改的是5个字节,若不加同步措施,假设下面一种情形:正在进行hook的线程刚好使用了一条mov指令改完了前4B,正准备改下一个字节的时候结果发生了线程切换,另外一个线程来到这个函数入口时,因为前4B已经被改成jmp xxxxxxxx指令的前4B内容,而第5个字节尚未改变,于是,此时的前5B就是 “Jmp xxxxxx xx” 形式,这导致错误的构造了jmp指令的操作码部分,将跳转到一个未知地方,引发不可预料的错误,常见现象就是蓝屏。前面5B,实际上是指前面8B的修改操作必须作为原子过程完成。怎么让他原子呢?
第一步:修改前,将当前cpu的irql提升至DISPATCH_LEVEL,防止当前cpu上发生线程切换。
然后讨论多cpu同步问题:
在多cpu机器上同步问题更复杂,除了需要保证同一个cpu上的线程同步,还需要保证各cpu之间同步
第二步:给其他各cpu发送一个DPC函数过去,DPC函数本身运行在DISPATCH_LEVEL即,所以可以让其他cpu始终运行在我们指定的DPC函数内,防止其他cpu上发生线程切换。
第三步:当确保了所有cpu都不会发生线程切换后,修改前5B进行hook,完事后,通知其他cpu的DPC函数退出。
在上面的过程中,当前cpu与其他cpu之间之间,必须使用自选锁方式,读取各个DPC函数的运行状态和hook状态。(详见《Rootkit—Windows内核安全防护》)
附:
Lock指令前缀的用途:一句话:用来将一条指令变成“原子”的
假设内存单元0x11110000处存放着一个公共的计时器变量,且初值为100
那么考虑两个cpu同时执行”inc [0x11110000]” 这条指令的情况。本来想要的目标是两个cpu一前一后执行这条指令,最终结果变成102的。但是,如果不加lock前缀,直接执行的话,由于这条指令内部分为取指、分析、取数、运算、回写这几个阶段,假设第一个cpu刚好取出了数据100,然后释放了内存总线(同时另一个cpu在此时将获得内存总线,然后他也同样去取数据,取出的也是100),然后计算得出101,然后占用内存总线,回写数据到0x11110000处,变成101,完事后,另外一个cpu也将进行同样的计算,得出101,回写到0x11110000处。最终,0x11110000处的值是101而不是102。这就是一种典型的多cpu同步引起的问题,其根源在于指令内部是分阶段占用内存总线的,而lock前缀指示cpu:在整个指令的执行过程中全程独占总线,直到指令执行完毕后才释放。