[转](57)模拟线程切换

 

一、回顾

 

在之前的课程中,我们学习了 EPROCESS, ETHREAD, KPCR 等重要的内核结构体,学习了存储等待线程的等待链表和调度线程的调度链表,这些知识都是为了后面学习线程切换打的基础。

 

这次课,我们将学习老师提供的模拟Windows线程切换的源码,这份代码可以在3环模拟线程调度,有助于我们理解真正的Windows线程调度源码。

 

二、运行结果

 

这份代码是我照着视频抄的(见文末),在后续的学习中,我们要往里面加东西,所以建议不要做过多的修改。

 

在这里插入图片描述

 

三、RegisterGMThread, InitGMThread, GMThreadStartup

 

void RegisterGMThread(char *name, void (*func)(void *lpParameter), void *lpParameter);
void InitGMThread (GMThread_t *GMThreadp, char *name, void (*func)(void *lpParameter), void *lpParameter);

 

RegisterGMThread 函数负责创建线程,它遍历线程调度队列,找到一个空位作为新线程结构体,然后调用 InitGMThread 初始化。

 

InitGMThread 函数负责初始化线程结构体;为线程申请堆栈内存;向堆栈压入必要的初始数据,包括线程结构体指针,GMThreadStartup 函数指针,以及一堆寄存器的初始值(PS. 压栈用的 PushStack 函数不解释);最后设置线程状态为“就绪”。

 

这里所做的所有压栈操作,没有一步是多余的。

 

7个寄存器是线程恢复时要pop的值,这里设置成0,表示第一次调度时给寄存器设置初始值0,也可以改成其他值。

 

所有线程都要通过 GMThreadStartup 函数调用自己的线程入口函数,而调用 GMThreadStartup 函数的地方以及传参的过程设计非常巧妙,这一步发生在 SwitchContext 函数中,恢复线程后,pop了7个寄存器,esp就指向了 GMThreadStartup,此时 SwitchContext 调用 ret 指令,就跳转到 GMThreadStartup 函数,完全模拟了 call 调用的堆栈,那个看起来没用的堆栈平衡值其实模拟的是 call 时压入堆栈的返回地址,而 GMThreadp 模拟的是 call 之前push的参数。进入 GMThreadStartup 后,函数从 ebp + 8 处取得参数1 GMThreadp 。

 

为什么说返回地址是模拟的?因为 GMThreadStartup 永远不会执行它的 return 语句。

 

// 此函数在 SwitchContext 的 ret 指令执行时调用,功能是调用线程入口函数
void GMThreadStartup(GMThread_t *GMThreadp)
{
	GMThreadp->func(GMThreadp->lpParameter);
	GMThreadp->Flags = GMTHREAD_EXIT;
	Scheduling();
	printf("这句永远不会执行,因为修改线程状态为退出,Scheduling 永远不会返回到这里.\n");
	return;
}

 

在这里插入图片描述

 

四、Scheduling 线程调度函数

 

void Scheduling();

 

这个函数负责遍历线程调度队列,如果遍历到“等待”状态的线程,判断它是否已经完成了“等待”,如果是,那么修改其状态为就绪。通过遍历,找出第一个“就绪”线程,如果遍历完都没有发现新的就绪线程,那么就认为主函数是“就绪”线程。

 

最后,调用 SwitchContext 函数“切换”到刚才找到的“就绪”线程。

 

五、SwitchContext 切换线程函数

 

SwitchContext 负责切换线程,旧线程调用 SwitchContext 时,首先把7个寄存器压到自己的栈顶,然后保存当前栈顶 esp 到 KernelStack,然后从新线程的线程结构体里取出 KernelStack 填到 esp,就完成了线程切换。

 

接下来就是从新线程的栈顶 pop 还原7个寄存器。pop 了那7个寄存器后,esp 一定是指向下一条指令的地址的,如果新线程尚未被调度过,那么栈顶一定是 GMThreadStartup;如果新线程曾被调度过,那么栈顶一定是新线程上一次调用 SwitchContext 的返回地址,即 Scheduling 函数的末尾。

 

