上一节我们分析了函数调用主要是将rbp
(栈基址寄存器)压栈最后函数退出后将其弹栈实现调用和恢复现场. 而本节我们再接再厉接着分析线程调用的过程.
在我们写代码之前要先对基本的寄存器有一个了解, 毕竟待会我们会先写一段汇编代码, 所以必须对寄存器要有一个简单的了解.
rsp
寄存器连用的, 因为rsp
会经常改变, 为了让编译器知道该栈的位置的地址就是用rbp
来保存栈顶, 所以, 每个函数在开始时都要保存原来的rbp
,设置自己的堆栈地址, 在函数结束返回时恢复原来的rbp
,让上级函数可以正常使用rbp
.pushfl
和popfl
进行全部压栈和出栈.对寄存器这样了解就行了, 毕竟我们不是讲解怎么学习汇编, 知道名字不陌生就行了.
线程调用在调用时与函数调用基本一致, 调用时会将当前线程的所有的寄存器全部压栈, 再将切换线程的所有寄存器出栈就实现了一次线程的调用.
因为线程会不停的切换, 所以在写代码之前, 我们先确定用一个变量来保存当前线程, 我在这里用的变量名是current_thread
.
.global switch_to // 告诉编译器这是我们定义的函数
switch_to: // 这是函数名
push %ebp
// 更改栈帧,用于寻参
mov %esp, %ebp
// 保存现场
push %edi
push %esi
push %eax
push %ebx
push %ecx
push %edx
// 将所有状态标志位压栈
pushfl
以上我们就做完了压栈的操作了, 前面两步我们在分析函数调用时都见过了, 这里也是照搬过来的, 下面的都没有在函数调用中见过(但是这些操作确实也执行了只是我们看不到)但实际功能就是将寄存区压栈.
明白了压栈那么出栈就简单了, 出栈与压栈的顺序相反.
// 恢复现场, 顺序一定要正确
popfl
popl %edx
popl %ecx
popl %ebx
popl %eax
popl %esi
popl %edi
popl %ebp
ret
用current_thread
变量保存当前线程
前面说过我们需要知道究竟执行的是哪一个线程, 就定义了这个变量, 现在就在汇编中操作这个变量吧. 不过, 再此之前有一个要求, 我们需要对当前的线程传递一个参数, 还记得函数调用是怎么传递参数的吗? 如果不太清楚了可以在函数调用汇编中看看, 如果明白的话, 那么我们现在执行的方法也是一样的哦, 可以搬过来用就行了, 不过参数需要在下一节c语言中分析.
// 切换栈
// 保存当前 esp
mov current_thread, %eax
mov %esp, thread(,%eax,4)
// 取下一个线程 id
mov 8(%ebp), %eax
// 将 current_thread 重置为下一个线程
mov %eax, current_thread
// 切换到下一个线程的栈
mov thread(,%eax,4), %esp
我们先将现在线程的current_thread
保存在eax
寄存器中, 在切换线程后将eax
保存在current_thread
即可.
但这里有一个thread
变量之前没有说过我准备放在下一节讲完后自己分析它的作用.
current_thread
操作执行的位置
上面我们分析了current_thread
, 但是具体位置应该在压栈还是存栈还是中间呢?
分析过函数调用应该很容易就知道, 压栈和出栈是函数(线程)所有操作之前和之后, 所以上面的操作肯定放在压栈后出栈前.
本小结我们分析了用汇编实现线程的切换, 虽然汇编大多数人不怎么接受, 但这样的汇编量非常少, 逻辑也相当的简单, 相信都容易理解的. 线程的调用方式与函数调用的方式都是一样的.
你会发现这节写的代码并不能执行啊, 怎么运行啊? 而且后面我们还要写汇编? 后面我们基本不写汇编了, 只写c语言. 那么怎么用c语言来实现线程切换呢? 这就是我们下一节所要讲解的.