课程设计大纲
课程编号 G2051272 课程名称 系统软件课程设计 计算机操作系统(第3版) 汤小丹、梁适用专业 计算机 课程性质 必修 英文名称 The course design of system software 学时/学分 80/2 选用教材 红兵、哲凤屏、 汤子瀛 西安电子科考核方式 上机+课程报告 技大学出版社 先修课程 操作系统
执笔人 叶琪 一、教学基本目标
本设计的目的是实现操作系统和相关系统软件的设计,其中涉及进程编程、I/O操作、
存储管理、文件系统等操作系统概念。
二、教学基本内容
1.Shell编程:掌握Linux O.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版)(英文版) 蒲晓蓉、周瑞、斯托林斯 (William Stallings)、 威廉?斯托林斯 (William Stallings) 电子工业出版社 (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都有自己语言的语法和语义。在标准Linux shell中,一个命令行具有以下形式: 命令名 参数1 参数2??
shell的工作是找到与命令名对应的可执行程序(命令),为其准备参数,并使用参数执行命令。
shell程序需具有以下几种功能和健壮性:
⑴ 支持目录检索功能,即文件不存在,继续打印提示符 ⑵ 支持以“&”结束的输入,进行并发执行(前台与后台) ⑶ 支持输入输出重定向,“<”,“>”为标志符 ⑷ 支持以“|”进行进程间通信操作(管道功能)
⑸ 支持一定的错误输入处理,例如:多于空格的出现,输入命令不存在,空输入等等 考虑shell为了完成工作必须采取以下步骤: ⑴ 打印提示符 存在一个默认的提示符,有时在硬化shell中,如单字符串%、#或>。当shell启动时,它查找在其上运行的机器名并将该字符串加到标准提示符前,如kiowa>。Shell也可以被设计为打印当前目录作为提示符的一部分,也即每次用户键入cd改变到不同的
目录时,提示字符串也会相应变化。一旦提示字符串确定下来,每当shell准备接受一个命令行时便把它打印到stdout。 ⑵ 得到命令行 为了得到一个命令行,shell执行一个阻塞读操作让执行shell的进程进入睡眠状态,直到用户键入一个命令行作为对提示符的相应。一旦用户键入命令行(以一个NEWLINE(“\\n”)字符结束),命令行字符串就被返回到shell。 ⑶ 解析命令 命令行的语法非常琐细。解析程序从命令行的左边开始扫描直到它遇到一个空白字符(如空格、制表符或者NEWLINE)。第一个单词是命令的名称,而后面的单词则是参数。
⑷ 查找文件 shell为每一个用户提供一组环境变量。虽然这些环境变量可以随时通过set命令修改,但它们首先在用户的.login文件中定义。PATH环境变量是一个有序的绝对路径列表。它指明了shell应该在什么地方寻找命令文件,如果.login具有一个类似以下的行
set path=(./bin/usr/bin)
那么shell将首先在当前目录中寻找(因为第一个完全路径名是用“.”代表当前目录),然后是/bin,最后是/usr/bin。如果(从命令行)在任何指定的目录中都没有找到与命令行同名的文件,那么shell将提示用户无法找到命令。 ⑸ 准备参数 shell简单地将参数传递到命令,作为指向字符串的argv数组。 ⑹ 执行命令 shell必须执行指定文件的可执行程序。UNIX shell总是设计为当它执行一个程序时保护原始进程不被破坏。也就是说,因为一个命令可以是任何可执行文件,那么执行shell的进程必须保护它自己以防止可执行文件包含致命的错误。
(2)Linux提供的重要函数: 用户键入的命令可能有误,即便找到要求的可执行文件也可能包含致命的错误,因此要求shell程序每接受一个命令后,均创建子进程,让子进程来执行命令文件。如果命令正确,并创建子进程成功,父进程等待子进程完成。如果命令不正确或可执行文件有错或创建子进程失败,则给出相应的错误提示,撤消子进程,回到shell,等待用户输入下一个命令。这样shell进程不会受到伤害。
编写这个模拟shell,需要调用UNIX风格的系统调用fork()、exec()和wait()等. ⑴ int fork(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); /*System V,BSD,and POSIX.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);
}
⑷ int dup(int fd)
把一个程序的标准输出连接到管道的写入端,把另一个程序的标准输入连接到管道的读出端。返回一个新的文件描述符,该描述符和fd指向同一个文件。新的文件描述符具有同样的存取模式,以及同样的读/写偏移量。用这个函数可以进行输入/输出重定向。
输出重定向步骤如下:
fid=open(file,O_WRONLY|O_CREAT); close(1); dup(fid); close(fid);
标准输入(stdin)、标准输出(stdout)和标准错误(stderr)分别占用0、1和2。要求先关闭标准输出,从而使1成为当前系统内可用作文件描述符的最小数(注意,0被标准输入占用),然后调用dup,使管道文件与文件描述符1(即标准输出)连接。
⑸ int pipe(int pipeID[2])
管道是单处理器Linux系统的主要IPC机制。默认情况下,管道采取异步发送并阻塞接受操作。管道是先进现出缓冲区,它设计为带有与文件I/O接口十分相似的API。一个管道可能在任何给定时刻包含系统定义最大数量的字节,通常为4KB。一个进程可以通过将数据写入到管道的一端来发送数据,而另一进程可以通过管道的另一端读取来接受数据。管道在内核中由一个文件描述符代表。一个欲建立管道的进程通过以下形式来调用内核:
int pipeID[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)条件。
int A_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(\ exit(1); /*错误终端子进程*/ }
if(fork()==0) /**另一个子进程/ { close(A_to_B[1]); close(B_to_A[0]); execve(\
exit(1); }
/*父进程代码*/ wait(...); wait(...); }
proc_A() {
while(TRUE) {
proc_B() {
while(TRUE) {
read(A_to_B[0],x,sizeof(int)); /*使用管道得到消息*/
write(B_to_A[1],y,sizeof(int)); /*使用管道发送消息*/
⑹ int open(const char *path, int oflag)
打开文件,path参数是一个字符串,包含要打开文件的路径,oflag是标志集合,控制文件的打开方式。可用标志有:
O_RDONLY 只读方式打开 O_WRONLY 只写方式打开 O_RDWR 读/写方式打开
O_CERAT 如果文件已存在,则该选项不执行任何操作。 ⑺ int close(int fd)
关闭文件。如果关闭成功,返回0,发生错误返回-1。 ⑻ int access(const char *path,int amode)
测试路径是否存在。参数path中包含了需要检查的文件路径名,amode是下面几个常量进行或操作的结果:
R_OK 测试文件是否可读; W_OK 测试文件是否可写; X_OK 测试文件是否可执行; F_OK 测试文件是否存在;
分离环境变量中的PATH参数可使用如下语句: paths=(char *)getevn(“PATH”);
PATH中路径用“:”分割,可分别取出每个路径,后面再加上文件的相对路径名组成绝对路径,便可使用access函数进行检测。
⑼char getenv(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 涵盖了要使用的头文件和基本的全局变量、数据结构 shellexe shell的可执行文件
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、Visual C++中的编译器为例):
myfunc proc near 函数内容 …… ret
myfunc end
可以看到,普通C语言函数在这里被编译成一个过程,在过程执行结束后,用一条ret指令返回调用处,而调用这个函数的语句将被编译成一条call myfunc指令。
在x86 CPU的执行过程中,一旦遇到call指令,CPU会把下一条指令的地址压入栈中,然后将程序计数器IP设为目标函数的地址,从目标函数处开始执行。而遇到ret指令,CPU将从栈中弹出一个值,并把程序计数器设为这个值,从这里开始执行。不难看出,无论是call,还是ret指令,都没有让CPU从用户态切换到核心态,或者从核心态转回用户态。而系统调用必须在用户态被调用,在核心态执行,这个过程中不可避免地要进行用户态/核心态的切换,因此系统调用不能像普通函数那样用call和ret指令实现。
在x86 CPU 中,用户态和核心态之间的切换,一般使用init指令,也就是中断指令。Linux系统调用正是通过中断实现的。Linux系统调用实际上是导出了可供用户态程序调用的内核
函数的名称,它们不能像普通的C库函数一样调用,而必须通过0x80号中断切换到核心态调用。
Linux系统调用是很多的,所有这些系统调用共享了0x80号中断,那么当0x80号中断发生的时候,系统如何得知当前被调用的是哪个呢?在Linux内核中,维护了一张系统调用人口表,这个入口表的源码集中在/usr/src/linux/arch/i386/kernel/entrys.S文件中,具有如下所示的形式:
.data
ENTRY(sys_call_table) /*入口*/ .long SYSMBOL_NAME(sys_ni_call) /*0,空项*/ .long SYSMBOL_NAME(sys_exit) /*1*/ .long SYSMBOL_NAME(sys_fork) /*2*/ .long SYSMBOL_NAME(sys_read) /*3*/ .long SYSMBOL_NAME(sys_write) /*4*/ .long SYSMBOL_NAME(sys_open) /*5*/ …………………………….
.long SYSMBOL_NAME(sys_signalstack) .long SYSMBOL_NAME(sys_sendfile) …………………………….
.long SYSMBOL_NAME(sys_ni_call) /*空项*/ .long SYSMBOL_NAME(sys_ni_call) /*空项*/ …………………………….
.endr /*结束*/
表项1包含了exit()系统调用(其实现为sys_exit内核函数),表项2包含了fork()系统调用,以此类推。sys_ni_call表示该表项暂时没有被使用。当增加一个新的系统调用时,可以找一个没有使用的表项,并且修改它。如下编辑该文件,增加自己的系统调用:
……………………………
.long SYSMBOL_NAME(sys_ni_call) /*222*/
.long SYSMBOL_NAME(sys_my_new_call) /*223,用户自定义系统调用*/ ……………………………. 注意,Linux系统自身保留了221个系统调用,自己增加的系统调用至少要在第222项以后开始,还要小于256(内核2.4的有的版本可能保留了多达237个系统调用,因此,建议选择较为靠后的序号)。
为了使普通的C语言程序在重新编译内核之后也能调用新的内核函数,还要编辑内核目录下的include/asm/unistd.h文件:
#define _NR_exit 1 #define _NR_fork 2 #define _NR_read 3 #define _NR_write 4 #define _NR_open 5 ………………………………………………
#define _NR_my_new_call 223
接下来,就要实现自己的内核函数了。可以单独在一个文件中添加该函数,也可以在已有的内核源代码中添加该函数。内核函数的实现具有以下形式:
asmlinkage
其中,asmlinkage关键字指明了函数的参数的传递方式------必须使用堆栈进行。这里涉及一个问题------系统调用发生时,到底做了些什么?实际上,它要完成以下一系列的动作:
(1)在系统调用表中找出对应于系统调用号的表项。 (2)确定系统调用的参数条目。
(3)将参数从用户地址空间拷贝到u区。 (4)保存当前进程的上下文。 (5)调用内核中的系统调用代码。 (6)返回用户进程。
这就决定了使用内核函数时,并不能直接使用指针型参数或者引用型参数,而必须使用copy_from_user和copy_to_user这两个内核API(也可以使用memcpy_fromfs和memcpy_tofs)实现参数的读入和写回。
如何在用户态调用内核系统函数呢?
原则上只有使用ini指令引发0x80中断一种方法。例如:当调用一个具有两个参数的系统调用sys_xxx_call()时,若要传入参数param1和param2并且将返回值存放在res中,调用语句将被编译成为下列代码:
long res;
_asm_ volatile(“int $0x80” \\ : “=a”(res) \\
:”0” (_NR_sys_xxx_call),”b”((long)(param1)), \\ “c” ((long)(param2))); return res; 这里用到了Linux中的x86汇编语言代码,和我们学习的x86汇编语言在语法上有所不同,但本质都是一样的。这几条语句的含义是:首先对 参数进行压栈处理,然后把前面定义的_NR_sys_xxx_call这一编号放在eax寄存器(相当于16位x86处理器的ax寄存器)中,再执行int 80h发生中断,最后,待中断处理完成后,把eax寄存器中的值作为返回值,系统调用结束。
这里的关键在于,int 80h指令发生中断后,操作系统做了哪些事情?
事实上,在Linux操作系统启动的时候,就已经通过设置中断门,使得int 80h指令会转到内核的system_call处执行。
system_call定义在arch/i386/kernel/entry.S中:
ENTRY(system_call) //转到此处执行 pushl êx //保存eax寄存器 SAVE_ALL //把寄存器压入堆栈 GET_CURRENT(?x)
testb $0x02,tsk_ptrace(?x) jne tracesys
cmpl $(NR_syscalls),êx jae badsys
call *SYSMBOL_NAME(sys_call_table)(,êx,4) //此时eax中存储的是系统调用的编号 movl êx,EAX(%esp) //把系统调用的返回值放入eax寄存器 ENTRY(ret_from_sys_call) //从系统调用返回
这里的关键在于call *SYSMBOL_NAME(sys_call_table)(,êx,4),通过这条指令,将会根据eax中的系统调用编号,从sys_call_table中找到相应的处理程序,并且调用处理程序。
最后要注意的是,在编译使用内核函数的C文件时,要通知编译器把这些代码当做内核
代码而不是普通的用户代码来编译,这可以通过向编译器传递_KERNEL_标志来实现,同时需要使用-W编译选项来向装载程序传递一个参数all,表示忽略警告信息。最终的编译命令如下所示:
gcc –Wall –D_KERNEL_ xxx.c 当然,如果采用重新编译内核的方法,那么并不需要写编译命令,只要修改内核源码目录的Makefile,并将自己的源码文件添加至O_OBJS列表就可以了。因为Makefile的FLAGS标志已经包含上述编译选项了。另外,系统定义了syscall0~syscall5这六个宏,分别封装有0个到5个参数的内核函数。
2、解决问题指导
在编写核心态程序时,只能使用内核函数。在编写自己的代码之前,要搞清楚内核中相关的数据结构的定义。
以下的例子实现了枚举当前的所有进程,并且查询每个进程的gid、uid和pid,将所得的结果(进程信息数组,进程数)写在内核的系统日志中:
#include
#include
asmlinkage int sys_mysyscall() {
int i=0;
struct task_struct *p;
p=&init_task; do {
//获取该进程的信息
printk(“pid=%d ”,p->pid); printk(“uid=%d ”,p->uid); printk(“gid=%d ”,p->gid); i++;
} while((p=p->next_task) && (p!=&init_task));
return i;
}
如何重新编译和引导新内核?
在使用新内核引导系统后,可以使用下面的测试程序,对新增的系统调用进行测试: #define _NR_my_syscall 223
/*这里告诉编译器,my_syscall系统调用的编号是223*/ int errno;
#include
/*这里告诉编译器,请把my_syscall作为一个系统调用来编译,而不是普通函数*/
main() {
My_syscall(); /*在main()函数里面使用了这个系统调用*/ }
编译并运行这个用户态程序,并使用dmesg命令查看内核的系统日志。如果能够看到一个列表,显示了进程的pid等信息,那么说明实验成功了。
如果修改了内核目录中的文件内容,请在完成测试之后,把环境恢复到最初的状态。如果试图在已经被别人修改过的环境中工作,那么会出现一些奇怪的问题。这个时候最好的解决办法是删除当前的内核源码,并把以前下载的源码压缩包重新解压缩,创造一个最初状态的工作环境。
3、编译Linux内核
(1)Linux内核源码的组织结构
Linux内核作为一个特殊的程序,同样需要经过编译、链接之后才能运行,仅仅是它执行时拥有特殊的权限,位于特定的空间,并且不会也不可能依赖于其它软件罢了。
Linux发展至今,其内核的组织结构日渐清晰,层次日渐分明。一旦基本系统安装完毕,具有系统管理员的用户即可编译内核。一般说来,Linux系统内核的源代码放置在/usr/src/linux目录下,它将依赖于体系结构的代码和独立于体系结构的代码分离开来,以下是几个重要的源代码目录:
arch目录 该目录存放具体地依赖于体系结构实现的代码。在这个目录下,针对不同体系结构所移植的版本都有三个子目录:kernel、lib和mm。kernel子目录包含依赖于体系结构实现的一般内核功能,例如信号处理、时钟处理等。lib子目录包含库函数的本地实现,如果从依赖于体系结构的源代码编译,则运行得更快。mm子目录包含存储管理实现的代码。
drivers目录 拥有50%以上的内核源代码,所有的驱动程序源代码均位于该目录之下。 fs目录 存放所有的系统支持的文件系统的实现代码。 inlcude目录 一些重要的头文件。
ipc目录 处理进程间通信的全部所需的代码都放在ipc目录下。 init目录 存放所有系统的初始化代码,许多重要的文件,例如main.c就位于该目录下。该目录还包含了许多核心代码,例如实现fork()的代码和最常执行的代码----cpuidle()循环。
lib目录 放置内核其它部分经常所需要的代码。
kernel目录 许多最常调用的内核函数放在该目录下。 mm目录 包含所有Linux实现虚拟内存管理的源代码。 net目录 存放所有提供网络支持的代码。
document目录 存放大量的内核代码相关文档,以及用户开发和维护手册。
(2)如何配置及编译Linux内核
在能够实际地编译内核之前,必须告诉编译程序需要哪些功能,还必须告诉它是将这些功能模块编到内核去还是将其配置成动态可加载的模块。下表为内核配置命令:
类型 文本提示 文本菜单 GUI(需运行X-Window) 命令(必须具有管理员权限) make config make menuconfig make xconfig
make config将打开一个字符模式的对话框,在终端上一个接一个地问问题直到回答了所有的问题。对每一问题有三种可能的答案:Yes、No和Module。Module告诉内核配置在运行时使用动态可装载模块,而不是静态地将功能连接到内核中。
make menuconfig 和make xconfig分别打开一个文本图形对话框和一个GUI对话框,其功能和make config基本相同,但是可以只配置自己关心的部分。另外,如果不希望对原始配置做过多的修改,则可以选用make oldconfig选项。
make config命令中有几个重要的选项,其意义如下:
---Code maturity level options:代码成熟等级。此处只有一项:prompt for development and/or incomplete code/drivers,如果要试验现在仍处于试验阶段的功能,就必须把该项选择为Y,否则可以把它选择为N。
---Loadable module support:对模块的支持。
---Processor type and features:选择支持的CPU类型
---General setup:对最普通的一些属性进行设置。一般使用默认设置就可以。 ---Memory Technology Device(MTD):MTD设备支持。 ---Parallel port support:并口支持。
---Plug and Play configuration:即插即用设备。 ---Block devices:块设备支持。
---Networking options:网络配置选项。 ---Telephony Support:电话支持。 ---SCSI support:SCSI设备的支持。 ---Character devices:字符设备。 ---File system:文件设备。
---Console drivers:控制台驱动。 ---Sound:声卡驱动。
---USB support:USB支持。
一旦满意地配置好内核,就可以进行编译了。
/usr/src/linux目录下的makefile是构建Linux内核的顶层makefile。在该目录下只用以下几个命令(必须具有超级用户或者管理员权限)就可以将内核重新构建出来:
#make clean #make config #make depend #make 其中,make clean入口将删除旧的可装载对象文件和其他临时文件。make config入口会导致configure的bash脚本的执行,如果前面已经配置好了内核,那么这条命令可以不用。make depend入口是根据各个文件之间的依赖关系,确定合适的编译顺序。make入口将编译所有内核源代码,生成内核可执行文件vmlinux。
另外,make boot可以压缩vmlinux文件来建立一个可启动的内核映像并将其安装到/usr/src/linux/arch/i386/boot/zImage中。如果使用LILO引导,也可以使用命令make bzImage。这时,会在该目录下生成一个名为bzImage的文件,将上述文件重命名后,拷贝到/etc/lilo所在的目录,在lilo.conf中增加新的引导选项,再次运行LILO,新的内核就可以被引导了。
实验三 Linux二级文件系统
一、实验目的
(1)本实验的目的是通过一个简单多用户文件系统的设计,加深理解文件系统的内部功能和内部实现。
(2)结合数据结构、程序设计、计算机原理等课程的知识,设计一个二级文件系统,进一步理解操作系统。
(3)通过分对实际问题的分析、设计、编程实现,提高学生实际应用、编程的能力
二、实验内容
在任一OS下,建立一个大文件,把它假象成一张盘,在其中实现一个简单的模拟Linux文件系统。在现有机器硬盘上开辟100M的硬盘空间,作为设定的硬盘空间。编写一管理程序simdisk对此空间进行管理,以模拟Linux文件系统,要求:盘块大小1k,空闲盘块的管理:Linux位图法;结构:超级块, i结点区, 根目录区。 1.可以实现下列几条命令:
Info 显示整个系统信息(参考Linux文件系统的系统信息),文件可以根据用户进行读写保护。目录名和文件名支持全路径名和相对路径名,路径名各分量间用“/”隔开。 check 检测并恢复文件系统:对文件系统中的数据一致性进行检测,并自动根据文件系统的结构和信息进行数据再整理。 login 用户登录 dir 列目录 create 创建文件 delete 删除文件 open 打开文件 close 关闭文件 read 读文件 copy 拷贝文件 write 写文件 cd 进出目录
2.列目录时要列出文件名,物理地址,保护码和文件长度 3.源文件可以进行读写保护 4.程序的总体流程为: 初始化文件目录;
输出提示符,等待接受命令,分析键入的命令;
对合法的命令,执行相应的处理程序,否则输出错误信息,继续等待新命令,直到键入EXIT退出为止。
三、实验提示
1.背景知识 (1)外存管理
文件系统是一个含有大量的文件及其属性,对文件进行操作、管理的软件,以及向用户提供使用文件的接口的一个集合。在逻辑上它的层次结构是这样的: 文件系统接口 对对象的操作和管理的软件集合 逻辑文件系统 基本I/O管理程序(文件组织模块) 基本文件系统(物理I/O层) I/O控制层(设备驱动程序) 对象及其属性说明 作为产品的操作系统有各自的文件系统。比如MS的WINDOWS系列使用的是FAT16、FAT32或NTFS的文件系统、LINUX使用的是EXT2、EXT3文件系统等等。 (2)linux的EXT2文件系统
linux使用一个叫虚拟文件系统的技术从而可以支持多达几十种的不同文件系统,而EXT2是linux自己的文件系统。它有几个重要的数据结构,一个是超级块,用来描述目录和文件在磁盘上的物理位置、文件大小和结构等信息。inode也是一个重要的数据结构。文件系统中的每个目录和文件均由一个inode描述。它包含:文件模式(类型和存取权限)、数据块位置等信息。 一个文件系统除了重要的数据结构之外,还必须为用户提供有效的接口操作。比如EXT2提供的OPEN/CLOSE接口操作。 (3)用内存来模拟外存
真正的文件系统对外存进行管理,涉及到许多硬件、设备管理方面的底层技术,一方面这些技术不属于操作系统核心内容,一方面过多的内容不免造成实验者顾此失彼,所以这里推荐一种使用内存来模拟外存的方式,可以跳过这些硬件技术而直接把精力放在数据结构设计和操作算法设计上面。
假定pInode是一个指向inode结构的指针,而且它已经放入的需要放入的数值了,现在需要将其写入到特定位置。可用如下代码: ??
fd=fopen(“filesystem”,”w+b”); //fd是FILE指针类型,w便是写方式,b表示二进制
fseek(fd, specific_area,SEEK_SET);// fd是文件指针;specific_area为整形, // 为需要入pInode的位置
fwrite(pInode, sizeof(inode), 1,fd); // 写入pInode信息
2、原理算法
本文件系统采用两级目录,其中第一级对应于用户账号,第二级对应于用户帐号下的文件。另外,为了简便文件系统未考虑文件共享,文件系统安全以及管道文件与设备文件等特殊内容。
首先应确定文件系统的数据结构:主目录、子目录及活动文件等。主目录和子目录都以文件的形式存放于磁盘,这样便于查找和修改。
用户创建的文件,可以编号存储于磁盘上。如:file0,file1,file2?并以编号作为物理地址,在目录中进行登记。