`

《30天自制操作系统》样章 多任务(1)——挑战任务切换(harib12a)

阅读更多

多任务(1) --挑战任务切换( harib12a)

  “话说,多任务到底是啥呢?”我们今天的内容,就从这个问题开始吧。

  多任务,在英语中叫做 “multitask”,顾名思义就是“多个任务”的意思。简单地说,在Windows等操作系统中,多个应用程序同时运行的状态(也就是同时打开好几个窗口的状态)就叫做多任务。

  对于生活在现代社会的各位来说,这种多任务简直是理所当然的事情。比如你会一边用音乐播放软件听音乐一边写邮件,邮件写到一半忽然有点东西要查,便打开 Web浏览器上网搜索。这对于大家来说这些都是家常便饭了吧。可如果没有多任务的话会怎么样呢?想写邮件的时候就必须关掉正在播放的音乐,要查东西的时候就必须先保存写到一半的邮件,然后才能打开 Web浏览器……光想象一下就会觉得太不方便了。

  然而在从前,没有多任务反倒是普遍的情形(那个时候大家不用电脑听音乐,也没有互联网)。在那个年代,电脑一次只能运行一个程序,如果要同时运行多个程序的话,就得买好几台电脑才行。

  就在那个时候,诞生了昀初的多任务操作系统,大家都觉得太了不起了。从现在开始,我们也要准备给“纸娃娃系统”添加执行多任务的能力了。连这样一个小不点儿操作系统都能够实现多任务,真是让人不由地感叹它生逢其时呀。

  稍稍思考一下我们就会发现,多任务这个东西还真是奇妙,它究竟是怎样做到让多个程序同时运行的呢?如果我们的电脑里面装了好多个 CPU的话,同时运行多个程序倒也顺理成章,但实际上就算我们只有一个 CPU,照样可以实现多任务。

  其实说穿了,这些程序根本没有在同时运行,只不过看上去好像是在同时运行一样:程序 A运行一会儿,接下来程序 B运行一会儿,再接下来轮到程序 C,然后再回到程序 A……如此反复,有点像日本忍者的“分身术”呢(笑)。

  为了让这种分身术看上去更完美,需要让操作系统尽可能快地切换任务。如果 10秒才切换一次,那就连人眼都能察觉出来了,同时运行多个程序的戏码也就穿帮了。再有,如果我们给程序 C发出一个按键指令,正巧这个瞬间系统切换到了程序 A的话,我们就不得不等上 20秒,才能重新轮到程序 C对按键指令作出反应。这实在是让人抓狂啊(哭)。




  在一般的操作系统中,这个切换的动作每 0.01~0.03秒就会进行一次。当然,切换的速度越快,让人觉得程序是在同时运行的效果也就越好。不过, CPU进行程序切换(我们称为“任务切换”)这个动作本身就需要消耗一定的时间,这个时间大约为 0.0001秒左右,不同的 CPU及操作系统所需的时间也有所不同。如果 CPU每0.0002秒切换一次任务的话,该 CPU处理能力的 50%都要被任务切换本身所消耗掉。这意味着,如果同时运行 2个程序,每个程序的速度就只有单独运行时的1/4,这样你会觉得开心吗?如果变成这种结果,那还不如干脆别搞多任务呢。

  相比之下,即便是每 0.001秒切换一次任务,单单在任务切换上面也要消耗 CPU处理能力的 10%。大概有人会想, 10%也没什么大不了的吧?可如果你看看速度快 10%的CPU卖多少钱,说不定就会恍然大悟,“对啊,只要优化一下任务切换间隔,就相当于一分钱也不花,便换上了比现在更快的 CPU嘛……”(笑),你也就明白了浪费 10%也是很不值得的。正是因为这个原因,任务切换的间隔昀短也得 0.01秒左右,这样一来只有 1%的处理能力消耗在任务切换上,基本上就可以忽略不计了。

  关于多任务是什么的问题,已经大致讲得差不多了,接下来我们来看看如何让 CPU来处理多任务。

  当你向CPU发出任务切换的指令时, CPU会先把寄存器中的值全部写入内存中,这样做是为了当以后切换回这个程序的时候,可以从中断的地方继续运行。接下来,为了运行下一个程序, CPU会把所有寄存器中的值从内存中读取出来(当然,这个读取的地址和刚刚写入的地址一定是不同的,不然就相当于什么都没变嘛),这样就完成了一次切换。我们前面所说的任务切换所需要的时间,正是对内存进行写入和读取操作所消耗的时间。

  接下来我们来看看寄存器中的内容是怎样写入内存里去的。下面这个结构叫做“任务状态段”(task status segment),简称TSS。TSS有16位和32位两个版本,这里我们使用 32位版。顾名思义, TSS也是内存段的一种,需要在 GDT中进行定义后使用。



  参考上面的结构定义,TSS共包含26个int成员,总计 104字节(摘自 CPU的技术资料),我特意把它们分成 4行来写。从开头的 backlink起,到 cr3为止的几个成员,保存的不是寄存器的数据,而是与任务设置相关的信息,在执行任务切换的时候这些成员不会被写入( backlink除外,某些情况下是会被写入的)。后面的部分中我们会用到这里的设定,不过现在你完全可以先忽略它。

  第2行的成员是 32位寄存器,第 3行是16位寄存器,应该没必要解释了吧……不对, eip好像到现在还没讲过呢。 EIP的全称是“extended instruction pointer”,也就是“扩展指令指针寄存器”的意思。这里的“扩展”代表它是一个 32位寄存器,也就是说其对应的 16位版本叫做 IP,类比一下的话,跟 EAX与AX之间的关系是一样的。

  EIP是CPU用来记录下一条需要执行的指令位于内存中哪个地址的寄存器,因此它才被称为
“指令指针”。如果没有这个寄存器,记性不好的 CPU就会忘记自己正在运行哪里的程序,于是程序就没办法正常运行了。每执行一条指令, EIP寄存器中的值就会自动累加,从而保证一直指向下一条指令所在的内存地址。

  说点题外话, JMP指令实际上是一个向 EIP寄存器赋值的指令。 JMP 0x1234这种写法, CPU会解释为 MOV EIP,0x1234,并向 EIP赋值。也就是说,这条指令其实是篡改了 CPU记忆中下一条该执行的指令的地址,蒙了 CPU一把。这样一来, CPU在读取下一条指令时,就会去读取 0x1234这个地址中的指令。你看,这不就相当于是做了一个跳转吗?

  对了,如果你在汇编语言里用 MOV EIP,0x1234这种写法是会出错的,还是不要尝试的好。在汇编语言中,应该使用 JMP 0x1234来代替MOV EIP,0x1234。


  如果在TSS中将EIP寄存器的值记录下来,那么当下次再返回这个任务的时候, CPU就可以明白应该从哪里读取程序来运行了。

  按照常识,段寄存器应该是 16位的才对,可是在 TSS数据结构中却定义成了 int(也就是 DWORD)类型。我们可以大胆想象一下,说不定英特尔公司的人将来会把段寄存器变成 32位的,这样想想也挺有意思的呢(笑)。

  第4行的ldtr和iomap也和第1行的成员一样,是有关任务设置的部分,因此在任务切换时不会被CPU写入。也许你会想,那就和第 1行一样,暂时先忽略好了——但那可是绝对不行的!如果胡乱赋值的话,任务就无法正常切换了,在这里我们先将ldtr置为0,将iomap置为0x40000000就好了。

  关于TSS的话题暂且先告一段落,我们回来继续讲任务切换的方法。要进行任务切换,其实还得用JMP指令。 JMP指令分为两种,只改写 EIP的称为near模式,同时改写 EIP和CS的称为far模式,在此之前我们使用的 JMP指令基本上都是 near模式的。不记得 CS是什么了? CS就是代码段(code segment)寄存器啦。

  说起来我们其实用过一次 far模式的JMP指令,就在 asmhead.nas的“bootpack启动”的昀后一句(见8.5节)。 MP DWORD 2*8:0x0000001b 这条指令在向 EIP存入0x1b的同时,将CS置为2*8(=16)。像这样在 JMP目标地址中带冒号( :)的,就是far模式的 JMP指令。

  如果一条 JMP指令所指定的目标地址段不是可执行的代码,而是 TSS的话, CPU就不会执行通常的改写 EIP和CS的操作,而是将这条指令理解为任务切换。也就是说, CPU会切换到目标 TSS所指定的任务,说白了,就是 JMP到一个任务那里去了。

  CPU每次执行带有段地址的指令时,都会去确认一下 GDT中的设置,以便判断接下来要执行的 JMP指令到底是普通的 far-JMP,还是任务切换。也就是说,从汇编程序翻译出来的机器语言来看,普通的 far-JMP和任务切换的 far-JMP,指令本身是没有任何区别的。

  好了,枯燥的讲解就到这里,让我们实际做一次任务切换吧。我们准备两个任务:任务 A和任务B,尝试从 A切换到B。

  

  现在两个TSS都创建好了,该进行实际的切换了。

  我们向TR寄存器存入3 * 8这个值,这是因为我们刚才把当前运行的任务定义为 GDT的3号。 TR寄存器以前没有提到过,它的作用是让 CPU记住当前正在运行哪一个任务。当进行任务切换的时候,TR寄存器的值也会自动变化,它的名字也就是“ task register”(任务寄存器)的缩写。我们每次给 TR寄存器赋值的时候,必须把 GDT的编号乘以 8,因为英特尔公司就是这样规定的。如果你有意见的话,可以打电话找英特尔的大叔投诉哦(笑)。

  给TR寄存器赋值需要使用 LTR指令,不过用 C语言做不到。唉,各位是不是都已经见怪不怪了啊?啥?你早就料到了?(笑)所以说,正如你所料,我们只能把它写进 naskfunc.nas里面。






  对了,LTR指令的作用只是改变 TR寄存器的值,因此执行了 LTR指令并不会发生任务切换。要进行任务切换,我们必须执行 far模式的跳转指令,可惜 far跳转这事 C语言还是无能为力,这种语言还真是不方便啊。没办法,这个函数我们也得在 naskfunc.nas里创建。



  也许有人会问,在 JMP指令后面写个 RET有意义吗?也对,通常情况下确实没意义,因为已经跳转到别的地方了嘛,后面再写什么指令也不会被执行了。不过,用作任务切换的 JMP指令却不太一样,在切换任务之后,再返回这个任务的时候,程序会从这条 JMP指令之后恢复运行,也就是执行JMP后面的RET,从汇编语言函数返回,继续运行 C语言主程序。

  另外,如果 far-JMP指令是用作任务切换的话,地址段(冒号前面的 4*8的部分)要指向 TSS这一点比较重要,而偏移量(冒号后面的 0的部分)并没有什么实际作用,会被忽略掉,一般来说像这样写 0就可以了。

  现在我们需要在 HariMain的某个地方来调用 taskswitch(),可到底该写在哪里呢?唔,有了,就放在显示“ 10[sec]”的语句后面好了。也就是说,程序启动 10秒以后进行任务切换。



大功告成了?不对,我们还没准备好 tss_b呢。在任务切换的时候需要读取 tss_b的内容,因此我们得在TSS中定义好寄存器的初始值才行。




  乍看之下,貌似会有很多看不懂的地方吧,我们从后半段对寄存器赋值的地方开始看。这里我们给cs置为GDT的2号,其他寄存器都置为 GDT的1号,asmhead.nas的时候也是一样的。也就是说,我们这次使用了和bootpack.c相同的地址段。当然,如果你用别的地址段也没问题,不过这次我们只是想随便做个任务切换的实验而已,这种麻烦的事情还是以后再说吧。

  继续看剩下的部分,关于 eflags的赋值,如果把 STI后的EFLAGS的值通过 io_load_eflags赋给变量的话,该变量的值就显示为 0x00000202,因此在这里就直接使用了这个值,仅此而已。如果还有看不懂的地方,大概就是 eip和esp的部分了吧。

。。。。。。

 在eip中,我们需要定义在切换到这个任务的时候,要从哪里开始运行。在这里我们先把 task_b_main这个函数的内存地址赋值给它。



  这个函数只执行了一个 HLT,没有任何实际作用,后面我们会对它进行各种改造,现在就先这样吧。

  task_b_esp是专门为任务 B所定义的栈。有人可能会说,直接用任务 A的栈不就好了吗?那可不行,如果真这么做的话,栈就会混成一团,程序也无法正常运行。



  总之先写成这个样子了。我们为任务B的栈分配了64KB的内存,并计算出栈底的内存地址。
请各位回忆一下向栈PUSH数据(入栈)的动作,ESP中存入的应该栈末尾的地址,而不是栈开
头的地址。

  好了,我们已经讲解得够多了,现在总算是万事俱备啦,马上“make run”一下吧。这个程
序如果运行正常的话应该是什么样子呢?嗯,启动之后的10秒内,还是跟以前一样的,10秒一到便执行任务切换,task_b_main开始运行。因为task_b_main只有一句HLT,所以接下来程序就全部停止了,鼠标和键盘也应该都没有反应了。

  唔……这样看起来好像很无聊啊,算了,总之我们先来“make run”吧。10秒钟的等待还真
是漫长……哇!停了停了!

  看来我们的首次任务切换获得了圆满成功。

分享到:
评论

相关推荐

    30天自制操作系统综合大设计harib12g.zip

    30天自制操作系统最后的实验验收,将近两千行的代码,两点在于开机动画的帧数多,密码,双击,时钟绘制的好看,直线的绘制,可以用拖动的方式绘制矩形,还可以通过输入的方式改变绘制的大小,以及窗口颜色来改变颜色

    OSASK.zip_OSA_osask系统下载_操作系统制作_自制 操作系统_自制操作系统

    标题"OSASK.zip_OSA_osask系统下载_操作系统制作_自制操作系统_自制操作系统"中,关键词主要包括"OSASK.zip"、"OSA"、"osask系统"、"操作系统制作"以及"自制操作系统"。这表明提供的压缩包文件包含了川合秀实(可能...

    harib02e.zip

    《深入探索:川合秀实的纸娃娃操作系统与30天自制OS之...对于那些对操作系统有兴趣,尤其是希望参与“30天自制操作系统”挑战的人来说,这是一个不容错过的起点。让我们打开这个压缩包,开始我们的操作系统探索之旅吧!

    harib11i.rar

    《30天自制操作系统》是一本引导读者逐步构建操作系统的学习教程,旨在帮助读者理解操作系统的原理和实现。在这个过程中,作者通常会提供每日任务,读者按照指导完成编码工作。"harib11i.rar" 是一个与这个教程相关...

    YunGameDownload.zip

    总之,操作系统显示中文字符涉及到了语言设置、字符编码、字体支持以及文件系统等多个方面。正确理解和配置这些元素,是保证中文用户在数字世界中流畅交流的基础。对于开发者和普通用户来说,了解这些知识都有助于...

Global site tag (gtag.js) - Google Analytics