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 的起始位置
再往下的语句就完全是我们前面实现过的恢复上下文了
没图没真相,必须看一下运行效果图,虽然效果跟没有保存上下文和恢复上下文的一样
多任务并行执行,最少也得实现两个任务吧,代码见: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);
上图
volatile TASK* current_task = NULL; // 当前任务指针,永远指向当前任务; current_task 代替 gTask
volatile U32 current_reg; // 当前任务的上下文起始位置; current_reg 代替 gRegAddr
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;
}
程序长时间运行后,出现了下面异常现象,打印出现了异常
本以为是进程切换代码有问题,查了好久,最终确定进程切换代码并没有问题。打印是需要硬件支持的,一个进程正在打印时被切换到了另一进程,此时另一个进程也需要打印,两个打印同时进行,硬件并不支持这种的情况
优化代码,在打印相关需要硬件参与的代码前加上关中断操作,在打印结束后开中断,保证硬件执行过程不被中断打断
在相关硬件操作前后加上下面代码,用了 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");
跟硬件有关的问题查起来都很难,没啥道理可言,这个调好了换一个硬件又有其它的问题,就不深入纠结了,就此 over
还有更好的优化方式,那就是给一个较大的打印数据循环缓冲区 + 硬件 DMA,只不过这种方式太麻烦了,我懒得搞,现在将就用吧,别频繁打印,慎用打印
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。