最简单的内存管理完成了,我们就可以实现多任务了。
一、x86如何实现多任务
1、多任务的含义
多任务是指一个单处理器同时执行多个不同任务。实际上这样的说法并不完全准确,因为一个单处理器在同一个时刻只能执行一个任务,但它可以在很短的时间内在多个任务之间切换,如每秒在多个任务之间切换100次,它们在同一时刻并不是并行的,但在使用者看来这些任务好似在同时运行的。我们先来看一下CPU在执行两个任务时的运行过程:
可以看到:任务A和任务B在小时间片中是串行的,而在大时间片中是并行的。在前10ms中CPU在执行任务A,在10-20ms时,CPU在执行任务B;而在大的时间片中,如前60ms或更大的时间片如1s中里来看任务A和任务B就是并行的。
2、TSS(Task Status Segment):
我们知道,每个任务在执行时都需要使用CPU中的寄存器,那么当每个任务执行时,CPU中的这些寄存器都必须是当前任务所正确使用的。而CPU在多个任务之间切换时就需要将这些任务所使用的寄存器的值做一些特殊的处理:当CPU从任务A切换到任务B时,需要将任务A所使用的所有寄存器的值保留下来,放入内存。然后将B所使用的寄存器的值由内存装入CPU的各个寄存器。当CPU从任务B再切换加任务A时,CPU又要从内存中装入任务原先使用的CPU寄存器的值到CPU寄存器中。
比如在10ms时刻,任务A所使用%eax寄存器的值为0x1111,%ebx的值为0x2222。此时CPU需要由任务A切换到任务B,在CPU切换任务之前需要将任务A所使用的寄存器的值保留到内存中,再切换到任务B,并将任务B之前所使用的CPU寄存器的值装入CPU寄存器中。在执行任务B的过程中,时间到了20ms,此时%eax寄存器的值为0x3333,%ebx寄存器的值为0x4444,也就是说这是任务B所使用的两个寄存器的值。此时CPU需要由任务A切换到任务B,在切换之前先要将任务B所使用的寄存器的值保留到内存中,再切换到任务B,并将之前任务A所使用的寄存器的值重新装入CPU寄存器中,恢复%eax和%ebx的值为0x1111和0x2222并以此方式继续……
当一个程序在执行的时CPU想要将其切换为其它程序之前,先要将它的相关信息和所有寄存器的信息保存到内存当中,并把将要切换成当前执行程序的相关信息和所有寄存器的信息由内存装入,再切换到新的程序中执行。CPU使用任务状态TSS(Task State Segment)来存储这些信息,TSS有104个字节,并且在GDT中有一个描述符指向这个TSS。当CPU进行任务切换时,CPU把任务信息自动的存储在TSS当中。TSS格式如下:
以上摘自https://www.askpure.com/course_KZ9HOJ83-2SQ4NLUR-ROQYGBLU-MGTCGLCD.html
讲的非常非常好,感谢作者。
3、什么是LDT
再回忆一下GDT的内容,GDT是全局描述符表,LDT就是局部描述符表,GDTR寄存器是48位,指明了GDT位于内存的位置和GDT表的大小,LDTR是16位,本质上是一个段选择子,用于从GDT表中选择某一项,这一项描述了LDT位于内存的位置。
对于cs、ds、es、ss等,它们相当于段选择子,当TI位为0时,则段基址从GDT中确定,当TI位为1时,段基从LDT中确定。
例如,我们将程序A放置在0xaaaaa处,程序B放置在0xbbbbb处,运行程序A时,我们配置cs,ds,es,ss的TI位都为0,则程序确定所有指令或者数据的段基址都是从GDT中取,比如说cs是GDT#1,ds是GDT#2,以此类推。假设GDT#3是一个LDT位置的描述项,而且当我运行程序B时,我将LDTR段选择子选择GDT#3,然后cs,ds,es,ss的TI位都为1,此时,程序确定所有的指令或数据的段基址都是从GDT#3所描述的那个LDT中获得,比如说cs是LDT#1,ds是LDT#2,以此类推。当然,我们现在暂时不启用LDT,因为我们测试程序切换功能的函数都位于内核内,我们必须使用内核的GDT来完成测试,否则某些函数将无法执行。
以后等完成了程序的读入功能,我们再完善LDT。此时将LDT置零即可,突然发现了GDT#0的意义所在。
4、如何访问TSS
说到这里,就得好好说说jmp这个命令了
jmp分为near和far,在AT&T汇编语言中使用不同的指令,ljmp为长跳跃,逗号前为段选择子,逗号后为偏移。jmp为短跳跃,直接接段内地址(符号也是地址)或者寄存器,这里有一个非常非常重要的知识,短跳跃jmp最后生成机器码的时候,生成的是目的地址与本条指令的相对值,以补码的形式保存(因为可能为负),大小范围为-32768至32767,汇编总是会在这个小知识点出错。短跳跃其实相当于直接修改了eip指针的值
长跳跃每次执行都会检查段选择子的值,判断段选择子的值,如果是代码段则确定代码所处内存位置的偏移量,如果是其他段的类型则执行相应的功能。在这里,我们添加一个特别的GDT,它指向了TSS在内存中的位置,更关键的是,当我们执行lcall/ljmp [TSS selector], [offset]时,系统会执行任务切换。
GDT的格式在之前已经讲过了,这是TSS GDT的格式,它的S位为0,表示这是一个系统描述段,也就是说,我们有一个GDT描述符,里面存放的是TSS的地址,比如说这个GDT的选择子为0x20那么在切换任务时就要执行lcall 0x20, 0。事实上,操作系统内核的任务切换过程很复杂,并采用复杂任务调度算法。在本小节中主要是针对TSS任务切换来学习CPU的多任务机制,所以采用了只有简单任务切换的办法,关于任务调度算法我们会在后续中改进。
对于每一个任务来说,它们都可以独立的使用CPU中的寄存器。为了与内核程序区分,我们要将这些普通程序运行的权限设置为3,也就是用户权限。这些程序的内存寻址方式与前面讲的内核程序寻址方式一致,但它们使用的不是GDT全局描述符,而是LDT局部描述符。其实LDT与GDT的本质是完全一样的,只不过LDT是为普通任务所用。在一个任务的TSS中有一个字段为LDT,这里存放的是一个GDT描述符,这个GDT描述符描述了这个任务的LDT描述符所在的内存地址,而这个LDT描述符描述了这个任务可用的内存段区域:代码段和数据段。
看一张图,非常清晰了。
对于LDT它的内容与GDT基本上是一样的,只是DLP字段(优先权)为3而不是像GDT那样为0。使用LDT时的选择子RPL(也是优先权)是3而不是0。TI字段的值如果是0则代表是GDT选择子,如果是1代表是LDT选择子。
二、通过时钟中断来控制切换速率
时钟中断是一个非常重要的硬中断,计算机体系中有一个叫作8253/54的芯片(PIT),相信学过计算机原理的对它也是深恶痛绝。在PIT 芯片上有3个计数器,它可以按预先设定好的方式每隔一小段时间触发一次中断(IRQ0,见中断那一节)。在早期的Linux系统内核中采用了10ms触发一次,这也被称作是Linux的心跳。在后续我们也要利用这个中断来实现系统内核的多任务处理机制。
PIT 芯片有1个控制寄存器和3个计数器。控制寄存器的访问端口为0x43。计数寄存器的访问端口为0x40、0x41和0x42。每个寄存器可以以6个不同模式计数,并可以选择用BCD或者二进制计数。先将控制字写入控制寄存器,告诉它我们选择哪个计数寄存器、写入方式和计数模式等。
因此初始化工作有2点:
(1)写入控制字;
(2)按控制字的要求写入计数初值。
8253 的每个计数器内有一个8 位控制寄存器,用来存放CPU 写入的工作方式控制字,工作方式控制字格式如下图所示,该寄存器只能执行写入操作,不能执行读出操作。8253 内部的3 个计数器在结构上相互独立,在使用时须对指定的计数器写入方式控制字,写入控制字的I/O 地址相同。