词条 | geekos |
释义 | GeekOS是一个基于X86架构的PC上运行的微操作系统内核,由美国马理兰大学的教师开发,主要用于操作系统课程设计,目的是使学生能够实际动手参与到一个操作系统的开发工作中。出于教学目的,这个系统内核设计简单,却又兼备实用性,它可以运行在真正的X86 PC硬件平台。 GeekOS教学操作系统概论操作系统是管理系统软,硬件资源,控制程序运行,改善人机界面,提供各种服务,合理组织计算机工作流程和为用户有效使用计算机提供良好运行环境的系统软件,它为用户使用计算机提供一个方便,灵活,安全,可靠的工作环境,也是其他应用软件赖以存在的基础.操作系统是计算机系统的重要组成部分,操作系统课程是计算机教育的必修课程,作为计算机专业的核心课程,不但高校计算机相关专业的学生必须学习操作系统,从事计算机行业的从业人员也需要深入了解它. 计算机操作系统课程是理论性和实践性都较强的课程,具有概念多,抽象,涉及面广的特点.在设置操作系统课程教学要求时,教师就要考虑对学生作出什么样的要求.是纯理论的对书本习题和概念作出解答就可以了呢,还是要求学生能动手参与实践.实践也有不同程度的要求,是单纯的实现操作系统的某些算法呢,还是实际编写或修改操作系统功能模块.由于实践环境的限制,许多高校目前都偏重对理论知识的要求,注重基本理论知识的掌握和一些典型算法的实践(一般选择UNIX或Linux作为实验环境,要求学生用C语言编程实现简单的进程创建,进程调度等算法),所以学生基本没有机会去了解,实践操作系统的内部结构和实现技术.实践证明,要真正学好操作系统原理和设计技术,最好的方法就是让学生参与到操作系统的开发工作中.因此,越来越多的高校在开设操作系统理论课程的同时,会要求学生对现有操作系统进行功能改进或再开发,以增加学生对操作系统核心技术的实践,真正做到理论与实践相结合. 那么,能用作学生操作系统课程实践的平台有哪些呢 大家一般很容易想到使用现有的商业操作系统和开放源代码的操作系统,也有很多这样的操作系统可供学生选择,比较流行的有Linux,Minix等.虽然也有一些学校确实采用这些操作系统作为实践平台,但采用这些 操作系统存在的缺点也是不容忽视的:这些操作系统一般都结构庞大,过于复杂,学生在短时间内很难理解,而且这些操作系统几乎已经实现了所有的功能(进程管理,存储器管理,文件系统等),不需要学生自行设计或实现一些子系统,因此从教学实践的角度讲,价值不高.最好的方法不是选择一个完整的,实用的,庞大的商业操作系统,而是选择一个既具备基本操作系统核心功能,与实际使用的操作系统比较接近,但又易于理解,规模较小的操作系统作为教学平台,在这个教学平台上,学生可以修改和扩充基本系统以实现更多功能,这种操作系统称为教学操作系统. 当我们决定选用教学操作系统作为我们的操作系统课程实践平台后,剩下的工作就是在现有的多种教学操作系统中选择一种.教学操作系统有两大类,一类是针对RISC结构MIPS处理器的,另外一类是针对CISC结构的Intel IA-32(或X86)通用处理器的.这样分类是因为:处理器是操作系统运行的硬件环境中最重要的部分. 针对RISC结构MIPS处理器的教学操作系统有Nachos(Not Another Completely Heuristic Operating System)和OS/161.其中Nachos是建立在软件模拟的虚拟机之上的教学操作系统,采用MIPS R2/3000的指令集,能模拟主存,中断,网络以及磁盘系统等所必须的硬件系统,美国加州大学伯克利分校多次采用该操作系统作为课程设计平台.OS/161是运行在与操作系统无关的System/161模拟器上的,操作系统代码是MIPS对应的机器代码.但无论是Nachos还是OS/161,若学生使用Windows或Linux 开发环境的话,都需要使用交叉编译器才能把代码编译成MIPS相应的机器代码. Minix和GeekOS是针对CISC结构的Intel IA-32 (或X86)通用处理器的.其中,Minix是Andrew S. Tanenbaum(AST)于1987年开发的,目前主要有1.5 版和2.0 版两个版本在使用.Minix系统是免费的,可以从许多FTP 上下载,但Minix是一个包括了虚拟内存管理,文件系统,设备驱动程序,网络和用户态程序等的比较完整的操作系统,它由两万多行代码组成,对于教学有点过于庞大和复杂,而且由于它已经实现了操作系统的全部基本功能,没有留下合适的练习让学生自己完成. 大家知道,最通用的处理器是CISC结构的Intel IA-32 (或X86)通用处理器,所以选用针对该结构的教学操作系统是比较合适的,我们选用GeekOS作为操作系统课程设计平台主要原因还有:它是一个用C语言开发的操作系统,学生可以在Linux或UNIX环境下对其进行功能扩充,也可以在Windows下使用Cygwin工具进行开发,且其针对进程,文件系统,存储管理等操作系统核心内容分别设计了7个难度逐渐增加的项目供教师选择.我们将在后面的章节中详细为大家介绍GeekOS教学操作系统. GeekOS教学操作系统一. GeekOS概述 GeekOS是一个基于X86架构的PC上运行的微操作系统内核,由美国马理兰大学的教师开发,主要用于操作系统课程设计,目的是使学生能够实际动手参与到一个操作系统的开发工作中.出于教学目的,这个系统内核设计简单,却又兼备实用性,它可以运行在真正的X86 PC硬件平台.作为一个课程设计平台,GeekOS由一个基本的操作系统内核作为基础,提供了操作系统与硬件之间的所有必备接口,实现了系统引导,实模式到保护模式的转换,中断调用及异常处理,基于段式的内存管理,FIFO进程调度算法以及内核进程,基本的输入输出(键盘作为输入设备,显示器作为输出设备),以及一个用于存放用户程序的只读文件系统PFAT. 二. GeekOS的存储器管理 GeekOS内核有两种存储器分配方式,分页分配方式和堆分配方式. 1.分页分配方式 系统中所有存储器都分成大小相等的块,称作页.在X86系统中,页的大小是4KB.若在GeekOS中增加了支持虚拟存储器的功能,页也可以是虚拟存储空间的存储单元.在不支持虚存的系统中,页也可以看作是一个固定大小的存储块,页的分配和回收用函数Alloc_Page()和Free_Page(),这两个函数的定义在头文件中.在GeekOS中每一页都是一个Page结构: struct Page { unsigned flags; /* 页状态 */ DEFINE_LINK(Page_List, Page); /* Page_List页链表指针*/ int clock; ulong_t vaddr; /* 页映射到的用户空间虚拟地址 */ pte_t *entry; /* 指向页表中本页的页表项*/ }; 其中DEFINE_LINK是在list.h文件中的宏定义,定义了指向链表节点的指针.GeekOS使用宏定义链表结构及操作,具体代码请参考list.h文件.系统全局页链表struct Page_List g_pageList记录内存所有页的Page结构,其中flags标记为PAGE_AVAIL的页为空闲页,s_freeList记录系统所有的空闲页. 2.堆分配方式 堆分配提供不同大小存储块的分配,使用函数Malloc()和Free()进行存储块的分配和回收. 3.系统初始化内存布局 系统初始化时由Init_Mem函数将系统内存划分为内核空间,可用空间等若干部分,如图1-1所示:其中内存空洞是系统设计时留作其他功能使用的,属保留区域.内核堆是一块用于动态分配和回收的内存,系统使用bget,brel,bpool三个函数管理这块空间,堆分配方式中的Malloc函数与Free函数就是通过调用这些函数实现动态分配和回收.可用空闲内存用于分配其他的系统数据. 图1-1 系统初始化内存布局 三. GeekOS支持的设备 1.文本显示器 文本显示器支持显示文本信息,GeekOS中的显示驱动仅能支持VT100和ANSI的一个子集,且不含方向移动和字符特性设置等功能.实现文本显示的函数在头文件中定义. 用户在编程时经常会用到的一个函数是print(),它是标准C语言函数Printf()的子集,功能是将文本信息输出到显示器.其他还有一些输出函数,如Put_Char()和Put_Buf(),使用这两个函数分别可以输出单个字符和字符串. 2.键盘 键盘设备驱动程序提供了一系列高级接口以使用键盘.用户需要注意的是键盘事件的逻辑关系:用户按键引发键盘中断,键盘中断读取用户按键并将键码放到键盘缓冲区s_queue中,而用户进程则将缓冲区的键盘码读出来作进一步处理. 若用户进程需要从键盘输入信息,可调用Wait_For_Key()函数,该函数首先检查键盘缓冲区是否有按键,如果有,就读取一个键码,如果此时键盘缓冲区中没有按键,就将进程放入键盘事件等待队列s_waitQueue,由于用户的按键操作触发了键盘中断,键盘中断处理函数Keyboard_Interrupt_Handler就会读取用户按键,将低级键扫描码转换为含ASCII字符的高级代码,并刷新键盘缓冲区,最后唤醒等待按键的进程继续运行.若用户按下Shift,Control,Alt等键时,也能同样处理.键盘处理程序的代码在头文件中,详见第8章项目设计0. 3.系统时钟 GeekOS中用户一般不直接使用任何时钟服务,系统时钟主要用于时钟中断,一般用于保证所有的线程都有机会占用CPU,即线程运行一段时间后会发生时钟中断,调度程序就选择另外的线程运行. 4.块设备:软盘和IDE硬盘 块设备是指按固定大小的块(扇区)存取信息的存储设备,块设备一般作为文件系统的基本存储设备,文件系统会在物理块存储的基础上创建文件,目录等以方便操作.不同块设备的扇区大小不完全一样,但GeekOS系统中假设所有块设备的扇区大小都一样——512个字节,并用宏SECTOR_SIZE常量进行了定义. GeekOS支持两种块设备:软盘和IDE硬盘,系统用名字fd0表示第一个软驱,ide0表示第一个IDE硬盘分区,ide1表示第二个IDE硬盘分区.块设备的分区信息用内核的BLOCK_DEVICE数据结构表示,用户要使用某个设备的时候只要调用函数Open_Block_Device(),函数参数就是用户要使用的设备名.打开设备后,用户就可以分别调用Block_Read()和Block_Write()来读,写设备指定扇区的信息.GeekOS系统中块设备操作处理过程如图1-2所示. 图1-2 GeekOS系统块设备操作处理流程 在GeekOS系统中,每个块设备都用一个Block_Device结构记录: struct Block_Device { char name[BLOCKDEV_MAX_NAME_LEN]; // 块设备名称,如"ide0" struct Block_Device_Ops *ops; // 指向块设备操作函数指针列表的指针 int unit; // 单位磁盘块的大小,如一个硬盘磁盘块为4KB bool inUse; // 设备是否在使用 void *driverData; // 实体块设备信息 struct Thread_Queue *waitQueue; // 等待访问块设备的进程的队列 struct Block_Request_List *requestQueue; // 块设备访问请求队列 DEFINE_LINK(Block_Device_List, Block_Device); // 块设备链表指针 }; 其中Block_Device_Ops结构定义: struct Block_Device_Ops { int (*Open)(struct Block_Device *dev); // 打开块设备 int (*Close)(struct Block_Device *dev); // 关闭块设备 int (*Get_Num_Blocks)(struct Block_Device *dev); //块号 }; 在GeekOS设计项目中,系统主要将块设备用作GeekOS文件系统的物理载体.在系统初始化时分别将检测到的软盘,硬盘作为块设备注册到系统块设备列表,在注册块设备时,系统将其Block_Device结构初始化,并添加到s_deviceList设备链表.这个链表由各个设备的Block_Device结构指针组成.系统提供了一系列块设备操作函数:Open_Block_Device,Block_Read,Block_Write,Close_Block_Device等.标准的块设备使用过程顺序是:首先调用Open_Block_Device函数打开s_deviceList设备列表中指定的设备,然后调用Block_Read或Block_Write函数执行读写操作,完成操作后调用Close_Block_Device关闭块设备.实现块设备存取的函数在头文件中进行定义. 四.GeekOS的中断和线程 1.中断 中断是用来向CPU通知重要事件的,中断的重要特性是:引发CPU控制权的转移.中断发生后,线程会暂停执行,而转去执行相应的中断处理程序,中断处理程序实际上是一个C语言程序.中断处理结束后,控制权返回给发生中断的线程,大多数情况下,线程会像没有发生中断一样继续执行.但由于中断可能引发线程交换,所以也可能导致共享的内核数据结构被修改等. 下面是一些不同类型的中断及中断处理方法. Exception(异常):若当前运行的线程执行了非法操作,则发生异常.异常是一种同步中断,因为异常的发生是可预知的.这类中断的例子有执行非法指令,被0整除等.因为这种异常无法恢复,所以通常都只能采取撤销父线程的方式处理. Faults(故障):故障和异常一样属于同步中断.和异常不同的是,故障通常是可恢复的,内核通过执行一系列操作,去除引起故障的条件,然后使发生故障的线程继续执行即可.缺页中断就是故障的一种,缺页中断是指CPU要访问的数据或代码还没有调入内存时发生的中断,当内核找到要访问的内容并调入内存空间时,发生中断的线程就可以继续执行了. Hardware interrupts(硬件中断):外设用硬件中断将某些事件通知CPU.硬件中断是不可预知的,所以是异步中断,也就是说,异步中断随时会发生.但有时系统可能无法立即处理异步中断,在这种情况下,系统会暂时屏蔽中断直到系统能处理中断时再处理中断.时钟中断就是一种硬件中断. Software interrupt(软件中断):用户态进程用于发出信号表示它需要系统内核的干涉.GeekOS中仅使用一种软件中断——系统调用.线程用系统调用向内核发出服务请求,如线程要打开文件,执行输入,输出操作,创建新线程等. 2.中断处理函数 GeekOS中断调用的实现主要涉及到两个数据结构:中断描述符表s_IDT与中断处理函数表g_interruptTable.s_IDT在内核中静态分配,每个描述符指向lowlevel.asm文件中定义的中断向量宏定义程序段,这些程序段定义了256个中断向量的调用.其中,0~17号中断为Intel CPU的CPU中断向量,18~225号为用户自定义的中断向量.GeekOS将大多数中断处理函数初始化为函数Dummy_Interrupt_Handler,该函数在中断时显示当时保存的计算机各寄存器状态.12~13号CPU中断处理函数为GPF_Handler,系统调用使用Syscall_Handler中断处理函数,键盘,计时器等硬件中断也有各自的外部中断处理函数. 3.线程 在支持线程的系统中,允许多个任务分时共享CPU.GeekOS中每个线程都是一个Kenerl_Thread对象,在中定义.系统有一个调度程序(scheduler)用于选择线程运行.任意时刻系统只能有一个线程在运行,这个运行线程就称作当前线程(current thread),在全局变量g_currentThread中有一个指针指向当前正在运行进程的Kenerl_Thread对象.如果线程已经准备好可以运行了,但不是正在运行的,就放入准备运行队列(run queue).线程若在等待某件事情发生则放入等待队列(wait queue).无论是等待队列还是准备运行队列的线程都用数据结构Thread_Queue记录,Thread_Queue是Kenerl_Thread对象中的一个链表结构. 系统中一些线程完全运行于核态,这类线程称为系统线程.系统线程一般用于完成服务请求,如reaper线程用于释放已经执行结束的线程所占用的资源;软盘和IDE接口的磁盘各使用一个系统线程用于I/O请求,I/O操作并将操作结果传输给请求线程. 进程不同于系统线程,进程大多情况下在用户态执行,我们对进程应该很熟悉了,当我们在Linux或Windows下运行一个程序时,系统都会为程序创建相应的进程.每个进程都占用各自的存储空间,文件,信号量等资源.GeekOS中的进程其实就是一个简单的线程,每个进程有一个特殊的数据结构User_Context,由于GeekOS中的进程是能运行于用户态的普通线程,所以也称为用户线程.由于进程在用户态运行,当发生中断时,系统会切换到核态进行中断处理,当中断处理结束后,系统再返回到用户态继续运行进程. 4.线程同步 GeekOS提供了高级线程同步机制:互斥信号量(mutexes)和条件变量(condition variables),它们的定义在头文件中.需要注意的是互斥信号量和条件变量只能用于线程同步,异步中断处理不能访问互斥信号量或条件变量. 互斥信号量用于保护临界区的互斥访问,即同一时刻仅允许一个线程访问临界区,如果一个线程要访问一个互斥信号量,而此时已经有另外一个线程已经在使用该互斥信号量,那该线程只能被阻塞,直到该互斥信号量可以被访问.下面是一个例子. #include struct Mutex lock; struct Node_List nodeList; void Add_Node(struct Node *node){ Mutex_Lock(&lock); Add_To_Back_of_Node_List(&nodeList , node); Mutex_Unlock(&lock); } 每个条件变量代表一个线程可以等待的条件,每个条件变量与一个互斥信号量相关,当检测或修改与条件变量有关的程序段时,必须保持互斥访问.下面是一个条件变量访问的例子. #include struct Mutex lock; struct Condition nodeAvail; struct Node_List nodeList; void Add_Node(struct Node *node){ Mutex_Lock(&lock); Add_To_Back_of_Node_List(&nodeList , node); Cond_Broadcast(&nodeAvail); Mutex_Unlock(&lock); } struct Node *Wait_For_Node(void){ struct Node *node; Mutex_Lock(&lock); While (Is_Node_List_Empty(&nodeList)){ /*等待另外一个线程调用Add_Node()*/ Cond_Wait(&nodeAvail,&lock); } node = Remove_From_Front_of_Node_List(&nodeList); Mutex_Unlock(&lock); Return node; } 5.线程和中断的交互 理解中断和线程的交互对扩充内核功能是极为重要的.GeekOS内核是允许抢占的,这就意味着线程切换可能随时发生.在某个抢占点,由调度程序选择哪个线程运行,一般情况下调度程序会选择最高优先级的运行态线程运行.但线程切换还是会经常发生,如为防止某个线程长时间占用CPU,系统提供时钟中断.时钟中断会引发异步线程切换;其他硬件中断,如软盘访问中断,也会引发异步线程切换.因为线程切换或中断处理会引起一些共享数据结构的修改,可能导致内核崩溃或其他不可预知的后果.好在系统可以通过屏蔽中断使抢占暂时不可用.调用函数Disable_Interrupts()就可以屏蔽中断了(函数原型定义在中),一旦调用这个函数,处理器将忽略所有外部硬件中断,其他线程和中断处理程序都不能运行,就可以保证当前线程对CPU的控制权,若要允许抢占,只要调用函数Enable_Interrupts()开中断就可以了.系统在某些情况下也必须屏蔽中断,如执行原子操作(构成操作的一系列指令需要当作一个整体执行,执行过程不能中断);修改调度程序使用的数据结构时也必须屏蔽中断,如将当前线程放入等待队列时等.下面有一个需要屏蔽中断的例程. /*线程等待*/ Disable_Interrupt(); While (!eventHasOccurred){ Wait(&waitQueue); } Enable_Interrupt(); 在这个例子中,线程在等待一个异步事件的发生,在该事件发生前,线程都将自我阻塞在等待队列.当等待的事件发生后,该事件的中断处理程序将eventHasOccurred标志改为true,并将该进程从阻塞队列移到运行队列.用户可以假设一下,如果在上面的代码中,没有屏蔽中断会怎么样.首先,事件的中断处理可能在检测eventHasOccurred标志和将线程放入等待队列的中间发生,那就意味着无论等待的事件是否已经发生,线程都将永远等待.第二种可能性是,在线程放入等待队列的过程中,可能会发生线程切换,中断处理程序会将当前运行的进程放入运行队列,而等待队列处于修改(不一致)状态.此时,若运行的代码需要访问等待队列,将引起系统崩溃.幸运的是,在GeekOS中,所有需要屏蔽中断才能运行的函数中都有断点,如果没有屏蔽中断就调用这些函数,系统会立即发出提示.所以,在函数代码中,我们经常可以看到这样的语句:KASSERT(!Interrupts_Enabled()).这个语句就表示若要继续执行下面的代码,请先屏蔽中断.刚开始写内核代码的时候,用户可能不容易知道运行哪些代码段是需要屏蔽中断的,慢慢的用户就能熟悉了. 五.GeekOS系统引导和初始化 GeekOS系统的开发是基于真正的硬件环境的,因此完全可以在一台x86 PC上安装并运行,但还是推荐大家在PC模拟器Bochs上运行,因为软件模拟硬件的可靠性比真实硬件高得多,不会因为硬件故障而导致系统崩溃,也便于内核程序的调试,不必要经常关闭和重新启动计算机系统.Bochs是用C++开发的可移植的IA-32(x86)PC模拟器,能够在大部分常见操作系统平台上运行,它包括了对Intel x86 CPU,通用I/O设备和定制BIOS等的模拟,当前是由Bochs项目组进行维护.在Bochs模拟器上也能够运行大部分的操作系统,Linux,Windows,DOS等.Bochs可以从网站http://bochs.sourceforge. net下载,详细的Bochs内容将在第2章介绍.用户首先启动Bochs模拟器,主机操作系统加载Bochs程序到主机的内存中.BIOS加载完成后,在Bochs中的操作就会像在真的计算机上操作一样. 1.GeekOS系统引导 GeekOS内核编译后,在build目录下会生成一个软盘镜像文件fd.img.在Bochs开始运行系统后,会自动检测启动设备.因为软盘首扇区最后一个字在编译时是写入55AA数据,而Bochs被配置为从软盘启动,这样Bochs得以成功地检测到GeekOS的启动软盘.之后Bochs就会像一台真正的计算机一样,首先导入软盘的首扇区数据到从内存地址0x7c00开始的一块内存区,之后跳转到这个地址,开始执行这段首扇区内的程序代码.首扇区内的代码是由位于/src/geekos目录中的fd_boot.asm编译生成的引导程序.这段汇编程序完成搜索并装载软盘中的GeekOS内核二进制文件的功能.在装载完毕后,装载程序执行段间跳转,转入程序Setup(/src/geekos目录中的setup.asm编译生成)继续执行. Setup程序完成装载临时GDT,IDT描述符,打开A20地址线,初始化PIC中断控制器,最后由实模式跳入保护模式.完成了实模式向保护模式的转换之后,Setup跳转到内核ENTRY_POINT入口点.至此,GeekOS的引导过程结束,内核初始化过程开始. 需要说明的一点是Setup除了跳转到保护模式之外,还作了一些内核初始化的准备工作:通过调用BIOS中断重置软盘驱动,检测计算机可用内存;之后Setup在堆栈创建一个Boot_Info结构,并将检测到的内存大小存放到这个数据结构中保存. 2.GeekOS系统初始化 GeekOS的内核入口点ENTRY_POINT指向的是内核Main函数的函数入口,在编译时完成对ENTRY_POINT的初始化.Main函数在src/src/geekos/main.c中实现.Main函数通过调用内核各模块的初始化函数来完成系统内核的初始化,这些函数绝大部分都由原始的GeekOS内核提供,不需要自己编制,这大大减轻了GeekOS的项目开发难度. 下面简要介绍这些内核各模块的初始化函数,不同项目的初始化函数基本相同. Init_BSS()函数:初始化内核的BSS段. Init_Screen()函数:初始化文本显示器,这之后内核可以使用Print函数输出文本字符信息. Init_Mem(bootInfo)函数:这里接收的参数bootInfo就是之前在Setup程序中初始化的Boot_Info结构,这个结构保存了Setup在实模式检测到的内存大小数据.Init_Mem函数根据这个值来初始化系统内存,首先将Setup中使用的临时GDT复制到内核,之后再重新划分内存使用空间,包括划分内核程序所占内存,内核堆内存,系统空闲内存,跳过内存空洞等;系统接着建立了一个系统空闲内存链表. Init_CRC32()函数:初始化一个CRC校验表,这个校验表用于内存分页管理系统的实现,在项目4中要用到. Init_TSS()函数:初始化TSS段描述符及TSS段,TSS段的空间是内核静态分配的,并全部初始化为零值. Init_Interrupts()函数:初始化中断向量表以及中断,异常处理的默认函数,之后允许中断.从这里开始内核就可以响应中断了. Init_VM(bootInfo)函数:初始化内存分页功能,这个函数用于分页内存管理系统的实现,在项目4中要用到. Init_Scheduler()函数:初始化内核主进程Main,创建用于管理进程的内核Idle进程和Repair进程,初始化全局进程列表. Init_Traps()函数:初始化12号CPU中断异常处理函数,13号CPU中断异常处理函数以及系统调用中断处理函数.GeekOS系统调用使用0x90号中断向量. Init_Timer()函数:初始化计时器,包括制定计时器中断周期时间片,初始化时钟中断处理函数;之前在初始化PIC外部中断控制器时屏蔽了所有的外部中断,这里最后将时钟中断重新开启.需要注意的是时钟中断处理函数,这个函数负责系统进程的调度. Init_Keyboard()函数:初始化键盘输入,包括初始化键盘缓冲区队列,初始化键盘中断处理函数,最后开启键盘中断. Init_DMA()函数:初始化DMA控制器. Init_Floppy()函数:初始化软盘驱动.GeekOS默认提供一个软驱与两个IDE接口硬盘.这里函数添加了用于处理软驱中断的中断处理函数,以及内核进程Floppy_Request_Thread专门用于处理访问软驱的请求. Init_IDE()函数:初始化块设备驱动,这之后系统识别出挂载的硬盘,并添加处理硬盘访问请求的内核进程IDE_Request_Thread.出于系统运行效率考虑,GeekOS没有为IDE中断编制专门的中断处理函数,而是采用直接读取IDE控制器状态字的形式判断IDE工作的完成状态. Init_PFAT()函数:初始化只读文件系统PFAT. Init_GOSFS()函数:初始化文件系统GOSFS,在项目5中要用到. Mount_Root_Filesystem()函数:挂载默认的文件系统PFAT系统.在后来的项目设计5中,用户会看到GeekOS最终挂载了PFAT与GOSFS两个文件系统. 至此系统初始化完毕,可以开始运行用户程序了. 1.2.6 GeekOS系统源代码结构和设计项目 GeekOS操作系统源文件可以从http://geekos.sourceforge. n et下载,最新版本是2005年4月的GeekOS-0.3.0.从下载压缩包解压出来后的GeekOS目录结构如图1-3所示. 在doc目录下文件hacking.pdf和index.htm,是GeekOS系统的参考文档.Scripts目录下有startProject和removeEmptyConflicts两个脚本文件.GeekOS系统的源文件在src目录下,分7个项目:project0,project1, project2,project3, project4,project5和project6.每个项目的文件结构都类似,以project0为例,结构如图1-4所示. 图1-3 GeekOS系统主目录 图1-4 项目文件结构图 在build文件夹中,包含系统编译后的可执行文件的文件,软盘镜像fd.img(project1等项目中还包含有磁盘镜像diskc.img),makefile项目管理文件.在Include文件夹中有geekos和libc两个子目录,在geekos子目录中有kthread.h,keyboard.h等头文件,在libc中包含有GeekOS支持的C语言标准函数string.h头文件.在scripts文件夹中是项目编译时要用到的一些脚本文件.src文件夹中存放系统内核源代码,用户修改GeekOS系统时要修改的源代码如main.c等都位于这个目录中.在User子目录中一般是用来存放用户的测试文件,在tools子目录中的代码是用来建立pfat测试文件系统的. 在提供的GeekOS内核系统的基础上,为学生设计了7个由易到难的设计项目用于GeekOS的改进.这些项目分别涵盖了操作系统内核的各个基本模块:系统启动,进程管理,存储管理,文件系统,访问控制以及进程间网络通信.7个项目都规定了改进的目标,并提供了一些设计指导性的意见,但没有提供源代码,所以学生首先必须熟悉GeekOS的基本工作原理,才能开展各个项目的设计与实现. 项目0:主要是让学生熟悉GeekOS的编译,运行过程,了解计算机系统的启动原理.项目0要求实现一个内核进程,功能是实现从键盘接收一个按键,并在屏幕上显示. 项目1:主要让学生熟悉可执行链接文件(ELF文件)的结构,并学会加载和运行可执行文件.项目要求学生熟悉ELF文件格式,并编写代码对ELF文件进行分析,并将分析结果传送给加载器. 项目2:要求学生实现对用户态进程的支持.在项目2实现之前,GeekOS一直使用内核进程.对用户态进程执行的支持包括用户态进程结构的初始化,用户进程空间的初始化,用户进程切换和用户程序导入等.该项目中,存储分配依然使用分段分配方式.实现项目2后,用户就可以使用GeekOS提供的命令行解释器Shell运行一些命令来执行PFAT文件系统内的用户测试程序. 项目3:要求学生改进GeekOS的调度程序,实现基于4级反馈队列的调度算法(初始GeekOS系统仅提供了FIFO进程调度算法),并实现信号量,支持进程间通信. 项目4:要求学生实现分页虚存管理,以替代在项目1和项目2中采用的分段存储管理.实现分页虚存管理后.系统在内存不够的情况下就可以将部分页调到硬盘,以释放内存实现虚拟存储技术. 项目5:要求实现GOSFS文件系统.由于GeekOS使用了虚拟文件系统,可以加载不同的文件系统,而系统默认加载的是PFAT只读文件系统.在这个项目中,需要实现一个多极目录的,可读写的文件系统. 项目6:要求为文件系统增加访问控制列表,并使用匿名半双工管道实现进程间通信. 在某种程度上,GeekOS就是一个简单的C程序,有功能函数,线程,内存分配等.但与在装有Linux或Windows操作系统中运行的C语言程序不同,一般C语言程序 是运行在用户态的,而GeekOS是运行在核态的(也称系统态).在核态运行的程序可 以完全控制计算机的CPU,内存和外部设备,所以编写运行于核态的程序时有一些需 要特别注意的问题,修改GeekOS内核代码时要特别注意GeekOS内核运行环境的一些限制. 1.有限库函数 因为操作系统是计算机中最底层的软件,内核使用的所有功能函数必须能在内核执行.这与用户程序不同,用户程序可与一系列包含常用函数的标准库连接. GeekOS中唯一能使用的标准C语言库函数是字符串函数的一个子集(strcpy(),memcpy()函数)和snprintf()函数,这些函数的原型定义在头文件中.除标准C语言库函数外,GeekOS内核还有一些与C语言库函数类似的函数,如Print()函数(函数原型定义在中),是标准C函数printf()函数的功能子集;Malloc()和Free()函数则相当于标准C的malloc()和free()函数(函数原型定义在中). 2.有限栈空间 GeekOS内核中的每个线程可以使用大小为4KB的栈.如果某个线程堆栈溢出将导致系统内核崩溃.因此,在编程时应谨慎使用栈空间: 不要用栈分配大的数据结构,使用栈时尽量使用堆栈分配函数Malloc()和Free(); 程序中不要使用递归,避免函数深层次的调用. 3.存储器保护限制 一旦系统内核开始运行,没有任何存储器保护,内核的每次存储器访问都是对物理内存单元的访问,即使是引用空指针系统也无法捕获(trapped).因此,系统在核态运行比在用户态运行时指针引用更容易出问题.要调试运行于核态的程序是比较难的,所以用户必须仔细检查编写的代码,确保没有内存访问错误. 如果用户为GeekOS内核增加虚拟内存管理功能,那内核将获得较好的内存访问保护,但用户在编写内核代码时仍然要仔细检查,避免一些不易调试的错误发生. 4.异步中断 当有重要事件发生的时候,许多硬件设备都用中断来通知CPU:如定时器定时到,I/O请求完成等.中断发生时,控制权异步传送给中断处理程序,当中断处理结束时,控制返回到中断点继续执行.值得注意的是,中断处理可能会引起线程交换,这就意味着在控制返回到被中断的线程前,系统可能会执行其他线程. 如果用户遵守上述的限制,在内核模式下编程会相对容易.不仅如此,为使用户更顺利地深入内核编程,还建议用户养成下列编程习惯. 使用断点. 在头文件中,有一个宏定义KASSERT(),参数是一个布尔表达式.如果表达式的值是false时,就打印出一条信息,并暂停内核运行.举例如下: void My_Function(struct Thread_Queue *queue) { KASSERT(!Interrupts_Enabled()); /* 必须禁止中断*/ KASSERT(queue != 0); /* 队列不能为空 */ ... } 用户应尽可能多地用断点检查函数执行的前提条件,后继条件和数据结构的特性等.使用断点有两个好处:第一,若程序执行出错,断点可以在内核崩溃前,精确快速的帮助程序员定位程序中的出错代码;第二,断点还可以帮助程序员检测代码正确性. 使用Print语句. 在头文件定义了Print()函数,它支持标准C语言函数printf()的大部分功能.Print是最常用也是最有效的用于调试内核代码的语句,因为调试时,用户采用的策略都是先作假设,然后再收集证据来支持或反驳假设. 尽可能提前,经常测试. 与用户程序相比,内核程序需要更好的可扩展性,所以内核代码常常分成一个个小的模块开发和测试.在系统开发过程中,每一个模块应尽早地,单独地进行测试,提高系统可靠性,当内核开发达到一个稳态时,最好采用版本控制,建议用户使用CVS软件来存储内核代码. |
随便看 |
百科全书收录4421916条中文百科知识,基本涵盖了大多数领域的百科知识,是一部内容开放、自由的电子版百科全书。