avatar

Catalog
模拟线程切换

我们知道CPU执行和调度的单位是线程,在有了线程结构体(ETHREAD)以及等待链表,调度链表的概念后,这一篇简单介绍一下线程切换,通过分析模拟线程切换的代码(源于滴水编程达人海东老师编写)来了解线程切换的过程及原理。

示例代码

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
#include 
#include

#define MAXGMTHREAD 0x100
#define GMTHREADSTACKSIZE 0x80000

#define GMTHREAD_CREATE 0x1
#define GMTHREAD_READAY 0x2
#define GMTHREAD_RUNING 0x4
#define GMTHREAD_SLEEP 0x8
#define GMTHREAD_EXIT 0x100
#define _SELF abcd1234

typedef struct //定义线程结构体
{
char* name; //线程名
int Flags; //定义状态:Ready/Sleep/Running
int SleepMillisecondDot; //休眠时间

void* InitialStack; //栈底
void* StackLimit; //栈限长
void* KernelStack; //栈顶

void *lpParameter; //线程函数参数
void (*func)(void *lpParameter); //线程函数
}GMThread_t;

int CurrentThreadIndex = 0;
void* WindowsStackLimit = NULL;
GMThread_t GMThreadList[MAXGMTHREAD] = {NULL, 0};

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




int main(int argc, char* argv[])
{
RegisterGMThread("Thread1", Thread1, NULL);
RegisterGMThread("Thread2", Thread2, NULL);
RegisterGMThread("Thread3", Thread3, NULL);
RegisterGMThread("Thread4", Thread4, NULL);

while(1)
{
Sleep(20);
Scheduling();
}


return 0;
}

//向栈中压入值
void PushStack(unsigned int** Stackpp, unsigned int v)
{
*Stackpp -= 1;
**Stackpp = v;

return;
}

//将一个函数注册为单独线程执行
int RegisterGMThread(char* name, void (*func)(void* lpParameter), void* lpParameter)
{
int i = 0;

for (i=1; GMThreadList[i].name; i++)
{
if (0 == strcmp(GMThreadList[i].name, name))
{
break;
}
}
InitGMThread(&GMThreadList[i], name, func, lpParameter);

return i;
}

//初始化线程的信息
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);

memset(StackPages, NULL, GMTHREADSTACKSIZE);

GMThreadp->InitialStack = StackPages + GMTHREADSTACKSIZE;

GMThreadp->StackLimit = StackPages;

StackDWORDParam = (unsigned int*)GMThreadp->InitialStack;

PushStack(&StackDWORDParam, (unsigned int)GMThreadp);
PushStack(&StackDWORDParam, (unsigned int)9);
PushStack(&StackDWORDParam, (unsigned int)GMThreadStartup);
PushStack(&StackDWORDParam, 5);
PushStack(&StackDWORDParam, 7);
PushStack(&StackDWORDParam, 6);
PushStack(&StackDWORDParam, 3);
PushStack(&StackDWORDParam, 2);
PushStack(&StackDWORDParam, 1);
PushStack(&StackDWORDParam, 0);

GMThreadp->KernelStack = StackDWORDParam;

GMThreadp->Flags = GMTHREAD_READAY;

return;

}

//启动线程的函数
void GMThreadStartup(GMThread_t* GMThreadp)
{
GMThreadp->func(GMThreadp->lpParameter);
GMThreadp->Flags = GMTHREAD_EXIT;

Scheduling();

return;
}

//线程调度函数,这个函数使得当前线程让出CPU,从队列里重新选择一个线程执行
void Scheduling(void)
{
int i;
int TickCount;
GMThread_t* SrcGMThreadp;
GMThread_t* DstGMThreadp;

TickCount = GetTickCount();

SrcGMThreadp = &GMThreadList[CurrentThreadIndex];
DstGMThreadp = &GMThreadList[0];

for (i=1; GMThreadList[i].name; i++)
{
if (GMThreadList[i].Flags & GMTHREAD_SLEEP)
{
if (TickCount > GMThreadList[i].SleepMillisecondDot)
{
GMThreadList[i].Flags = GMTHREAD_READAY;
}
}

if (GMThreadList[i].Flags & GMTHREAD_READAY)
{
DstGMThreadp = &GMThreadList[i];
break;
}
}

CurrentThreadIndex = DstGMThreadp - GMThreadList;
SwitchContext(SrcGMThreadp, DstGMThreadp);

}

//切换线程
__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
}
}


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)
{
while(1)
{
printf("Thread1\n");
GMSleep(500);
}
}

void Thread2(void* lpParameter)
{
while(1)
{
printf("Thread2\n");
GMSleep(200);
}
}

