跟我一起写操作系统

    返回首页    发表留言
本文作者:李德强
          第二节 任务切换
 
 

        首先定义TSS的数据结构:

typedef struct
{
        u32 back_link;
        u32 esp0, ss0;
        u32 esp1, ss1;
        u32 esp2, ss2;
        u32 cr3;
        u32 eip;
        u32 eflags;
        u32 eax, ecx, edx, ebx;
        u32 esp, ebp;
        u32 esi, edi;
        u32 es, cs, ss, ds, fs, gs;
        u32 ldt;
        u32 trace_bitmap;
} s_tss;

        再来定义一个pcb的数据结构,pcb全称为process control block被内核称为程序控制块,即用来存放控制程序的相关信息。同样的,这里所使用的pcb非常的简单,并不会像Linux的pcb那样功能繁多而庞大:

typedef struct s_pcb
{
        s_tss tss;
        s_gdt ldt[2];
} s_pcb;

        这里的pcb只有两个内容tss和ldt。其中ldt与前面教程中学习过的gdt是一样的,这里使用一个ldt数据用来描述这个程序的能够使用的代码段和数据段内存。对于这些ldt所描述的代码段和数据段均为0~4G。这样做的目的是为了方便以后做内存分页和虚拟内存处理,也就是说让每一个程序都可以使用4G大小的内存。现在先不去管它,只知道ldt描述了0~4G的数据段和代码段即可。

        在多任务之间切换之前还要设置两个寄存器的内容,一个是TR,另一个是LDTR。它们作用是告诉CPU下一个要切换的任务的TSS和LDT在哪里。分别使用ltr和lldt来装入TSS和LDT。

__asm__ volatile("ltrw        %%ax"::"a"(0x20));
___asm__ volatile("lldt        %%ax"::"a"(0x28));

        下面来安装两个任务:

//定义栈大小
int size = 0x800;
//任务A
pcb_A = alloc_mm(sizeof(s_pcb));
init_process(pcb_A);
pcb_A->tss.eip = (u32) &run_A;
pcb_A->tss.esp = (u32) alloc_mm(size) + size;
pcb_A->tss.esp0 = (u32) alloc_mm(size) + size;
//任务B
pcb_B = alloc_mm(sizeof(s_pcb));
init_process(pcb_B);
pcb_B->tss.eip = (u32) &run_B;
pcb_B->tss.esp = (u32) alloc_mm(size) + size;
pcb_B->tss.esp0 = (u32) alloc_mm(size) + size;
//初始化空任务
s_pcb *pcb = alloc_mm(sizeof(s_pcb));
init_process(pcb);
pcb->tss.eip = 0;
pcb->tss.esp = 0;
pcb->tss.esp0 = 0;
//设置TSS所在地址的GDT
addr_to_gdt(GDT_TYPE_TSS, (u32) &pcb->tss, &gdts[4], GDT_G_BYTE, sizeof(s_tss) * 8);
//设置LDT所在地址的GDT
addr_to_gdt(GDT_TYPE_LDT, (u32) pcb->ldt, &gdts[5], GDT_G_BYTE, sizeof(s_gdt) * 2 * 8);
//载入tss和ldt
load_tss(GDT_INDEX_TSS);
load_ldt(GDT_INDEX_LDT);

        对于run_A和run_B为这两个任务的代码,也就是这两任务在执行时所运行的代码段,它们的定义如下:

void run_A()
{
        char *p = (char *) 0xb8000;
        p += ((23 * 80 + 74)) * 2;
        int i = 33;
        while (1)
        {
                *p = i;
                if (++i >= 127)
                {
                        i = 33;
                }
        }
}   
     
void run_B()
{
        char *p = (char *) 0xb8000;
        p += ((23 * 80 + 76)) * 2;
        int i = 64;
        while (1)
        {
                *p = i;
                if (++i >= 127)
                {
                        i = 33;
                }
        }
}

        这两个函数的作用是在屏幕的右下角不停的显示一些Ascii字符。

        有两个问题需要注意:首先,这两个函数是定义在内核程序中的,对于普通任务在实际运行时是不能够直接调用内核程序的。第二,在char *p = (char *) 0xb8000;这里程序直接对0xb8000内存进行访问并对其修改。对于普通任务所可以访问的代码段与数据段在逻辑上是0 ~ 4G,也包括了内核程序在内存中的部分,如果普通程序可以你内核程序那样直接修改内核程序所在的内存,会对内核程序造成非常大的破坏。对于这样的“内存保护”的问题我们会在内存分页和虚拟内存章节中来学习如果让程序只能访问自己所具有权限的那一部分内存。

        有了任务A和任务B之后,接下来就是要通过时钟中断来进行任务的切换工作。时钟中断已经被我们设定为100次/秒。在响应时钟中断函数int_timer中加入任务调度函数来实现多任务之间的切换功能:

void int_timer()
{
        //通知PIC可以接受新中断
        outb_p(0x20, 0x20);        
        //任务调度算法
        schedule();
}

        schedule()函数的实现非常简单,只是在每一个时间片里在任务A和任务B之间来回切换:

void schedule()
{
        s_pcb *pcb = NULL;        
        if (timer++ % 2 == 0)
        {
                pcb = pcb_A;
        }
        else
        {
                pcb = pcb_B;
        }        
        addr_to_gdt(GDT_TYPE_TSS, (u32) &pcb->tss, &gdts[4], GDT_G_BYTE, sizeof(s_tss) * 8);
        addr_to_gdt(GDT_TYPE_LDT, (u32) pcb->ldt, &gdts[5], GDT_G_BYTE, sizeof(s_gdt) * 2 * 8);        
        call_tss();
}

      本节用来处理与多任务相关功能的代码均写在了process.c文件中,在kernel.c中加入install_process();并在Makefile中加入process.c的编译选项,编译运行并查看结果:



 

        目前任务A和任务B还是直接执行了内核程序中定义的两个方法run_A();和run_B();这样做并不好,在后续章节中我们会学习如何让内核载入一个磁盘中的可执行程序并将其运行。并且通过系统中断的形式来切入内核,执行内核中提供的函数。
 

        源代码的下载地址为:

https           https://github.com/magicworldos/lidqos.git 
git             git@github.com:magicworldos/lidqos.git 
subverion       https://github.com/magicworldos/lidqos 
branch          v0.14

    返回首页    返回顶部
#1楼  yivies  于 2023年10月22日23:55:50 发表
 
这样中断处理函数会不会导致爆栈呢?
  看不清?点击刷新

 

  Copyright © 2015-2023 问渠网 辽ICP备15013245号