CSDN博客

img xiaohan13916830

操作系统中任务调度的实现

发表于2004/7/3 11:15:00  8180人阅读

分类: 操作系统实验

说起任务调度,我想我们大家都非常熟悉,任何一本操作系统教程,都对此描述得非常详细,不过,他们大多数都是站在理论高度,然而具体到某台实际的机器上,某个实际的操作系统当中,它们具体是怎样用代码去实现的,描述却相对较少。本报告将站在一个编写实际操作系统的角度,去研究一下在Intel 386 CPU结构上怎样去实现任务调度。

本实验的任务调度采用了最简单的时间片轮转算法,因为它的目的并不在于研究采用何种算法调度更有效,而在于研究怎样去调度。我想,在知道了CPU硬件是怎样支持任务调度之后,你可以采用任何一种你喜欢的算法去完成这样的调度。

本实验将同样基于 pyos 系统,你可以在http://purec.binghua.com(纯C论坛)上找到它的全部源代码及资料。

 

任务调度概述

现在的CPU都拥有很强的计算能力,运算速度非常快,然而外部设备却与之速度差距甚远,比如说一个进程需要从磁盘上读取一个文件,那么,此进程需要先通过CPU发送一个读磁盘命令,在磁盘控制器得到这个命令后,磁盘控制器需要启动磁盘驱动器,找到欲读写的磁头、磁道、扇区,然后发出读命令,将数据读到磁盘控制器的缓冲区中,最后再通知CPU到缓冲区中取走数据,我们可以将这一段磁盘驱动器办事的时间称之为数据准备时间,而在这一段数据准备时间内,当前进程因为等待数据而无事可干,进而CPU也无事可干,这样,CPU的效率就显得极低。于是,我们就采用一种更好的策略,先将这个进程调出CPU,而将一个新的进程调入CPU内,当原来进程在等待数据的时候,CPU可以开始执行这个新的进程,当原进程数据准备好后,CPU再将现在执行的这个进程调出去,重新调入原来的进程继续执行,这样CPU就不会出现空等待的情况,CPU的利用率也将因此而大大提高。

于是,现在的CPU都可以同时启动多个进程,不过只有一个进程正在被CPU执行,而其余的进程都因为这样或那样的原因在等待着CPU重新调入执行。通行的做法是,由操作系统给每个进程分配一定的时间片,这个时间片表明了该进程一次可以使用多长时间的CPU,当此进程使用时间到了之后,操作系统就会将其调出CPU,同时将另外一个进程调入CPU让其执行,由于每个进程所分得的时间片都很小,通常是几十个毫秒,因此一秒钟就能让很多进程都执行一两次,这样快的速度,会让人感觉不到切换的进行,与是人们就以为是多个进程在同时运行,其实,这里的同时是宏观上而非微观上的一个观念。

把一个进程调出CPU之后,应当把哪一个进程调入CPU,这就是一种调度算法,通常操作系统会根据这样的一种算法来选定一个将被调入的进程,这是操作系统的事,而CPU并不知道操作系统会怎样选择这个进程,它只知道现在操作系统命令它调入一个新的进程。

要把一个新的进程调入,就需要把原来的进程调出,由于等一会儿说不定还需要回到原来的进程继续运行,因此就必须保存现在的CPU状态,这很像中断处理中的保护现场,那么CPU都需要保护那些信息呢?这些信息又保存在什么地方呢?这个时候,TSS(任务状态结构)就出场了。(注:一般的书上都将其称为“任务状态段”,但我认为这个称呼会与代码段、数据段之类的东东弄混。因此,这里我将其称为“任务状态结构”,其实它本质上就是一块用来存储任务相关信息的空间。)

 

TSS(任务状态结构)的结构

我们先来看看TSS的结构:



上图,就是
TTS的最基本的结构,在它的后面,操作系统还可以另外增加若干字节以存放一些额外的数据,不过CPU只使用最基本的共104字节的空间。从上面的结构中我们可以看见,里面几乎保存了一个进程运行所需要使用的CPU的所有信息,下面,我们就来详细研究下这个结构。