void Thread3(void* lpParameter)
{
while(1)
{
printf("Thread3\n");
GMSleep(10);
}
}

void Thread4(void* lpParameter)
{
while(1)
{
printf("Thread4\n");
GMSleep(1000);
}
}

代码分析

上述代码较长,且每行长短不一,故注释较乱,这里进行一些简要分析

模拟线程结构体

c
1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct					//定义线程结构体
{
char* name;
int Flags; //定义状态:Ready/Sleep/Running
int SleepMillisecondDot; //线程等待时间

void* InitialStack; //栈底
void* StackLimit; //栈限长
void* KernelStack; //栈顶

void *lpParameter; //线程函数参数
void (*func)(void *lpParameter); //线程函数
}GMThread_t;

这是这份代码里最重要的结构体,它定义了我们模拟线程的结构,实际上,就是一个乞丐版的ETHREAD,只是很多ETHREAD中的成员我们用不到,就省去了,但仍然可以模拟线程切换的过程,这也算是个五脏俱全的线程结构体,我们来看看都有哪些成员吧:

  • name:很好理解,线程的名字,用于标记线程
  • Flags:线程的状态,我们可以根据线程的状态将它放入等待链表或者让它执行
  • SleepMillisecondDot:线程的休眠时间。
  • InitialStack/StackLimit/KernelStack:可以说这是线程切换最重要的3个成员,每个线程执行时都需要有自己的堆栈,而具体该如何分配堆栈就要依靠这3个值,InitialStack提供了线程的栈底(ebp);KernelStack提供了栈顶(esp);StackLimit决定了栈的边界,可以这样理解,该线程的堆栈只能位于[ebp, ebp+StakLimit]的范围内,一旦超出这个范围,就会发生错误
  • lpParameter/func:分别是线程函数参数和线程函数,可以执行特定函数显示具体线程

全局变量和宏

c
1
2
3
4
5
6
7
8
9
10
11
#define MAXGMTHREAD 0x100
#define GMTHREADSTACKSIZE 0x80000

#define GMTHREAD_CREATE 0x1
#define GMTHREAD_READAY 0x2
#define GMTHREAD_RUNING 0x4
#define GMTHREAD_SLEEP 0x8
#define GMTHREAD_EXIT 0x100

int CurrentThreadIndex = 0;
GMThread_t GMThreadList[MAXGMTHREAD] = {NULL, 0};
  • MAXGMTHREAD:指明线程最多能有多少个
  • GMTHREADSTACKSIZE:这里说的是线程分配的堆栈能有多大,每个线程都拥有自己的堆栈,但是不能无限大,大小的限制由KTHREAD结构里的KernelStack决定
  • GMTHREAD_CREATE/READAY/RUNING/SLEEP/EXIT:均为线程的状态
  • CurrentThreadIndex:可以理解为Index,用于遍历,这里作为全局变量进行声明。
  • GMThreadList:这里的类型是GMThread_t,说明这是模拟线程结构体链表,在KTHREAD结构体中,使用了WaitListEntry和SwapListEntry,根据线程的状态,将线程放入不同的链表中。这里,海东老师只用了一个数组,用来存放线程,其中下标0的位置,存放主函数的线程,其余位置存放不同状态的线程。

