进程线程003 模拟线程切换


之前我们已经了解过线程的等待链表和调度链表,为了更好的学习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,接下来执行的就是线程函数了。

这个函数是线程切换的核心,也是两个线程的转折点,上半部分一个进程进来,下半部分另一个线程出去。

总结

  1. 线程不是被动切换的,而是主动让出CPU
  2. 线程切换并没有使用TSS来保存寄存器,而是使用堆栈
  3. 线程切换的过程就是堆栈切换的过程
发布了99 篇原创文章 · 获赞 89 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/qq_38474570/article/details/104245111
今日推荐