之前我们已经了解过线程的等待链表和调度链表,为了更好的学习Windows的线程切换,我们要先读一份代码
示例代码
ThreadSwitch.h
#pragma once
#include <windows.h>
#include "stdio.h"
//最大支持的线程数
#define MAXGMTHREAD 100
//线程信息的结构
typedef struct
{
char* name; //线程名 相当于线程ID
int Flags; //线程状态
int SleepMillsecondDot; //休眠时间
void* initialStack; //线程堆栈起始位置
void* StackLimit; //线程堆栈界限
void* KernelStack; //线程堆栈当前位置,也就是ESP
void* lpParameter; //线程函数的参数
void(*func)(void* lpParameter); //线程函数
}GMThread_t;
void GMSleep(int MilliSeconds);
int RegisterGMThread(char* name, void(*func)(void*lpParameter), void* lpParameter);
void Scheduling();
ThreadSwitch.cpp
#include "ThreadSwitch.h"
//定义线程栈的大小
#define GMTHREADSTACKSIZE 0x80000
//当前线程的索引
int CurrentThreadIndex = 0;
//线程的列表
GMThread_t GMThreadList[MAXGMTHREAD] = { NULL, 0 };
//线程状态的标志
enum FLAGS
{
GMTHREAD_CREATE = 0x1,
GMTHREAD_READY = 0x2,
GMTHREAD_SLEEP = 0x4,
GMTHREAD_EXIT = 0x8,
};
//启动线程的函数
void GMThreadStartup(GMThread_t* GMThreadp)
{
GMThreadp->func(GMThreadp->lpParameter);
GMThreadp->Flags = GMTHREAD_EXIT;
Scheduling();
return;
}
//空闲线程的函数
void IdleGMThread(void* lpParameter)
{
printf("IdleGMThread---------------\n");
Scheduling();
return;
}
//向栈中压入一个uint值
void PushStack(unsigned int** Stackpp, unsigned int v)
{
*Stackpp -= 1;//esp - 4
**Stackpp = v;//
return;
}
//初始化线程的信息
void initGMThread(GMThread_t* GMThreadp, char* name, void(*func)(void* lpParameter), void* lpParameter)
{
unsigned char* StackPages;
unsigned int* StackDWordParam;
//结构初始化赋值
GMThreadp->Flags = GMTHREAD_CREATE;
GMThreadp->name = name;
GMThreadp->func = func;
GMThreadp->lpParameter = lpParameter;
//申请空间
StackPages = (unsigned char*)VirtualAlloc(NULL, GMTHREADSTACKSIZE, MEM_COMMIT, PAGE_READWRITE);
//初始化
ZeroMemory(StackPages, GMTHREADSTACKSIZE);
//初始化地址地址
GMThreadp->initialStack = StackPages + GMTHREADSTACKSIZE;
//堆栈限制
GMThreadp->StackLimit = StackPages;
//堆栈地址
StackDWordParam = (unsigned int*)GMThreadp->initialStack;
//入栈
PushStack(&StackDWordParam, (unsigned int)GMThreadp); //通过这个指针来找到线程函数,线程参数
PushStack(&StackDWordParam, (unsigned int)0); //平衡堆栈的(不用管)
PushStack(&StackDWordParam, (unsigned int)GMThreadStartup); //线程入口函数 这个函数负责调用线程函数
PushStack(&StackDWordParam, (unsigned int)5); //push ebp
PushStack(&StackDWordParam, (unsigned int)7); //push edi
PushStack(&StackDWordParam, (unsigned int)6); //push esi
PushStack(&StackDWordParam, (unsigned int)3); //push ebx
PushStack(&StackDWordParam, (unsigned int)2); //push ecx
PushStack(&StackDWordParam, (unsigned int)1); //push edx
PushStack(&StackDWordParam, (unsigned int)0); //push eax
//当前线程的栈顶
GMThreadp->KernelStack = StackDWordParam;
//当前线程状态
GMThreadp->Flags = GMTHREAD_READY;
return;
}
//将一个函数注册为单独线程执行
int RegisterGMThread(char* name, void(*func)(void*lpParameter), void* lpParameter)
{
int i;
for (i = 1; GMThreadList[i].name; i++)
{
if (0 == _stricmp(GMThreadList[i].name, name))
{
break;
}
}
initGMThread(&GMThreadList[i], name, func, lpParameter);
return (i & 0x55AA0000);
}
//切换线程 1:当前线程结构体指针 2:要切换的线程结构体指针
__declspec(naked) void SwitchContext(GMThread_t* SrcGMThreadp, GMThread_t* DstGMThreadp)
{
__asm
{
//提升堆栈
push ebp
mov ebp, esp
//保存当前线程寄存器
push edi
push esi
push ebx
push ecx
push edx
push eax
mov esi, SrcGMThreadp
mov edi, DstGMThreadp
mov[esi + GMThread_t.KernelStack], esp
//经典线程切换,另外一个线程复活
mov esp, [edi + GMThread_t.KernelStack]
pop eax
pop edx
pop ecx
pop ebx
pop esi
pop edi
pop ebp
ret //把startup(线程函数入口)弹到eip 执行的就是线程函数了
}
}
//这个函数会让出cpu,从队列里重新选择一个线程执行
void Scheduling()
{
int TickCount = GetTickCount();
GMThread_t* SrcGMThreadp = &GMThreadList[CurrentThreadIndex];
GMThread_t* DstGMThreadp = &GMThreadList[0];
for (int i = 1; GMThreadList[i].name; i++)
{
if (GMThreadList[i].Flags & GMTHREAD_SLEEP)
{
if (TickCount > GMThreadList[i].SleepMillsecondDot)
{
GMThreadList[i].Flags = GMTHREAD_READY;
}
}
if (GMThreadList[i].Flags & GMTHREAD_READY)
{
DstGMThreadp = &GMThreadList[i];
break;
}
}
CurrentThreadIndex = DstGMThreadp - GMThreadList;
SwitchContext(SrcGMThreadp, DstGMThreadp);
return;
}
void GMSleep(int MilliSeconds)
{
GMThread_t* GMThreadp;
GMThreadp = &GMThreadList[CurrentThreadIndex];
if (GMThreadp->Flags != 0)
{
GMThreadp->Flags = GMTHREAD_SLEEP;
GMThreadp->SleepMillsecondDot = GetTickCount() + MilliSeconds;
}
Scheduling();
return;
}
main.cpp
#include "ThreadSwitch.h"
extern int CurrentThreadIndex;
extern GMThread_t GMThreadList[MAXGMTHREAD];
void Thread1(void*)
{
while (1)
{
printf("Thread1\n");
GMSleep(500);
}
}
void Thread2(void*)
{
while (1)
{
printf("Thread2\n");
GMSleep(200);
}
}
void Thread3(void*)
{
while (1)
{
printf("Thread3\n");
GMSleep(10);
}
}
void Thread4(void*)
{
while (1)
{
printf("Thread4\n");
GMSleep(1000);
}
}
int main()
{
RegisterGMThread((char*)"Thread1", Thread1, NULL);
RegisterGMThread((char*)"Thread2", Thread2, NULL);
RegisterGMThread((char*)"Thread3", Thread3, NULL);
RegisterGMThread((char*)"Thread4", Thread4, NULL);
for (;;)
{
Sleep(20);
Scheduling();
}
return 0;
}
关键结构体
//线程信息的结构
typedef struct
{
char* name; //线程名 相当于线程ID
int Flags; //线程状态
int SleepMillsecondDot; //休眠时间
void* initialStack; //线程堆栈起始位置
void* StackLimit; //线程堆栈界限
void* KernelStack; //线程堆栈当前位置,也就是ESP
void* lpParameter; //线程函数的参数
void(*func)(void* lpParameter); //线程函数
}GMThread_t;
在Windows里,每一个线程都有一个结构体叫ETHREAD
,这个结构体就是模拟的线程结构体,只不过把和线程无关的属性删掉了,只保留了线程切换必须要用到的成员
调度链表
线程在不同的状态下存储的位置是不一样的,正在运行中的线程在KPCR里,等待状态中的线程在等待链表里,就绪的线程在32个就绪链表里
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uk3ROhFd-1581302990410)(assets/1581237190834.png)]
模拟线程切换的代码没有写的那么复杂,而是用一个数组来表示所有线程,然后通过Flags来区分线程的状态。所谓的创建线程,就是创建一个结构体,并且挂到这个数组里,此时线程处于创建状态,这个创建的过程就结束了。
这里面有一个小细节,在这个数组里存结构体的时候,是从下标为1的位置开始存的,而不是从最开始的位置开始存的;最开始的这个位置是给当前的线程用的
当我们把结构体挂到数组以后,当前线程处于创建状态,还不能进行调度;因为如果一个线程想要执行,一定要有自己的初始化的一些环境,例如寄存器的值要确定,当前线程的堆栈在哪里也要确定。现在仅仅有这么一个结构体,这些初始化的值还没有。
所以接下来还要做一件非常重要的事情,就是初始化当前线程的堆栈
初始化线程堆栈
当我们创建一个线程的时候,第一件事情就是为线程结构体赋值,把线程名 线程函数以及线程函数的参数填充到这个结构体。
当这些值赋好了以后,当前的线程处于创建状态
接下里就要为当前的线程准备一份堆栈。首先通过VirtualAlloc函数申请了一个堆栈。然后给堆栈的相关参数赋值。当线程初始化代码执行完成之后,线程状态如图所示:
灰色的部分就是给当前线程分配的一块堆栈,VirtualAlloc分配的地址加上堆栈的大小指向的就是堆栈开始的位置,中间的一部分就是这个线程所需要的堆栈。
StackLimit指向当前堆栈的边界。KernelStack是当前线程的栈顶,相当于ESP
接下来往堆栈里push了一堆数据,这些数据就是当线程开始执行的时候必须要用到的一些数据。
所谓的创建线程就是创建一个线程结构体,所谓的初始化线程就是为当前的线程再准备一份堆栈
线程切换
被动切换
接下来看一下线程是如何进行调度的
当代码执行到for循环的时候,已经创建好了四个线程。也就是说四个线程结构体已经挂到了数组里,四个线程所需要的堆栈也分配完成了。
接着代码模拟系统时钟每隔20毫秒进行线程切换,系统时钟的存在会让线程每隔一段时间进行切换,这种切换是被动的。
主动切换
还有一种切换是主动切换
这里的GMSleep模拟的是W32 API,只要进程调用了API,就会主动产生线程切换。
接下来看看线程是如何进行主动切换的,跟进GMSleep这个函数,这里要把它当成所有的W32 API
这里会修改线程的状态为Sleep,然后调用Scheduling函数进行线程切换和调度
线程调度
那么线程是如何进行调度的呢?
这个函数首先会做一件事情,就是遍历这个数组,找到第一个处于READY状态的线程
真正进行线程切换的是SwitchContext
函数,这个函数有两个参数,第一个参数当前线程的线程结构体指针,第二个参数是要切换的线程的线程结构体指针。
这个函数首先执行了一堆压栈的操作,将当前线程用到的寄存器存储到堆栈里。
此时的ESI是当前线程的结构体指针,EDI里存储的是要切换的线程结构体指针。接着把当前线程的ESP存储在当前线程的KernelStack里。
然后再把要切换的线程的KernelStack赋值给ESP,此时堆栈切换,另一个线程复活,如图:
此时当前的ESP指向目标线程的EAX
接着执行一系列pop指令使线程的堆栈指向Startup,接着ret将Startup(线程的函数入口)弹到eip,接下来执行的就是线程函数了。
这个函数是线程切换的核心,也是两个线程的转折点,上半部分一个进程进来,下半部分另一个线程出去。
总结
- 线程不是被动切换的,而是主动让出CPU
- 线程切换并没有使用TSS来保存寄存器,而是使用堆栈
- 线程切换的过程就是堆栈切换的过程