主函数

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc, char* argv[])
{
RegisterGMThread("Thread1", Thread1, NULL); //注册线程1, 2, 3,4
RegisterGMThread("Thread2", Thread2, NULL);
RegisterGMThread("Thread3", Thread3, NULL);
RegisterGMThread("Thread4", Thread4, NULL);

while(1)
{
Sleep(20); //短暂等待
Scheduling(); //线程调度
}

return 0

程序是从主函数开始执行的,我们按照函数执行的顺序进行分析

  • RegisterGMThread():将一个函数注册为单独的线程来执行
  • Scheduling():调度函数,使得当前线程让出CPU,并从队列中(GMThreadList)重新选择一个线程执行

线程注册函数

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int RegisterGMThread(char* name, void (*func)(void* lpParameter), void* lpParameter)
{
int i = 0;

for (i=1; GMThreadList[i].name; i++)
{
if (0 == strcmp(GMThreadList[i].name, name))
{
break;
}
}
InitGMThread(&GMThreadList[i], name, func, lpParameter);

return i;
}
  • 参数:线程名,线程函数,线程函数参数
  • 前面提到了,下标0的位置,存放着是main线程,所以这里从下标1开始写入,对数组中未初始化的线程通过初始化函数InitiGMThread()进行初始化

压栈函数

c
1
2
3
4
5
6
7
void PushStack(unsigned int** Stackpp, unsigned int v)	
{
*Stackpp -= 1; //栈减一个int长度,就是4字节
**Stackpp = v; //并在这个位置存值

return;
}

在介绍线程初始化函数前,先看一下这个压栈函数,这个函数非常简单,传了2个参数,一个指针,一个数。压栈函数的作用就是,指针-1(因为是*Stackpp,所以减的是int类型,即4字节),并在压栈后的地址存这个数,文字叙述可能不好理解,我们把这个转换一下就好理解了,其实就是代码实现的一个简单压栈操作

Code
1
2
3
4
5
6
7
8
9
10
11
_asm {
sub esp, 4
mov eax, v
mov esp, eax
}

or

_asm {
push v
}

线程初始化函数

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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;

//分配80个连在一起的可以直接用的物理页
StackPages = (unsigned char*)VirtualAlloc(NULL, GMTHREADSTACKSIZE, MEM_COMMIT, PAGE_READWRITE);
//将分配的内存先都清0
memset(StackPages, NULL, GMTHREADSTACKSIZE);
//设置栈底ebp
GMThreadp->InitialStack = StackPages + GMTHREADSTACKSIZE;
//设置栈的最大上限
GMThreadp->StackLimit = StackPages;
//将ebp赋值StackDWORDParam
StackDWORDParam = (unsigned int*)GMThreadp->InitialStack;

PushStack(&StackDWORDParam, (unsigned int)GMThreadp);
PushStack(&StackDWORDParam, (unsigned int)9);
PushStack(&StackDWORDParam, (unsigned int)GMThreadStartup);
PushStack(&StackDWORDParam, 5); //ebp
PushStack(&StackDWORDParam, 7); //edi
PushStack(&StackDWORDParam, 6); //esi
PushStack(&StackDWORDParam, 3); //ebx
PushStack(&StackDWORDParam, 2); //ecx
PushStack(&StackDWORDParam, 1); //edx
PushStack(&StackDWORDParam, 0); //eax

//令KernelStack指向栈顶esp
GMThreadp->KernelStack = StackDWORDParam;
//修改线程状态Create->Ready
GMThreadp->Flags = GMTHREAD_READAY;

return;
}

线程初始化:线程初始化总共分为2步,一个是对线程结构体的初始化,另一个是对线程所在堆栈的初始化

  • 线程结构体初始化:对代码中定义的简约版线程结构体GMThread_t中的部分成员进行初始化,包括线程状态,线程名,线程函数及参数。

  • 线程堆栈初始化:我们知道,每一个线程,都得有属于自己堆栈,总不能跑到别人的堆栈上执行吧,这样就乱套了因此线程得拥有自己的堆栈。来看一下模拟线程切换的代码中是如何实现的:

    • 第一步用VirutalAlloc函数申请一块连续的内存(分配类型使用MEM_COMMIT)
    • 初始化这块内存(置零)
    • 设置栈底,栈顶,边界,这部分非常关键,也是设置线程堆栈的核心步骤。在KTHREAD结构中,有InitialStack/StackLimit/KernelStack决定线程的堆栈相关参数。本次的模拟程序里也定义了这三个成员,我们来看下他们是如何运作的。
      • InitialStack:这个成员相当于栈底,也就是ebp,在Windows中,堆栈的由高地址向低地址延申的,所以这里设置ebp的值为申请内存的首地址+堆栈限制大小
      • StackLimit:这个成员定义栈的边界,栈的范围应在[InitialStack - StackLimit, InitialStack]内,这里令其等于申请内存的首地址,因为栈由高地址向下延申,因此栈的边界会位于此处
      • KernelStack:这个成员指向栈顶,相当于esp。这里的几步非常关键,按照顺序依次push了线程结构体,一个数,一个执行线程的函数GMThreadStartup(),接着又是一堆数,最后,将栈顶(通过压栈函数减了很多次),赋值给了KernelStack

    以上就是线程初始化最关键的部分,可以参考这张图

线程调度函数

回到主函数,线程注册函数执行完后(线程初始化函数中的线程调用函数并未执行,只是被压栈了,所以稍后分析),就到了线程调度函数,一起来看一下线程调度函数都做了些什么吧

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void Scheduling(void)
{
int i;
int TickCount;

GMThread_t* SrcGMThreadp;
GMThread_t* DstGMThreadp;

TickCount = GetTickCount();
//指向正在执行的线程
SrcGMThreadp = &GMThreadList[CurrentThreadIndex];
//指向准备执行的线程
DstGMThreadp = &GMThreadList[0];

//遍历线程数组,找到第一个状态为就绪的线程
for (i=1; GMThreadList[i].name; i++)
{
if (GMThreadList[i].Flags & GMTHREAD_SLEEP)
{
if (TickCount > GMThreadList[i].SleepMillisecondDot)
{
GMThreadList[i].Flags = GMTHREAD_READAY;
}
}

if (GMThreadList[i].Flags & GMTHREAD_READAY)
{
DstGMThreadp = &GMThreadList[i];
break;
}
}
//得到即将执行的线程下标
CurrentThreadIndex = DstGMThreadp - GMThreadList;
//线程切换
SwitchContext(SrcGMThreadp, DstGMThreadp);

}