在上图中,已经用三种色彩将上述的TSS结构分成了三部份,对于“未用”部份我们可以不必理会,另外还有两个部份:“CPU自动更新区”及“CPU只读区”,所谓“自动更新”是指当CPU在进行任务切换的时候,会自动将当前任务的相关信息存放到TSS的相应位置中,这样,CPU就保存了当前任务的相关信息。“只读区”是指CPU在任务切换时会从中读取相关信息,但是,在任务被切换出去的时候,不会保存它们,因此,“只读区”的信息是由操作系统在创建任务的时候就指定好的,CPU只是读取而不会去修改它们。

从上图中,我们知道了CPU将当前任务的相关信息保存在什么地方,不过这个TSS实在是太大了!它不可能放在CPU中,而只能被放在内存中,因此我们还需要一个指针只向内存中的TSS,这样CPU就可以通过这个指针找到相应的TSS了,这样的指针有两种,我们分别将其称为“TSS描述符”和“任务门”。

 

“TSS 描述符”及“任务门”的结构与用途

下面我们还是先来看看“TSS描述符”的结构:



上图就是“
TSS描述符”结构,从图中我们可以看见,它给出了一个TSS所在的内存位置以及TSS的大小。这里需要注意的是,从前面的TSS基本结构图中我们可以知道一个TSS基本结构最小不能小于104字节,因此,这里的TSS的大小限制最小是103TSS的最小结构的大小 104 – 1)。另外还要特别引起注意的就是图中的“B”位,这一标志位用来标志一个任务是否是忙任务。如果B位被清零,那么我们就说这个任务是一个“可用任务”,如果B位被置1,我们说这个任务是一个“忙任务”,所谓“忙任务”是指它正在被运行,或者它是一个被任务链链接起来的任务,明白“B”位的用处在实际的编程序非常重要,但在这里不打算详加描述,把详细的描述留给下面的文字完成。

从上图我们可以看出,一个“TSS描述符”指代了一个TSS结构,按理来说,这已经完全足够使用了,但是用于Intel允许在中断的时候也可以进行任务切换,这样,我们就可以把中断处理程序作为一个专门的任务,然而,中断描述符表中存放的只能是门描述符,而上面的“TSS描述符”并不是一种门描述符,因此,它不能被放在中断描述符表中,于是Intel又定义了一种“任务门”,它其实指向的是一个“TSS描述符”,但由于它是一种门描述符,因此,它可以被放在中断描述符表中,这样当发生中断的时候,CPU通过中断号查询中断描述符表,得到相应的门描述符,如果发现它是一个“任务门”,则通过它找到相应的“TSS描述符”,再通过相应的“TSS描述符”找到相应的“TSS结构”。其实,我总觉得定义“任务门”有点多此一举,但Intel已经这样做了,我们也就不得不照办。下面,我们就来看看“任务门”的结构:



上图就是“任务门”的结构,其中被用到的地方极少,
Intel真是浪费啊!P位与DPL位与前面的“TSS描述符”中的相应位的作用是一样的,这里就不多述说了,余下就说说“TSS选择符”吧。

从前面的TSS描述符我们以经知道了,一个“TSS描述符”指代了一个TSS结构,通过它我们可以知道一个TSS结构在内存中的位置。那么我们又怎样得到一个“TSS描述符”的呢?它又是放在什么地方的呢?

在操作系统中,这样的TSS描述符由于会被CPU、中断服务程序、其它进程访问,因此它只能放在“全局描述符表”中(有“关全局描述符表”在《操作系统引导探究》一文中有详细描述)。因此我们需要用一个索引来指出“TSS描述符”在全局描述符表中的位置,这样我们就可以找到相应的“TSS描述符”了,这个索引就被称之为“TSS选择符”,顾名思义,它是用来在“全局描述符表”中选择“TSS描述符”的。

下面,我们通过一个图来看看CPU是怎么进行任务切换的,这个地方比较有趣的是“任务门”与“TSS描述符”都是放在“全局描述符”表中的,并且都需要一个索引指出它们在表中的位置,而这个索引都是一个选择符,分别称为“任务门选择符”与“TSS选择符”。

 

