操作系统课程设计GeekOS操作系统的研究与实现.docx
《操作系统课程设计GeekOS操作系统的研究与实现.docx》由会员分享,可在线阅读,更多相关《操作系统课程设计GeekOS操作系统的研究与实现.docx(30页珍藏版)》请在冰豆网上搜索。
![操作系统课程设计GeekOS操作系统的研究与实现.docx](https://file1.bdocx.com/fileroot1/2023-7/22/9ae4964c-fdb9-4986-9d80-83bdece7730d/9ae4964c-fdb9-4986-9d80-83bdece7730d1.gif)
操作系统课程设计GeekOS操作系统的研究与实现
操作系统课程设计---GeekOS操作系统的研究与实现
操作系统课程设计说明书
题目:
GeekOS操作系统的研究与实现
系别:
计算机科学与工程学院
1GeekOS概述
GeekOS是一个基于X86架构的PC上运行的微操作系统内核。
由美国马理兰大学的教师开发,是一个用C语言开发的操作系统。
主要用于操作系统课程设计,目的是使学生能够实际动手参与到一个操作系统的开发工作中。
出于教学目的,这个系统内核设计简单,却又兼备实用性,它可以运行在真正的X86PC硬件平台。
作为一个课程设计平台,GeekOS由一个基本的操作系统内核作为基础,提供了操作系统与硬件之间的所有必备接口,实现了系统引导、实模式到保护模式的转换、中断调用及异常处理、基于段式的内存管理,FIFO进程调度算法以及内核进程、基本的输入输出(键盘作为输入设备、显示器作为输出设备),以及一个用于存放用户程序的只读文件系统PFAT。
学生可以在Linux或Unix环境下对其进行功能扩充,且其针对进程、文件系统、存储管理等操作系统核心内容分别设计了7个难度逐渐增加的项目供学生选择
1.1GeekOS系统源代码结构
GeekOS操作系统源文件geekos-0.3.0.zip可以从下载。
解压后的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所示:
在build文件夹中,包含系统编译后的可执行文件的文件、软盘镜像或是硬盘镜像、makefile项目管理文件。
在inculde文件夹中有geekOS和libc两个子目录,在geekOS子目录中有kthread.h,keyboard.h等头文件,在libc中包含有geekOS支持的C语言标准函数string.H头文件。
在scripts文件夹是项目编译时要用到的一些脚本文件。
Src文件夹中存放系统内核源代码,用户修改geekOS系统时要修改的源代码如main.c都位于这个目录中。
在User子目录中一般是用来存放用户的测试文件,在tools子目录中的代码是用来建立PFAT测试文件系统的。
2实验环境
(1)硬件环境:
本次课设是在虚拟机上安装Linux进行开发调试,具体安装使用方法如下:
<1>、安装linux虚拟机本次课设的虚拟机是运行在oracleVMVirtualbox上的,下载Linux镜像文件后,即可按提示即可安装。
<2>、GeekOS:
是一个基于X86架构的PC机上运行的微操作系统内核,由美国马理兰大学的教师开发,是一个用C语言开发的操作系统,GeekOS主要用于操作系统课程设计,目的是使学生能够实际动手参与到一个操作系统的开发工作中。
GeekOS的使用:
打开linux虚拟机,直接解压GeekOS压缩包就可使用,无需安装。
<3>、Bochs安装和使用:
在Linux系统中需先解压软件包,然后再配置编译生成系统文件。
(2)软件环境:
标准C语言
3项目实现
3.1project0
3.11项目设计目的
熟悉GeekOS的项目编译、调试和运行环境,掌握GeekOS运行工作过程。
3.12项目设计要求
(1)搭建GeekOS的编译和调试平台,掌握GeekOS的内核进程工作原理。
(2)熟悉键盘操作函数,编程实现一个内核进程。
该进程的功能是:
接受键盘输入的字符并显示到屏幕上,当输入Ctrl+D时,结束进程的运行。
3.13项目设计原理
项目要求从键盘输入,因此要用到键盘处理函数。
GeekOS的键盘处理函数定义在keyboard.h与keyboard.c文件中。
键盘的初始化是在Main函数中调用Init_Keyboard进行的,Init_Keyboard主要功能是设置初始状态下存放键盘扫描码的缓冲区,并为键盘中断设置处理函数。
而键盘中断处理过程是:
首先从相应的I/O端口读取键盘扫描码,根据是否按下Shift键,分别在键值表中寻找扫描码对应的按键值,经过处理后将键值放入键盘缓冲区,最后通知系统重新调度进程。
若用户进程需要从键盘输入信息,可调用Wait_For_Key()函数,进程调用该函数后,会阻塞进入按键操作的等待队列,直到按键操作结束,进程才会被唤醒。
Start_Kernel_Thread函数主要功能就是建立一个内核线程。
本项目主要要求设计一个函数对键盘的中断进行响应。
这主要通过使用Geekos提供的键盘响应函数Wait_For_Key()进行键盘中断的响应及返回键值。
该函数首先检查键盘缓冲区是否有按键,如果有,就读取一个键码,如果此时键盘缓冲区中没有按键,就将线程放入键盘事件等待队列。
于是可分两步完成:
1编写函数,函数功能是:
接受键盘输入的按键,并将键值显示到显示器,当输入Ctrl+D退出。
2在Main函数体内调用Start_User_Thread函数,将编写的函数地址传递给startFunc,建立一个内核进程。
3.13项目具体实现
编写的函数
staticvoidkeyin(void)
{
Keycodekeycode;
Print("\n--------Waitforyourinput,Ctrl+dtoexit--------\n");
while
(1)
{
keycode=Wait_For_Key();//读取键盘按键状态
if(!
((keycode&0x0100)||(keycode&0x8000)))//处理非特殊按键的按下事件
{
intasciiCode=keycode&0x03ff;//低8位为Ascii码
if((keycode&0x4000)==0x4000&&asciiCode=='d')//按下Ctrl键
{
Print("\n-----------------Bye-----------------\n");
Exit
(1);
}
else
{
Print("%c",(asciiCode=='\r')?
'\n':
asciiCode);
}
}
}
}
(2)首先注释Main()函数中TODO宏,并调用Start_Kernel_Thread函数,将步骤1编写的函数地址传递给参数,建立一个内核级进程
structKernel_Thread*thread;
thread=Start_Kernel_Thread(&project0,0,PRIORITY_NORMAL,false);
(3)执行makedepend及make命令,此时会在build目录下生成fd.img
(4)编写brochs配置文件
vgaromimage:
file=/usr/share/bochs/VGABIOS-lgpl-latest
romimage:
file=/usr/share/bochs/BIOS-bochs-latest,address=0xf0000
megs:
8
boot:
a
floppya:
1_44=fd.img,status=inserted
#floppya:
1_44=fd_aug.img,status=inserted
log:
./bochs.out
keyboard_serial_delay:
200
floppy_command_delay:
500
vga_update_interval:
300000
ips:
1000000
mouse:
enabled=0
private_colormap:
enabled=0
i440fxsupport:
enabled=0
3.14调试运行结果
进入/os/project0/build目录
执行makedepend
执行make
成功之后在build目录下生成fd.img文件。
启动bochs
在build目录中执行
bochs–fbochsrc
成功后,运行结果:
3.2project1
3.21项目设计目的
熟悉ELF文件格式,了解GeekOS系统如何将ELF格式的用户可执行程序装入内存,建立内核进程并运行的实现技术。
3.22项目设计要求
(1)修改/geekos/elf.c文件:
在函数Parse_ELF_Executable()中添加代码,分析ELF格式的可执行文件(包括分析得出ELF文件头、程序头,获取可执行文件长度,代码段、数据段等信息),并填充Exe_Format数据结构的域值。
(2)掌握GeekOS在核心态运行用户程序的原理,为项目2的实现做准备。
3.23项目设计原理
ELF是UNIX系统实验室作为应用程序二进制接口而开发和发布的。
有两个平行视图。
连接程序视图
执行程序视图
ELF头部
ELF头部
程序头部表(可选)
程序头部表
节区1
段1
...
节区n
段2
...
...
...
节区头部表
节区头部表(可选)
GeekOS中的用户程序全部在系统的编译阶段完成编译和连接,形成可执行文件,用户可执行文件保存在PFAT文件系统中。
本项目要完成的就是在系统启动后,从PFAT文件系统将可执行文件装入内存,建立进程并运行得到相应的输出。
在磁盘中的ELF文件的映像和在内存中执行程序镜像间的对应关系如下图所示
而此过程主要由Spawner函数实现,其主要经过简要概述为:
先调用Read_Fully函数将文件读入内存,后调用Parse_ELF_Executable函数分析ELF文件,最后调用Spawn_Program函数将可执行程序的代码段和数据段等装入内存,此后便可以开始运行一个内核级进程了。
如下图所示:
在本项目中,我们要完成Parse_ELF_Executable函数,此函数的作用为根据ELF文件格式,从exeFileData指向的内容中得到ELF文件头,继续分析可得到程序头和程序代码段等信息。
3.23项目具体实现
修改project/project1/src/geekos/elf.c文件:
在函数Parse_ELF_Executable()中添加代码,分析
ELF格式的可执行文件(包括分析得出ELF文件头、程序头,获取可执行文
件长度,代码段、数据段等信息),并填充Exe_Format数据结构中的域值
1、elf.c:
将ELF格式的可执行程序装入到内存,建立内核进程并运行.
==================elf.c====================
intParse_ELF_Executable(char*exeFileData,ulong_texeFileLength,structExe_Format*exeFormat)
{
inti;
elfHeader*head=(elfHeader*)exeFileData;
programHeader*proHeader=(programHeader*)(exeFileData+head->phoff);
KASSERT(exeFileData!
=NULL);
KASSERT(exeFileLength>head->ehsize+head->phentsize*head->phnum);
KASSERT(head->entry%4==0);
exeFormat->numSegments=head->phnum;
exeFormat->entryAddr=head->entry;
for(i=0;iphnum;i++)
{
exeFormat->segmentList[i].offsetInFile=proHeader->offset;
exeFormat->segmentList[i].lengthInFile=proHeader->fileSize;
exeFormat->segmentList[i].startAddress=proHeader->vaddr;
exeFormat->segmentList[i].sizeInMemory=proHeader->memSize;
exeFormat->segmentList[i].protFlags=proHeader->flags;
proHeader++;
}
return0;
}
=========================================
2、编译,成功后生成两个镜像文件:
fd.img和diskc.img。
其中,Diskc.img为模拟器能引导的操作系统镜像。
3、编写相应的bochs配置文件
由于生成了diskc.img,因此配置文件需加上以下内容
config_interface:
textconfig
megs:
8
vgaromimage:
file=$BXSHARE/VGABIOS-lgpl-latest
romimage:
file=$BXSHARE/BIOS-bochs-latest
floppya:
1_44=./fd.img,status=inserted
ata0:
enabled=1,ioaddr1=0x1f0,ioaddr2=0x3f0,irq=14
ata1:
enabled=0,ioaddr1=0x170,ioaddr2=0x370,irq=15
#ata2:
enabled=0,ioaddr1=0x1e8,ioaddr2=0x3e0,irq=11
#ata3:
enabled=0,ioaddr1=0x168,ioaddr2=0x360,irq=9
ata0-master:
type=disk,path="diskc.img",mode=flat,cylinders=40,heads=8,spt=64
#ata0-slave:
type=cdrom,path="/dev/cdrom",status=inserted
boot:
a
ips:
1000000
log:
./bochs.out
vga_update_interval:
300000
keyboard_serial_delay:
250
keyboard_paste_delay:
100000
private_colormap:
enabled=0
}
3.24调试运行结果
进入/os/project1/build目录
执行makedepend
执行make
成功之后在build目录下生成fd.img、diskc.img文件。
启动bochs
在build目录中执行
成功后,运行结果:
3.3project2
3.31项目设计目的
扩充GeekOS操作系统内核,使得系统能够支持用户级进程的动态创建和执行。
3.32项目设计要求
开始本项目前需要阅读/src/geekos目录中的entry.c、lowlevel.asm、kthread.c、userseg.c,其中在userseg.c中主要关注Destroy_User_Context()和Load_User_Program()两个函数。
本项目要求用户对以下几个文件进行修改:
1)“src/GeekOS/user.c”文件中的函数Spawn(),其功能是生成一个新的用户级进程;
2)“src/GeekOS/user.c”文件中的函数Switch_To_User_Context(),调度程序在执行一个新的进程前调用该函数以切换用户地址空间;
3)“src/GeekOS/elf.c”文件中的函数Parse_ELF_Executable()。
该函数的实现要求和项目1相同。
4)“src/GeekOS/userseg.c”文件中主要是实现一些为实现对“src/GeekOS/user.c”中高层操作支持的函数。
Destroy_User_Context()函数的功能是释放用户态进程占用的内存资源。
Load_User_Program()函数的功能通过加载可执行文件镜像创建新进程的User_Context结构。
Copy_From_User()和Copy_To_User()函数的功能是在用户地址空间和内核地址空间之间复制数据,在分段存储器管理模式下,只要段有效,调用memcpy函数就可以实现这两个函数的功能。
Switch_To_Address_Space()函数的功能是通过将进程的LDT装入到LDT寄存器来激活用户的地址空间;
5)“src/GeekOS/kthread.c”文件中的Start_User_Thread函数和Setup_User_Thread函数。
Setup_User_Thread()函数的功能是为进程初始化内核堆栈,堆栈中是为进程首次进入用户态运行时设置处理器状态要使用的数据。
Start_User_Thread()是一个高层操作,该函数使用User_Context对象开始一个新进程。
6)“src/GeekOS/kthread.c”文件中主要是实现用户程序要求内核进行服务的一些系统调用函数定义。
要求用户实现的有Sys_Exit()函数、Sys_PrintString()函数、Sys_GetKey()、Sys_SetAttr()、Sys_GetCursor()、Sys_PutCursor()、Sys_Spawn()函数、Sys_Wait()函数和Sys_GetPID()函数。
7)在main.c文件中改写生成第一个用户态进程的函数调用:
Spawn_Init_Process(void)。
3.33项目设计原理
Geekos的初始系统不支持用户态进程,但提供了用户态进程上下文接口和实现用户态进程需要用到的数据结构。
所以,用户态进程及相关函数都要开发者实现。
在Geekos中为了区分用户态进程和内核进程,在Kernel_Thread结构体中设置了一个字段userContext,指向用户态进程上下文。
对于内核进程来说,这个指针为空,而用户态进程都拥有自己的用户上下文。
因此在Geekos中要判断一个进程是内核进程还是用户态进程,只要通过userContext字段是否为空来判断就可以了。
GeekOS的进程结构图如10-1所示:
图10.1用户态进程结构
每个用户态进程都拥有属于自己的内存段空间,如:
代码段、数据段、堆栈段等,每个段有一个段描述符(segmentdescriptor),并且每个进程有一个段描述符表(LocalDescriptorTable),用于保存该进程的所有段描述符。
操作系统中还设置一个全局描述符表(GDT,GlobalDescriptorTable),用于记录了系统中所有进程的ldt描述符。
GDT、LDT和User_Context的关系如图10-2所示
图10-2GDT、LDT和User_Context的关系
程序流程图:
GeekOS的用户级进程创建过程可以描述如下:
(1)Spawn函数导入用户程序并初始化:
调用Load_User_Program进行User_Context的初始化及用户级进程空间的分配及用户程序各段的装入;
(2)Spawn函数调用Start_User_Thread(),初始化一个用户态进程,包括初始化进程Kernel_Thread结构以及调用Setup_User_Thread初始化用户级进程内核堆栈;
(3)最后Spawn函数退出,这时用户级进程已被添加至系统运行进程队列,可以被调度了。
3.23项目具体实现
1、添加代码
==================user.c===============
//产生一个进程(用户态)
intSpawn(constchar*program,constchar*command,structKernel_Thread**pThread)
{
//TODO("Spawnaprocessbyreadinganexecutablefromafilesystem");
intrc;//标记各函数的返回值,为0则表示成功,否则失败
char*exeFileData=0;//保存在内存缓冲中的用户程序可执行文件
ulong_texeFileLength;//可执行文件的长度
structUser_Context*userContext=0;//指向User_Conetxt的指针
structKernel_Thread*process=0;//指向Kernel_Thread*pThread的指针
structExe_FormatexeFormat;//调用Parse_ELF_Executable函数得到的可执行文件信息
if((rc=Read_Fully(program,(void**)&exeFileData,&exeFileLength))!
=0)
{//调用Read_Fully函数将名为program的可执行文件全部读入内存缓冲区
Print("FailedtoReadFile%s!
\n",program);
gotofail;
}
if((rc=Parse_ELF_Executable(exeFileData,exeFileLength,&exeFormat))!
=0)
{//调用Parse_ELF_Executable函数分析ELF格式文件
Print("FailedtoParseELFFile!
\n");
gotofail;
}
if((rc=Load_User_Program(exeFileData,exeFileLength,&exeFormat,command,&userContext))!
=0)
{//调用Load_User_Program将可执行程序的程序段和数据段装入内存
Print("FailedtoLoadUserProgram!
\n");
gotofail;
}
//在堆分配方式下释放内存并再次初始化exeFileData
Free(exeFileData);
exeFileData=0;
/*开始用户进程,调用Start_User_Thread函数创建一个进程并使其进入准备运行队列*/
process=Start_User_Thread(userContext,false);
if(process!
=0){//不是核心级进程(即为用户级进程)
KASSERT(process->refCount==2);
/*返回核心进程的指针*/
*pThread=process;
rc=process->pid;//记录当前进程的ID