转发: https://blog.csdn.net/u011454830/article/details/49704839
线程调度
计算机通常只有一个cpu,在任意时刻只能执行一条机器指令,每个线程只有获得cpu的使用权才能执行指令.所谓多线程的并发运行,其实是从宏观上看,各个线程轮流获取cpu的使用权,分别执行各自的任务.在运行池中,会有多个处于就绪状态的线程在等待cpu, JAVA虚拟机的一项任务就是负责线程的调度.线程调度是指按照特定机制为多个线程分配CPU的使用.
调度方式
1. 分时调度模式: 是指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的cpu的时间片.
2. 抢占式调度模式: JAVA虚拟机采用抢占式调度模式,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那就随机选择一个线程,使其占用CPU.处于运行状态的线程会一直运行,直至它不得不放弃CPU.
如何调度
NTSTATUS NtYieldExecution (VOID)
{
KIRQL OldIrql;
PKTHREAD NewThread;
PRKPRCB Prcb;
NTSTATUS Status;
PKTHREAD Thread;
if (KiGetCurrentReadySummary() ==0) { //如果当前CPU没有就绪线程
return STATUS_NO_YIELD_PERFORMED;
} else {
Status =STATUS_NO_YIELD_PERFORMED;
Thread =KeGetCurrentThread(); //获取当前线程
OldIrql =KeRaiseIrqlToSynchLevel();//将运行级别提高到DISPATCH_LEVEL级别
Prcb = KeGetCurrentPrcb();//获取重要的PRCB机构
if (Prcb->ReadySummary !=0) { //如果有就绪线程
KiAcquireThreadLock(Thread);//锁定当前线程
KiAcquirePrcbLock(Prcb);//锁定当前PRCB
if (Prcb->NextThread== NULL) {
Prcb->NextThread =KiSelectReadyThread(1, Prcb);
} //如果没有线程准备好运行的话就向前找下一个可以运行的线程
if ((NewThread =Prcb->NextThread) != NULL) {
Thread->Quantum =Thread->QuantumReset; //前面讲了很久的时限了,看到源代码的时候还是惊喜了一些
Thread->Priority =KiComputeNewPriority(Thread, 1); //这里很有趣哦,下面会专门讲这个函数的。
KiReleaseThreadLock(Thread);//释放线程锁
KiSetContextSwapBusy(Thread);//将SwapBusy设置成TURE
//将新线程设置成正在运行
Prcb->NextThread =NULL;
Prcb->CurrentThread = NewThread;
NewThread->State =Running;
Thread->WaitReason= WrYieldExecution;
KxQueueReadyThread(Thread, Prcb);//旧线程进入本CPU的就绪队列
Thread->WaitIrql =APC_LEVEL;
ASSERT(OldIrql <=DISPATCH_LEVEL);
KiSwapContext(Thread,NewThread);//上下文切换
Status =STATUS_SUCCESS;
} else {
//如果没有可运行的线程,当前线程继续执行
KiReleasePrcbLock(Prcb);
KiReleaseThreadLock(Thread);
}
}
KeLowerIrql(OldIrql);//恢复CPU的运行级别
return Status;
}
}
这段代码不长,就干了一件事,让当前线程主动暂时放弃运行,但是又不进入睡眠,说白了就是为别的线程让路.看完上面的代码和注释后你会发现几个很重要的函数调用,这几个函数就是我们接下来要去研究的内容了。在WRK-v1.2\base\ntos\ke\Ki.h文件里有KiSelectReadyThread的定义:
FORCEINLINE
PKTHREAD
KiSelectReadyThread (
IN KPRIORITY LowPriority,
IN PKPRCB Prcb
)
{
ULONG HighPriority;
PRLIST_ENTRY ListEntry;
ULONG PrioritySet;
PKTHREAD Thread;
PrioritySet = Prcb->ReadySummary >> LowPriority;//若低于给定的优先级就不考虑了,一定要大于给定的最低优先级
Thread = NULL;
if (PrioritySet != 0) {
KeFindFirstSetLeftMember(PrioritySet, &HighPriority);//找出符合条件的优先级最高的线程
ASSERT((PrioritySet & PRIORITY_MASK(HighPriority)) != 0);
HighPriority += LowPriority;
//保证最高优先级列表里不为空
ASSERT(IsListEmpty(&Prcb->DispatcherReadyListHead[HighPriority])== FALSE);
ListEntry = Prcb->DispatcherReadyListHead[HighPriority].Flink;
Thread = CONTAINING_RECORD(ListEntry, KTHREAD, WaitListEntry);//获取最高优先级列表里面的第一个线程
//保证这个线程是合法的
ASSERT((KPRIORITY)HighPriority == Thread->Priority);
ASSERT((Thread->Affinity & AFFINITY_MASK(Prcb->Number)) != 0);
ASSERT(Thread->NextProcessor == Prcb->Number);
/*从队列中摘下该线程
if (RemoveEntryList(&Thread->WaitListEntry) != FALSE) {
//如果队列已空
Prcb->ReadySummary ^=PRIORITY_MASK(HighPriority);//修改就绪队列位图
}
}
ASSERT((Thread == NULL) || (Thread->BasePriority == 0) ||(Thread->Priority != 0));
return Thread;//如果找到合适的线程的话就返回其地址
}
看了代码,你会发现代码量不多吧,但是各种复杂啊,将很多信息放到我们之前的各种复杂结构里面去了,所以在这个重要的代码里面你会发现如此轻易的就进行了,多简洁啊,这时候你随便拿个原理书看就知道书上的原理到底是如何实现的了,革命尚未成功,我们还是要继续努力的,前面我们说过了一个很重要的东西就是分发器,我解释了一些我们为什么要设置这个模块,在这里的代码里你就发现了,ReadSummary是什么啊?其实它就是分发器里面为就绪队列维护的一个摘要了,PRCB里面也有它的访问字段,我们这里就是这样访问的,什么是严格按优先级调度?我要好好的解释一下了:ReadSummary是个位图,只要那个优先级的就绪队列中有线程在等待,这个优先级的标志位就是1.从高位到地位扫描这个位图,遇到的第一个为1的标志位就代表着当时有就绪线程存在最高优先级。从这个队列中取出排在最前面的线程,这就是接下来要运行的线程了。由此可见,Windows的调度原则是严格按照优先级调度的,相同的优先级是按在队列中的先后顺序的。这个部分的内容讲的很清楚了,下面我们还是要继续回到NtYieldExcution()的代码中,接下来就是看看KiComputrNewPriorty(),在WRK-v1.2\base\ntos\ke\Ki.h里代码如下:
FORCEINLINE
SCHAR
KiComputeNewPriority (
IN PKTHREAD Thread,
IN SCHAR Adjustment
)
{
SCHAR Priority;
//合理性检查
ASSERT((Thread->PriorityDecrement >= 0) &&(Thread->PriorityDecrement <= Thread->Priority));
ASSERT((Thread->Priority < LOW_REALTIME_PRIORITY) ? TRUE :(Thread->PriorityDecrement == 0));
Priority = Thread->Priority;//获取当前进程的优先级
if (Priority < LOW_REALTIME_PRIORITY) {//如果优先级低于低实时优先级的话
Priority = Priority -Thread->PriorityDecrement - Adjustment;//降低优先级分为两部分,函数的参数提供了一部分,另外一个就是当前线程的衰减率了,这个主要是因为内核在唤醒一个线程的时候往往会调高其临时优先级,所以在这里需要降下来
if (Priority <Thread->BasePriority) {
Priority =Thread->BasePriority;//这一步是为了不越界
}
Thread->PriorityDecrement = 0;//这里就将其重置为0了,对应上面的注释应该能够懂了
}
ASSERT((Thread->BasePriority == 0) || (Priority != 0));//最后再进行一些合理性检查
return Priority;//返回一个新的优先级
}
这函数干嘛的?我前面介绍过QuantumReset的,这个是随优先级而变的,要是一个线程的优先级变了,我们就得修改这部分的值,而上述代码就是去确定优先级的,以便对线程进行微调,如果优先级要改,那么线程就需要挂到其他的队列上去,而QuantumReset也是需要变的。这里需要注意一下,如果一个线程不是实时的话,它的优先级会逐步的降低的,代码里的注释我也写了。判断完优先级之后, KxQueueReadyThread()函数出现了,看名字就看得出大概意思了,将某个线程挂到就绪队列里面去,还是Ki.h里面,代码如下:
FORCEINLINE
VOID
KxQueueReadyThread (
IN PKTHREAD Thread,
IN PKPRCB Prcb
)
{
BOOLEAN Preempted;
KPRIORITY Priority;
//合理性检查
ASSERT(Prcb == KeGetCurrentPrcb());
ASSERT(Thread->State == Running);
ASSERT(Thread->NextProcessor == Prcb->Number);
#if !defined(NT_UP)//判断该线程是否允许在本CPU上运行
if ((Thread->Affinity & Prcb->SetMember) != 0) {
#endif
Thread->State = Ready;//将线程的状态设为就绪
Preempted = Thread->Preempted;//看一下是否有被抢占
Thread->Preempted = FALSE;
Thread->WaitTime = KiQueryLowTickCount();
Priority = Thread->Priority;
ASSERT((Priority >= 0) && (Priority <= HIGH_PRIORITY));//检查
if (Preempted != FALSE) {
//被抢占的情况下
InsertHeadList(&Prcb->DispatcherReadyListHead[Priority],
&Thread->WaitListEntry);//补偿一下,插到头部
} else {
InsertTailList(&Prcb->DispatcherReadyListHead[Priority],
&Thread->WaitListEntry);//相反插到尾部
}
Prcb->ReadySummary |= PRIORITY_MASK(Priority);//更新一下ReadySummary
ASSERT(Priority == Thread->Priority);
KiReleasePrcbLock(Prcb);//释放PRCB锁,之前被锁定了的
#if !defined(NT_UP)
} else {
Thread->State = DeferredReady;
Thread->DeferredProcessor = Prcb->Number;
//释放锁,放到准备好就绪队列
KiReleasePrcbLock(Prcb);
KiDeferredReadyThread(Thread);
}
#endif
return;
}
写到这里,你已经差不多能理解书上和资料上那些原理了,因为我已经把最核心的东西都解释出来了,而 KiSwapContext(Thread, NewThread);我们在很早以前就解释,上下文切换,还记得吗?关于线程调度到这里我就差不多要结束了,最后我从宏观角度来讲一下线程调度的问题。
1.Windows内核严格按照优先级调度。
2.同一优先级按照顺序执行。
3.每个正在运行的线程都有一个时间片Quantum,没有被剥夺的情况下,就是自己一直执行下去直到时间用光,如果不是实时的线程的话,该线程的优先级会逐步的降低,但是不会低于其基本优先级。
4.如果线程在运行过程中被抢占了的话,将其挂到相应优先级就绪队列的头部,否则挂到尾部。
5.线程运行中被抢占了的话,这种情况下,线程切换不会马上切换,而要等待内核的级别降到DISPATCH_LEVEL级以下,这时候才能进行线程切换。