CPU任务切换行为概述



上图就是一个任务切换发生情况的示意图,我们下面就来详细说明一下这个图。

从图上我们可以看出,CPU在下述三种情况下发生了任务切换:

1. 使用了jmpcall指令,而指令的操作符,也即目标地址指向了一个“TSS描述符”或者一个“任务门描述符”。(而这个任务门描述符其实还是指向了一个“TSS描述符”)。

2. 产生了中断,而中断向量指向了中断向量表中的一个“任务门描述符”。

3. 使用了IRET指令,并且在执行这个指令的时候EFLAGS寄存器中的NT位被置1了。

而在这三种情况下发生的任务切换CPU还有不同的动作,下面就让我们来详细的看一看。

1. 如果这个切换是由 jmp 指令引起的,那么,CPU会首先进行特权检查,并检查目标任务的B位(忙位)是否为0,然后CPU将目标任务的B位置为1,把当前任务的B位清零,随后,CPU把当前任务的状态信息压入相应的TSS结构中,并从目标任务的TSS结构中取出相应信息,这样,就完成了一次任务切换。

2. 如果这个切换是由 call 指令或中断引起的,那么,CPU在进行完特权检查后,同样会检查目标任务的B位是否为0,然后,CPU将目标任务的EFLAGS中的NT为置1,并将当前任务的“TSS描述符”放入目标任务的Link字段,之后,CPU把当前任务的状态信息压入相应的TSS结构中,并从目标任务的TSS结构中取出相应信息,完成任务切换。

3. 如果这个切换是由 iret 指令引起的(注意,此时当前任务的EFLAGS中的NT位是被置1的),那么CPU在进行完特权检测之后,会检查目标任务的B位是否为1(注意,这里是要求为1,而不是先前的0),之后,会把当前任务的B位清零,并且把当前任务的EFLAGS中的NT位清零,随后,CPU把当前任务信息压入相应的TSS结构中,并从目标任务的TSS结构中取出相应信息,完成任务切换。

从上面所描述的CPU的行为特征中我们不难发现,jmp 指令只是一个很单纯的跳转指令,从一个任务跳转到另一个任务,CPU不会做更多的操作,而在call指令与中断引发的任务切换中,CPU会把当前任务的“TSS描述符”放入目标任务的“TSS描述符”的Link字段中,并设置相应的状态位,这样,就相当于一个链表一样将当前任务与目标任务链接起来了,这样当使用iret指令的时候,就可以从目标任务返回原任务中执行。这一特性在中断处理中特别有用,因为,你可以直接用 iret 指令,从中断服务任务中返回原任务继续运行。

描述了这么多,有这么多的任务,这么多的“TSS描述符”同时存在,那么CPU怎样知道哪个“TSS描述符”所指代的任务是当前正在执行中的当前任务呢?其实在CPU内部有一个TR(任务寄存器),它里面存放的就是当前正在执行的任务的“TSS描述符”,在发生任务切换的时候,CPU也会自动将新任务的“TSS描述符”载入其中。

好了,有关本实验所用到的基本知识已介绍完毕,下面我们将来看看在实际的操作系统中,这一切都是怎么通过代码去实现的。

Go~~

 

pyos 本实验相关问题概述

通过前面几个实验,也许大家对 pyos 已经比较熟悉了,不过作为一个正在开发中的系统来说,不断的修改与完善是必不可少的,在本次实验中,pyos 又在结构上做了比较大的调整。本实验报告所用的 pyos 2004_05_31_10_00 版,你可以在http://purec.binghua.com(纯C论坛)上找到它的全部源代码。

由于 pyos 目前还没有完成磁盘驱动与文件管理这块,因此,本实验所用到的两个进程,A进程与B进程,是直接写到映象文件中的指定位置,并在系统启动后由setup程序直接读到内存指定位置的。

下面我们来看看这两个进程的代码:

#include "video.h"

 