最后,附上一张线程A切换到线程B的堆栈图,帮助理解。

 

在这里插入图片描述

 

六、总结

 

其他的函数就很简单了,限于篇幅,我不打算花笔墨解释其他函数,因为注释已经说明了我没有提到的细节。

 

最重要的 SwitchContext 函数,要理解它是怎么通过切换堆栈来实现切换线程的,要理解 ret 指令跳转到什么地方;Scheduling 函数也很关键,它用了一种非常简单的遍历算法寻找“就绪”线程。

 

这个程序模拟了时钟中断切换线程(主函数的循环),和调用API的主动切换(线程主动调用 GMSleep)。

 

你可以在主函数循环里加一个打印语句:

 

	for (;;)
	{
		Sleep(20);
		Scheduling();
		// 如果回到主线程,说明没有找到就绪线程,CurrentThreadIndex 一定是 0
		//printf("时钟中断. %d\n", CurrentThreadIndex);
	}

 

你会发现,这和空闲线程的执行非常像,后续的课程里,会添加很多功能,比如模拟空闲线程,此处暂时不表。

 

在这里插入图片描述

 


 

七、单文件版本源码

 

#include <stdio.h>
#include <tchar.h>
#include <string.h>
#include <Windows.h>

#pragma warning(disable: 4996)

//--------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------


#define MAXGMTHREAD 0x100

#define GMTHREAD_CREATE		0x01
#define GMTHREAD_READY		0x02
#define GMTHREAD_RUNNING	0x04
#define GMTHREAD_SLEEP		0x08
#define GMTHREAD_EXIT		0x100

#define GMTHREADSTACKSIZE 0x80000

//--------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------

// 线程结构体(仿ETHREAD)
typedef struct {
	char *name;							// 线程名,相当于线程TID
	int Flags;							// 线程状态
	int SleepMillisecondDot;			// 休眠时间
	void *InitialStack;					// 线程堆栈起始位置
	void *StackLimit;					// 线程堆栈界限
	void *KernelStack;					// 线程堆栈当前位置,即ESP0
	void *lpParameter;					// 线程函数参数
	void (*func)(void *lpParameter);	// 线程函数
} GMThread_t;

//--------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------

// 当前调度线程下标
int CurrentThreadIndex = 0;

// 线程调度队列
GMThread_t GMThreadList[MAXGMTHREAD] = { 0 };

void *WindowsStackLimit = NULL;

//--------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------

void SwitchContext(GMThread_t *OldGMThreadp, GMThread_t *NewGMThreadp);
void GMThreadStartup(GMThread_t *GMThreadp);
void IdleGMThread(void *lpParameter);
void PushStack(unsigned int **Stackpp, unsigned int v);
void InitGMThread (GMThread_t *GMThreadp, char *name, void (*func)(void *lpParameter), void *lpParameter);
int RegisterGMThread(char *name, void (*func)(void *lpParameter), void *lpParameter);
void Scheduling();
void GMSleep(int Milliseconds);
void Thread1(void *lpParameter);
void Thread2(void *lpParameter);
void Thread3(void *lpParameter);
void Thread4(void *lpParameter);

//--------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------

int _tmain(int argc, _TCHAR* argv[])
{
	// 初始化线程环境
	RegisterGMThread("Thread1", Thread1, NULL);
	RegisterGMThread("Thread2", Thread2, NULL);
	RegisterGMThread("Thread3", Thread3, NULL);
	RegisterGMThread("Thread4", Thread4, NULL);

	// 仿Windows线程切换,模拟系统时钟中断,是被动切换
	//Scheduling();
	for (;;)
	{
		Sleep(20);
		Scheduling();
		// 如果回到主线程,说明没有找到就绪线程,CurrentThreadIndex 一定是 0
		//printf("时钟中断. %d\n", CurrentThreadIndex);
	}
	return 0;
}

