从零开始搭建环境编写操作系统 AT&T GCC (十)多任务

来源:互联网 发布:淘宝邮票真假怎么鉴定 编辑:程序博客网 时间:2024/06/05 13:35

  最简单的内存管理完成了,我们就可以实现多任务了。
一、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 地址相同。
  这里写图片描述
  这里写图片描述
  (话说当年博主就死在了这道题上,六种模式谁记得住啊)(不贴出来了,感兴趣的自己搜吧)
  
 三、实现多任务
   理论够了,开始码代码
   1、新建文件夹,命名为tasks,新建tasks.c和tasks.h,添加进入makefile
   2、main.c初始化计时器
   

void InitTimer(){    //计算divisor    unsigned short divisor = 1193180 / 100; //每秒中断100次    //写入8253的控制寄存器    FunctionOut8( 0x43,0x36); //0011 0110,00号寄存器,11先低八位后高八位,011工作模式3,0采用二进制    //写入频率低8位到计数器0    FunctionOut8(0x40,divisor & 0xff);    //写入频率高8位到计数器0    FunctionOut8(0x40,divisor >> 8);    //打开PIC的时钟中断IRQ0    FunctionOut8(0x21,FunctionIn8(0x21) & 0xfe);}

  不要忘记设置中断向量表IDT:
  

    //timer    idt[0x20].offset1 = (short) ((int) (void*) (TimerIntCallBack));    idt[0x20].selector = 0x0008;    idt[0x20].no_use = 0x8e00;    idt[0x20].offset2 = (short) (((int) (void*) (TimerIntCallBack)) >> 16);

  在functions.s中设置回调函数TimerIntCallBack:
  

TimerIntCallBack:    cli        pushal        pushfl        //调用鼠标中断处理函数        call IntTimer        popfl        popal        sti        iret

  因为timer的函数不多,我把中断处理函数IntTimer放在main.c中
  随便写个显示函数
  

void IntTimer(){    static int i;    DrawRectangle(24,24,150,50,0x363636);    printf(24, 24, 0xff0000, "counts:%d",i);    i++;        //通知PIC可以接受新中断    FunctionOut8(0x20, 0x20);}

  这里写图片描述
  3、建立TSS数据结构
  打开tasks.h,定义数据结构

//TSS数据结构typedef struct{        unsigned int back_link;        unsigned int esp0, ss0;        unsigned int esp1, ss1;        unsigned int esp2, ss2;        unsigned int cr3;        unsigned int eip;        unsigned int eflags;        unsigned int eax, ecx, edx, ebx;        unsigned int esp, ebp;        unsigned int esi, edi;        unsigned int es, cs, ss, ds, fs, gs;        unsigned int ldt;        unsigned int trace_bitmap;} tss_struct;//gdt数据结构typedef struct{        unsigned short limit;        unsigned short baseaddr;        unsigned char baseaddr2;        unsigned char p_dpl_s_type;        unsigned char g_db_d_avl_limit2;        unsigned char baseaddr3;} gdt_struct;//gdtr结构,为了让结构体连续,我先取消了对齐,然后恢复默认对齐,具体可百度 字节对齐意义#pragma pack (1)typedef struct{        unsigned short size;        unsigned int base;} gdtr_struct;#pragma pack ()//缺省对齐

  4、管理GDT表
  当新的程序申请运行时,要向GDT中存入TSS项目和LDT项目,因此我们写一套GDT的管理函数。
  system.s中这一段始终没有用到,我直接删掉了,然后把GDT的size改为3*8-1=23

############3# stack GDT    movl    $0x00007a00,   24(%eax)    movl    $0x00409600,   28(%eax)

  
  tasks.c,注释写在代码里了

#include "tasks.h"#include "../font/font.h"gdtr_struct gdtr={    .size = 3*8-1,    .base = 0x80000};//全局gdtr寄存器结构体,与GDTR寄存器内容同步extern void FunctionLgdt(gdtr_struct gdtr);//这个在functions.s中,用于导入gdtgdt_struct *AddGdt(gdt_struct gdt_source);//增加gdtvoid DeleteGdt(gdt_struct *gdt);//删除某一项gdtvoid InitMultiTasks(){    gdt_struct gdt_source;    gdt_source.limit = 0xffff;    gdt_source.baseaddr = 0x8200;    gdt_source.baseaddr2 = 0x00;    gdt_source.p_dpl_s_type = 0x9a;//1001 1010    gdt_source.g_db_d_avl_limit2 = 0x40;//0100 0000    gdt_source.baseaddr3 = 0x00;    AddGdt(gdt_source);//这里连续插入两个gdt进行测试    AddGdt(gdt_source);}gdt_struct *AddGdt(gdt_struct gdt_source){    //每次申请增加一个GDT,至于满了怎么办以后再说    gdt_struct *gdt = (gdt_struct *)(0x80000 + (gdtr.size + 1) - 0x8200);//下一个GDT项的位置,不要忘记减去基址0x8200    if (gdtr.size + 1 < 8192 * 8)//如果没满    {        gdt->baseaddr = gdt_source.baseaddr;        gdt->baseaddr2 = gdt_source.baseaddr2;        gdt->baseaddr3 = gdt_source.baseaddr3;        gdt->g_db_d_avl_limit2 = gdt_source.g_db_d_avl_limit2;        gdt->limit = gdt_source.limit;        gdt->p_dpl_s_type = gdt_source.p_dpl_s_type;        gdtr.size = ((gdtr.size + 1)/8 + 1) * 8 - 1;//GDT大小+8        FunctionLgdt(gdtr);//传入gdtr寄存器,结构体连续,所以可以直接这样传    }    else    {        printf(0,400,0xff0000,"GDT full!!!!");    }    return gdt;}void DeleteGdt(gdt_struct *gdt)//现在删除某一项gdt就是把它置零,以后再完善这里{    gdt->baseaddr = 0;    gdt->baseaddr2 = 0;    gdt->baseaddr3 = 0;    gdt->g_db_d_avl_limit2 = 0;    gdt->limit = 0;    gdt->p_dpl_s_type = 0;}

  functions.s就一句话,不要忘记.global

FunctionLgdt: #void FunctionLgdt(gdtr_struct *gdtr)    lgdt    4(%esp)    ret

  5、配置多进程
  当S=0, TYPE=1001或者TYPE=1011的时候,表示这是一个TSS段描述符。当TSS段没被加载进 tr 寄存器时,TYPE=1001,一旦TSS被加载进 tr 寄存器,TYPE就变成了1011。
  在tasks.c中,我定义了结构体:
  (不要忘记include “../memmory/memmory.h”)

typedef struct  //process control block{    unsigned int id; //程序编号    tss_struct tss;  //tss结构    gdt_struct *gdt_tss;//指向tss的gdt    gdt_struct *gdt_ldt;//指向ldt的gdt    app_memmory_struct app_memmory;} pcb_struct;

  unsigned int id 在这里是至关重要的,我们切换到某一程序,通过此ID定位,每个程序的ID是唯一的。从程序0开始。
  这里我的思路大体是这样的,每一个程序拥有一个pcb管理结构,其中id是程序的编码,tss是程序的TSS结构,*gdt_tss是指向这个TSS结构的GDT,因为当我们使用gdt_struct *AddGdt(gdt_struct gdt_source)这个函数的时候,会把GDT所在的这一项的地址返回,因此我们使用这个指针存储这一项在内存中的实际位置,*gdt_ldt也是同理,最后app_memmory保存了这个程序使用内存的情况。
  我们创建一个这样的函数,每调用一次,都会在GDT中增加两项,一项指向了LDT,另一项指向了TSS,然后将结果(*gdt_tss,*gdt_ldt,程序ID)保存在PCB结构中。

int InitOneTask(pcb_struct *program){    program->id = pcb_num_global;//////////////////////////////////////////////////////////////指向LDT的GDT,暂时不用,全部置0    gdt_struct ldt_gdt;    ldt_gdt.limit = 0x0000;    ldt_gdt.baseaddr = 0x0000;    ldt_gdt.baseaddr2 = 0x00;    ldt_gdt.p_dpl_s_type = 0x00;//1001 1010    ldt_gdt.g_db_d_avl_limit2 = 0x00;//0100 0000    ldt_gdt.baseaddr3 = 0x00;/////////////////////////////////////////////////////////////指向TSS的GDT    gdt_struct tss_gdt;    tss_gdt.limit = 103;    tss_gdt.baseaddr = (((unsigned int)(&(program->tss))) + 0x8200) & 0xffff;    tss_gdt.baseaddr2 = ((((unsigned int)(&(program->tss))) + 0x8200)>>16) & 0xff;    tss_gdt.p_dpl_s_type = 0xe9;//1110 1001    tss_gdt.g_db_d_avl_limit2 = 0x40;//0100 0000    tss_gdt.baseaddr3 = ((((unsigned int)(&(program->tss))) + 0x8200)>>24) & 0xff;    program->gdt_ldt = AddGdt(ldt_gdt);    program->gdt_tss = AddGdt(tss_gdt);    pcb_num_global++;    return 0;}

  还差最后一步,切换程序。程序切换的过程就是长跳跃的过程,因此写了一个汇编函数来完成这项功能,当然在切换到第一个程序之前,我们先将现在这个进程(内核进程)的TR寄存器赋值。
  在functions.s中增加函数:
  

LoadTr: #void LoadTr(int tr)    ltr 4(%esp)    retFunctionJmp: #void FunctionJmp( int eip,int cs)    ljmp    *4(%esp) #我查过intel用户手册了,这么写是正确的,详见intel用户手册p1063    ret

  初始化几个程序,其中program0是系统内核
  最后的LoadTr(4 * 8);把内核的TSS先存起来,调用program1的时候,内核进程可以被储存

void InitMultiTasks(){    program0.tss.ldt = 0;    program0.tss.trace_bitmap = 0x40000000;    InitOneTask(&program0);    program1.tss.ldt = 0;    program1.tss.trace_bitmap = 0x40000000;    AllocatePages(1,&program1.app_memmory);//申请一页内存    program1.tss.eip = (int) &program1_main;    program1.tss.eflags = 0x00000202; /* IF = 1; */    program1.tss.eax = 0;    program1.tss.ecx = 0;    program1.tss.edx = 0;    program1.tss.ebx = 0;    program1.tss.esp = program1.app_memmory.start_page * 0x1000;    program1.tss.ebp = 0;    program1.tss.esi = 0;    program1.tss.edi = 0;    program1.tss.es = 2 * 8;    program1.tss.cs = 1 * 8;    program1.tss.ss = 2 * 8;    program1.tss.ds = 2 * 8;    program1.tss.fs = 2 * 8;    program1.tss.gs = 2 * 8;    InitOneTask(&program1);    program2.tss.ldt = 0;    program2.tss.trace_bitmap = 0x40000000;    AllocatePages(1,&program2.app_memmory);//申请一页内存    program2.tss.eip = (int) &program2_main;    program2.tss.eflags = 0x00000202; /* IF = 1; */    program2.tss.eax = 0;    program2.tss.ecx = 0;    program2.tss.edx = 0;    program2.tss.ebx = 0;    program2.tss.esp = program2.app_memmory.start_page * 0x1000;    program2.tss.ebp = 0;    program2.tss.esi = 0;    program2.tss.edi = 0;    program2.tss.es = 2 * 8;    program2.tss.cs = 1 * 8;    program2.tss.ss = 2 * 8;    program2.tss.ds = 2 * 8;    program2.tss.fs = 2 * 8;    program2.tss.gs = 2 * 8;    InitOneTask(&program2);    LoadTr(4 * 8);}

  在program1中写点什么,program2空着了,我们测试内核进程与program1之间的切换

void program1_main(){    printf(200,100,0x00ff00,"program1_main!");    while(1);}void program2_main(){}

  然后写一个切换函数:
  

void SwitchTask(int task_id){    FunctionJmp(0, (3+2*task_id+1)*8);}

  在main.c中,每当计数器中断100次(1秒),program0与program1切换一次
  

void IntTimer(){    static int i;    DrawRectangle(24, 24, 150, 50, 0x363636);    printf(24, 24, 0xff0000, "counts:%d", i);    i++;    //通知PIC可以接受新中断    FunctionOut8(0x20, 0x20);    if ((i % 200) == 0)    {        SwitchTask(0);    }    else if(((i+100) % 200) == 0)    {        SwitchTask(1);    }}

  运行,成功!这界面我调试弄的好乱呀,哈哈,鼠标一会儿能动,一会儿不能动就是成功了。
  这里写图片描述