extern "C" void user_main()

{

  static int i = 0 ;

  for( ;; ){   

    if( ++i == 0xfffffff ){ // 延迟

      class_pyos_Video::Print( "_A_" ) ;       

      i = 0 ;

    }

  }

}

上面的代码就是A进程的代码,B进程的代码与之几乎完全一样,只不过输出的不是“_A_”而是“-B-”。

首先我们使用 pyos_gcc 将它们分别编译:

out/pyos_gcc.exe source/A_Process.cpp 0x80000 out/A_Process.bin

out/pyos_gcc.exe source/B_Process.cpp 0x90000 out/B_Process.bin

其中的 0x80000,是指A进程将会被放在内存中的地址,同样0x90000B进程被放在内存中的位置,在启动的时候,它们分别会被setup.asm程序读到上述指定的内存地址中。有关这部份编译内容在源代码包中compile.bat文件里有详细描述,这里就不多说了,感兴趣的朋友可以看看相关源代码。

下面我们还是直接来看看内核代码吧:

/* 内核主函数 */

extern "C" void pyos_main()

{ 

  /* 系统初始化 */

  class_pyos_System::Init() ;

 

  /* 清屏,打印欢迎信息 */

  class_pyos_Video::Clear() ;

  class_pyos_Video::Print( "Pyos Task Switch Experiment/n" ) ;

 

  /* 安装时钟中断 */

  class_pyos_System::InstallCpuTimeInterrupt() ;

 

  /* 许可时钟中断 */

  class_pyos_System::EnableCpuTimeInterrupt() ;

 

  /* 打开中断 */

  class_pyos_Interrupt::OpenInterrupt() ;

 

  for( ;; ) ;

}

内核代码非常简单,注释也很详细,这里就只捡重点的说了。当内核运行之后,会打开时钟中断,在时钟中断里,系统会进行计数,看看到底发生了多少次中断,而每一次时钟中断,在pyos中都看做是一个最基本的时间片。随后,中断服务程序会与当前正在运行的进程所拥有的时间片进行比较,看看进程的时间片是否用光,如果用光了,时钟中断服务程序就进行任务切换。下面,我们就来看看这段代码:

/* 时钟中断处理函数 */

void class_pyos_System::HandleCpuTimeInterrupt()

{  

  // 发生时钟中断,准备进行任务切换

  static int i = 0 ; 

  unsigned int processNo = class_pyos_Process::CurrentProcessNumber ;

 

  if( processNo == 0 ){

    // 如果现在是内核进程,则切换到下个进程,并保证不再切换到内核进程

    processNo = processNo % 2 + 1 ;   

  }

  else{

    // 如果不是内核进程,则时间片+1

    ++i ;

    if( i == class_pyos_Process::ProcessQueue[ processNo ].CpuTime ){

      // 如果一个进程的cpu时间到,且切换到下一进程

      processNo = processNo % 2 + 1 ;     

      // 重新计算时间片

      i = 0 ;

    }   

  }

  class_pyos_Process::CurrentProcessNumber = processNo ; 

 

  unsigned int base ;

 

  // 先进行解连处理,取得先前任务的 TSS 描述符

  base = m_gdt.TSS[ 3 ].Base_0_15 ;

  base |= ( m_gdt.TSS[ 3 ].Base_16_23 << 16 ) ;

  base |= ( m_gdt.TSS[ 3 ].Base_24_31 << 24 ) ;

  struct_pyos_TSS *pTss = ( struct_pyos_TSS * )base ;

  base = pTss->Link ;

 

  // 将先前任务标记为非忙任务

  base += ( unsigned int )&m_gdt ;

  struct_pyos_TSSItem* p = ( struct_pyos_TSSItem * )base ;

  p->B = 0 ; 

 

  // 将欲执行的任务描述符标记为忙任务

  class_pyos_System::m_gdt.TSS[ processNo ].B = 1 ; 

 

  // 把当前任务(时钟中断处理程序的Link换成欲跳转到的任务选择子)

  pTss->Link = class_pyos_Process::ProcessQueue[ processNo ].TssSelector ;

  return ;

}

