上一章中,我们的操作系统已经支持内核共享,这为任务的加载和运行做好了准备。
本章将要实现的是0特权级任务的加载与任务切换。
11.1 任务切换的原理
11.1.1 协同式与抢占式任务切换
如果CPU上只运行着Kernel.c
的main
函数,那么情况非常简单,只需要不断执行下一条指令即可。然而,如果现在有不止一个任务需要运行,CPU就必须在这几个任务之间不断切换,使每个任务都能得到运行的机会。那么,CPU在何时进行任务切换?又怎么进行任务切换呢?
最简单的任务切换方案被称为协同式任务切换。这种方案的运作方式为:操作系统提供一种任务切换的方法,各个任务均应在合适的时机主动使用这个方法,完成任务切换。协同式任务切换的优点是效率高且灵活,通过精心设计任务切换的时机,可以最大限度的利用CPU。但其缺点也很明显:又是"合适的时机",又是"主动",都是非强制的手段,一个任务完全可以永远不进行任务切换,让CPU一直为自己服务。因此,任务切换应当是一个具有周期性和强制性的过程。
时钟中断很适合被用于任务切换。这是因为,一方面,时钟中断的发起具有周期性;另一方面,外中断的发起具有强制性,不受任务的控制。因此,可以在时钟中断发生期间进行任务切换。
这种由硬件强制进行的任务切换,被称为抢占式任务切换。
11.1.2 任务队列与任务控制块
想要实现任务切换,就需要有一个能存取任务的数据结构。当进行任务切换时,先将当前任务添加到此数据结构,再从中取出一个新任务,并切换到这个新任务。队列是实现任务切换的合适数据结构,其可使用链表实现。
在这个队列中,每个任务都是一个节点,这个节点由链表指针和其他信息构成,其被称为任务控制块(Task Control Block,TCB)。TCB的设计目标是:只要拿到TCB,就能得到这个任务的全部信息。
11.1.3 任务的执行环境
任务对任务切换的发生必须是无感知的。所以,在任务切换时,当前任务的执行环境需要被保存起来,以供将来恢复。
一个任务的执行环境包含以下内容:
- 8个通用寄存器
- 6个段寄存器
- EFLAGS
- EIP
- CR3
- 虚拟地址位图
也就是说,只要能在任务切换时将任务的这些内容保存好,其就能恢复到任务切换前的状态,且对任务对此毫无感知。
中断发生时,CPU会自动将EFLAGS、CS、EIP压栈,然后进入中断处理函数。这其实意味着:任务的一部分信息已经保存在栈中了。而TCB的设计目标是:只要拿到TCB,就能得到这个任务的全部信息。所以,一个非常巧妙的设计是:将任务的栈和TCB放置在同一页的两头,这样一来,只要任务进行了至少一次压栈(在中断发生时一定如此),就能通过ESP & 0xfffff000
得到TCB的地址。所以,此时可以继续将8个通用寄存器压栈。由于6个段寄存器对于每个任务来说都是一样的,所以无需压栈。
现在还剩下CR3和虚拟地址位图,这两个信息可以保存在TCB中。并且,其在任务的运行期间是不变的,所以,不需要在每次任务切换时重复保存。
至此,ESP就成了任务恢复的关键,只要拿到ESP,就能得到TCB和任务的栈,进而将任务恢复。所以,ESP的当前值也需要保存在TCB中。
综上,TCB中保存的信息如下:
TCB + 0x1000
处是任务的栈顶。栈中保存有EFLAGS、CS、EIP以及8个通用寄存器- CR3
- 虚拟地址位图
- ESP
11.1.4 任务切换的完整过程
综上,任务切换的完整过程如下:
- 由时钟中断发起任务切换
- 执行
pusha
指令,将任务的8个通用寄存器压栈 - 发送中断响应信号
- 通过
ESP & 0xfffff000
取得任务的TCB - 将ESP保存在TCB中
- 将TCB添加到任务队列中
- 从任务队列中取出新的TCB
- 将ESP和CR3用新的TCB中的值覆盖
- 执行
popa
和iret
指令,切换到新任务
11.1.5 新任务的创建
上文一直在讨论任务切换。然而,任务切换有一个隐含的前提:任务在切换时应当是正在运行的,这样才谈得上切换。
内核在任务切换时确实是正在运行的,但对于一个从来没有运行过的新任务,该怎么办呢?
一个非常巧妙的办法是:伪造这个新任务的TCB,使其好像是先前被切换过一样。这样,一个新任务就可以"混入"任务队列中了。具体来说,新任务的创建分为以下几个步骤:
- 分配3页,分别作为新任务的TCB、CR3以及虚拟地址位图
- 将内核页目录表的第768~1022项复制到任务的CR3中,并将任务的CR3的最后一个PDE指向其自己
- 伪造新任务的栈。在任务切换时,栈顶从上往下依次是EFLAGS、CS、EIP以及8个通用寄存器,一共
11 * 4
字节。所以,TCB中存放的ESP应设为TCB地址 + 0x1000 - 11 * 4
。然后,在TCB的顶部填好这些寄存器的值,当任务启动时,这些值就是各个寄存器的初始值 - 初始化虚拟地址位图
- 此时,新任务的TCB已经和其他任务的TCB没有区别了。所以,将其添加到任务队列中
11.1.6 内核任务
内核本身也是一个任务,也需要参与任务切换。并且,由于内核确实是一个正在运行的任务,所以不需要伪造栈,只需要设置好CR3和虚拟地址位图即可。
事实上,内核的TCB已经在上一章中准备好了,它位于0xc009f000
。上一章Mbr.s
中的mov esp, 0xc00a0000
正是出于这个目的。
11.2 任务切换的实现
11.2.1 任务队列
想要实现任务切换,就需要先实现一个队列,队列的底层可使用链表实现。
队列的实现位于本章代码11/Queue.h
和11/Queue.hpp
中。这套实现与普通链表唯一的区别在于:在queueEmpty
函数,queuePush
函数以及queuePop
函数的头尾增加了开关中断的指令。这是一种最简单的锁,可以保证这三个函数在运行期间不会发生任务切换,从而避免了由于任务切换而引发的错误。
11.2.2 任务切换
请看本章代码11/Task.h
。
第7~13行,定义了TCB结构体。
第16行,声明了外部链接的任务队列。
第18~20行,声明了任务模块中的各种函数。
接下来,请看本章代码11/Int.s
。
第4~6行,声明了外部链接的queuePush
函数,queuePop
函数以及任务队列taskQueue
。
intTimer
函数是任务切换的核心。
第106行,将8个通用寄存器压栈。
第108~110行,向8259A发送中断响应信号。
第112~113行,取得TCB的地址。
第115行,将ESP存入TCB中。
第117~120行,调用queuePush
函数,将TCB添加到任务队列中。
第122~124行,调用queuePop
函数,从任务队列中取出一个新的TCB。
第126~127行,将CR3切换到新任务上。
第129行,将ESP切换到新任务上。
第131行,将8个通用寄存器切换到新任务上。
第133行,将EFLAGS,CS,EIP切换到新任务上。
至此,任务切换完成。
11.2.3 安装内核任务
请看本章代码11/Task.hpp
。
第9行,定义了任务队列taskQueue
。任务切换时,当前任务会被添加到这个队列,新任务会从这个队列中取出。
__installKernelTask
函数用于安装内核TCB。
第13行,取得位于0xc009f000
处的内核TCB。
第15行,将内核页目录表的物理地址0x100000
填入TCB。
第17行,初始化内核的虚拟地址位图。这行代码曾经位于Memory.hpp
的memoryInit
函数中。
taskInit
函数是queueInit(&taskQueue)
与__installKernelTask
函数的封装。
接下来,请看本章代码11/Kernel.c
。
第24行,调用taskInit
函数,完成任务模块的初始化。
11.2.4 新任务的创建
请看本章代码11/Task.hpp
。
getTCB
函数使用ESP & 0xfffff000
取得TCB。
__getEFLAGS
函数用于取得EFLAGS的值。
loadTaskPL0
函数用于创建新任务。
第55行,分配3页。第1页用于新任务的TCB;第2页用于新任务的CR3;第3页用于新任务的虚拟地址位图。
第59行,使用第8章中的公式得到CR3的物理地址。
第64行,将新任务的页目录表清空。
第65行,将内核页目录表的第768~1022项复制到新任务的页目录表中。这里同样使用了第8章中的技术。memcpy
函数的实现位于本章代码11/Memory.hpp
中。
第67行,将新任务的页目录表的最后一项指向自己。
第69~83行,伪造新任务的栈。需要注意的是:新任务的EFLAGS中的IF位(第9位)必须为1;否则,在第一次切换到新任务后,就不会再发生任务切换了。
第85行,初始化新任务的虚拟地址位图。
第87行,将新任务的TCB添加到任务队列中。
11.2.5 内存管理系统的微调
引入TCB后,任务的虚拟地址位图被迁移到TCB中。所以,内存管理系统需要一些微调。
请看本章代码11/Memory.h
。
第6行,声明了memcpy
函数。
接下来,请看本章代码11/Memory.hpp
。
原__vMemoryBitmap
全局变量,以及memoryInit
函数中对此变量的初始化代码已删除;allocateKernelPage
函数和deallocateKernelPage
函数中的&__vMemoryBitmap
现在修改为&((TCB *)0xc009f000)->vMemoryBitmap
。
memcpy
函数是本章新增的函数,其用于内存复制。
11.3 测试
本章代码11/Kernel.c
以__testTask
函数作为测试任务,并创建了两个这样的任务。所以,输出结果中Task
字符串的数量应为Kernel
字符串的两倍。在打印字符串时使用了开关中断的指令,以避免由于任务切换而引发的错误。
我们的操作系统目前还不支持任务回收,所以,用于测试的任务不能退出。
标签:11,操作系统,级任务,队列,任务,切换,TCB,函数 From: https://www.cnblogs.com/yingyulou/p/17825524.html