首页 > 其他分享 >一个操作系统的设计与实现——第11章 任务(二):0特权级任务

一个操作系统的设计与实现——第11章 任务(二):0特权级任务

时间:2023-11-12 09:55:38浏览次数:39  
标签:11 操作系统 级任务 队列 任务 切换 TCB 函数

上一章中,我们的操作系统已经支持内核共享,这为任务的加载和运行做好了准备。

本章将要实现的是0特权级任务的加载与任务切换。

11.1 任务切换的原理

11.1.1 协同式与抢占式任务切换

如果CPU上只运行着Kernel.cmain函数,那么情况非常简单,只需要不断执行下一条指令即可。然而,如果现在有不止一个任务需要运行,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中保存的信息如下:

  1. TCB + 0x1000处是任务的栈顶。栈中保存有EFLAGS、CS、EIP以及8个通用寄存器
  2. CR3
  3. 虚拟地址位图
  4. ESP

11.1.4 任务切换的完整过程

综上,任务切换的完整过程如下:

  1. 由时钟中断发起任务切换
  2. 执行pusha指令,将任务的8个通用寄存器压栈
  3. 发送中断响应信号
  4. 通过ESP & 0xfffff000取得任务的TCB
  5. 将ESP保存在TCB中
  6. 将TCB添加到任务队列中
  7. 从任务队列中取出新的TCB
  8. 将ESP和CR3用新的TCB中的值覆盖
  9. 执行popairet指令,切换到新任务

11.1.5 新任务的创建

上文一直在讨论任务切换。然而,任务切换有一个隐含的前提:任务在切换时应当是正在运行的,这样才谈得上切换。

内核在任务切换时确实是正在运行的,但对于一个从来没有运行过的新任务,该怎么办呢?

一个非常巧妙的办法是:伪造这个新任务的TCB,使其好像是先前被切换过一样。这样,一个新任务就可以"混入"任务队列中了。具体来说,新任务的创建分为以下几个步骤:

  1. 分配3页,分别作为新任务的TCB、CR3以及虚拟地址位图
  2. 将内核页目录表的第768~1022项复制到任务的CR3中,并将任务的CR3的最后一个PDE指向其自己
  3. 伪造新任务的栈。在任务切换时,栈顶从上往下依次是EFLAGS、CS、EIP以及8个通用寄存器,一共11 * 4字节。所以,TCB中存放的ESP应设为TCB地址 + 0x1000 - 11 * 4。然后,在TCB的顶部填好这些寄存器的值,当任务启动时,这些值就是各个寄存器的初始值
  4. 初始化虚拟地址位图
  5. 此时,新任务的TCB已经和其他任务的TCB没有区别了。所以,将其添加到任务队列中

11.1.6 内核任务

内核本身也是一个任务,也需要参与任务切换。并且,由于内核确实是一个正在运行的任务,所以不需要伪造栈,只需要设置好CR3和虚拟地址位图即可。

事实上,内核的TCB已经在上一章中准备好了,它位于0xc009f000。上一章Mbr.s中的mov esp, 0xc00a0000正是出于这个目的。

11.2 任务切换的实现

11.2.1 任务队列

想要实现任务切换,就需要先实现一个队列,队列的底层可使用链表实现。

队列的实现位于本章代码11/Queue.h11/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.hppmemoryInit函数中。

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

相关文章

  • 一个操作系统的设计与实现——第10章 任务(一):共享内核
    一直以来,我们的操作系统在启动后,运行的都是Kernel.c中的main函数。只运行这一个函数是不够的,操作系统应当有能力加载并运行其他程序。从本章开始,将使用四章的篇幅讨论操作系统如何加载并运行任务。这里的任务(Task)与进程(Process)是同义词,在操作系统领域中,任务这个词更为常用,请读者......
  • 一个操作系统的设计与实现——第13章 任务(四):任务回收
    在前面的两章中,我们的操作系统均不支持任务回收,所以任务不能退出。本章将要实现的是任务回收功能。13.1任务回收的原理如果一个任务位于任务队列中,其就会被运行。所以,如果一个任务的运行已经结束,它就应该从任务队列中删除。仅仅将任务从任务队列中删除是不够的,这是因为任务还......
  • 20231111
    2023/11/11补昨天vp的906div2补题到E1还是挺不容易的今天vp一场,打了一场,本来想去打周赛玩一下的,结果6点人还在食堂。。。D-Doremy'sConnectingPlan题意:给定两个数字n、c和一个长度为n的数组,现有n个孤立点,第i个孤立点的权值为,现需要通过建边将所有点全部连通。在第......
  • Nov.11
    看到队友游记又伤感了。过去快一个月了,也不知道自己有没有走出来。目前完完全全成为了校oi组的头号小丑,队友们也都一致觉得我不应该学oi的样子。说不定就是自己真的不配学oi?毕竟这光辉战绩没几人有了。很感谢自己的队友们曾经细心教我各种我不会的妙妙算法和妙妙题,或许我......
  • 11.11博客
    今天跟两位同学一起去完成调查报告任务,现在已经完成。晚上的时候1.想了想流程图咋写,写了个总的流程图,接下来该建数据库,把菜单先罗列出来,再分工开干。2.学习javaweb,根据黑马接口文档完成相应功能3.遇到点问题,找1班齐文博帮助了下,自己没仔细看,没注意。......
  • Linux命令(117)之split
    linux命令之split1.split介绍linux命令split是按照指定的大小或行数分割文件。输出文件名为“前缀aa”、“前缀ab”。默认前缀以“x”开头,默认文件大小为1000行2.split用法split[参数]filename[前缀]split参数参数说明-l指定输出文件有多少行-a指定长度的后缀,默认:2-b指定输出文......
  • 11.11 陷哗
    求一个好用的linux音乐播放器,我现在在用elisa早上为了搞PGP密钥搞到了十二点四十,结果最后不知道为啥导入密钥失败了......tails发行版用的GnuPG也是2.2不支持x448的版本。操想着要打省选的别假了......
  • 11.11鲜花
    **楼***,今天看了芙宁娜演示。感觉剧情真的出乎意料,有点刀(不是刃),后面涉及剧透就不再说了,总之编剧真的神还有今天晚上OJ炸了,然后某人惊奇地打开了****(在强烈要求下山区了)开始颓。(一共死了28次)()然后他对着那个若智的建模差点笑死......
  • 每日总结(补11.10)
    今日收获今天主要是在准备11号的比赛啦~下午坐高铁到邯郸啦~看到了好大的雪~我们几个商量题目到12点多了~希望明天取得好成绩嘞~明天预计希望比赛取得好成绩!!......
  • 闲话11.11
    今天打了一场模拟赛,又垫底了......