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

操作系统中任务调度的实现  
作者:谢煜波
Email
xieyubo@126.com
QQ
13916830
2004-5-31

说起任务调度,我想我们大家都非常熟悉,任何一本操作系统教程,都对此描述得非常详细,不过,他们大多数都是站在理论高度,然而具体到某台实际的机器上,某个实际的操作系统当中,它们具体是怎样用代码去实现的,描述却相对较少。本报告将站在一个编写实际操作系统的角度,去研究一下在 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 的大小限制最小是 103 TSS 的最小结构的大小 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. 使用了 jmp call 指令,而指令的操作符,也即目标地址指向了一个“ 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 进程将会被放在内存中的地址,同样 0x90000 B 进程被放在内存中的位置,在启动的时候,它们分别会被 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 描述符放入中断服务程序的 TSS Link 中,这样,当执行 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 描述符与任务门是按什么顺序存放的,这是理解代码中类似于 0x28 0x30 ,之类的描述符的关键,这部份代码是在 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 并不旨在成为一个高效系统,它只是一个实验系统,用它来检验所学,进行实验。


 

 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值