线程调度函数不是很复杂,比较好理解,这里简要概括下:

  1. 开头部分定义了两个线程结构体指针:SrcGMThreadp,DstGMThreadp
  2. SrcGMThreadp指向正在运行的线程,DstGMThreadp遍历线程数组找到第一个状态为就绪的线程并指向它
  3. 保存DstGMThreadp指向的线程在数组中的下标(下次调度时好知道,正在运行的线程位于什么位置)
  4. 通过SwitchContext将这两个线程进行切换

线程切换函数

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
__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
}
}

这是本篇最高能的地方了,我们来详细分析一下,这个看上去简单的代码是如何实现线程切换的。我们来一步步的看:

  1. 最开始,一堆push,非常好理解,就是保存寄存器的值嘛!
  2. 接下来,两个mov操作,将指向正在运行的线程结构体的指针赋给了esi,将指向准备运行的线程结构体的指针赋给了edi
  3. 然后,线程切换最经典的操作来了!将当前esp,赋值给esi指向线程的KernelStack;同时,将edi指向线程的KernelStack赋给esp。我们知道KernelStack存的是线程自己堆栈的esp,程序中的esp,是当前CPU执行的时的堆栈,而这个操作就是把当前堆栈保存到即将被切换的线程的KernleStack中,同时,让CPU执行所在的堆栈变成切换后的线程的KernelStack,说简单点,这个操作就是一次堆栈的切换
  4. 还没完!后面还有一堆pop,你以为就没用了嘛?仔细想想,堆栈已经发生了切换了!所以即将pop的那些值已经不是上面push进去的值了!那pop出来的值又是什么值呢? 没错,就是在线程初始化函数中Push进去的那些值,一直到pop ebp都比较好理解
  5. 接下来,一个ret,又是一个精髓指令,通过这个ret指令,刚好调用一个用来执行线程的函数GMThreadStartup(),这个函数会让线程调用自己的线程函数。这里有一个细节,就是这个函数传递了一个线程结构体指针,但是在裸函数中,ret语句执行完就跳转到GMThreadStartup()函数的开始处执行,那么它又是如何获取参数的呢?我们来查看一下反汇编 根据这个函数的反汇编可以发现,它是通过[ebp+8]来获取参数的,而这个位置,刚好就是在初始化函数中,第一个push进去的线程结构体,紧接着push了一个9,仅仅是用来占位,从而使得[ebp+8]刚好可以指向线程结构体,从而获取参数,u1s1,这里细节妙不可言

这里贴一张群友张嘉杰做的笔记,做的非常好,结合着看更易看懂代码

执行线程函数

c
1
2
3
4
5
6
7
8
9
void GMThreadStartup(GMThread_t* GMThreadp)
{
GMThreadp->func(GMThreadp->lpParameter);
GMThreadp->Flags = GMTHREAD_EXIT;
//线程切换
Scheduling();

return;
}

这个函数,在上面刚讲过,主要就是最后,会再执行一次线程调度函数,实现下一次的线程切换,说明了一点,线程是主动切换的,主动让出CPU

程序运行结果

最后,来看一下程序运行时的样子,就是在不断的线程切换

总结

至此,程序主要部分就基本分析完毕,真的是非常巧妙的代码,海东老师太厉害了!这里对模拟线程切换做一个总结:

  • 线程不是被动切换的,而是主动让出CPU
  • 线程切换并没有使用TSS来保持寄存器,而是使用堆栈。
  • 线程切换的过程就是切换堆栈的过程

参考教程:https://www.bilibili.com/video/BV1NJ411M7aE?p=47

参考文章:

  1. https://blog.csdn.net/qq_38474570/article/details/104245111
  2. https://blog.csdn.net/qq_41988448/article/details/103098367

参考笔记:张嘉杰,Joney,米高扬设计局,馍馍

Author: cataLoc
Link: http://cata1oc.github.io/2020/03/31/%E6%A8%A1%E6%8B%9F%E7%BA%BF%E7%A8%8B%E5%88%87%E6%8D%A2/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