我们知道CPU执行和调度的单位是线程 ,在有了线程结构体(ETHREAD)以及等待链表,调度链表的概念后,这一篇简单介绍一下线程切换,通过分析模拟线程切换 的代码(源于滴水编程达人海东老师编写)来了解线程切换的过程及原理。
示例代码 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; 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 ; } 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 ); } }
代码分析 上述代码较长,且每行长短不一,故注释较乱,这里进行一些简要分析
模拟线程结构体 1 2 3 4 5 6 7 8 9 10 11 12 13 typedef struct //定义线程结构体{ char * name; int Flags; 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:分别是线程函数参数和线程函数,可以执行特定函数显示具体线程
全局变量和宏 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的位置,存放主函数的线程 ,其余位置存放不同状态的线程。
主函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 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
程序是从主函数开始执行的,我们按照函数执行的顺序进行分析
RegisterGMThread():将一个函数注册为 单独的线程 来执行
Scheduling():调度函数,使得当前线程让出CPU,并从队列中(GMThreadList)重新选择一个线程执行
线程注册函数 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()进行初始化
压栈函数 1 2 3 4 5 6 7 void PushStack (unsigned int ** Stackpp, unsigned int v) { *Stackpp -= 1 ; **Stackpp = v; return ; }
在介绍线程初始化函数前,先看一下这个压栈函数,这个函数非常简单,传了2个参数,一个指针,一个数。压栈函数的作用就是,指针-1(因为是*Stackpp,所以减的是int类型,即4字节),并在压栈后的地址存这个数,文字叙述可能不好理解,我们把这个转换一下就好理解了,其实就是代码实现的一个简单压栈操作
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 }
线程初始化函数 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; 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 ; }
线程初始化:线程初始化总共分为2步,一个是对线程结构体的初始化 ,另一个是对线程所在堆栈的初始化
线程调度函数 回到主函数,线程注册函数执行完 后(线程初始化函数中的线程调用函数并未执行,只是被压栈了,所以稍后分析),就到了线程调度函数 ,一起来看一下线程调度函数都做了些什么吧
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); }
线程调度函数不是很复杂,比较好理解,这里简要概括下:
开头部分定义了两个线程结构体指针:SrcGMThreadp,DstGMThreadp
SrcGMThreadp指向正在运行的线程,DstGMThreadp遍历线程数组 ,找到第一个状态为就绪的线程 并指向它
保存DstGMThreadp指向的线程在数组中的下标(下次调度时好知道,正在运行的线程位于什么位置)
通过SwitchContext将这两个线程进行切换
线程切换函数 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 } }
这是本篇最高能 的地方了,我们来详细分析一下,这个看上去简单的代码是如何实现线程切换的。我们来一步步的看:
最开始,一堆push,非常好理解,就是保存寄存器的值嘛!
接下来,两个mov操作,将指向正在运行的线程结构体的指针赋给了esi ,将指向准备运行的线程结构体的指针赋给了edi
然后,线程切换最经典的操作 来了!将当前esp,赋值给esi指向线程的KernelStack;同时,将edi指向线程的KernelStack赋给esp。我们知道KernelStack存的是线程自己堆栈的esp ,程序中的esp,是当前CPU执行的时的堆栈,而这个操作就是把当前堆栈保存到即将被切换 的线程的KernleStack中,同时,让CPU执行所在的堆栈变成切换后的线程的KernelStack ,说简单点,这个操作就是一次堆栈的切换 !
还没完!后面还有一堆pop,你以为就没用了嘛?仔细想想,堆栈已经发生了切换 了!所以即将pop的那些值已经不是上面push进去的值 了!那pop出来的值又是什么值呢? 没错,就是在线程初始化函数中Push进去的那些值,一直到pop ebp都比较好理解
接下来,一个ret,又是一个精髓指令 ,通过这个ret指令,刚好调用一个用来执行线程的函数GMThreadStartup(),这个函数会让线程调用自己的线程函数 。这里有一个细节,就是这个函数传递了一个线程结构体指针,但是在裸函数中,ret语句执行完就跳转到GMThreadStartup()函数的开始 处执行,那么它又是如何获取参数的呢?我们来查看一下反汇编 根据这个函数的反汇编可以发现,它是通过[ebp+8]来获取参数 的,而这个位置,刚好就是在初始化函数中,第一个push进去的线程结构体,紧接着push了一个9,仅仅是用来占位 ,从而使得[ebp+8]刚好可以指向线程结构体 ,从而获取参数,u1s1,这里细节妙不可言
这里贴一张群友张嘉杰做的笔记,做的非常好,结合着看更易看懂代码
执行线程函数 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
参考文章:
https://blog.csdn.net/qq_38474570/article/details/104245111
https://blog.csdn.net/qq_41988448/article/details/103098367
参考笔记:张嘉杰,Joney,米高扬设计局,馍馍