这里,时钟中断首先进行了解链处理,因为在前面的描述中我们知道了,在中断引起的任务切换中,会将原任务的TSS描述符放入中断服务程序的TSS中的Link字段中,这样在中段返回的时候,就可以返回到原任务去执行,现在由于在中断服务程序中需要切换到另外一个任务去执行,而不能直接返回到被中断的原任务中运行(因为,如果还是返回到被中断的原任务中继续运行,就无法完成任务调度了)。所以需要首先解除掉原来被中断的任务与中断服务程序的关系,然后,与准备调度的任务建立起链接关系,把准备调度的任务的TSS描述符放入中断服务程序的TSSLink中,这样,当执行iret指令返回时,就会返回到(切换到)准备调度的任务中去执行,这样就完成了任务调度。

在上面的代码中,使用了很多结构,它们均在process.h这个文件中定义完成,这个文件定义了一个用来处理任务的类:

// 进程管理类

class class_pyos_Process{

  public:

    static struct_pyos_Process ProcessQueue[ 4 ] ; // 进程队列

    static struct_pyos_TSS TSS[ 4 ] ; // 任务状态段

    static void Init() ; // 进程管理初始化

    static unsigned int CurrentProcessNumber ; // 当前正在执行的进程号

} ;

在本实验的 pyos 中,共定义了四个进程,

0号进程:内核进程

1号进程:A进程

2号进程:B进程

3号进程:时钟中断服务进程(也是任务调度进程)

struct_pyos_Process 这个结构定义了一些进程所需要使用的状态信息,也就是操作系统教程中常常提到的进程结构体吧,它用来标识一个进程:

// 定义进程结构

struct struct_pyos_Process{

  unsigned int CpuTime ; // 进程被分配的 cpu 时间大小

  unsigned int TssSelector ; // 进程的 TSS 段选择符

} ;

由于在本实验中只用到了进程的上述信息,因此这个结构体十分简单。

下面,我们来看看进程初始化函数,它的主要工作就是为准备运行的进程初始化进程结构体及相应的TSS结构体及TSS描述符,以备操作系统随时调用:

// 进程初始化函数

void class_pyos_Process::Init()

