操作标准系统课内实验书.docx
- 文档编号:10401167
- 上传时间:2023-05-25
- 格式:DOCX
- 页数:43
- 大小:311.37KB
操作标准系统课内实验书.docx
《操作标准系统课内实验书.docx》由会员分享,可在线阅读,更多相关《操作标准系统课内实验书.docx(43页珍藏版)》请在冰点文库上搜索。
操作标准系统课内实验书
操作系统原理课内实验指导书
实验一:
用户接口实验
准备知识
为了使用户通过操作系统完成各项管理任务,操作系统必须为用户提供各种接口来实现人机交互。
经典的操作系统理论将操作系统的接口分为控制台命令和系统调用两种。
前者主要提供给计算机的操作人员对计算机进行各种控制;而后者则提供个程序员,使他们可以方便地使用计算机的各种资源。
1.控制台命令接口
操作系统向用户提供一组控制台命令,用户可以通过终端输入命令的方式获得操作系统的服务,并由此来控制自己作业的运行。
一般来讲,控制台命令应该包含:
一组命令、终端处理程序以及命令解释程序。
1)bash的由来
当登录Linux或者打开一个xterm时,当前默认的shell就是bash。
Bash是GNUProject的shell。
GNUProject是自由软件基金会(FreeSoftwareFoundation)的一部分。
它对Linux下的许多编程工具负责。
Bash(BourneAgainShell)是自由软件基金会发布的Bourneshell的兼容程序。
它包含了其他有些shell的许多良好的特性,功能非常的全面。
很多Linux版本都供bash。
2)bash的大致原理
bash处理自己的脚本时,先找到需要处理的命令名称,进而在当前用户的默认命令目录中找到对应的命令,这些默认目录一般是/usr/bin、/bin或/sbin。
在执行这些命令时,先使用进程创建系统调用fork(),在使用exex()来执行这些命令。
3)建立bash脚本
Ø编辑文件
可以用最熟悉的编辑器来编辑这个文本文件,比如文件名为script,在shell下输入:
$viscript
进入vi编辑器,在编辑器中输入以下内容
#!
/bin/bash
echoHelloWorld!
然后保存,退出。
Ø测试脚本。
使用指令:
$$sourcescript
Ø更改脚本属性
使用指令:
$chmoda+xscript
将脚本程序设置为可执行。
Ø执行脚本
使用指令:
$./script
4)关键字参考
echo在终端上显示
bash特殊变量1~9,保存当前进程或脚本的前9个参数。
ls列举目录
wc统计数量
function定义函数
2.系统调用
系统调用是操作系统为程序员提供的接口服务。
使用系统调用,程序员可以更充分的利用计算机资源,使编写的程序更加灵活,功能更加强大。
程序员在对系统充分了解的情况下甚至可以订做系统调用,实现那些非专业程序员所难以实现的功能。
1)添加源代码
第一个任务是编写添加到内核的源程序,即添加到内核文件中的一个函数。
该函数的名称应该是在新的系统调用名称之间前加上sys_标志。
假设新加的系统调用为foo(),功能为原值返回输入的整型数。
格式为intfoo(intiNumber),返回的值就是出入的参数。
在/usr/src/linux/kernel/sys.c文件中添加源代码,如下所示:
asmlinkageintsys_foo(intx)
{printf(“%d\n”,x);
}
注意:
目录“/usr/src/linux“是linux各个版本的统称,它因系统内核的版本不同而名称不同。
例如,当前操作系统是Linux7.1器内核四Linux-2.4.2,所以在”usr/src”目录下有两个文件:
Linux-2.4和Linux-2.4.2,其中Linux-2.4是Linux-2.4.2的连接文件,程序员可以进入任何一个目录,它对内核的修改都是一样的。
2)连接新的系统调用
添加新的系统调用之后,下一个任务是让Linux内核的其余部分知道该程序的存在。
为了从已有的内核程序中增加新函数的链接,需要进行下面的操作:
(1)进入目录/usr/src/linux/include/asm-i386/,打开文件unistd.h。
这个文件包含了系统调用的清单,用来给每个系统调用分配一个唯一的号码。
系统调用号的定义格式如下:
#define_NR_nameNNN
其中,name以系统调用名称代替,而NNN是该系统调用对应的号码,应该将新的系统调用名称放到清单的最后,并给它分配已经用到的系统调用号后面的一个号码,比如:
#define_NR_foo222
以上的系统调用号便是222。
Linux内核自身用的系统调用号已经用到了221了。
如果读者还要自行增加系统调用,就必须从223开始。
(2)进入/usr/src/linux/arche/i386/kernel/,打开文件entry.S。
该文件中有类似下面的清单:
ENTRY(sys_call_table)
.longSYSMBOL_NAME(sys_ni_syscall)
.longSYSMBOL_NAME(sys_exitl)
.longSYSMBOL_NAME(sys_fork)
….
在该表的最后加上:
.longSYSMBOL_NAME(sys_foo)
3)重新编译内核
为了使新的系统调用生效,需要重建Linux的内核,首先必须以root的身份登录。
进入目录:
/usr/src/linux/,重建内核:
[root@linuxserverroot]#makemenuconfig//配置新内核
[root@linuxserverroot]#makedep//创建新内核
[root@linuxserverroot]#makemoduless_install//加入模块
[root@linuxserverroot]#makeclean//清楚多余创建的文件
[root@linuxserverroot]#makebzImage//生成可执行内核引导文件
4)使用新编译的内核
cp–a/usr/src/linux-2.4.2/arch/i386/boot/bzImage/boot
5)重新配置/etc/lilo.conf文件
使用vi编辑器编辑/etc/lilo.conf文件:
vi/etc/lilo.conf
在其中加入如下几行:
Image=/boot/bzImage#启动内核的位置,即自己
#新配置的内核所在目录
label=xhlinux#给内核起一个名称,配
#置完成,重新启动的时候,#会显示这个名称
read_only#定义新的内核为只读
root=/dev/hda5#定义硬盘的启动位置为
#/dev/hda5,在该设计中没有变
#仿照以前内核引导位置,不
#用修改,用以前的就可以了
6)重启系统
完成以上配置后,重新启动系统进入自己的新系统
实验指导
1.控制台命令接口实验指导
1)查看bash版本
在shell提示符下输入:
$echo$BASH_VERSION
2)编写bash脚本:
统计/my目录下c语言文件的个数
通过bash脚本,可以有多种方式实现这个功能,而使用函数是其中个一个选择。
在使用函数之前,必须先定义函数。
(1)进入自己的工作目录,用vi编写名为count的文件
cd/home/student#在home/student目录下编程
vicount
下面是脚本程序:
#!
/bin/bash
functioncount
{
echo–n"Numberofmatchesfor$1:
"#接收程序的第一个参数
ls$1|wc–l#对子程序的第一个参数所在的目录进行操作
}
(2)执行
将count文件复制到当前目录下,然后在当前目录下建立文件夹my:
mkdirmy
3)cdmy
vi1.c#在my目录下建立几个c文件,以便用来进行测试
...
cd...
chmod+xcount
./count‘./my/*.c’(单引号)
2.系统调用实验指导
1)编程调用一个系统调用fork()
在使用程序中调用系统调用fork()非常简单,下面的程序可以很清楚的显示出有fork()系统调用生成了子进程,而产生的分叉作用:
#include
intmain()
{
intiUid;
iUid=fork();
if(iUid==0)
for(;;){printf("Thisisparent.\n");
sleep
(1);
}
if(iUid>0)
for(;;){
printf("Thisischild.\n");
sleep
(1);
}
if(iUid<0)printf("Cannotusesystemcall.\n");
return0;
}
下面是可能得到的一种结果:
thisischild.
thisisparent.
thisischild.
thisisparent.
thisisparent.
thisischild.
thisischild.
thisisparent.
thisisparent.
thisischild.
thisischild.
thisisparent.
thisisparent.
thisischild.
2)编程调用创建的系统调用foo()
foo()是本实验系统设计者自行添加的一个简单的系统调用。
其实现过程在前面一进介绍了,它的功能很简单,就是向标准输出一个特定的整数。
程序test.c如下:
#include
#include
_syscall1(char*,foo,int,ret)
main()
{
intI,J;
I=100;
J=0;
J=foo(I);
printf("Thisistheresultofnewkernel\n");
printf("%d",j);
}
程序编译:
gcc–o–I/usr/src/linux-2.4.2/includetest.c
注意:
由于需要引入内核头文件unistd.h,不能简单的用普通的编译命令,而应该这样设置参数。
运行测试程序
./test
解释:
当函数还没有定义在内核中时,如果运行以上程序,系统将显示一个未定义的值-1,而在内核中定义了以后,系统将显示100.说明我们内核添加系统调用已经成功。
3)创建系统调用mycall(),实现功能:
显示字符串到屏幕上。
(1)编写系统调用相应的函数
在/usr/src/linux/kernel/sys.c文件中添加如下代码:
asmlinkagevoidmycall(char*str)
{
printk("%s\n",str);
}
注意:
在上面的例子中,有一个很少见的特殊函数printk(),它的功能与printf()类似。
之所以不用printf()的原因在于,它不是内核的函数。
要在内核实现在控制台显示字符串的功能,就必须以内核所提供的printk()函数来代替。
Printk()不同于printf()的地方是:
printk()会把输出的结果送到内核的缓冲区里面,最终调用控制台的写函数将其打印出来。
(2)添加系统调用号
打开文件/usr/src/linux/include/asm-i386/unistd.h,添加如下一行:
#include__NR_mycall223//因为__NR_foo是222,所以这个只能用223了
(3)改动系统调用表
打开文件/usr/src/linux/arch/i386/kernel/entry.s,在“.longSYMBOL_NAME(sys_foo)”,下面添加一行:
.longSYMBOL_NAME(sys_mycall)
(4)重新编译内核
对重新编译内核和使用新内核引导熟悉的读者,可以不必阅读(4)和(5)步。
以root身份登录,进入目录/usr/src/linux,重建内核:
[root@linuxserverroot]#makemenuconfig//配置新内核
[root@linuxserverroot]#makedep//创建新内核
[root@linuxserverroot]#makemodules_install//加入模块
[root@linuxserverroot]#makeclean//清除多余创建的文件
[root@linuxserverroot]#makebzImage//生成可执行内核引导文件
[root@linuxserverroot]#cp编译完毕之后,新的内核引导文件在目录:
/usr/src/linux/arch/i386/boot/中,叫baImage.把它复制到/boot/下面:
[root@linuxserverroot]#cp/usr/src/linux/arch/i386/boot/bzImage/boot/
(5)用新的内核引导
这需要修改文件/etc/lilo.conf,先打开该文件,然后进行修改。
修改完成后,存盘退出,运行命令:
[root@linuxserverroot]#/sbin/lilo
(6)重新启动系统、
4)编程调用自己创建的系统调用
系统调用mycall()是读者自己创建的,自己应该对该系统调用表非常的熟悉。
然而使用这个系统调用还需要注意:
系统调用需要装换宏。
这个转换宏是将系统调用命令装换为对应参数的INT80中断请求,一般格式是syscallN(),这个N可以是0~6,对应与系统调用的参数格式,此处N=1,下面是一个简单的源代码test1.c
#include
syscall1(char*,mycall,int,ret)
intmain()
{
char*str;
charstring[50];
str=string;
str="Thisstringwillbedisplayed.";
mycall(str);
return0;
}
实验二:
进程管理实验指导
准备知识
基本概念
2)进程的概念
3)进程与程序的区别
4)并发执行的概念
5)进程互斥的概念
6)进程通信的基本概念
系统调用
系统调用是一种进入系统空间的办法。
通常,在OS的核心中都设置了一组用于实现各种系统功能的子程序,并将他们提供给程序员使用。
程序员在需要OS提供某种服务的时候,便可以调用一条系统调用命令,去实现希望的功能,这就是系统调用。
因此,系统调用就像一个黑箱子一样,对用户屏蔽了操作系统的具体动作而只是提供了调用功能的接口。
不同的操作系统有各自的系统调用方法。
如WindowsAPI,便是Windows的系统调用。
Linux的系统调用与之不同的是源于Linux内核代码完全公开,所以可以细致地分析出其系统调用的机制。
2.1系统调用和普通函数的区别
1)运行于不同的状态
用户程序可以通过系统调用进入系统空间,在核心态执行;而普通函数则只能在用户空间当中进行。
2)通过软中断切断
由于用户程序使用系统调用后要进入系统空间,所以需要调用一个软中断;而普通函数在被调用时则没有这个进程。
2.2系统调用的类型
系统调用的作用与它的宿主操作系统有密切关系。
根据操作系统的性质不同,它们所提供的系统调用会有一定的差异,不过对于普通操作系统而言,应该具有下面几类系统调用:
Ø进程控制类
Ø文件操纵类
Ø进程通信类
Ø信息维护类
2.3系统调用的实现机制
由于操作系统的不同,其系统调用的实现方式可能不一样,然而实现机制应该大致相同的,一般包含下面两个步骤。
1)设置系统调用号
在系统当中,往往设置多条系统调用命令,并赋予每条系统调用命令一个惟一的系统调用号。
根据分配系统调用号方式的不同分为:
直接方式和参数表方式。
系统调用的参数表方式分为直接和间接两种方式,如图1所示。
图1系统调用的参数表方式
2)处理系统调用
操作系统中有一张系统调用入口表。
表中的每个表目都对应一条系统调用命令,它包含有该系统调用自带参数的数目、系统调用命令处理程序的入口地址等等。
操作系统内核便是根据所输入的系统调用号在该表中查找到相应的系统调用,进而转入它的入口地址去执行系统调用程序。
Linux的系统调用机制
Linux的系统调用是通过中断机制实现的。
中断这个概念涉及计算机系统结构方面的知识,显然它与微处理器等硬件有着密不可分的关系。
中断(Interrupt),是指计算机在执行期间,系统内发生任何非寻常的或非预期的急需处理事件,使得CPU不得不暂时中断当前正在执行的程序而转去执行相应的事件处理程序,待处理完毕后再返回原来被中断处继续执行的进程。
其发生一般而言是“异步”的。
换句话说就是在无法预测的情况下发生的(如系统掉电)。
所以计算机的软硬件对于中断的相应反应完全是被动的。
软中断,是对硬中断的一种模拟,发送软中断就是向接收进程的proc结构中的相应项发送一个特定意义的信号。
软中断必须等到接收进程执行时才能生效的。
陷阱(Trap),即由软件产生的中断,指处理机和内存内部产生的中断,它包括程序运算引起的各种错误,如地址非法、校验错误、页面失效等。
它有专门的指令,如X86中的“INTn”,在程序中是有意产生的。
所以说陷阱是主动的、“同步”的。
异常(Exception),一般也是异步的,大多是由于不小心或无意造成的,比如在进行除法操作时除数为0,就会产生一次异常。
相关函数
fork函数
fork()函数用于创建一个新进程(子进程)。
其调用格式为:
intfork();
正确返回:
等于0,创建子进程,从子进程返回的ID值。
等于1,从父进程返回的子进程的进程ID值。
错误返回:
等于-1,创建失败。
1)子进程和父进程的调度执行
子进程被创建后就进入就绪队列和父进程分别分别独立地等待调度。
子进程继承父进程的程序段代码,子进程被调度执行时,也会和父进程一样从fork()返回。
从共享程序段代码的角度来看,父进程和子进程所执行的程序代码是同一个,在内存中只有一个程序段副本;但是从编程的角度来看,为了使子进程和父进程做不同的事,要在程序中区分父进程和子进程的代码段,这就需要借助于从fork()带回的值来标志当前程序身份。
从fork()返回后,都会执行如下语句:
pid=fork();
得到返回的值pid,有如下几种情况:
(1)若pid小于0,则表示fork()出错,相应语句为
if(pid<0){
printf("forkerror\n");
exit(0);}
(2)若pid等于0,则表示当前进程是子进程,继续执行的后面的代码是子进程要做的事,相应的语句可写成
if(pid==0){
printf("Thechildprocessisrunningnow!
\n");
exit(0);}
(3)若pid大于0,则表示当前进程是父进程,继续执行的后面的代码是父进程要做的事,相应的语句可写成
if(pid>0){
printf("Theparentprocessisrunningnow!
\n");
exit(0);}
由于父进程和子进程分别独立地进入就绪队列等待调度,所以谁会先得到调度是不确定的,这与系统的调度策略和系统当前的资源状态有关。
因此谁先从fork()返回,继续执行后面的语句也是不确定的。
2)父进程和子进程的存放及资源共享
当父进程创建子进程时,首先为子进程分配由task向量数组指向的task_struct结构的内存、所需的堆栈和页表等,创建进程标识号(在系统的进程标识号组中是惟一的),并且将其父进程的进程标识号填入task_struct中的家族信息中。
然后将父进程的task_struct的相关内容复制到子进程的task_struct,对一些数据成员进行初始化,如图2所示。
子进程从父进程处继承的资源包括:
真实用户标识号和组标识号、有效用户标识号和组标识号、进程组标识号、对话标识号、控制终端、根目录与当前工作目录、设置用户标识号和设置组标识号计位、信号标识、文件描述符、文件默认创建权限掩码、可访问的内存区、线程、环境变量及其他资源分配。
图2父进程和子进程的内存映像
wait()函数
wait()函数常用来控制父进程与子进程的同步。
在父进程中调用wait()函数,则父进程被阻塞,进入等待队列,等待子进程结束。
当子进程结束时,产生一个终止状态字,系统会向父进程发出SIGCHLD信号。
当接到信号后,父进程提取子进程的终止状态字,从wait()函数返回继续执行原来的程序。
其调用格式为:
#include
#include
(pid_t)wait(int*statloc);
正确返回:
大于0,子进程的进程ID值。
等于0,其他。
错误返回:
等于-1,调用失败。
exit()函数
exit()函数是进程结束时最常调用的函数,在main()函数中调用return,最终也是调用exit()函数。
这些都是进程的正常终止。
在正常终止时,exit()函数返回进程结束状态。
其调用格式为:
#include
voidexit(intstatus);
其中,status为进程结束状态。
kill()函数
kill()函数用于删除执行中的程序或者任务。
其调用格式为:
kill(intPID,intIID);
其中,PID是要被杀死的进程号,IID为向将被杀死的进程发送中的中断号。
Signal()函数
signal()函数是允许调用进程控制软中断信号的处理。
其调用格式为:
#include
intsig;
void(*func)();
signal(sig,function);
其中,
(1)sig的取值如下:
信号
功能
值
SIGHUP
挂起
1
SIGINT
键盘中断,键盘按Delete键或Break键
2
SIGQUIT
键盘按Quit键
3
SIGILL
非法指令
4
SIGTRAP
跟踪中断
5
SIGIOT
IOT指令
6
SIGBUS
总线错
7
SIGFPE
浮点运算溢出
8
SIGKILL
要求终止进程
9
SIGUSR1
用户定义信号#1
10
SIGSEGV
段违法
11
SIGUSR2
用户定义信号#2
12
SIGPIPE
向没有读进程的管道上写
13
SIGALRM
定时器告警,时间到
14
SIGTERM
kill发出的软件结束信号
15
SIGCHLD
子进程死
17
SIGCONT
若已停止则继续
18
SIGPWR
电源故障
30
(2)function的解释如下:
SIG_DFL默认操作。
对除SIGPWR和SIGCHLD外
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 操作 标准 系统 实验