一、线程优先级
线程32个优先级,16~31是实时类别 1~15是动态类别 0是系统类别
KPROCESS.BasePriority是线程创建之后的KTHREAD.BasePriority值(线程的基本、静态优先级),这个值基本不变,KTHREAD.Priority是动态优先级。
KiComputeNewPriority 用来计算非实时优先级线程的优先级(优先级值要小于16),而且这个优先级不能超过基本优先级的值。
R3可以调节进程的基本优先级(如任务管理器),但只能调节 EPROCESS.PriorityClass ,分为未知,Idle,普通,高,实时,低于普通高于普通这几种,分别对应的基本优先级是8 4 8 13 24 6 10。而NtSetInFormationProcess可以对优先级进行微调。
举例线程的优先级在以下情况可能被提升
1. IoCompleteRequest 可以指定等待IO完成的线程的优先级提升量
2. 等待事件和信号量的线程
3. 前台线程从等待中唤醒
4. 平衡及管理器中对于等待4s依然没有执行的线程,将优先级提到15(避免优先级反转)
5. 窗口线程因为窗口的活动被唤醒,优先级可提升
二、线程的状态转移
typedef enum _KTHREAD_STATE {
Initialized, //线程初始化完毕 但还没有挂入进程链表 更没有开始执行
Ready, //已经准备就绪,等待调度,挂入KPCR的链表中
Running, // 正在运行
Standby, // 被选中作为下一个运行的线程,KPRCB. NextThread
Terminated, //线程已经结束
Waiting, // 正在等待某个事件
Transition, //已经做好准备,但内核栈被换出,换回来之后就可以准备就绪
DeferredReady, //延迟就绪,尚未指定cpu,多核中才有
GateWait //等待门对象(这是啥玩意?)
} KTHREAD_STATE;
向就绪的转化
初始化,转移中,等待 都可以转化成延迟的就绪,再转化为就绪(可以被调度,但进程内存可能还没被换进来)
KiReadyThread做了这件事,改变线程状态为就绪,进程不再在内存中线程的内核栈在,就设置一个事件通知KiSwappingThread将进程换入操作(进程的SwapListEntry被挂入KiProcessInSwapListHead).如果内核栈不在内存中,线程会被设置为转移中,同样通知KiSwappingThread(线程的SwapListEntry被挂入KiStackInSwapListHead),如果以上条件都已经满足,那就KiInserDeferredReadList 插入延迟就绪链表(线程被改成延迟就去状态)
这个函数在以下情形下被调用
1. 等待一个对象结束了
2. QUEue挂入新成员 线程结束等待
3. 线程挂靠到一个进城之后
4. KiInSwapProcess 换入进程时 进程中的线程设置为就绪
5. KiOutSwapProcess 检测到待换出进程 就绪链表非空 将线程设置为就绪 尽快执行
6. 线程初始化之后进行激活
接下来看延迟就绪的线程如果变成就绪
KPRCB中有两个成员
LIST_ENTRY DispatcherReadyListHead[MAXIMUM_PRIORITY];这个是一个链表数组,不同优先级的就绪链表
SINGLE_LIST_ENTRY DeferredReadyListHead;
延迟就绪的线程Thread->SwapListEntry 被挂入到DeferredReadyListHead
KiProcessDeferredReadyList用来遍历这个单向链表并调用KiDeferredReadyThread.
KiProcessDeferredReadyList调用的时机:
1. 硬件中断之后 将调用KiRetireDpcList派发DPC 之后会调用这个函数
2. KiExitDispatcher
3. KiSwapThread 线程切换的时候
KiDeferredReadyThread调整线程优先级,选择适合运行程序的CPU。
1.如果有空闲CPU,线程状态就被设置成备用(KiIdleSummmary 中用相应的位来表示现在哪个CPU正在运行空闲线程)
否则 2.选择一个处理器,尝试抢占这个线程,如果优先级更高,被抢占的线程回到延迟就绪状态(3.如果没有选出CPU的下一个执行线程,即TargetPrcb->NextThread ==Null
,那么被抢占线程可能直接变成下一个要运行的线程),新线程运行
4.如果新线程抢占失败,就被设置为就绪状态,放入DispatcherReadyListHead中
上述所提到的被抢占线程是指执行的下一个线程
就绪向运行的转化
也就是DispatcherReadyListHead[MAXIMUM_PRIORITY] 中的线程如果运行起来的问题,KiSelectReadyThread找一个优先级上的线程,KiFindReadyThread在所有优先级的线程中由高到低寻找一个合适的线程。
线程调度,同步,DPC等等机制都需要一个调度点。
线程转化为运行状态的调度点:
1. 空闲线程KiIdleSchedulejian检测到有备用线程存在,使备用线程运行。 否则,调用KiSelectReadyThread寻找,再不成调用KiFindReadyThread在所有处理器的链表中寻找。找到线程后即进入运行状态
2. KiSwapThread跟1的情形类似
3. 线程时限用完,KiQuantumEnd 跟当前线程重设优先级并且在高级别优先级的链表中找到新线程去运行
4. 线程自己主动放弃执行
5. 优先级、CPU亲和性、时限被改变时,线程状态变化
存在等待的线程转移
几个导致线程等待的基本函数:
1. KeDelayExcutionThread 将KTHREAD.Timer 放入定时器表中,把线程状态设置为等待,调用KiSwapThread
2. KeWaitForSingleObject /Multiple 等待的条件被满足以后(事件同步的细节暂略),KeSetEvent 以及互斥、信号量的相似函数,调用KiWaitTest测试线程是否满足等待结束的条件,满足后调用KiUnwaitThread间接调用KiReadyThread将线程恢复
3. KeRemoveQueue (据说是为线程池提供支持,暂略)
等待的线程会被挂入PRCB的WaitListHead
三、线程的时限管理
运行起来的线程,既没有被抢占也没有进入等待,那么它将运行一段固定的时间,一个时限
KTREAD.Quantum 剩余的时间,KTREAD.QuantumReset 运行一次的完整时间
每次时钟中断时,都会减少当前线程的时限,如果时限已经用尽,将请求一个软中断进行线程调度
if ((Thread != Prcb->IdleThread) &&
((Thread->Quantum -= CLOCK_QUANTUM_DECREMENT) <= 0)) {
Prcb->QuantumEnd = TRUE;
KiRequestSoftwareInterrupt(DISPATCH_LEVEL);
}
CLOCK_QUANTUM_DECREMENT 是每次时钟中断减少的时限值
WAIT_QUANTUM_DECREMENT 是线程从一次等待中恢复减少的时限值
四、线程环境切换 总结
之前说过,线程的调度切换有两种情况:一是由于等待某个对象,Sleep 或者显式的调用API声明要主动放弃自己的执行权;二是时限用尽或者被其他优先级较高的线程抢占(这个抢占,抢占的是StandBy线程,那么切换到抢占线程这个动作发生在什么时候?事实上HalpClockInterrupt是时钟处理函数,会依次调用KeUpdateSystemTime,HalRequestSoftwareInterrupt,所以有机会进行调度)
第一种情况
线程自己会调用KiSwapThread。
这个函数会修改线程的状态,然后调用KiSwapContext完成切换
所有调用KiSwapThread的情况:
1. KeDelayxx和KeWaitForxx
2. 线程终止
3. 等待队列中的项
4. Attach到其它进程,但进程还没有被换入内存,只能放弃执行
第二种情况
KiDeferredReadyThread 将延迟就绪的线程转化为就绪,如果优先级高于当前线程,就会发生抢占,这个线程变成备用线程并且在下一次时钟中断时进行切换
时间中断函数调用DISPATCH_LEVEL的中断(在哪里调用?)
KiDispatchInterrupt会检查时限是否用尽,用尽就KiQuantumEnd切换线程
检查KPRCB中NextThread是否有值,如果有也SwapContext进行线程切换,即实现抢占(在x64的KeUpdateRunTime中只看到时限结束后才会进行软中断,那什么时候进行抢占?有派发DPC的操作,想走到DISPATCH_LEVEL的中断处理还是很容易的,也就是这种抢占随时会发生?)
=============
SwapContext是进行线程切换的函数(KiSwapContext已经将几个寄存器入栈,下面用到的都是各个线程相同的寄存器,切换之后不会有问题)
除去处理一些同步标志位和计数外,这个函数的主要流程就是
异常链表头入栈,CR0,保存栈指针在KTHREAD.KernelStack
然后栈指针切换到新线程,切换CR0 CR3 TEB指向
这里TSS的ESP0 设置为内核栈指针 IO位图也被重设
检测是否是DPC中,如果是直接BSOD
检测是否有APC请求,如果有就给一个APC中断