前言
话说,多任务到底是啥呢?”我们今天的内容,就从这个问题开始吧。 多任务,在英语中叫做“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中进行定义后使用。
struct TSS32 {
int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3;
int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;
int es, cs, ss, ds, fs, gs;
int ldtr, iomap;
};
参考上面的结构定义,TSS共包含26个int成员,总计104字节(摘自CPU的技术资料),我 特意把它们分成4行来写。从开头的backlink起 ,到cr3为止的几个成员,保存的不是寄存器的 数据,而是与任务设置相关的信息,在执行任务切换的时候这些成员不会被写入(backlink除 外,某些情况下是会被写入的)。后面的部分中我们会用到这里的设定, 不过现在你完全可以 先忽略它。 第2行的成员是32位寄存器, 第3行是16位寄存器,应该没必要解释了吧…….不对,eip好像 “extended instruction pointer”,也就是“扩展指令指针寄存器” 到现在还没讲过呢。EIP的全称是 代表它是一个32位寄存器,也就是说其对应的16位版本叫做IP,类比一 的意思。这里的“扩展” 下的话,跟EAX与AX之间的关系是一样的。 EIP是CPU用来记录下一条需要执行的指令位于内存中哪个地址的寄存器,因此它才被称为 “指令指针”。如果没有这个寄存器,记性不好的CPU就会忘记自己正在运行哪里的程序,于是程 序就没办法正常运行了。 每执行一条指令,EIP寄存器中的值就会自动累加,从而保证一直指向下一条指令所在的内存地址。
如果在TSS中将EIP寄存器的值记录下来,那么当下次再返回这个任务的时候,CPU就可以明 白应该从哪里读取程序来运行了。 按照常识, 段寄存器应该是16位的才对,可是在TSS数据结构中却定义成了int(也就是 DWORD)类型。我们可以大胆想象一下,说不定英特尔公司的人将来会把段寄存器变成32位的, 这样想想也挺有意思的呢(笑)。 第4行的ldtr和iomap也和第1行的成员一样,是有关任务设置的部分,因此在任务切换时不会被CPU写入。也许你会想,那就和第1行一样, 暂时先忽略好了 – 但那可是绝对不行的!如果胡乱赋值的话,任务就无法正常切换了,在这里我们先将ldr置为0, 将iomap置为0x40000000就好了。
说点题外话,JMP指令实际上是一个向EIP寄存器赋值的指令。JMP0x1234这种写法, CPU会解释为MOVEIP,0x1234,并向EIP赋值。也就是说,这条指令其实是基改了CPU记 蒙了CPU一把。这样一来,CPU在读取下一条指令时, 忆中下一条该执行的指令的地址,就会去读取0x1234这个地址中的指令。你看,这不就相当于是做了一个跳转吗? 对了,如果你在汇编语言里用MOV EIP,0x1234这种写法是会出错的,还是不要尝试的 好。在汇编语言中,应该使用JMP0x1234来代替MOVEIP,0x1234。
关于TSS的话题暂且先告一段落,我们回来继续讲任务切换的方法。 要进行任务切换,其实 还得用JMP指令。JMP指令分为两种, 只改写EIP的称为near模式, 同时改写EIP和CS的称为far模 在此之前我们使用的JMP指令基本上都是near模式的。 不记得CS是什么了?CS就是代码段式, (code segment)寄存器啦。 说起来我们其实用过一次far模式的JMP指令, 就在asmhead.nas的 “bootpack启动” 的最后一句(见第八天)。
JMP DWORD 2*8:0x0000001b
这条指令在向EIP存人0x1b的同时,将CS置为2*8(=16)。像这样在JMP目标地址中带冒号(
标签:task,15,--,30,timer,任务,切换,sht,fifo From: https://blog.csdn.net/suy123/article/details/145214134