ThreadSwitch源码分析
线程结构体(ThreadSwitch源码仿照EHREAD)
typedef struct {
char *name;//线程名 相当于线程TID
int Flags;//线程状态
int SleepMillisecondDot;//休眠时间
void *InitialStack;//线程堆栈起始位置
void *StackLimit;//线程堆栈界限
void *KernelStack;//线程堆栈当前位置,也就是ESP
void *lpParameter;//线程函数的参数
void (*func)(void *lpParameter);//线程函数
}GMThread_t;
调度链表(ThreadSwitch)
//线程结构体数组 extern GMThread_t GMThreadList[MAXGMTHREAD];
所谓创建线程,就是创建一个结构体,并且挂到这个数组中,此时的线程状态为:创建
这个ThreadSwitch进行简化,在Windows下正在运行的线程在KPCR里,等待的线程
在等待链表表里,就绪线程在就绪链表里。
ThreadSwitch分析发现只有一个线程结构体数组,其中的Flags线程状态便是代表了
线程不同的状态。
在ThreadSwitch线程结构体中其余线程是从下标1开始的,而下标0是当前线程,
main线程运行在0下标。
经典线程堆栈切换
__asm{
push ebp
push ebp,esp
push edi
push esi
push ebx
push ecx
push edx
push eax
mov esi,当前线程结构体指针
mov edi,你要切换线程的结构体指针
mov [esi+KernelStack当前线程结构体中esp的位置属性进行保存],esp
//经典堆栈切换,另一个线程复活
mov esp,[edi+KernelStack将要切换的线程esp拿出来赋值给esp]
pop eax
pop edx
pop ecx
pop ebx
pop esi
pop edi
pop ebp
ret
}
模型线程切换总结
1.线程切换不是被动切换的,而是主动让出CPU。
2.线程切换并没有使用TSS来保存寄存器,而是使用堆栈。
3.线程的切换本质上就是堆栈的切换。
4.可是使用任意的堆模拟栈的机制,将寄存器保存后进行堆栈的切换即可实现线程的变换。
主动切换
线程切换
在ThreadSwitch项目中有一个重要的函数,
SwitchContext调用这个函数后会触发经典的堆栈切换,
导致线程的切换,在Windows中也有类似的函数:
KiSwapContext。
主动线程切换总结
1.Windows中绝大部分API都调用了SwapContext函数,
也就是说,当线程只要调用了API,也就是导致了线程切换。
2.线程切换时会比较是否属于同一个进程,如果不是,切换CR3
,CR3换了,进程也就切换了。
3.调用过程:KiSwapContext的父函数,KiSwapThread
时钟中断切换
SwapContext
绝大多数的内核函数都会使用SwapContext来实现线程的切换。
如何中断一个正在执行的程序
1.异常 比如缺页,或者INT N指令。
2.中断 比如时钟中断。
系统时钟
(IDT表)中断号 |IRQ |说明
0x30 IRQ0 时钟中断
Windows系列操作系统:10-20毫秒
如要获取当前时钟间隔值,可以使用WIN32 API:
GetSystemTimeAdjustment
时钟中断要执行流程
时钟中断执行流程:
KiStartUnexpectedRange
KiEndUnexpectedRange
KiUnexpectedInterruptTail
HalBeginSystemInterrupt
HalEndSystemInterrupt
KiDispatchInterrupt
SwapContext
时钟中断切换总结
线程切换的几种情况:
1.主动调用API函数
2.时钟中断
3.异常处理
如果一个线程不调用API,在代码中屏蔽中断(CLI指令),并且
不会出现异常,那么当前线程将永久占有CPU,单核占有率100%,
2核就是50%。
时间片管理
时钟与线程切换
时钟中断会导致线程切换,但并不是说
只要有时钟中断就一定会切换线程,时钟中断时,两种情况会导致线程切换。
1.当前的线程CPU时间片到期
2.有备用线程(KPCR.PrcbData.NextThread)
有备用线程就算当前线程的时间片没有到期仍然会切换线程,
备用线程优先级高于时间片。
CPU时间片
1.当一个新的线程开始执行时,初始化程序会在_KTHREAD.Quantum
赋初始值,该值的大小由_KPROCESS.ThreadQuantum决定。
2.每次时钟中断会调用KeUpdateRunTime函数,该函数每次将当前线程
Quantum减少3个单位,如果减到0,则将KPCR,PrcbData.QuantumEnd的值设置为0
3.KiDispatchInterrupt判断时间片到期:
调用KiQuantumEnd(重新设置时间片,找到要运行的线程)。
线程切换的3种情况
1.当前线程主动调用API:
API函数->KiSwapThread->KiSwapContext->SwapContext
2.当前线程时间片到期:
KiDispatchInterrupt->KiQuantumEnd->SwapContext
3备用线程
KiDispatchInterrupt->SwapContext
线程与TSS
SwapContext
SwapContext这个函数是Windows线程切换的核心,
无论是主动切换还是系统时钟导致的线程切换,最终都会调用这个函数。
该函数除了切换堆栈以外,还做了很多其他的事情。
调用API进0环
普通调用:通过TSS.ESP0得到0环堆栈。
快速调用:从MSR得到一个临时0环栈,代码执行后仍然通过
TSS.ESP0得到当前线程0环堆栈。
TSS
Intel设计TSSS的目的是为了任务切换(线程切换),但Windows和
Linux并没有使用。而是采用堆栈保存线程的各种寄存器。
线程与FS
线程切换与FS寄存器之间的关系
FS:[0]寄存器在3环时指向TEB,进入0环后FS:[0]指向KPCR,
系统中同时存在多个线程,这就意味着FS:[0]在3环时指向的TEB要有多个(每个线程一份)。
但是在实现中3环查看不同线程的FS寄存器时,FS的段选择子都是相同的,具体是操作系统
将新的TEB地址从KPCR里读出来之后,写到当前GDT表段描述符的Base基址里面,因此
在3环中FS的段选择子是不同修改的,改变的FS对应的基址,变成的了新的线程的TEB的基址。
线程优先级
三种情况导致线程切换
(1)当前线程主动调用API
API函数:KiSwapThread、KiSwapContext、SwapContext
(2)当前线程时间片到期
KiDispatchInterrupt: KiQuantumEnd、SwapContext
(3)有备用线程(KPCR.PrcbData.NextTread)
KiDispatchInterrupt SwapContext
线程调用链表
在xp中调度链表有32个,优先级最高的位于0号链表其次位于1号链表,
以此类推,类似于KiFindReadyThread查找线程级别,是一种比较暴力
的线程优先级查找方式,比如先从31查看是否有线程,在查看30..29..28是否有线程,
如果31有线程则不会再去查找30是否有线程,需要注意的是只有在本次查找中,当
高级别的线程在执行的时候,使用了API或者时间片到期了,低级别的线程仍然有机会运行。
Windows调度链表算法
在xp中调度链表有32个,每次都重都开始查找效率较低,Windows使用DWORD变量
来进行记录,当向调度链表(32个)中挂入或者摘除某个线程时,会判断当前级别的链表
是否为空,为空将DWORD变量对应位置0,否则置1(具有等待调度线程),DWORD正好是32位。
多CPU会随机寻找KiDispatcherReadyListHead指向的数组中的线程,线程可以绑定某个
CPU(使用API:setThreadAffinityMask)。
如果没有就绪线程如何处理
在KPCR中有一个子结构体叫做PrcbData有3个重要成员
+0x004 CurrentThread:Ptr32_KTHREAD 当前线程
+0x008 NextThread:Ptr32_KTHREAD 备用线程
+0x00C ldleThread:Ptr32_KTHREAD 空闲线程
空闲线程:当CPU查找调度链表如果没有其他需要调度的线程,
这就会调度空闲线程,每个KPCR都会指定一个空闲线程。
|