引言
- 回顾前面章节,我们成功的实现了多任务并行执行,然而这些任务都是一直循环执行,那么系统中的所有任务都是一直不停的执行的吗?
思考
- 任务显然不是必须一直执行的,那么任务执行结束后,又返回到哪里呢?
- 回顾前面所讲的恢复上下文示意图中 ③ eip 寄存器保存任务入口地址,然后通过 iret 指令跳转到任务入口执行
- 很显然,在跳转前,我们并没有将跳转前程序执行地址入栈保存,所以 iret 跳转后是程序没办法返回的
- 可见,当前我们已实现的方案是通过 iret 直接跳转到入口函数执行,并非函数调用,因此无法返回
如何实现返回
- 为什么需要任务返回?
- 任务结束后,我们需要释放任务占用资源,而如何知道任务执行结束呢?答案就是通过任务返回
- 想要破局实现返回,我们可以构造出来一个函数调用,借助函数调用来实现返回
- 听上去很抽象,别着急,我们一点一点具体实现
- 先在 TASK 结构体中新增 TASK_FUNC task_entry 成员
typedef struct TASK { U32 id; // 任务 id U08* name; // 任务名称 U08* stack_addr; // 任务栈基址 U16 stack_size; // 任务栈大小 REG reg; // 任务上下文,即任务执行状态下所有寄存器的值 TASK_FUNC task_entry; // 任务函数入口 } TASK;
- 在 “task.c” 中新增一个函数 TaskEntry
static void TaskEntry(void) { if(current_task) { current_task->task_entry(); } printk("%s end!!!\n", current_task->name); // 调试 // 任务销毁工作 while(1); }
- TaskEntry 函数具体是怎么使用的呢?见 TaskCreat 函数
E_RET TaskCreat(TASK* task, TASK_FUNC pFunc, U08* stackAddr, U16 stackSize, U08* name) { ... // task->reg.eip = (U32)pFunc; task->reg.eip = (U32)TaskEntry; task->task_entry = pFunc; ... }
- 现在的程序逻辑就变成了所有任务的入口都是 TaskEntry 函数,在 TaskEntry 函数中再借助
current_task->task_entry(); 跳转到各自任务函数执行
void TaskDFunc(void) // 任务执行函数 { static U32 count = 0; while(1) { if(count++ % 100000 == 0) { static U32 j = 0; asm volatile("cli"); SetCursorPos(0, 12); printk("TASK D: %x\n", j++); asm volatile("sti"); } if(count > 1000000) // 跳出循环 break; } }
- 测试结果,TASK D 执行结束,其它任务仍在运行
引入系统调用
- 回到 TaskEntry 函数,前面的实验中我们并没有具体实现任务销毁工作,仅仅只是写了个注释,然后 while(1) 循环
- 在实现销毁任务前还需要深入思考一下,在什么情况下才能销毁任务呢?答案:0 特权级,也就是内核态才能执行销毁任务操作
- 思考:TaskEntry 函数执行的特权级是多少? 0 还是 3
- 显然,TaskEntry 是任务入口,那么其必定工作在 3 特权级,那么,其不具备销毁任务的操作权限
- 问题又来了,怎么跳转到 0 特权级呢?
- 答案,哈哈,终于说到点子上了,那就是通过中断跳转到 0 特权级,我们可以使用 0x80 号软中断实现跳转,于是,引入了系统调用
实现第一个系统调用
- 系统调用是任务与内核之间的交互接口
- 涉及特权级的转换(DPL3 --> DPL0)
- 接下来我们将通过 0x80 号中断(软中断)实现系统调用
- 代码见:syscall.c、syscall.h、task.c、interrupt.asm
- 创建 “syscall.c” 和 “syscall.h” 文件,其用于系统调用相关代码,这个不用再多说了
- 在 TaskEntry 函数中添加如下代码触发 0x80 号软中断
asmvolatile("int $0x80\n");
- 不过,需要注意的是,在 “interrupt.asm” 的中断入口 Int0x80_Entry 也需要保存并恢复上下文
extern Int0x80Handle Int0x80_Entry: ; 保存上下文, ss esp eflags cs eip 这 5 个寄存器已被 CPU 自动入栈保存 pushad ; 保存通用寄存器 push ds push es push fs push gs mov esp, KERNEL_STACK ; 重新指定栈顶 esp 到内核栈,以供接下来的逻辑功能代码部分使用 call Int0x80Handle ; 中断逻辑功能 mov esp, [current_reg] ; 使栈顶 esp 指向上下文数据结构 reg 的起始位置 ; 恢复上下文 pop gs pop fs pop es pop ds popad ; 恢复通用寄存器 iret
- 保存上下文和恢复上下文的代码在 Int0x20_Entry 中断入口已经写过一遍了,本着代码不重复的原则,使用汇编宏优化一下代码
%macro BeginISR 0 ; 保存上下文, ss esp eflags cs eip 这 5 个寄存器已被 CPU 自动入栈保存 pushad ; 保存通用寄存器 push ds push es push fs push gs mov esp, KERNEL_STACK ; 重新指定栈顶 esp 到内核栈,以供接下来的逻辑功能代码部分使用 %endmacro %macro EndISR 0 mov esp, [current_reg] ; 使栈顶 esp 指向上下文数据结构 reg 的起始位置 ; 恢复上下文 pop gs pop fs pop es pop ds popad ; 恢复通用寄存器 iret %endmacro Int0x20_Entry: BeginISR call Int0x20Handle ; 中断逻辑功能 EndISR ... Int0x80_Entry: BeginISR call Int0x80Handle ; 中断逻辑功能 EndISR
- 中断都已经触发,然而对应的中断服务程序还没实现,我们把其放到 “syscall.c” 文件中
void Int0x80Handle(U16 ax) { SetCursorPos(0, 14); printk("Int0x80Handle\n"); }
- 万事具备,编译运行看看结果
- 很不幸,程序崩溃了, bochs 提示如下:
interrupt(): soft_int && (gate.dpl < CPL)
- 一看就是特权级的问题,查看一下 “loader.asm”, 发现其中中断描述符表 IDT 的属性特权级为 DA_DPL0,应改为 DA_DPL3
- 再次编译运行,这回终于成功实现打印
- 别着急,这还仅仅是调通了 0x80 号软中断,想要区分不同的系统调用就必须给 Int0x80Handle 函数传参,TaskEntry 函数中触发 0x80 号中断的代码就要改成如下方式,利用 ax 寄存器传参
asm volatile( "movw $0, %%ax\n" "int $0x80\n" : : : "ax" // 告诉 gcc 编译器,ax 寄存器被内嵌汇编使用,需要 gcc 自动添加保护和恢复操作(入栈和出栈) );
- 根据 C与汇编混合编程 中的 C 调用约定,汇编作为主调者,要从右到左入栈,函数调用后还要负责恢复栈,于是 Int0x80_Entry 中断入口处代码如下
Int0x80_Entry: BeginISR push ax call Int0x80Handle ; 中断逻辑功能 add esp, 4 EndISR
- 中断服务程序逻辑代码 Int0x80Handle 中再以 ax 为参数执行不同的函数调用
void Int0x80Handle(U16 ax) { if(0 == ax) { SetCursorPos(0, 14); printk("Kill Task\n"); } }
- 嗯~,到最后我们都没有真正实现销毁任务代码,哈哈,只以打印字符串 "Kill Task" 替代,这是因为目前我们的主要目的是实现系统调用。关于任务销毁的具体实现,待后面章节实现
- 最后看一下代码执行效果