系统软件课程设计操作系统大纲.docx
- 文档编号:18194347
- 上传时间:2023-08-13
- 格式:DOCX
- 页数:28
- 大小:54.41KB
系统软件课程设计操作系统大纲.docx
《系统软件课程设计操作系统大纲.docx》由会员分享,可在线阅读,更多相关《系统软件课程设计操作系统大纲.docx(28页珍藏版)》请在冰点文库上搜索。
系统软件课程设计操作系统大纲
课程设计大纲
课程编号
G
适用专业
计算机
课程名称
系统软件课程设计
课程性质
必修
英文名称
Thecoursedesignofsystemsoftware
学时/学分
80/2
选用教材
计算机操作系统(第3版)汤小丹、梁红兵、哲凤屏、汤子瀛西安电子科技大学出版社
考核方式
上机+课程报告
先修课程
操作系统
执笔人
叶琪
一、教学基本目标
本设计的目的是实现操作系统和相关系统软件的设计,其中涉及进程编程、I/O操作、存储管理、文件系统等操作系统概念。
二、教学基本内容
1.Shell编程:
掌握LinuxO.S.环境,掌握基本环境等
2.Linux系统调用:
掌握系统调用的执行流程,增加一个新的系统调用。
3.Linux二级文件系统:
编写程序实现模拟Linux文件系统
详见附录。
三、教学进度安排
1.课程设计要求说明4学时
2.软件设计16学时
3.上机编程32学时
4.调试4学时
5.检查考核8学时
四、课程设计报告要求
1.对进行认真分析,列出实验具体步骤,写出符合题目要求的程序清单,准备出调试程序使用的数据。
2.以完整的作业包的形式提交原始代码、设计文档和可运行程序。
提交的软件应当包括:
设计题目,程序清单,运行结果分析,所选取的算法及其优缺点,以及通过上机取得了哪些经验。
程序清单要求格式规范,注意加注释(包含关键字、方法、变量等),在每个模块前加注释,注释不得少于20%。
课程设计要求同时上交打印文档,设计报告包括设计题目,算法分析,关键代码及其数据结构说明,运行结果分析以及上机实践的经验总结。
五、考核方式与成绩评定方法
根据以下几方面情况评定实验成绩:
1.分析与设计能力
2.系统实现能力
3.系统文档以及设计报告的规范性
4.是否在规定时间内完成设计
5.工作态度
六、参考书
1.操作系统:
精髓与设计原理(第7版)(英文版)蒲晓蓉、周瑞、斯托林斯(WilliamStallings)、威廉•斯托林斯(WilliamStallings)电子工业出版社(2013-07)
2.计算机操作系统(第3版)汤小丹、梁红兵、哲凤屏、汤子瀛西安电子科技大学出版社(2007-08)
3.计算机操作系统教程(第3版)张尧学、史美林、张高清华大学出版社(2006-10)
七、审核:
(系主任签字)审核:
(实验室主任签字)
批准:
(副院长签字)
实验一Shell编程
一、实验目的
学习如何编写一个Linux的外壳——Shell,即命令解释程序。
学习怎样接受命令、解释命令、执行命令,特别是采用创建子进程的方式来执行一个程序,以及父进程如何继续子进程的工作。
二、实验内容
1、编写一个C语言程序作为Linux内核的shell命令行解释程序,所执行的结果需和系统命令行方式保持一致,从而使学生了解系统是怎样进行命令的解析和执行的。
同时,还提供认识进程创建、进程等待、进程执行、前/后台执行、管道和I/O重定向含义的机会。
基本运行方式如下:
用户敲入命令行
identifier[identifier[identifier……]]
shell应该解析命令行参数指针数组argv[argc]。
使用Linux的系统调用
fork()、wait()和execv()等完成。
2、对用户编写的shell增加后台运行功能。
即用户可以使用”&”作为一个命令
结束,以启动下一个命令。
3、修改程序,增加I/O重定向功能。
即用户可以使用”<”、”>”和”|”符号改变程序/文件的输入和输出。
三、实验提示
1、基本的Shell
(1)命令行工作方式
每个shell都有自己语言的语法和语义。
在标准Linuxshell中,一个命令行具有以下形式:
命令名参数1参数2……
shell的工作是找到与命令名对应的可执行程序(命令),为其准备参数,并使用参数执行命令。
shell程序需具有以下几种功能和健壮性:
⑴支持目录检索功能,即文件不存在,继续打印提示符
⑵支持以“&”结束的输入,进行并发执行(前台与后台)
⑶支持输入输出重定向,“<”,“>”为标志符
⑷支持以“|”进行进程间通信操作(管道功能)
⑸支持一定的错误输入处理,例如:
多于空格的出现,输入命令不存在,空输入等等
考虑shell为了完成工作必须采取以下步骤:
⑴打印提示符
存在一个默认的提示符,有时在硬化shell中,如单字符串%、#或>。
当shell启动时,它查找在其上运行的机器名并将该字符串加到标准提示符前,如kiowa>。
Shell也可以被设计为打印当前目录作为提示符的一部分,也即每次用户键入cd改变到不同的目录时,提示字符串也会相应变化。
一旦提示字符串确定下来,每当shell准备接受一个命令行时便把它打印到stdout。
⑵得到命令行
为了得到一个命令行,shell执行一个阻塞读操作让执行shell的进程进入睡眠状态,直到用户键入一个命令行作为对提示符的相应。
一旦用户键入命令行(以一个NEWLINE(“\n”)字符结束),命令行字符串就被返回到shell。
⑶解析命令
命令行的语法非常琐细。
解析程序从命令行的左边开始扫描直到它遇到一个空白字符(如空格、制表符或者NEWLINE)。
第一个单词是命令的名称,而后面的单词则是参数。
⑷查找文件
shell为每一个用户提供一组环境变量。
虽然这些环境变量可以随时通过set命令修改,但它们首先在用户的.login文件中定义。
PATH环境变量是一个有序的绝对路径列表。
它指明了shell应该在什么地方寻找命令文件,如果.login具有一个类似以下的行
setpath=(./bin/usr/bin)
那么shell将首先在当前目录中寻找(因为第一个完全路径名是用“.”代表当前目录),然后是/bin,最后是/usr/bin。
如果(从命令行)在任何指定的目录中都没有找到与命令行同名的文件,那么shell将提示用户无法找到命令。
⑸准备参数
shell简单地将参数传递到命令,作为指向字符串的argv数组。
⑹执行命令
shell必须执行指定文件的可执行程序。
UNIXshell总是设计为当它执行一个程序时保护原始进程不被破坏。
也就是说,因为一个命令可以是任何可执行文件,那么执行shell的进程必须保护它自己以防止可执行文件包含致命的错误。
(2)Linux提供的重要函数:
用户键入的命令可能有误,即便找到要求的可执行文件也可能包含致命的错误,因此要求shell程序每接受一个命令后,均创建子进程,让子进程来执行命令文件。
如果命令正确,并创建子进程成功,父进程等待子进程完成。
如果命令不正确或可执行文件有错或创建子进程失败,则给出相应的错误提示,撤消子进程,回到shell,等待用户输入下一个命令。
这样shell进程不会受到伤害。
编写这个模拟shell,需要调用UNIX风格的系统调用fork()、exec()和wait()等.
⑴intfork(void)
创建一个新进程。
调用fork的进程称为父进程,新进程是子进程。
Fork系统调用为父子进程返回不同的值:
子进程中返回0,父进程中返回子进程的PID,如创建不成功返回负数值。
⑵exec系列
exec系统调用用新程序覆盖调用它的进程的地址空间。
exec把一个新的程序装入调用进程的内存空间,来改变调用进程的执行代码。
当exec()返回时,进程从新程序的第一条指令恢复执行。
exec没有建立一个与调用进程并发执行的新进程,而是用新进程取代了老进程。
exec加后缀,可有多种格式。
execl(path,arg0,arg1,…argn,(char*)0);
execv(path,argv);
execlp(file,arg0,arg1,…argn,(char*)0);
execvp(file,argv);
其中l代表长格式,v代表利用argv传参,e代表从envp传递环境变量,p代表从PATH指定路径搜索文件。
⑶wait系列
wait系统调用可以使进程等待子进程终止。
该函数将阻塞调用进程,直到该进程的某一个子进程结束运行(如果子进程收到一个信号而结束运行,该函数也会返回)。
子进程的结束状态保存在status指向的整数中。
如果status为空,就不会返回子进程的结束状态。
如果在调用wait之前子进程结束运行,wait会立即返回子进程的结束状态,wait返回值为子进程的进程ID。
若子进程不存在,则返回-1。
wait(int*stat_loc);/*SystemV,BSD,andPOSIX.1*/
wait3(stausp,options,rusagep);/*BSD*/
waitpid(pid,*stat_loc,options);/*POSIX.1*/
waited(idtype,id,infop,options);/*SVR4*/
以上三个系统调用联用时,形成shell用于执行命令的一个代码框架。
if(fork()==0){
//这是子进程
//执行于和父进程相同的环境变量中
execvp(full_pathname,command->argv,0);
}else
{
//这是父进程――等待子进程终止
wait(status);
}
⑷intdup(intfd)
把一个程序的标准输出连接到管道的写入端,把另一个程序的标准输入连接到管道的读出端。
返回一个新的文件描述符,该描述符和fd指向同一个文件。
新的文件描述符具有同样的存取模式,以及同样的读/写偏移量。
用这个函数可以进行输入/输出重定向。
输出重定向步骤如下:
fid=open(file,O_WRONLY|O_CREAT);
close
(1);
dup(fid);
close(fid);
标准输入(stdin)、标准输出(stdout)和标准错误(stderr)分别占用0、1和2。
要求先关闭标准输出,从而使1成为当前系统内可用作文件描述符的最小数(注意,0被标准输入占用),然后调用dup,使管道文件与文件描述符1(即标准输出)连接。
⑸intpipe(intpipeID[2])
管道是单处理器Linux系统的主要IPC机制。
默认情况下,管道采取异步发送并阻塞接受操作。
管道是先进现出缓冲区,它设计为带有与文件I/O接口十分相似的API。
一个管道可能在任何给定时刻包含系统定义最大数量的字节,通常为4KB。
一个进程可以通过将数据写入到管道的一端来发送数据,而另一进程可以通过管道的另一端读取来接受数据。
管道在内核中由一个文件描述符代表。
一个欲建立管道的进程通过以下形式来调用内核:
intpipeID[2];
……
pipe(pipeID);
该函数可以创建两个文件描述符,pipeID[0]用于读,pipeID[1]用于写。
这两个文件描述符连接起来就是一个管道。
对于使用管道作为IPC(进程间通信)的两个或多个进程,这些进程的共同祖先在创建进程之前就必须创建管道。
因为fork命令创建一个含有打开文件表拷贝的子进程,子进程继承父进程所创建的管道。
为了使用管道,它只需要读写适当的文件描述符。
例如,假如一个父进程创建了管道。
那么它可以创建一个子进程并通过使用如下代码与之通信。
Pipe(pipeID);
If(fork()==0){/*子进程*/
……
read(pipeID[0],childBuf,len);
/*处理childBuf中的消息*/
……
}else{
……
/*发送一个消息到子进程*/
write(pipeID[1],msgTochild,len);
……
}
管道使得进程可以把信息从一个地址空间拷贝到另一个地址空间。
管道的读写端可以通过与文件描述符相同的方式用于大多数系统调用。
下例中解释了Linux中如何使用管道来实现进程A和B(分别执行PROC_AP和PROC_B)之间的并发处理,其中A执行了一些计算,发送一个值x给B,接着执行第二阶段的计算,从B中读取值y,然后重复执行。
同时B读取值x,执行第一阶段的计算,写入一个值到A,再执行更多的计算,然后重复执行。
不打算使用管道的进程应该关闭以使可以检测到文件结束(EOF)条件。
intA_to_B[2],B_to_A[2];
main()
{
pipe(A_to_B);
pipe(B_to_A);
if(fork()==0)/*第一个子进程*/
{
close(A_to_B[0]);
close(B_to_A[1]);
execve("prog_A.out",...);
exit
(1);/*错误终端子进程*/
}
if(fork()==0)/**另一个子进程/
{
close(A_to_B[1]);
close(B_to_A[0]);
execve("prog_B.out",...);
exit
(1);
}
/*父进程代码*/
wait(...);
wait(...);
}
proc_A()
{
while(TRUE)
{
write(A_to_B[1],x,sizeof(int));/*使用管道发送消息*/
read(B_to_A[0],y,sizeof(int));/*使用管道得到消息*/
}
}
proc_B()
{
while(TRUE)
{
read(A_to_B[0],x,sizeof(int));/*使用管道得到消息*/
write(B_to_A[1],y,sizeof(int));/*使用管道发送消息*/
}
}
⑹intopen(constchar*path,intoflag)
打开文件,path参数是一个字符串,包含要打开文件的路径,oflag是标志集合,控制文件的打开方式。
可用标志有:
O_RDONLY只读方式打开
O_WRONLY只写方式打开
O_RDWR读/写方式打开
O_CERAT如果文件已存在,则该选项不执行任何操作。
⑺intclose(intfd)
关闭文件。
如果关闭成功,返回0,发生错误返回-1。
⑻intaccess(constchar*path,intamode)
测试路径是否存在。
参数path中包含了需要检查的文件路径名,amode是下面几个常量进行或操作的结果:
R_OK测试文件是否可读;
W_OK测试文件是否可写;
X_OK测试文件是否可执行;
F_OK测试文件是否存在;
分离环境变量中的PATH参数可使用如下语句:
paths=(char*)getevn(“PATH”);
PATH中路径用“:
”分割,可分别取出每个路径,后面再加上文件的相对路径名组成绝对路径,便可使用access函数进行检测。
⑼chargetenv(char*name)
获得特定环境变量。
参数name应当是要获取的环境变量的名字。
如果该变量存在,就返回该变量的值;否则返回NULL。
read(),write()等等。
2、解决问题指导
(1)基本步骤
⑴打印提示符。
在打印提示符之前要使用get_current_dir_name()得到当前路径。
返回一个指向当前目录绝对路径的字符串指针,然后在stdout中输出。
⑵解析用户输入的指令,包括命令和参数。
命令和参数被保存在字符串数据argv[argc]中。
在这里要区分命令和参数,其中argv[0]是命令,程序要识别一般命令并并执行;能识别“>”、“<”和“|”,根据不同的符号转入到不同的程序模块,执行不同的过程。
普通的命令在主函数main()中执行即可,当遇到“>”和“<”是转移到子函数redirect()执行;当遇到“|”则转移到子函数pipe()执行。
⑶寻找命令文件。
子函数is_fileexit()用来查找命令是否存在。
Shell为每个用户提供了一组环境变量。
这些变量定义在用户的.login文件中。
其中路径变量PATH是一组绝对路径的列表,表明shell如何搜索命令文件。
只要我们获取了路径变量,然后依次搜索各个路径,就可以确定用户输入的命令文件的位置了。
Leave指令用来退出自己的shell回到linux的shell下。
⑷执行命令。
通过调用fork()创建一个子进程,在子进程中执行命令。
在子进程中通过使用execv()函数来执行命令。
⑸实现重定向的。
首先使用open()函数创建一个文件描述符,然后将其复制到文件描述表第二项。
这样该进程的所有输出都写到重定向的文件中。
使用dup(fid)函数将标准输出重定向到fd_out上(就是重定向文件中)。
⑹管道的实现。
通过函数pipe(pipeID),则pipeID[0]是一个文件描述符,指向管道的读端;对应的,pipeID[1]是一个指向管道写端的指针。
创建第一个子进程执行管道符前的命令,并将输出写到管道。
然后,创建第二个进程执行管道符后的指令,并从管道读输入流。
(2)程序结构分析
⑴可以分成如下几个主要的文件:
main.c基本调度shell的执行流程
function.c包括了shell功能里的各个功能子模块
head.h涵盖了要使用的头文件和基本的全局变量、数据结构
shellexeshell的可执行文件
d1,d2,testresult为测试时,临时生成的文件
⑵主要的函数组成如下:
main()负责调度整个shell执行的流程
init_shell()简单初始化shell执行前的signal
init_command()初始化shell执行命令前的全局变量初值
get_string()从screen读取一个字符串
set_background()检测是否存在&,设置background标志符
delete_space()过滤掉输入字符串中的多余的空格,包括前后和连续的多个空格
split()将输入字符串,分拆成一个个单词
fill_cmds()将单词存储到shellcmd结构、infile、outfile文件中去
searchfile()分离环境变量PATH中设定的路径,调度scanfile()
scanfile()在当前路径和PATH设定的路径中,检索每一个命令是否存在
execute()设置infile,outfile,循环调度do_execute()
do_execute()执行每一个shellcmd中的命令
head.h包含了要使用的头文件和所有的全局变量和数据结构
⑶程序调用流程:
以上程序流程图仅作为编程参考。
其中含有重复或不尽合理的地方。
编程实现时,可以自己另外划分函数与模块功能。
要求学生编写的shell,符合所使用机器安装Linux系统的shell风格。
因此,学生要先熟悉Linux的基本命令,命令行方式、参数等,再按要求完成程序功能。
有能力的学生,可以根据shell的基本语法和规则,结合编译知识,考虑试着在一个命令行中,同时实现“>、“<”、“|”、“&”等功能,或同时实现多管道、多重定向等功能。
实验二Linux系统调用
一、实验目的
学习系统调用的执行流程,理解Linux系统调用,并且在内核中增加一个新的系统调用。
二、实验内容
1.下载最新版本的Linux内核源代码,在Linux系统中解压缩,大致观察内核源代码的组成结构。
然后编译这个版本的内核代码,并尝试用编译出的内核重新引导系统。
2.添加一个新的系统调用,完成任意一个功能,重新编译和运行内核,使新的系统调用可用。
3.编写一个用户态的程序,使用增加的系统调用,以证明它确实可以用。
三、实验提示
1、内核及系统调用原理
Linux中有许多系统调用,如open()、read()、fork()、pipe()、socket()等,我们可以像使用普通的C库函数一样去使用这些系统调用,向操作系统发出请求,操作系统会尽量满足我们的请求。
操作系统在处理每个系统调用的时候必须暂时切入核心态,待完成用户请求后再返回用户态继续执行。
一般在Linux系统中的/usr/src/linux*.*.*(*.*.*代表的是内核版本,如2.6.34)目录下就是内核源代码,如果没有类似目录,则是因为在安装Linux时没有选择安装内核源代码,这时可从Internet上免费下载。
以2.6.34版本的内核为例,有两种类型的代码包:
linux-2.6.34.tar.gz和linux-2.6.34.tar.bz2,其内容完全一样,只是压缩程序不同:
.gz是用gzip压缩的,.bz2是用bzip2压缩的。
Linux系统调用的执行原理和普通的C语言函数大不相同。
当编写一个普通的C语言函数myfunc()时,这个函数将被编译成类似于下面所示的机器代码(以x86系列CPU、VisualC++中的编译器为例):
myfuncprocnear
函数内容
……
ret
myfuncend
可以看到,普通C语言函数在这里被编译成一个过程,在过程执行结束后,用一条ret指令返回调用处,而调用这个函数的语句将被编译成一条callmyfunc指令。
在x86CPU的执行过程中,一旦遇到call指令,CPU会把下一条指令的地址压入栈中,然后将程序计数器IP设为目标函数的地址,从目标函数处开始执行。
而遇到ret指令,CPU将从栈中弹出一个值,并把程序计数器设为这个值,从这里开始执行。
不难看出,无论是call,还是ret指令,都没有让CPU从用户态切换到核心态,或者从核心态转回用户态。
而系统调用必须在用户态被调用,在核心态执行,这个过程中不可避免地要进行用户态/核心态的切换,因此系统调用不能像普通函数那样用call和ret指令实现。
在x86CPU中,用户态和核心态之间的切换,一般使用init指令,也就是中断指令。
Linux系统调用正是通过中断实现的。
Linux系统调用实际上是导出了可供用户态程序调用的内核函数的名称,它们不能像普通的C库函数一样调用,而必须通过0x80号中断切换到核心态调用。
Linux系统调用是很多的,所有这些系统调用共享了0x80号中断,那么当0x80号中断发生的时候,系统如何得知当前被调用的是哪个呢?
在Linux内核中,维护了一张系统调用人口表,这个入口表的源码集中在/usr/src/linux/arch/i386/kernel/entrys.S文件中,具有如下所示的形式:
.data
ENTRY(sys_call_table)/*入口*/
.longSYSMBOL_NAME(sys_ni_call)/*0,空项*/
.longSYSMBOL_NAME(sys_exit)/*1*/
.longSYSMBOL_NAME(sys_fork)/*2*/
.longSYSMBOL_NAME(sys_read)/*3*/
.longSYSMBOL_NAME(sys_write)/*4*/
.longSYSMBOL_NAME(sys_open)/*5*/
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 系统软件 课程设计 操作系统 大纲