{

  struct_pyos_TSS* tss ;

  struct_pyos_TSSItem* tssItem ;

  unsigned int tssAddr ;

 

  /* 设置当前的进程号是 0 */

  CurrentProcessNumber = 0 ;

 

  /* 初始化 0 号进程所用的任务段 */

  tss = &class_pyos_Process::TSS[ 0 ] ;

  tss->Cr3 = 0 ;

  tss->Ldt = 0 ;

   

  /* 初始化 0 号进程所用的任务段描述符 */

  tssAddr = ( unsigned int )&class_pyos_Process::TSS[ 0 ] ;

  tssItem = &class_pyos_System::m_gdt.TSS[ 0 ] ;

  tssItem->B = 0 ;

  tssItem->Base_0_15 = tssAddr ;

  tssItem->Base_16_23 = tssAddr >> 16 ;

  tssItem->Base_24_31 = tssAddr >> 24 ;

  tssItem->DPL = 0 ;

  tssItem->G = 1 ;

  tssItem->LimitLength_0_15 = 103 ;

  tssItem->LimitLength_16_19 = 0 ;

  tssItem->P = 1 ;

  tssItem->Saved_00 = 0 ;

  tssItem->Saved_010 = 2 ;

  tssItem->Saved_1 = 1 ;

  

  /* 载入 0 号 进程也即当前进程的 TSS 的选择符到 TR(任务寄存器)中 */

  __asm__( "movw $0x28 , %ax" ) ;

  __asm__( "ltr %ax" ) ; 

 

  // 设置 1 号进程的进程队列参数

  class_pyos_Process::ProcessQueue[ 1 ].CpuTime = 200 ; // 分配 1 号进程 200 个时间片

  class_pyos_Process::ProcessQueue[ 1 ].TssSelector = 0x30 ;

 

  /* 初始化 1 号进程(A进程)所用的任务段 */

  tss = &class_pyos_Process::TSS[ 1 ] ;

  tss->Cr3 = 0 ; 

  tss->Cs = 0x8 ;

  tss->Ds = 0x10 ;

  tss->Eax = 0 ;

  tss->Ebp = 0 ;

  tss->Ebx = 0 ;

  tss->Ecx = 0 ;

  tss->Edi = 0 ;

  tss->Edx = 0 ;

  tss->Eip = 0x80000 ;

  tss->Es = 0x10 ;

  tss->Esi = 0 ;

  tss->Esp = 0x8ffff ;

  tss->Fs = 0x10 ;

  tss->Gs = 0x10 ;

  tss->Ldt = 0 ;

  tss->Ss = 0x10 ; 

  tss->Eflags = 0x202 ; // 指定 Eflags,其中断标志设为开中断

 

  /* 初始化 1 号进程所用的任务段描述符 */

  tssAddr = ( unsigned int )&class_pyos_Process::TSS[ 1 ] ;

  tssItem = &class_pyos_System::m_gdt.TSS[ 1 ] ;

  tssItem->B = 0 ;

  tssItem->Base_0_15 = tssAddr ;

  tssItem->Base_16_23 = tssAddr >> 16 ;

  tssItem->Base_24_31 = tssAddr >> 24 ;

  tssItem->DPL = 0 ;

  tssItem->G = 1 ;

  tssItem->LimitLength_0_15 = 103 ;

  tssItem->LimitLength_16_19 = 0 ;

  tssItem->P = 1 ;

  tssItem->Saved_00 = 0 ;

  tssItem->Saved_010 = 2 ;

  tssItem->Saved_1 = 1 ;

 

  // 设置 2 号进程的进程队列参数

  class_pyos_Process::ProcessQueue[ 2 ].CpuTime = 400 ; // 分配 2 号进程 400 个时间片

  class_pyos_Process::ProcessQueue[ 2 ].TssSelector = 0x38 ;

 

  /* 初始化 2 号进程(B进程)所用的任务段 */

  tss = &class_pyos_Process::TSS[ 2 ] ;

  tss->Cr3 = 0 ; 

  tss->Cs = 0x8 ;

  tss->Ds = 0x10 ;

  tss->Eax = 0 ;

  tss->Ebp = 0 ;

  tss->Ebx = 0 ;

  tss->Ecx = 0 ;

  tss->Edi = 0 ;

  tss->Edx = 0 ;

  tss->Eip = 0x90000 ;

  tss->Es = 0x10 ;

  tss->Esi = 0 ;

  tss->Esp = 0x9ffff ;

  tss->Fs = 0x10 ;

  tss->Gs = 0x10 ;

  tss->Ldt = 0 ;

  tss->Ss = 0x10 ; 

  tss->Eflags = 0x202 ;

 

  /* 初始化 2 号进程所用的任务段描述符 */

  tssAddr = ( unsigned int )&class_pyos_Process::TSS[ 2 ] ;

  tssItem = &class_pyos_System::m_gdt.TSS[ 2 ] ;

  tssItem->B = 0 ;

  tssItem->Base_0_15 = tssAddr ;

  tssItem->Base_16_23 = tssAddr >> 16 ;

  tssItem->Base_24_31 = tssAddr >> 24 ;

  tssItem->DPL = 0 ;

  tssItem->G = 1 ;

  tssItem->LimitLength_0_15 = 103 ;

  tssItem->LimitLength_16_19 = 0 ;

  tssItem->P = 1 ;

  tssItem->Saved_00 = 0 ;

  tssItem->Saved_010 = 2 ;

  tssItem->Saved_1 = 1 ;

 

  /* 初始化 3 号进程(时钟中断)所用的任务段 */

  tss = &class_pyos_Process::TSS[ 3 ] ;

  tss->Cr3 = 0 ; 

  tss->Cs = 0x8 ;

  tss->Ds = 0x10 ;

  tss->Eax = 0 ;

  tss->Ebp = 0 ;

  tss->Ebx = 0 ;

  tss->Ecx = 0 ;

  tss->Edi = 0 ;

  tss->Edx = 0 ;

  tss->Eip = ( unsigned int )pyos_asm_interrupt_handle_for_cpu_time ;

  tss->Es = 0x10 ;

  tss->Esi = 0 ;

  tss->Esp = 0x7ffff ; // 时钟中断所用的堆栈区,(时钟中断单独使用自己的堆栈区,以免内核出现堆栈错误)

  tss->Fs = 0x10 ;

  tss->Gs = 0x10 ;

  tss->Ldt = 0 ;

  tss->Ss = 0x10 ;

  tss->Eflags = 0x202 ;

 

  /* 初始化 3 号进程所用的任务段描述符 */

  tssAddr = ( unsigned int )&class_pyos_Process::TSS[ 3 ] ;

  tssItem = &class_pyos_System::m_gdt.TSS[ 3 ] ;

  tssItem->B = 0 ;

  tssItem->Base_0_15 = tssAddr ;

  tssItem->Base_16_23 = tssAddr >> 16 ;

  tssItem->Base_24_31 = tssAddr >> 24 ;

  tssItem->DPL = 0 ;

  tssItem->G = 1 ;

  tssItem->LimitLength_0_15 = 103 ;

  tssItem->LimitLength_16_19 = 0 ;

  tssItem->P = 1 ;

  tssItem->Saved_00 = 0 ;

  tssItem->Saved_010 = 2 ;

  tssItem->Saved_1 = 1 ; 

}

