进程之间互相切换
1.简介
上一节 我们初步介绍了进程相关的具体概念
特别是讲解了进程切换相关的数据结构 也就是TSS
也实现了进程的自我切换
本节 我们看看如何从当前的进程切换到新进程 然后再切换回来
进程A -切换->进程B-切换->进程A.
2.代码
先看看进程B的实现 一个进程主要包含一个主函数 我们把进程B的主函数实现如下
void task_b_main(void) { showString(shtctl, sht_back, 0, 144, COL8_FFFFFF, "enter task b"); struct FIFO8 timerinfo_b; char timerbuf_b[8]; struct TIMER *timer_b = 0; int i = 0; fifo8_init(&timerinfo_b, 8, timerbuf_b); timer_b = timer_alloc(); timer_init(timer_b, &timerinfo_b, 123); timer_settime(timer_b, 500); for(;;) { io_cli(); if (fifo8_status(&timerinfo_b) == 0) { io_sti(); } else { i = fifo8_get(&timerinfo_b); io_sti(); if (i == 123) { showString(shtctl, sht_back, 0, 160, COL8_FFFFFF, "switch back"); taskswitch7(); } } } }
进程B函数的逻辑是这样的
当进入到进程B后 通过它的主函数现在桌面上打印出一个字符串”enter task b”
当这个字符串出现在桌面时 表示进程完成了切换
然后它初始化一个时钟 这个时钟超时是五秒 五秒过后 它调用函数taskswitch7重新切回到进程A
进程A就是主入口函数CMain 既然要切换进程B
那显然 我们需要一个描述进程B的TSS结构并进行相应的初始化
代码如下
int addr_code32 = get_code32_addr(); tss_b.eip = (task_b_main - addr_code32); tss_b.eflags = 0x00000202; tss_b.eax = 0; tss_b.ecx = 0; tss_b.edx = 0; tss_b.ebx = 0; tss_b.esp = 1024;//tss_a.esp; tss_b.ebp = 0; tss_b.esi = 0; tss_b.edi = 0; tss_b.es = tss_a.es; tss_b.cs = tss_a.cs;//6 * 8; tss_b.ss = tss_a.ss; tss_b.ds = tss_a.ds; tss_b.fs = tss_a.fs; tss_b.gs = tss_a.gs;
上面的代码需要详细解释下
首先我们把tss_b.eflags设置成0x202
这个值可以当做一个写死的值
然后 我们把进程B的段寄存器设置成跟A一样
我们看看进程A的各个段寄存器分别指向哪个全局描述符 tss_a.cs 的值是8
对应全局描述符表的下标就是1(数值要除以8,上一节讲解过)
下标为1的描述符是这样的
LABEL_DESC_CODE32: Descriptor 0, 0fffffh, DA_CR | DA_32 | DA_LIMIT_4K
这个描述符指向一段内存 这段内存的性质是可执行代码段
这段内存的起始地址在内核的汇编部分进行了初始化 如下
xor eax, eax mov ax, cs shl eax, 4 add eax, LABEL_SEG_CODE32 mov word [LABEL_DESC_CODE32 + 2], ax shr eax, 16 mov byte [LABEL_DESC_CODE32 + 4], al mov byte [LABEL_DESC_CODE32 + 7], ah
上面的代码把描述符指向的内存地址的起始位置设置为LABEL_SEG_CODE32
tss_a.ds 的值为24 除以8后为3 也就是对应描述符在全局描述符表中的下标是3 这个描述符内容如下
LABEL_DESC_VRAM: Descriptor 0, 0fffffh, DA_DRWA | DA_LIMIT_
4K
这个描述符指向的内存起始地址是0 长度为0fffffh
这段内存的性质是可读写数据段 也就是从0到0fffffh 这段长度的内存是可读写的数据
tss_a.ss 的值是32 除以8后得4 因此对应的是下标为4的描述符 该描述符的内容如下
LABEL_DESC_STACK: Descriptor 0, LenOfStackSection, DA_DRWA | DA_32
它描述的是一段32位可读写的内存 长度为LenOfStackSection
它对应的这段内存是我们在内核的汇编部分分配的内存 具体如下
[SECTION .gs] ALIGN 32 [BITS 32] LABEL_STACK: times 512 db 0 TopOfStack1 equ $ - LABEL_STACK times 512 db 0 TopOfStack2 equ $ - LABEL_STACK LenOfStackSection equ $ - LABEL_STACK
上面分配了两个512字节 总共1024字节的内存
LABEL_STACK将会设置成下标为4的描述符所对应内存的起始地址
第一个512字节 作为进程A的堆栈
第二个512字节 将作为进程B的堆栈 上面tss_b的初始化代码中有这么一句:
tss_b.esp = 1024;
它的作用就是让进程把把堆栈指针指向第二个512字节的末尾处 大家要记得
堆栈是有高地址向低地址生长的 所以设置堆栈指针时 要把它指向内存的末尾
在内核的汇编部分 有代码将下标为4的描述符对应的内容起始地址设置为了LABEL_STACK
代码如下
xor eax, eax mov ax, cs shl eax, 4 add eax, LABEL_STACK mov word [LABEL_DESC_STACK + 2], ax shr eax, 16 mov byte [LABEL_DESC_STACK + 4], al mov byte [LABEL_DESC_STACK + 7], ah
最重要的三个段寄存器 cs, ds, ss,设置好
其余寄存器 设置成跟进程A一样即可
接下来最重要的设置是eip指针 这个指针将指向要执行代码的首地址
我们要执行的函数是task_b_main
因此eip应该指向这个函数 但注意 我们不能直接把这个函数的地址直接赋值给eip
eip指向的是相对于代码段起始地址的偏移 当前代码段的其实地址是LABEL_SEG_CODE32
因此我们需要把task_b_main的地址减去LABEL_SEG_CODE32
所得的结果就是相对偏移了
这也是eip初始化的逻辑
tss_b.eip = (task_b_main - addr_code32);
get_code32_addr是内核的汇编部分实现的行数
目的就是返回LABEL_SEG_CODE32对应的地址
实现如下
get_code32_addr: mov eax, LABEL_SEG_CODE32 ret
上一节 我们已经看到
我们通过代码 将一个描述符指向结构tss_b了
代码如下
set_segmdesc(gdt + 9, 103, (int) &tss_b, AR_TSS32);
指向tss_b结构的描述符下标是9 初始化好tss_b后
只要通过一个jmp语句 跳转到下标为9的描述符
那么就能将当前指向进程切换成运行task_b_main的进程了 这个跳转语句实现如下
taskswitch9: jmp 9*8:0 ret
进程A运行的是CMain函数 它会创建一个5秒的计时器 一旦超时 则调用上面的函数实现任务切换
for(;;) { ..... else if (fifo8_status(&timerinfo) != 0) { io_sti(); int i = fifo8_get(&timerinfo); if (i == 10) { showString(shtctl, sht_back, 0, 176, COL8_FFFFFF, "switch to task b"); //switch task taskswitch9(); } ..... }
在跳转前 我们会在桌面上打印出一句switch to task b表示即将进行任务切换
task_b_main的实现我们已经看过了 进入task_b_main后
它会在桌面打印一条语句 表示跳转成功
然后启动一个5秒的计时器 五秒过后 通过taskswitch7重新跳转回进程A
一旦跳转到task_b_main 桌面会打印出相关字符串
然后光标会停止住 等5秒后
进程从task_b_main 切换回进程A,进程A恢复执行
于是在卡死5秒后 在跳转会进程A前
task_b_main会打印出一条语句”switch back”
当这条语句出现在桌面上时 控制器转回到进程A 于是光标会重新开始闪烁
这样的话 我们就实现了进程从A切换到B再从B切换回A的整个流程
3.编译运行