在前面的两章中,我们的操作系统均不支持任务回收,所以任务不能退出。本章将要实现的是任务回收功能。
13.1 任务回收的原理
如果一个任务位于任务队列中,其就会被运行。所以,如果一个任务的运行已经结束,它就应该从任务队列中删除。
仅仅将任务从任务队列中删除是不够的,这是因为任务还持有一些内存没有释放,这包括以下两部分:
- 任务地址空间中的页。包括3特权级栈、任务的加载地址,以及任务在运行期间申请的内存(我们的操作系统不支持此操作)
- 任务加载时分配的3页(
loadTaskPL0
函数和loadTaskPL3
函数的第一行代码)
回收这两部分内存都不是难事,但问题是,谁来回收这两部分内存?是内核,还是任务自己?
如果由内核负责回收,那么,想要回收任务地址空间中的页,就需要临时切换到任务的CR3(就像任务加载时那样)。这个方案是可行的,只是略显麻烦。感兴趣的读者可以自行尝试。
如果由任务负责回收,那么,任务地址空间中的页可以直接回收,不需要切换CR3。但内核空间中的3页如果也由任务回收的话,将导致任务的0特权级栈立即失效,而此时,任务的回收还没有彻底结束,必然存在至少一条需要使用0特权级栈的指令,如ret
,这条指令一旦执行,就会引发页错误。所以,这个方案是不可行的。(这段描述可能会令读者感到困惑,读者不妨先跳过这段话,在实现了任务回收以后再行理解)。
综上,由任务回收任务地址空间中的页比较方便,而内核中的3页必须由内核回收。因此,可将任务的回收分为两部分:
- 任务负责回收任务地址空间中的页
- 内核负责回收任务加载时分配的3页
任务切换是基于任务队列进行的。同理,任务回收也可以基于一个队列进行,不妨称之为退出队列。任务退出时,需要先回收任务地址空间中的页,然后将TCB添加到退出队列。另一方面,内核需要定期查看退出队列,如果其中存在TCB,就将其取出,并回收这个任务的3页内存。
13.2 任务回收的实现
13.2.1 回收任务地址空间中的页
请看本章代码13/Memory.h
。
第11行,声明了deallocateTaskCR3
函数。
接下来,请看本章代码13/Memory.hpp
。
deallocateTaskCR3
函数是本章新增的函数,其用于回收任务地址空间中的页。页的回收从PDE开始,如果PDE的P位为0,就可以直接跳过这个PDE;如果PDE的P位为1,就需要进一步遍历其中的1024个PTE,如果PTE的P位为1,就需要回收PTE指向的页,最后,还要回收PDE指向的页。
第141行,循环768次,每次考察一个PDE。768个PDE对应的是3G的任务地址空间。
第143行,使用第8章中的公式取得PDE指针。
第145行,判断PDE的P位。
第147行,遍历当前PDE中的每一个PTE。
第149行,使用第8章中的公式取得PTE指针。
第151行,判断PTE的P位。
第153行,从物理地址位图中删除PTE指向的页。PTE带有页属性,需要去除后才是物理地址。由于任务马上就要被回收了,所以虚拟地址位图无需修改。
第157行,从物理地址位图中删除PDE指向的页。同样的,PDE带有页属性,需要去除后才是物理地址,且不需要修改虚拟地址位图。
13.2.2 回收任务加载时分配的页
请看本章代码13/Task.h
。
第19行,声明了外部链接的退出队列exitQueue
。
第25行,声明了deleteTask
函数。
接下来,请看本章代码13/Task.hpp
。
第13行,定义了退出队列exitQueue
。
第51行,对退出队列进行初始化。
deleteTask
函数是本章新增的函数,其用于回收任务加载时分配的3页。
任务加载时,使用的是allocateKernelPage(3)
以分配连续的3页,且第一页被用于TCB。因此,回收任务时,可以从退出队列中取出一个TCB,然后使用deallocateKernelPage
函数直接回收连续的3页。
第216行,循环直至退出队列为空。
第218行,从退出队列中取出一个TCB,然后回收连续的3页。
接下来,请看本章代码13/Kernel.c
。
第22行,在循环中不断调用deleteTask
函数,从而定期回收退出的任务。
第24行,将CPU挂起,等待外中断的唤醒。
13.2.3 任务退出系统调用
请看本章代码13/Int.s
。
第9~10行,声明了外部链接的退出队列exitQueue
,以及deallocateTaskCR3
函数。
taskExit
函数是本章新增的函数。这是一个系统调用,用于任务退出。
第162行,调用deallocateTaskCR3
函数,回收任务地址空间中的页。
第164~165行,取得TCB地址。
第167~170行,将TCB添加到退出队列中。
第172~190行与时钟中断处理函数的后半部分,即第130~148行一致。用于从任务队列中取出一个TCB并切换到这个TCB。
第248行,保留系统调用号1给后续章节使用(读取键盘输入的系统调用)。
第249行,将taskExit
函数安装到系统调用表中,其系统调用号为2。
13.2.4 任务的自动回收
现在,2号系统调用已经安装,任务应在退出时发起这个系统调用。即,任务的代码应当是这样:
int main()
{
// ...
__asm__ __volatile__(
"int 0x30\n\t"
:
: "a"(2)
);
}
然而,要求每个main
函数都在最后加这样一段代码是不现实的。这段代码应在任务退出时自动执行,这就是_start
存在的意义。_start
是一个任务真正的入口,main
函数是由_start
调用的,因此,在调用main
函数的前后,还能进行其他操作。
请看本章代码13/Start.s
。
第1行,将编译模式设定为32位。
第3行,声明外部链接的main
函数。
第5行,将_start
声明为外部链接的。
第7行,开始定义_start
。
第9行,调用main
函数。
第11~12行,在main
函数退出后,自动发起2号系统调用。
接下来,请看本章代码13/Makefile
。
第6行,编译Start.s
。
第9行,将Test.o
与Start.o
共同链接。此时,不再需要-e main
。
现在,每个任务都需要与Start.o
共同链接,以具备自动发起2号系统调用的能力。在实际的操作系统中,含有_start
的库会被链接器自动使用,所以,用户无需显式的写出这个库。
13.3 测试
本章使用的测试任务是13/Test.c
。现在,我们的操作系统有任务的自动回收功能了,所以,任务可以正常退出,不再需要无限循环了。