代码非常简单,并有很详尽的注释,这里就不多说了,具体的一些细节可以参看实际的代码。

最后,我们来看看在全局描述符表中这些个TSS描述符与任务门是按什么顺序存放的,这是理解代码中类似于0x280x30,之类的描述符的关键,这部份代码是在system.h中定以的。

/* GDT */

struct struct_pyos_Gdt{

  struct_pyos_GdtItem gdtNull ;       //空段,Intel保留

  struct_pyos_GdtItem gdtSystemCode ; //系统代码段

  struct_pyos_GdtItem gdtSystemDate ; //系统数据段

 

  /* 系统调用门 */

  struct_pyos_InvokeGate InvokeGate[ 2 ] ;

 

  // 任务段(TSS)描述符

  struct_pyos_TSSItem TSS[ 4 ] ; //0(0x28)号给 0 号进程用 1(0x30) 号给 A 进程用

                                 //2(0x38) 号给 B 进程用 3(0x40) 号给时钟中断用

 

  // 任务门

  struct_pyos_TSSGate TssGate ; //0x48

} ;

整个pyos在本实验中像下图一样运行:

 

实验的补充说明

本实验的代码与程序相对而言都非常简单,因此你完全可以在此基础上编写更复杂的调度算法,也可以修改一下程序中分给不同进程的时间片的大小,看看其对进程执行速度的影响。

需要说明一点的时,本实验旨在探讨一种操作系统中,实际处理任务调度的方法,“是也许可以这样,而不是应该这样”。从代码中可以看出,pyos 是一个相当蜗牛的系统,因为它在调度一个任务的时候进行了多次任务切换,而每一次切换cpu都会保存104字节的数据,这种操作是相当费时的。因此,如果是一个性能优良的实际系统,应当避免这样的操作,这可以使用软切换而不是通过CPU硬件切换来实现。不要总以为硬件就一定快,因为软件可以根据需要调整,可以做得很小,可以只保存最最重要的数据,这样,实际的切换将会快许多。不过,pyos并不旨在成为一个高效系统,它只是一个实验系统,用它来检验所学,进行实验。


 

参考资料:

Pentium Family User’s Manual Volume3: Architecture and Programming Manual.Intel. 1994

80X86 汇编语言程序设计教程》.杨季文. 清华大学出版社 19986月第一版

原文:http://purec.binghua.com/Article/ShowArticle.asp?ArticleID=217

0 0

相关博文

我的热门文章

img
取 消
img