前言
上一节我们用模仿函数调用的方法来实现了线程调用, 不过都是用汇编实现的, 而本节就来写用c语言来调用.
线程调用
1. 全局变量
上节汇编中thread
和current_thread
都是被定义的全局变量, 使用全局变量是为了容易可以直接在汇编中调用.
int thread[3] = {0}; // 定义3个线程
int current_thread = 0; // 保存当前运行的线程
2. 定义线程调用函数
还记得汇编的第一句是global switch_to
告诉编译器这是定义的函数, 而函数体使用汇编实现的, 声明是在另一个文件中.
void switch_to(int current_thread);
3. 初始化线程调用
下面定义的线程调用时执行的函数, 因为后面会提及, 这里先罗列出来
void thread1()
{
while(1)
{
printf("thread1\n");
sleep(1);
switch_to(2);
}
}
void thread2()
{
while(1)
{
printf("thread2\n");
sleep(1);
switch_to(1);
}
}
void thread_start(int current_thread)
{
if(current_thread == 1)
{
thread1();
}
else if(current_thread == 2)
{
thread2();
}
}
线程在调用的时候会将esp, eax等重要的寄存器保存起来, 这样才能保证线程还能正常的被切换回来, 但是我们前面所写的汇编代码并没有申请用来保存空间啊. 怎么解决呢? 想想程序的数据都是放在内存空间中的, 而程序的部分内存空间我们都喜欢称为栈和堆, 那么我们是不是只需要将寄存器等数据放在栈或堆中就行了. 我们写简单的方法就行, 用数组来当作线程调用时的线程栈那这个问题解决了.
具体这个线程栈的空间需要多大呢? 建议最好能装进去所以寄存器就行了, 不过我写的时候线程栈为1024. 大一点总不坏.
int main()
{
// 1024是自己设置的一个值, 1kb的大小足够线程栈使用空间了
int thread1[1024] = {0};
int thread2[1024] = {0};
...
}
好了, 线程定义了两个线程栈了, 也就可以支持两个线程调度了.
int main()
{
// 1024是自己设置的一个值, 1kb的大小足够线程栈使用空间了
int thread1[1024] = {0};
int thread2[1024] = {0};
thread[1] = (int)(thread1+1013);
thread[2] = (int)(thread2+1013);
...
}
现在在原基础上加上了两句. 这两句意思是将`thread保存线程栈栈顶的位置, 而离栈顶(1024)还有一部分的空间就是为了用来保存寄存器等重要数据. 而1013就是代表分界线, 往栈顶方向是寄存器, 往栈底方向就是可以用来存储的数据空间.
接下来就来写往栈顶方向的数据吧.
int main()
{
// 1024是自己设置的一个值, 1kb的大小足够线程栈使用空间了
int thread1[1024] = {0};
int thread2[1024] = {0};
thread[1] = (int)(thread1+1013);
thread[2] = (int)(thread2+1013);
/*--------- 线程1 ---------*/
// 创建 thread1 线程
// 初始 switch_to 函数栈帧
int n = 1013;
// 一下都是寄存器的值, 这里并没有用, 可以顺便赋值
thread1[1013] = thread1[1014] = thread1[1015] = thread1[1016] = 0;
thread1[1017] = thread1[1018] = thread1[1019] = thread1[1020] = 0;
// 返回的是 thread_start 的地址
// thread_start 函数栈帧,刚进入 thread_start 函数的样子
thread1[1021] = (int)thread_start;
// thread_start 执行结束,线程结束
thread1[1022] = 0;
// thread_start 函数的参数
thread1[1023] = 1;
...
}
1013往下的连续用来存储通用寄存器等的值(eax, ebx, di, ZF), 但是这些数据我们实际用不到, 可以随便进行赋值, 重要的是下面的.
接下来的20行线程栈用保存一个函数的地址, 这是为了我们在线程返回时能够回到我们指定的函数(thread_start)中, 这样不就实现了指定函数的调用了吗. 23行是函数 调用结束ret
返回的数值, 这里我们也没有具体使用, 可以随便修改; 26行, 也就是传入的是我们需要的传入线程函数的参数, 这里第一个线程函数向thread_start
中传入的是1.
再接下来线程2也是同样的操作
int main() { ...
/*----------- 线程2 ----------*/
// 创建 thread2 线程
// 初始 switch_to 函数栈帧
n = 1013;
for(int i = 0; i < 8; i++)
thread2[n++] = i; // 这一句现在要不要都是一样的, 重要的是下面的三句
// 返回的是 thread_start 的地址
// thread_start 函数栈帧,刚进入 thread_start 函数的样子
thread2[n++] = (int)thread_start;
// thread_start 执行结束,线程结束
thread2[n++] = 0;
// thread_start 函数的参数
thread2[n++] = 2;
...
}
操作基本一样, 除了18行向thread_start
函数中传入的是2.
4. 线程调度
完成上面所有初始化准备工作后, 那么怎么实现我们的线程调度呢? 还记得汇编的实现的函数功能的函数名不? 对, 就是switch_to
函数, 直接调用调度函数进行线程调度.
int main() { ...
switch_to(1);
}
所有的工作准备完成, 我们简单的线程切换就能够实现了.
5. 运行结果
直接make
后生成可执行文件main, 运行程序将一直在两个线程之间进行切换.
rpz@0505:mythread1$ ./main
thread1
thread2
thread1
thread2
thread1
thread2
thread1
小结
本节也就基本实现了对线程切换的功能了, 但是你可能对这样的写法感到不愉悦, 毕竟操作都很暴露, 没有封装性, 那么你可以适当的重新编写一个封装性更好, 或者再加上时间片起步更好, 这些都是可以完成的.