// 线程切换函数
__declspec(naked) void SwitchContext(GMThread_t *OldGMThreadp, GMThread_t *NewGMThreadp)
{
	__asm
	{
		// 当前线程保存寄存器到自己的栈顶
		push ebp;
		mov ebp,esp;
		push edi;
		push esi;
		push ebx;
		push ecx;
		push edx;
		push eax;

		mov esi,OldGMThreadp; // mov esi, [ebp + 0x08]
		mov edi,NewGMThreadp; // mov edi, [ebp + 0x0C]

		mov [esi + GMThread_t.KernelStack], esp; // 保存旧ESP
		mov esp,[edi + GMThread_t.KernelStack]; // 设置新ESP

		// 从新线程的栈里恢复寄存器的值
		pop eax;
		pop edx;
		pop ecx;
		pop ebx;
		pop esi;
		pop edi;
		pop ebp;

		// 返回到新线程之前调用 SwitchContext 的地方;如果是第一次调度,则跳转到 GMThreadStartup
		ret;
	}
}

// 此函数在 SwitchContext 的 ret 指令执行时调用,功能是调用线程入口函数
void GMThreadStartup(GMThread_t *GMThreadp)
{
	GMThreadp->func(GMThreadp->lpParameter);
	GMThreadp->Flags = GMTHREAD_EXIT;
	Scheduling();
	printf("这句永远不会执行,因为修改线程状态为退出,Scheduling 永远不会返回到这里.\n");
	return;
}

// 空闲线程,没事做就调用它
void IdleGMThread(void *lpParameter)
{
	printf("IdleGMThread-------------------\n");
	Scheduling();
	return;
}

// 模拟压栈
void PushStack(unsigned int **Stackpp, unsigned int v)
{
	*Stackpp -= 1;
	**Stackpp = v;

	return;
}

// 初始化线程结构体和线程栈,设置状态为“就绪”
void InitGMThread (GMThread_t *GMThreadp, char *name, void (*func)(void *lpParameter), void *lpParameter)
{
	unsigned char *StackPages;
	unsigned int *ESP;
	// 结构初始化赋值
	GMThreadp->Flags = GMTHREAD_CREATE;
	GMThreadp->name = name;
	GMThreadp->func = func;
	GMThreadp->lpParameter = lpParameter;
	// 申请栈空间
	StackPages = (unsigned char*)VirtualAlloc(NULL,GMTHREADSTACKSIZE, MEM_COMMIT, PAGE_READWRITE);
	// 清零
	memset(StackPages,0,GMTHREADSTACKSIZE);
	// 栈初始化地址
	GMThreadp->InitialStack = (StackPages + GMTHREADSTACKSIZE);
	// 栈限制
	GMThreadp->StackLimit = StackPages;
	// 栈地址
	ESP = (unsigned int *)GMThreadp->InitialStack;

	// 初始化线程栈
	PushStack(&ESP, (unsigned int)GMThreadp);		// 通过这个指针来找到:线程函数、函数参数
	PushStack(&ESP, (unsigned int)0);				// 平衡堆栈,此值无意义,详见 SwitchContext 函数注释
	PushStack(&ESP, (unsigned int)GMThreadStartup);	// 线程入口函数,这个函数负责调用线程函数
	PushStack(&ESP, (unsigned int)0);				// push ebp,此值无意义,是寄存器初始值
	PushStack(&ESP, (unsigned int)0);				// push edi,此值无意义,是寄存器初始值
	PushStack(&ESP, (unsigned int)0);				// push esi,此值无意义,是寄存器初始值
	PushStack(&ESP, (unsigned int)0);				// push ebx,此值无意义,是寄存器初始值
	PushStack(&ESP, (unsigned int)0);				// push ecx,此值无意义,是寄存器初始值
	PushStack(&ESP, (unsigned int)0);				// push edx,此值无意义,是寄存器初始值
	PushStack(&ESP, (unsigned int)0);				// push eax,此值无意义,是寄存器初始值

	GMThreadp->KernelStack = ESP;

	GMThreadp->Flags = GMTHREAD_READY;

	return;
}

