引言
- 我们的目标是仅仅就是创建一个任务,然后运行这个任务吗?
- 显然不是,我们的目标是要多个任务同时运行
- 问题:创建两个任务:TASK A 和 TASK B,执行 TASK A,那么,什么时候执行 TASK B 呢,又由谁来进行任务切换执行呢?
任务切换过程
- TASK A 执行过程中,如果不被打断,那么 TASK B 就永远不会执行,需要在 TASK A 运行期间无条件的将其打断,如何打断呢?利用时钟中断打断任务的执行,而这个时钟中断我们在上一个章节中已经实现了
- 打断了之后又要做什么工作吗?很显然,打断之后还必须保存打断前 TASK A 的运行状态,我们将其称之为保存上下文
- 接下来就是要切换到 TASK B 执行,如何做到呢?答案就是恢复 TASK B 的上下文
- 继续看一下更加详细的过程
目标
- 使用时钟中断打断任务(每个任务执行固定的时间片)
- 中断发生后立即进入中断服务程序
- 在中断服务程序中完成上下文的保存并切换任务
- 接下来就来分步实现代码
任务与时钟中断同时实现
- 前面我们已经单独实现了 TASK A 与时钟中断,接下来就让它们两个同时实现在同一个程序中
- 就在上一章节的代码基础上改动吧,上一章节已经实现了时钟中断,我们再加上任务 TASK A 的相关代码就可以了,见 main.c
- 看起来一切都好,然而,当我们运行起来后,发现只有 TASK A 任务在执行,中断貌似并没有执行,什么原因呢?
- 哈哈哈,深入查找原因,发现在创建任务 TaskCreat 函数中,eflags 被赋值为 0x3002(IPOL=3), 其中 bit9 IF 位并未被置 1, IF 为 0 表示 CPU 不使能外部中断,把 IF 位置 1
task->reg.eflags=0x3202; // IOPL=3: 允许任务(特权级 3)进行 /O 操作; IF=1: 允许外部中断
- 编译运行,感觉应该没问题了,然而实际上问题好像更严重了,这次不光中断没有执行,就连 TASK A 任务好像也没有执行,懵逼树上懵逼果,怪事练练
- Ctrl + C 退出程序,使用 “reg” 命令,发现 esp 寄存器的值为 0x1b9357e4, 莫名其妙的一个值,并不是任务栈,也不是内核栈,这说明是栈出问题
- 继续思考,当中断发生时,程序由特权级 3 跳转到特权级 0,CPU 使用栈由使用任务栈转到使用内核栈,而内核栈的是由 TSS 决定的,于是我们查看 loader.asm 中的 TSS,发现 esp0 的值为 0,栈顶怎么能是 0 呢,我们将内核栈顶设置为 0x7c00(BOOT_START_ADDR)
TSS_SEGMENT: dd 0 dd BOOT_START_ADDR ; esp0 dd DATA32_FLAT_SELECTOR; ss0 dd 0 ; esp1 dd 0 ; ss1 dd 0 ; esp2 dd 0 ; ss2 times 4 * 18 dd 0 dw 0 dw $ - TSS_SEGMENT + 2 db 0xFF TSS_LEN equ $ - TSS_SEGMENT
- 再次编译运行,哈哈,这次终于一切 OK,看一下效果
深入思考
- 上面的中断服务程序和任务看起来都成功执行了,似乎并没什么错误,那么,整个过程就一定是正确的吗?
- 中断服务程序仅完成了逻辑功能,但在中断发生时并没有保存上下文,在中断发生后也没有恢复上下文
中断服务程序的重新设计
- 中断发生时,立即保存上下文(寄存器)
- 逻辑功能实现
- 中断返回时恢复上下文
保存上下文
- 任务切换的整个流程我们也已经有了一定的了解,各部分代码也基本上都实现过了,就剩下最后一个功能,保存上下文
- 在 进程的初步实现 中,我们已经学习过了如何恢复上下文,那么保存上下文就是跟恢复上下文反着操作而已
- 首先我们要做的工作就是在中断发生时,让栈顶指针 esp 指针指向 reg 数据结构的末尾,不然中断发生时,① 中的 5 个寄存器会被入栈到内核栈中,这显然不是我们想要的
- 如何才能实现中断发生时,栈顶指针 esp 指向 reg 数据结构的末尾呢?
- 方法:把 reg 数据结构的末尾地址值放到 TSS 中 esp0 处即可
- 问题又来了, TSS 在 loader 中实现,现在在内核中,找不到 TSS 位置
- 解决方案:共享内存,在 “loader.asm” 把 TSS 位置信息放入共享内存中,在内核 “share.h” 宏定义其位置就可以了
; loader.asm 中 PutDataToShare: ... ; 将 TSS 基地址放到共享内存 TSS_ENTRY_ADDR 中 mov dword [TSS_ENTRY_ADDR], TSS_SEGMENT ; 将 TSS 大小放到共享内存 TSS_SIZE_ADDR 中 mov dword [TSS_SIZE_ADDR], TSS_LEN ... ret // share.h 中 #define TSS_ENTRY_ADDR (SHARE_START_ADDR + 24) #define TSS_SIZE_ADDR (SHARE_START_ADDR + 28)
- TSS 数据结构我们还没有定义,在 “desc.h” 中定义一下吧
typedef struct TSS { U32 previous; // 上一个任务链接(TSS 选择符) U32 esp0; // 内核栈顶 U32 ss0; // 内核栈基址 U32 unused[22]; // 不使用 U16 reserved; // 保留 U16 iomb; // I/O 位图基地址 } TSS;
- 利用指针很容易就实现对 TSS 中 esp0 进行修改
TSS* tss = (TSS*)(*(U32*)TSS_ENTRY_ADDR); // 找到 TSS tss->esp0 = (U32)(&taskA.reg) + sizeof(taskA.reg); // 修改 TSS.esp0 = reg 的末尾
增加保存与恢复上下文代码实现
- 任务切换的所有部分知识都已经了解了,接下来就来真正的实现代码吧
- 主要相关代码:interrupt.asm、main.c、desc.h
- 在任务切换 0x20 号中断服务程序如下:
Int0x20_Entry: ; 保存上下文, ss esp eflags cs eip 这 5 个寄存器已被 CPU 自动入栈保存 pushad ; 保存通用寄存器 push ds push es push fs push gs mov esp, KERNEL_STACK ; 重新指定栈顶 esp 到内核栈,以供接下来的逻辑功能代码部分使用 call Int0x20Handle ; 中断逻辑功能 mov esp, [gRegAddr] ; 使栈顶 esp 指向上下文数据结构 reg 的起始位置 ; 恢复上下文 pop gs pop fs pop es pop ds popad ; 恢复通用寄存器 iret
- 由于中断前我们就已经将 TSS.esp0 修改位任务 TASK A 的上下文 reg 的末尾,CPU 进入中断时会自动将 ss esp eflags cs eip 这 5 个寄存器入栈,即上面图中 ①
- 接下来的 ② ③ 保存上下文工作就好理解了
- 再往后是 “mov esp, KERNEL_STACK” 执行语句,它是放在 “call Int0x20Handle” 之前的,因为此时 esp 经过上面的步骤后已经指向了 reg 数据结构的起始位置,接下来再进行入栈操作的话就会破坏 reg 上面的内存区数据,所以必须重新设置一下 esp 的值(KERNEL_STACK 值为 0x7c00)
- “call Int0x20Handle” 这条语句没什么说的,这是中断逻辑功能部分
- 再往下就是要恢复中断上下文,想要恢复上下文,关于恢复上下文我们在 进程的初步实现 中已经做过详细介绍了
- 想要恢复上下文,首先就是找到 reg 的起始位置,然而目前情况是好像并不能找到 reg 的起始位置
- 于是就利用全局变量来记住 reg 的起始位置
gRegAddr=(U32)&taskA.reg;
- 在有了上面的赋值操作后, “mov esp, [gRegAddr]” 这个语句就很好理解了吧,就是使得 esp 指向 reg 的起始位置
- 再往下的语句就完全是我们前面实现过的恢复上下文了
- 没图没真相,必须看一下运行效果图,虽然效果跟没有保存上下文和恢复上下文的一样
再加一个任务 TASK B
- 多任务并行执行,最少也得实现两个任务吧,代码见:main.c
- 参照 TASK A 的代码,复制一个 TASK B,为了区分 TASK A,我们让 TASK B 循环打印 26 个字母
TASK taskB = {0}; // 任务对象 U08 taskB_stack[512]; // 任务私有栈 void TaskBFunc(void) // 任务执行函数 { static U32 count = 0; while(1) { count++; if(count % 1000 == 0) { static U32 j = 0; SetCursorPos(0, 6); printk("TASK B: %c\n", (j++%26)+'A'); } } }
- 调用任务创建函数进行 TASK B 的初始化
TaskCreat(&taskB, TaskBFunc, taskB_stack, 512, "Task B"); // 创建任务 TASK B
- 任务切换是借助时钟中断实现的,所以任务切换代码肯定要写在时钟中断服务程序的逻辑函数
• Int0x20Handle 中 volatile TASK* gTask = NULL; void Int0x20Handle(void) { static U32 count = 0; if(count++ % 5 == 4) { gTask = (gTask == &taskA) ? &taskB : &taskA; TSS* tss = (TSS*)(*(U32*)TSS_ENTRY_ADDR); // 找到 TSS tss->esp0 = (U32)(&gTask->reg) + sizeof(gTask->reg); // TSS.esp0 指向任务上下文数据结构 reg 的末尾 gRegAddr = (U32)(&gTask->reg); // gRegAddr 指向任务上下文数据结构 reg 的起始位置 } write_m_EOI(); }
- 下面这条语句的作用是让 TSS.esp0 指向将要跳转的任务的上下文末尾,其作用是在下一次中断服务程序 Int0x20_Entry 进入时保存上下文
tss->esp0 = (U32)(&gTask->reg) + sizeof(gTask->reg);
- 下面这条语句的作用是使 gRegAddr 指向任务上下文数据结构 reg 的起始位置,当 Int0x20Handle 函数执行结束后,程序将执行 Int0x20_Entry 的下半部分,即恢复任务上下文,而恢复上下文的前提条件就是 esp 指向任务上下文数据结构 reg 的起始位置
gRegAddr = (U32)(&gTask->reg);
- 上图
代码优化
- main 函数里面有点乱,简单优化一下
- 重新在 “task.c” 文件中定义变量,替代 gTask 和 gRegAddr,并在 “task.h” 文件中使用 extern 声明这两个变量
volatileTASK*current_task=NULL; // 当前任务指针,永远指向当前任务; current_task 代替 gTask
volatileU32current_reg; // 当前任务的上下文起始位置; current_reg 代替 gRegAddr
- 把 main 函数里面的杂乱语句整理放到一个函数中,该函数接口设计如下
- 函数名称: E_RET TaskStart(TASK* task0)
- 输入参数: TASK* task0 --任务指针
- 输出参数: 无
- 函数返回: E_OK:成功; E_ERR:失败
- 其它说明:想要启动所有任务,只要启动第一个任务就可以了,其它任务将由任务调度启动
- 函数具体实现:
E_RET TaskStart(TASK* task0) { // 检查参数合法性 if(NULL == task0) return E_ERR; current_task = task0; // 当前任务指针指向 task0 TSS* tss = (TSS*)(*(U32*)TSS_ENTRY_ADDR); // 找到 TSS tss->esp0 = (U32)(&task0->reg) + sizeof(task0->reg); // TSS.esp0 指向 task0 的上下文数据结构 reg 的末尾 current_reg = (U32)&task0->reg; // current_reg 指向任务上下文数据结构 reg 的起始位置 asm volatile("sti"); // 开中断 SWITCH_TO(task0); // 切换到 TASK A 执行 return E_OK; }
- 因为变量名更改,所以 Int0x20Handle 函数和 Int0x20_Entry 中都要记得修改。
- 改动后的 main.c
解决打印异常
- 程序长时间运行后,出现了下面异常现象,打印出现了异常
- 本以为是进程切换代码有问题,查了好久,最终确定进程切换代码并没有问题。打印是需要硬件支持的,一个进程正在打印时被切换到了另一进程,此时另一个进程也需要打印,两个打印同时进行,硬件并不支持这种的情况
- 优化代码,在打印相关需要硬件参与的代码前加上关中断操作,在打印结束后开中断,保证硬件执行过程不被中断打断
- 在相关硬件操作前后加上下面代码,用了 eflags_c 是为了记住硬件操作前的状态,执行完毕后要恢复到之前的状态
// 获取 eflags 寄存器的值放到 eflags_c 变量中 asm volatile("pushf;popl %%eax":"=a"(eflags_c)); // 关闭外部中断 asm volatile("cli"); // 硬件操作,省略... // 若 bit9(IF 位) 为 1,则开启外部中断 if(EFLAGS_IF(eflags_c)) asm volatile("sti");
- 打印函数只优化了 printk,其它打印相关函数并没有优化,主要是因为懒,那么此时 “print.h” 中相关打印函数声明就去掉了,只保留 printk 这一个接口函数以供使用,统一了也挺好,省的乱七八糟的
- 另外,在设置光标位置 SetCursorPos 中端口操作后加了一定的延时,给硬件一定的处理时间
- 稍不注意还有遗漏,在 “main.c” 中, SetCursorPos 和 printk 同时使用,也有极低概率出现 SetCursorPos 刚设置完光标位置,又被切换到其它任务的 printk 处执行,此时也会出现打印异常情况,最好是在整个设置光标和打印期间都关闭外部中断
asm volatile("cli"); SetCursorPos(0, 4); printk("change: %d\n", count); asm volatile("sti");