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

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

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

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

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

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

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

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

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

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

[img]http://b105.photo.store.qq.com/psb?/V12gMU7j3dMtSc/O8yp5o3Ijpr3KLzUb1amW4ceiB4gMdFH2xRkCZavyGs!/b/YXPVmD7HGwAAYo0OqD4fGwAA[/img]


  在一般的操作系统中,这个切换的动作每 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中进行定义后使用。

[img]http://b105.photo.store.qq.com/psb?/V12gMU7j3dMtSc/sUWVZcicdkmMAG4Kdou2eU1eeLoyIIPmyhQhDEp.5ig!/b/YZOaqT79EwAAYtBhnT6LGwAA[/img]

  参考上面的结构定义,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寄存器中的值就会自动累加,从而保证一直指向下一条指令所在的内存地址。

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

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

  如果在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。

  [img]http://b104.photo.store.qq.com/psb?/V12gMU7j3dMtSc/UJKBp6zyNzdc0UhOeNlONV9vbZWKN1DkTP5FzfFXGQo!/b/YThhCT7EagAAYhBDAz7BaQAA[/img]

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

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

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

[img]http://b103.photo.store.qq.com/psb?/V12gMU7j3dMtSc/wy89dzg.vUB0SMF6k0sYxjR3r8TMlW1.kCKig3rN4uU!/b/YQE4bD1JagAAYgjIbT2hagAA[/img]

[img]http://b107.photo.store.qq.com/psb?/V12gMU7j3dMtSc/*dNF3tFMFiJaBAEkJUmniQjOnG*xpA6R*BGTHkCJ*dM!/b/YfaKzj89CwAAYmem1D*iCgAA[/img]


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

[img]http://b106.photo.store.qq.com/psb?/V12gMU7j3dMtSc/MM9erdaP7Lv0WHRIEp*dorPuRscdComJsZmgVksJxrE!/b/YW58Nz*8CQAAYh8rQj*kCQAA[/img]

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

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

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

[img]http://b100.photo.store.qq.com/psb?/V12gMU7j3dMtSc/Ca3Wo7AR3QnKN3w24y.LHmHyRONFjvzxJ9MDuTnE95o!/b/YUklrTuVjQAAYuqhrjtJkQAA[/img]

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

[img]http://b106.photo.store.qq.com/psb?/V12gMU7j3dMtSc/IlXAk9m0R.z37a5rOIW*rTAwLfrlKpcW.3c5gDGEsj8!/b/YQmBNz86CgAAYib.NT*zCQAA[/img]
[img]http://b106.photo.store.qq.com/psb?/V12gMU7j3dMtSc/s6*fMa1ZL68wR2.Jths28n6cb0ladAC.CSrv19lRDng!/b/YZZwND*ECQAAYlZfMT.3CQAA[/img]

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

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

。。。。。。

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

[img]http://b101.photo.store.qq.com/psb?/V12gMU7j3dMtSc/e4igmgAOkHIj*HZEi0ltxHtYQ5A.t.z.nHXIJtfiTAM!/b/YRBxNjyJjgAAYuMZPjy8jwAA[/img]

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

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

[img]http://b103.photo.store.qq.com/psb?/V12gMU7j3dMtSc/JgDaRW27t89eNG1OsTTcpEywQdb.obLGJXWbeFLiypk!/b/Yfo.bz2EawAAYrqeZz26agAA[/img]

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

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

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

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

[img]http://b103.photo.store.qq.com/psb?/V12gMU7j3dMtSc/3cshy8ahcgzC7dVNmR.0fuklgt*DGdotI4LyyJdeWQg!/b/YR8eZj1qaQAAYm1AbD2xagAA[/img]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值