// 添加新线程到调度队列,然后初始化线程
int RegisterGMThread(char *name, void (*func)(void *lpParameter), void *lpParameter)
{
	int i;

	// 找一个空位置,或者是name已经存在的那个项
	// 下标0是当前正在运行的线程,所以从1开始遍历
	for (i = 1; GMThreadList[i].name; i++)
	{
		if (0 == stricmp(GMThreadList[i].name, name))
		{
			break;
		}
	}
	// 初始化线程结构体
	InitGMThread(&GMThreadList[i], name, func, lpParameter);

	return (i | 0x55AA0000);
}

// 线程调度函数,功能是遍历调度队列,找到“就绪”线程,然后切换线程
void Scheduling()
{
	int i;
	int TickCount;
	GMThread_t *OldGMThreadp;
	GMThread_t *NewGMThreadp;

	TickCount = GetTickCount(); // GetTickCount 返回操作系统启动到目前为止经过的毫秒
	// 正在调度的线程,第一次是 GMThreadList[0],这个表示主线程
	OldGMThreadp = &GMThreadList[CurrentThreadIndex];

	// 遍历线程调度队列,找第一个“就绪”线程
	// 如果找不到,就回到主函数,模拟时钟中断
	NewGMThreadp = &GMThreadList[0];	
	for (i = 1; GMThreadList[i].name; i++)
	{
		// 如果达到“等待时间”,就修改状态为“就绪”
		if (GMThreadList[i].Flags & GMTHREAD_SLEEP)
		{
			if (TickCount > GMThreadList[i].SleepMillisecondDot)
			{
				GMThreadList[i].Flags = GMTHREAD_READY;
			}
		}
		// 找到“就绪”线程
		if (GMThreadList[i].Flags & GMTHREAD_READY)
		{
			NewGMThreadp = &GMThreadList[i];
			break;
		}
	}
	// 更新当前调度线程下标
	CurrentThreadIndex = NewGMThreadp - GMThreadList;
	// 线程切换
	SwitchContext(OldGMThreadp, NewGMThreadp);
	return;
}

// 正在运行的线程主动调用此函数,将自己设置成“等待”状态,然后让调度函数调度其他线程
void GMSleep(int Milliseconds)
{
	GMThread_t *GMThreadp;
	GMThreadp = &GMThreadList[CurrentThreadIndex];

	if ((GMThreadp->Flags) != 0)
	{
		GMThreadp->SleepMillisecondDot = GetTickCount() + Milliseconds;
		GMThreadp->Flags = GMTHREAD_SLEEP;
	}

	Scheduling();
	return;
}

void Thread1(void *lpParameter)
{
	int i;
	for (i = 0; i < 3; i++)
	{
		printf("Thread1\n");
		GMSleep(100); // 主动切换,模拟WIN32 API
	}

	return;
}

void Thread2(void *lpParameter)
{
	int i = 0;
	while (++i)
	{
		printf("	Thread2(%d)\n", i);
		GMSleep(200); // 主动切换,模拟WIN32 API
	}

	return;
}

void Thread3(void *lpParameter)
{
	int i = 0;
	while (++i)
	{
		printf("		Thread3(%d)\n", i);
		GMSleep(200); // 主动切换,模拟WIN32 API
	}

	return;
}

void Thread4(void *lpParameter)
{
	int i = 0;
	while (++i)
	{
		printf("			Thread4(%d)\n", i);
		GMSleep(400); // 主动切换,模拟WIN32 API
	}

	return;
}



---------------------
作者:hambaga
来源:CSDN
原文:https://blog.csdn.net/Kwansy/article/details/109554283
版权声明:本文为作者原创文章,转载请附上博文链接!
内容解析By:CSDN,CNBLOG博客文章一键转载插件

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值