129598552654062500操作系统原理实验指导书

更新时间:2024-03-14 14:05:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

目 录

实验一 系统功能调用 ........................................................ 1 实验二 熟悉Linux环境 ...................................................... 4 实验三 shell脚本 .......................................................... 4 实验四 进程控制 ........................................................... 17 实验五 进程间通信 ......................................................... 21 (一) 信号量机制实验 ........................................................ 21 实验五 进程间通信 ......................................................... 27 (二) 进程的管道通信实验 ..................................................... 0 27

实验五 进程间通信 ......................................................... 31 (三) 消息的发送与接收实验 .................................................. 31 实验五 进程间通信 .......................................................... 37 (四) 共享存储区通信 ........................................................ 37 实验六 死锁避免的算法 ..................................................... 43 实验七 存储管理 ........................................................... 45 1. 常用页面置换算法 ..................................................... 45 2.动态分区分配算法 ..................................................... 54 实验八 文件操作 ............................................................ 55

实验一 系统功能调用

【实验目的】

1、熟悉操作系统的系统功能调用。

2、掌握用C语言实现系统功能调用的方法和步骤。

3、掌握利用10H号功能调用(BIOS的显示I/O功能调用)来实现对屏幕的操作与控制。

【预习内容】

1、预习DOS、BIOS系统功能调用。

2、预习C语言实现系统功能调用的方法。

【实验内容】

1、在屏幕的指定区域内显示字符串。(必做题) 2、在屏幕的制定区域内画框,在框内显示字符串。(提高题) 3、在屏幕上指定区域内画框并以动画形式显示字符串。(选做题)

【实验报告】

1、列出调试通过程序的清单,并加注释。

2、给出必要的程序设计思路和方法(或列出流程图)。 3、总结上机调试过程中所遇到的问题和解决方法及感想。

【实验相关资料】

int86(0X10, &r, &r)函数:对BIOS调用的标准函数

其中0X10是BIOS调用类型号(相当于INT n调用的中断类型号n),是10H号中断调用,是一个显示I/O调用。BIOS借助此中断产生的功能调用控制PC机屏幕上的文本和图形。通过给AH 寄存器设置适当的值选择想要的功能调用,然后发中断10H。

第一个&r是指向联合类型REGS的指针,用于接收调用的功能号及其它一些指定的入口参数,以便传给相应寄存器。

第二个&r是指向联合类型REGS的指针,用于接收功能调用后的返回值,即出口参数。 在dos.h中REGS定义如下: struct WORDREGS {

unsigned int ax,bx,cx,dx,si,di,cflag,flags; };

struct BYTEGEGS {

unsigned int al,ah,bl,bh,cl,ch,dl,dh; }

union REGS {

struct WORDREGS x;

struct BYTEGEGS h; }

10H号中断调用下所包含的部分功能调用: AH 功能调用

00H 置显示方式 01H 置光标类型

1

02H 置光标位置 06H 当前显示页上滚 07H 当前显示页下滚 09H 写字符和属性

02H功能调用: 入口参数:AH=02H

DH 光标的行位置(行数),0行是顶行,24行是底行。 DL 光标的列位置(列数),0列是最左边一列。

BH 光标显示页数,40列方式,可用0到7,80列方式,可用0到3。 出口参数:无

06H功能调用: 入口参数:AH=06H

AL 指明滚动的文本行数。如放置‘0’,则整个窗口为空白。 CH 指明窗口左上角的行位置(从0到24)。

CL 指明窗口左上角的列位置(从0到79,80列)。 DH 指明窗口右下角的行位置(从0到24)。

DL 指明窗口右下角的列位置(从0到79,80列)。 BH 指明加到窗口底部的空行显示属性。 对于彩色/图形适配器,属性字节如下所示:

位 说 明

7 置“1”用于字符闪烁,置“0”字符不闪烁。 6-4 背景色。可用的是:

6 5 4 颜色 0 0 0 黑 0 0 1 蓝 0 1 0 绿 0 1 1 青 1 0 0 红 1 0 1 绛 1 1 0 褐 1 1 1 浅灰

3 前景字符的亮度,置“1”高亮度,置“0”正常亮度。 2-0 前景色。可使用的彩色与背景色相同。 出口参数:屏幕上窗口适当地上滚。

09H功能调用: 入口参数:AH=09H

BH 在文本方式中,此寄存器指明正在写的显示页。在40列方式中,可以用0到7页,在80列方式中,可用0到3页。在图形方式中总是0。 AL 放要写字符的ASCII码。

BL 在文本方式中,为写字符的属性。 CX 将字符和属性写到屏幕上的次数。 出口参数:一个字符或多个字符显示在屏幕上。 清屏子程序: void cls(void)

2

{ union REGS r;

r.h.ah=6; r.h.al=0; r.h.ch=0; r.h.cl=0;

r.h.dh=24; r.h.dl=79;

r.h.bh=7; int86(0x10,&r,&r); }

/*子功能号*/

/*左上角坐标*/ /*右下角坐标*/ /*7表示黑色*/ /*系统功能调用*/

定位子程序:

void locate(int x, int y) { union REGS r;

r.h.ah=2; /*子功能号*/ r.h.dh=x; /*定位点坐标*/ r.h.dl=y; r.h.bh=0;

int86(0x10,&r,&r); }

在指定位置写参数:

void writech(int x , int y, char ch ,int attr) { union REGS r; locate(x ,y); r.h.ah=9; r.h.bh=0; r.h.al=ch; r.h.bl=attr r.x.cx=1;

int86(0x10,&r,&r); }

屏幕画框可以用制表符来画。其主要方法是不断调用writech函数,在指定区域周围显示制表符,从而勾勒出一个窗口的轮廓。

制表符 ASCII码 ┛ 217 ┏ 218 ┓ 191 ┗ 192 ┃ 179 ━ 196

3

实验二 熟悉Ubuntu环境

【实验目的】

1.了解Ubuntu系统基本操作方法,学会独立使用该系统。 2.熟悉Ubuntu下如何编辑、编译和运行一个C语言程序。 3.学会利用gcc、gdb编译、调试C程序。

【预习内容】

1.预习Ubuntu下各种应用程序的使用。 2.预习常用的SHELL命令。

3.预习vim编辑器和emacs编辑器使用方法。 4.预习Ubuntu下C程序编辑、编译和运行过程。

【实验内容】

一、登陆Linux

Linux 进入,再选择Ubuntu登陆窗口,输入用户名: students ,输开机,选择 入密码: 111111 ,进入Ubuntu图形桌面环境。

二、熟悉Ubuntu图形桌面环境

桌面包含上方的菜单栏和下方的任务栏。菜单栏包含“应用程序菜单”、“位置”、“系统”。

通过主菜单可访问应用程序。 (1) “应用程序”菜单

“应用程序”菜单包含“办公”、“附件”、“互联网”、“图形”、“游戏”等。 “办公”包含了文字处理及电子表格等应用程序。

“附件”下包含了“搜索工具”、“计算器”、“文本编辑器”和“终端”等应用程序。

(2)“位置”菜单

“位置”菜单包含“主文件夹”、“桌面”等信息。

(3)“系统”菜单

“系统”菜单包含“首选项”和“系统管理”等信息。

(4)启动终端模拟器

GNOME终端模拟器用一个窗口来模拟字符终端的行为。终端常常被称为命令行或者

shell,Linux 中绝大部分工作都可以用命令行完成。要启动一个终端,可以选择 应用程序 → 附件 → 终端。 三、目录和文件系统

Linux 和 Unix 文件系统被组织成一个有层次的树形结构。文件系统的最上层是 /,或称为 根目录。在 Unix 和 Linux 的设计理念中,一切皆为文件——包括硬盘、分区和可插拔介质。这就意味着所有其它文件和目录(包括其它硬盘和分区)都位于根目录中。 例如:

4

/home/student/sy1.c 给出了正确的完整路径,它指向 sy1.c 文件,而该文件位于 student 目录下,该目录又位于 home 目录,最後,home 目录又位于根(/) 目录下。 在根 (/) 目录下,有一组重要的系统目录,在大部分 Linux 发行版里都通用。直接位于根 (/) 目录下的常见目录列表如下:

? ? ? ? ? ? ? ? ? ? ?

/bin - 重要的二进制 (binary) 应用程序 /boot - 启动 (boot) 配置文件 /dev - 设备 (device) 文件 /etc - 配置文件、启动脚本等 (etc) /home - 本地用户主 (home) 目录 /lib - 系统库 (libraries) 文件

/lost+found - 在根 (/) 目录下提供一个遗失+查找(lost+found) 系统 /media - 挂载可移动介质 (media),诸如 CD、数码相机等 /mnt - 挂载 (mounted) 文件系统

/opt - 提供一个供可选的 (optional) 应用程序安装目录

/proc - 特殊的动态目录,用以维护系统信息和状态,包括当前运行中进程 (processes) 信息。

? ? ? ? ? ?

/root - root (root) 用户主文件夹,读作“slash-root” /sbin - 重要的系统二进制 (system binaries) 文件 /sys - 系统 (system) 文件 /tmp - 临时(temporary)文件

/usr - 包含绝大部分所有用户(users)都能访问的应用程序和文件 /var - 经常变化的(variable)文件,诸如日志或数据库等

四、打开PROC目录了解系统配置

把/proc作为当前目录,就可使用ls命令列出它的内容。

/proc 文件系统是一种内核和内核模块用来向进程 (process) 发送信息的机制 。这个伪文件系统让你可以和内核内部数据结构进行交互,获取有关进程的有用信息,在运行中改变设置 (通过改变内核参数)。 与其他文件系统不同,/proc 存在于内存之中而不是硬盘上。 1.察看 /proc 的文件

/proc 的文件可以用于访问有关内核的状态、计算机的属性、正在运行的进程的状态等信息。大部分 /proc 中的文件和目录提供系统物理环境最新的信息。尽管 /proc 中的文件是虚拟的,但它们仍可以使用任何文件编辑器或像'more', 'less'或 'cat'这样的程序来查看。

2.得到有用的系统/内核信息

/proc 文件系统可以被用于收集有用的关于系统和运行中的内核的信息。下面是一些重要的文件:

5

? ? ? ? ? ? ? ?

/proc/cpuinfo - CPU 的信息 (型号, 家族, 缓存大小等) /proc/meminfo - 物理内存、交换空间等的信息 /proc/mounts - 已加载的文件系统的列表 /proc/devices - 可用设备的列表 /proc/filesystems - 被支持的文件系统 /proc/modules - 已加载的模块 /proc/version - 内核版本

/proc/cmdline - 系统启动时输入的内核命令行参数

proc 中的文件远不止上面列出的这么多。想要进一步了解的读者可以对 /proc 的每一个文件都'more'一下 。 3.有关运行中的进程的信息

/proc 文件系统可以用于获取运行中的进程的信息。在 /proc 中有一些编号的子目录。每个编号的目录对应一个进程 id (PID)。这样,每一个运行中的进程 /proc 中都有一个用它的 PID 命名的目录。这些子目录中包含可以提供有关进程的状态和环境的重要细节信息的文件。

/proc 文件系统提供了一个基于文件的 Linux 内部接口。它可以用于确定系统的各种不同设备和进程的状态。对他们进行配置。因而,理解和应用有关这个文件系统的知识是理解你的 Linux 系统的关键。

五、文件权限

Linux 系统上的所有文件都有权限,以允许或者阻止其它用户查阅、修改或者执行。超级用户“root”则可以访问系统上的任意文件。每个文件都有访问限制、用户限制以及隶属于某个用户/组(owner/group)。 所有文件皆有如下三组权限加以保护,按重要性依次排列:

用户(user) :适用于该文件的所有者 组(group) :适用于该文件所属的组 其他(other) :适用于所有其他用户

上述三组权限信息的每一组都包含了实际权限。这些权限信息,连同它们对文件和目录具有的不同含义,概述如下:

读(read) :允许显示/打开该文件 可以显示目录内容 写(write) :可以编辑或删除该文件 可以更改目录内容

可执行(execute) :可执行文件可以作为程序运行 可以进入该目录 例如:ls –l

普通文件:-rw-¦r--¦r-- 1 bin bin 2208 May 28 1987 /etc/rc

¦ ¦

目录文件:drwx¦r-x¦r-x 2 bin bin 240 Nov 13 1987 dos

¦ ¦

特殊文件:brw-¦r-x¦r-x 3 bin bin 2,7 Jan 12 15:07 /dev/fd0

c-w-¦-w-¦-w- 2 bin bin 6,0,Jan 11 13:37 /dev/Lp

存取权

所 同 其 6

有 组 他 者 用 用 户

注:对于普通文件,类型为-;对于目录文件,类型为d;在系统目录/dev下的文件都是特殊文件,如类型b代表块设备,c代表字符设备,p代表有名管道文件,s代表套接字文件。 用chmod改变文件或目录的存取权限:

格式:

chmod 谁 操作符 许可权 {文件名(或目录名)} a + r g w - x o s u = t u g o

其中,a:代表all,即所有的用户; g:代表group,即小组用户; o:代表others,即一般用户; u:代表 user,即文件的所有者;

+:代表增加许可权; -:代表删除许可权;

=:代表赋予指定的许可权。

例如:$chmod u+w display 完成对u用户增加对文件display的写权限。 注:上述命令是由文件所有者或超级用户进行修改。 六、练习常用的Shell命令。(重点)

当用户登录到字符界面系统或使用终端模拟窗口时,就是在和称为shell的命令解释程序进行通信。当用户在键盘上输入一条命令时,shell程序将对命令进行解释并完成相应的动作。这种动作可能是执行用户的应用程序,或者是调用一个编辑器、GNU/Linux实用程序或其他标准程序,或者是一条错误信息,告诉用户输入了错误的命令。 1.目录操作

mkdir abc 创建一个目录abc cd abc 将工作目录改变到abc cd 改变当前目录到主目录 ls 列出当前目录的内容

ls -l 输出当前目录内容的长列表,每个目录或文件占一行 pwd 显示当前目录的全路径 2.文件显示实用程序

cat mx.c 显示mx.c文件内容 more mx.c 分屏显示mx.c内容 tail mx.c 显示文件后几行 cat file1 file2 连接file1 和file2

head filename 显示文件filename的开始10行

wc filename 统计文件filename中的行数、单词数和字符数 od 文件 查看非文本文件 3.文件管理实用程序

7

cp file1 file2 将文件1复制到文件2 mv file1 file2 将文件重命名为file2 rm filename 删除文件filename rm -i filename 请求用户确认删除

4.数据操作实用程序

tty 显示当前终端的路径和文件名 who 显示当前登录用户的列表

sort filename 显示文件filename中的行的排序结果 spell filename 检查文件filename中的拼写错误

5.其他实用程序

date 输出系统日期和时间

cal 显示本月的日历。cal 2002 显示2002年的日历 clear 清除终端屏幕

history 显示你以前执行过的命令的列表

man 显示实用程序的有用信息,并提供该实用程序的基本用法 echo 读取参数并把它写到输出 七、熟悉vim编辑器

在编写文本或计算机程序时,需要创建文件、插入新行、重新排列行、修改内容等,计算机文本编辑器就是用来完成这些工作的。

Vim编辑器的两种操作模式是命令模式和输入模式(如图2所示)。当vim处于命令模式时,可以输入vim命令。例如,可以删除文本并从vim中退出。在输入模式下,vim将把用户所输入的任何内容都当作文本信息,并将它们显示在屏幕上。 vi的工作模式见图2所示。

⑴命令模式

命令模式 插入、添加、打开、替换或更改 冒号(:) 最后一回车 行模式 ESC 输入模式 图2 vi编辑器下的模式 在输入模式下,按ESC可切换到命令模式。命令模式下,可选用下列指令离开vi:

命令 作 用

:q! 离开vi,并放弃刚在缓冲区内编辑的内容 :wq 将缓冲区内的资料写入当前文件中,并离开vi :ZZ 同wq :x 同wq

8

:w 将缓冲区内的资料写入当前文件中,但并不离开vi

:q

离开vi,若文件被修改过,则要被要求确认是否放弃修改的内容,此指令可与:w配合使用

命令模式下光标的移动 :

命 令 作 用

h或左箭头 左移一个字符 J 下移一个字符 k 上移一个字符 l 右移一个字符 0 移至该行的首 $ 移至该行的末

^ 移至该行的第一个字符处 H 移至窗口的第一列 M 移至窗口中间那一列 L 移至窗口的最后一列 G 移至该文件的最后一列 W, W 下一个单词 (W 忽略标点) b, B 上一个单词 (B 忽略标点) + 移至下一列的第一个字符处 - 移至上一列的第一个字符处 ( 移至该句首 ) 移至该句末 { 移至该段首 } 移至该段末

nG

移至该文件的第n列

⑵输入模式

输入以下命令即可进入vi输入模式:

命 令

作 用

a(append) 在光标之后加入资料 A

在该行之末加入资料 i(insert) 在光标之前加入资料 I 在该行之首加入资料

o(open) 新增一行于该行之下,供输入资料用 O 新增一行于该行之上,供输入资料用 Dd 删除当前光标所在行 X 删除当前光标字符 X 删除当前光标之前字符 U 撤消 · 重做 F 查找

s 替换,例如:将文件中的所有\换成\用\ESC

离开输入模式

9

启动vim命令:

命令

vim filename vim +filename vim -r filename vim -R filename

更多用法见 info vi。

vim 下程序录入过程:

①$ vim aaa.c ↙ 进入vim命令模式

② i ↙ 进入输入模式输入 C源程序(或文本) ③ ESC ↙ 回到命令模式

④ ZZ ↙ 保存文件并推出vim

⑤ CAT aaa.c ↙ 显示aaa.c 文件内容 八、熟悉gcc编译器

GNU/Linux中通常使用的C编译器是GNU gcc。编译器把源程序编译生成目标代码的任务分为以下4步:

a. 预处理,把预处理命令扫描处理完毕;

b. 编译,把预处理后的结果编译成汇编或者目标模块;

c. 汇编,把编译出来的结果汇编成具体CPU上的目标代码模块; d. 连接,把多个目标代码模块连接生成一个大的目标模块;

1.使用语法:

gcc [ option | filename ]...

其中 option 为 gcc 使用时的选项,而 filename 为 gcc要处理的文件。

2.GCC选项

GCC的选项有很多类,这类选项控制着GCC程序的运行,以达到特定的编译目的。 ⑴全局选项(OVERALL OPTIONS)

全局开关用来控制在“GCC功能介绍”中的GCC的4个步骤的运行,在缺省的情况下,这4个步骤都是要执行的,但是当给定一些全局开关后,这些步骤就会在 某一步停止执行,这产生中间结果,例如可能你只是需要中间生成的预处理的结果或者是汇编文件(比如你的目的是为了看某个CPU上的汇编语言怎么写)。 ① –x language

对于源文件是用什么语言编写的,可以通过文件名的后缀来标示,也可以用这开关。指定输入文件是什么语言编写的,language 可以是如下的内容 a. c

b. objective-c c. c-header d. c++

e.cpp-output f.assembler

g.assembler-with-cpp ②–x none

把-x开关都给关掉了。

10

作用

从第一行开始编辑filename文件 从最后一行开始编辑filename文件 在系统崩溃之后恢复filename文件 以只读方式编辑filename文件

③ –c

编译成把源文件目标代码,不做连接的动作。 ④–S

把源文件编译成汇编代码,不做汇编和连接的动作。 ⑤–E

只把源文件进行预处理之后的结果输出来。不做编译,汇编,连接的动作。 ⑥ –o file (常用)

指明输出文件名是file。 ⑦–v

把整个编译过程的输出信息都给打印出来。 ⑧–pipe

由于gcc的工作分为好几步才完成,所以需要在过程中生成临时文件,使用-pipe就是用管道替换临时文件。

⑵ 语言相关选项(Language Options) 用来处理和语言相关的选项。 ①–ansi

这个开关让GCC编译器把所有的GNU的编译器特性都给关掉,让你的程序可以和ansi标准兼容。 ②–include file

在编译之前,把file包含进去,相当于在所有编译的源文件最前面加入了一个#include 语句, ③–C

同-E参数配合使用。让预处理后的结果,把注释保留,让人能够比较好读它。 ⑶连接开关(Linker Options)

用来控制连接过程的开关选项。 ① –llibrary

连接库文件开关。例如-lugl,则是把程序同libugl.a文件进行连接。 ② –lobjc

这个开关用在面向对象的C语言文件的库文件处理中。 ③ –nostartfiles

在连接的时候不把系统相关的启动代码连接进来。 ④ –nostdlib

在连接的时候不把系统相关的启动文件和系统相关的库连接进来。 ⑤–static

在一些系统上支持动态连接,这个开关则不允许动态连接。 ⑥shared

生成可共享的被其他程序连接的目标模块。 ⑷目录相关开关(Directory Options) 用于定义与目录操作相关的开关。 –Ldir

搜寻库文件(*.a)的路径。

⑸调试开关(Debugging Options) –g

把调试开关打开,让编译的目标文件有调试信息。 –V version

11

用来告诉编译器使用它的多少版本的功能,version参数用来表示版本。 九、掌握Ubuntu下C程序编辑运行过程(重点) Ubuntu下编写C程序要经过以下几个步骤:

⑴启动常用的编辑器,键入C源程序代码。 例如,点击应用程序/附件/文本编辑器,进入编辑环境,输入C源程序,保存并命名为hello.c # include void main(void) {

Printf(“Hello world!\\n”); }

⑵编译源程序

点击应用程序/附件/终端,进入命令行。用gcc编译器对C源程序进行编译,以生成一个可执行文件。方法:

gcc -o hello.out hello.c ↙

⑶运行可执行文件 ·/hello.out ↙

注:命令行中 -o选项表示要求编译器输出可执行文件名为hello.out文件,hello.c是源程序文件。

【实验报告】

1.举例列出常用的shell命令使用方法。

2.通过实例总结上机调试C语言程序的过程及此次上机的感想。

12

实验三 shell脚本

【实验目的】

1、了解和熟悉创建并使用脚本的步骤。 2、熟悉bash的控制结构。 3、学会简单的shell编程。

【实验内容】

1、 创建一个简单的列目录和日期的shell脚本并运行之。

步骤:

⑴输入下列命令,创建一个新文件: cat >new_scrip ⑵输入下列行:

echo “Your files are” ls

echo “today is” date

按回车键将光标移到一个新行,按Ctrl+D键保存并退出。 ⑶检查文件内容,确保它是正确的: cat new_script

⑷运行脚本,输入它的文件名: new_script 该脚本不运行。

⑸输入下列命令,显示文件的权限: ls –l new _script

权限表明该文件不是可执行。要通过简单调用文件名来运行脚本,必须有权限。 ⑹输入下列命令,使new_script变成可执行文件。 chmod +x new_script ⑺要查看新的权限,输入: ls –l

现在拥有文件的读、写和执行权限。 ⑻输入新脚本的名字以执行它: new_script

所有输入到文件的命令都执行,并输出到屏幕上。 ⑼如果接收到错误信息,比如: command not found 输入下列命令: ./new_script

该命令行通知shell到哪里寻找shell脚本new_script,即您的当前目录“.”。

2、 用Shell语言编制一Shell程序,该程序在用户输入年、月之后,自动打印数出该年该

月的日历。

13

<参考程序>

echo “Please input the month:” read month

echo “Please input the year:” read year

cal $month $year

3、 编程提示用户输入两个单词,并将其读入,然后比较这两个单词,如果两个单词相同则

显示“Match”,并显示“End of program”,如果不同则显示“End of program”。 <参考程序> $ cat > if1

echo –n “word 1:” read word1

echo –n “word 2:” read word2

if test “$word1” = “$word2” then

echo “Match” fi

echo “End of program.”

<程序说明>

①if?then控制结构的语法是:

If test-command False Ture then command fi if test_command then commands fi 图3.1 if…then流程图

②其中test_command为test “$word1” = “$word2”, test是一个内置命令,如果它的第一个参数和第三个参数存在第二个参数所指定的关系,那么test将返回ture。Shell将执行then和fi之间的命令。否则执行fi后面语句。

4、修改上述程序,编程提示用户输入两个单词,并将其读入,然后比较这两个单词,如果两个单词相同显示“Match”,不同则显示“Not match”,最后显示“End of program”。 <编程提示>请使用 if…then…else 控制结构。

5、编程使用case结构创建一个简单的菜单,屏幕显示菜单: a. Current date and time b. User currently logged in

c. Name of the working directory d. Contents of the working directory Enter a,b,c or d:

根据用户输入选项做相应操作。 <参考程序>

echo –e “\\n COMMAND MENU\\n”

14

echo “ a. Current date and time” echo “ b. User currently logged in”

echo “ c. Name of the working directory”

echo “ d. Contents of the working directory\\n” echo –n “Enter a,b,c or d:” read answer echo

case “$answer” in a)

date ;; b)

who ;; c)

pwd ;; d) ls ;; *)

Echo “There is no selection : $answer” ;; esac

6、修改上题,使用户可以连续选择直到想退出时才退出。 7、编程使用select结构生成一个菜单如下:

1)apple 3)blueberry 5)orange 7)STOP

2) banana 4)kiwi 6)watermelon

Choose your favorite fruit from these possibilities: 用户输入所选项,如 1 显示:

You chose apple as you favorite. That is choice number 1.

<参考程序> #!/bin/bash

ps3=“Chose your favorite fruit from these possibilities:”

select FRUIT in apple banana blueberry kiwi orange watermelon STOP do

if [ $FRUIT = STOP ] then echo “Thanks for playing!” break fi

echo “You chose $FRUIT as you favorite.” echo “That is choice number $REPLY.” echo done

15

<程序说明>

①select 结构的语法如下:

select varname[in arg?]

do commands done ②REPLY是键盘变量。

【思考题】

1、什么选项通知rm、cp和mv在删除或覆盖文件前得到用户的确认? 2、如何确认自己在主目录中?然后再主目录中创建一个名为Dannty的目录,再进入到Danny目录,并确认你的位置?

3、命令echo$PATH的输出是什么? 4、下列命令的运行结果是什么? who | grep $USER grep \\$HOME file1

【实验报告】

1.列出调试通过程序的清单,并加注释。 2.回答思考题。

3.总结上机调试过程中所遇到的问题和解决方法及感想。

【实验相关资料】

创建并使用脚本的步骤: ⑴创建shell命令文件。

⑵使用chmod命令使文件可执行。 ⑶通过输入脚本文件名执行文件。

在执行脚本时,shell读取脚本并按其指示执行。它逐行执行脚本,就像这些行是从键盘输入的一样。脚本中所有的实用程序都执行。

16

实验四 进程控制

【实验目的】

1、掌握进程的概念,明确进程和程序的区别。 2、认识和了解并发执行的实质。

3、分析进程争用资源的现象,学习解决进程互斥的方法。

【实验内容】

1、进程的创建(必做题)

编写一段程序,使用系统调用fork( )创建两个子进程,在系统中有一个父进程和两个子进程活动。让每个进程在屏幕上显示一个字符;父进程显示字符“a”,子进程分别显示字符“b” 和“c”。试观察记录屏幕上的显示结果,并分析原因。 <参考程序>

# include main()

{ int p1, p2;

while((p1=fork())= = -1); if(p1= =0)

putchar(‘b’); else

{ while((p2=fork())= =-1); if(p2= =0)

putchar(‘c’); else

putchar( ‘a’); } }

2、 修改已编写的程序,将每个进程的输出由单个字符改为一句话,再观察程序执行时屏幕上出现的现象,并分析其原因。(必做题)

<参考程序>

# include main()

{ int p1, p2, i;

while((p1=fork())= = -1); if(p1= =0)

for(i=0;i<500;i++)

printf(“child%d\\n”,i); else

{ while((p2=fork())= =-1); If(p2= =0)

17

for(i=0;i<500;i++) printf(“son%d\\n”,i); else

for(i=0;i<500;i++)

printf(“daughter%d\\n”,i); } }

3、编写程序创建进程树如图1和图2所示,在每个进程中显示当前进程识别码和父进程识别码。(必做题)

父进程

b c d 图1 进程树

图2 进程树

子进程 b c a a d e 【思考题】

1、系统是怎样创建进程的?

2、当首次调用新创建进程时,其入口在哪里? 3、当前运行的程序(主进程)的父进程是什么?

【实验报告】

1、列出调试通过程序的清单,分析运行结果。

2、给出必要的程序设计思路和方法(或列出流程图)。 3、回答思考题。

4、总结上机调试过程中所遇到的问题和解决方法及感想。

【实验相关资料】

一、进程概念

1.进程

UNIX中,进程既是一个独立拥有资源的基本单位,又是一个独立调度的基本单位。一个进程实体由若干个区(段)组成,包括程序区、数据区、栈区、共享存储区等。每个区又分为若干页,每个进程配置有唯一的进程控制块PCB,用于控制和管理进程。PCB的数据结构如下:

⑴ 进程表项(Process Table Entry)。

包括一些最常用的核心数据,如: 进程标识符PID、用户标识符UID、进程状态、事件描述符、进程和U区在内存或外存的地址、软中断信号、计时域、进程的大小、偏置值nice、指向就绪队列中下一个PCB的指针P_Link、指向U区进程正文、数据及栈在内存区域的指针。

⑵ U区(U Area)。

用于存放进程表项的一些扩充信息。每一个进程都有一个私用的U区,其中含有:进程

18

表项指针、真正用户标识符u-ruid(read user ID)、有效用户标识符u-euid(effective user ID)、用户文件描述符表、计时器、内部I/O参数、限制字段、差错字段、返回值、信号处理数组。

由于UNIX系统采用段页式存储管理,为了把段的起始虚地址变换为段在系统中的物理地址,便于实现区的共享,所以还有: ⑶ 系统区表项。

以存放各个段在物理存储器中的位置等信息。系统把一个进程的虚地址空间划分为若干个连续的逻辑区,有正文区、数据区、栈区等。这些区是可被共享和保护的独立实体,多个进程可共享一个区。为了对区进行管理,核心中设置一个系统区表,各表项中记录了以下有关描述活动区的信息:区的类型和大小、区的状态、区在物理存储器中的位置、引用计数、指向文件索引结点的指针。 ⑷ 进程区表

系统为每个进程配置了一张进程区表。表中,每一项记录一个区的起始虚地址及指向系统区表中对应的区表项。核心通过查找进程区表和系统区表,便可将区的逻辑地址变换为物理地址。

2. 进程映像

UNIX系统中,进程是进程映像的执行过程,也就是正在执行的进程实体。它由三部分组成:

⑴ 用户级上、下文。主要成分是用户程序;

⑵ 寄存器上、下文。由CPU中的一些寄存器的内容组成,如PC,PSW,SP及通用寄存器等; ⑶ 系统级上、下文。包括OS为管理进程所用的信息,有静态和动态之分。

3.进程树

在UNIX系统中,只有0进程是在系统引导时被创建的,在系统初启时由0进程创建1进程,以后0进程变成对换进程,1进程成为系统中的始祖进程。UNIX利用fork( )为每个终端创建一个子进程为用户服务,如等待用户登录、执行SHELL命令解释程序等,每个终端进程又可利用fork( )来创建其子进程,从而形成一棵进程树。可以说,系统中除0进程外的所有进程都是用fork( )创建的。 二、所涉及的中断调用

1、fork( )

创建一个新的子进程。其子进程会复制父进程的数据与堆栈空间,并继承父进程的用户代码、组代码、环境变量、已打开的文件代码、工作目录和资源限制。 系统调用格式:

int fork()

如果Fork成功则在父进程会返回新建立的子进程代码(PID),而在新建立的子进程中则返回0。如果fork失败则直接返回-1。 2、wait( )

等待子进程运行结束。如果子进程没有完成,父进程一直等待。wait( )将调用进程挂起,直至其子进程因暂停或终止而发来软中断信号为止。如果在wait( )前已有子进程暂停或终止,则调用进程做适当处理后便返回。 系统调用格式:

int wait(status) int *status;

其中,status是用户空间的地址。它的低8位反应子进程状态,为0表示子进程正常结束,非0则表示出现了各种各样的问题;高8位则带回了exit( )的返回值。exit( )返

19

回值由系统给出。核心对wait( )作以下处理:

(1)首先查找调用进程是否有子进程,若无,则返回出错码;

(2)若找到一处于“僵死状态”的子进程,则将子进程的执行时间加到父进程的执行时间上,并释放子进程的进程表项;

(3)若未找到处于“僵死状态”的子进程,则调用进程便在可被中断的优先级上睡眠,等待其子进程发来软中断信号时被唤醒。 3、exit( )

终止进程的执行。 系统调用格式:

void exit(status) int status;

其中,status是返回给父进程的一个整数,以备查考。为了及时回收进程所占用的资源并减少父进程的干预,UNIX/LINUX利用exit( )来实现进程的自我终止,通常父进程在创建子进程时,应在进程的末尾安排一条exit( ),使子进程自我终止。exit(0)表示进程正常终止,exit(1)表示进程运行有错,异常终止。

如果调用进程在执行exit( )时,其父进程正在等待它的终止,则父进程可立即得到其返回的整数。核心须为exit( )完成以下操作: (1)关闭软中断 (2)回收资源 (3)写记帐信息

(4)置进程为“僵死状态”

20

实验五 进程间通信

UNIX/LINUX系统的进程间通信机构(IPC)允许在任意进程间大批量地交换数据。本实验的目的是了解和熟悉LINUX支持的信号量机制、管道机制、消息通信机制及共享存储区机制。

(一)信号量机制实验

【实验目的】

1、了解什么是信号。

2、熟悉LINUX系统中进程之间软中断通信的基本原理。

【实验内容】

1、编写一段程序,使用系统调用fork( )创建两个子进程,再用系统调用signal( )让父进 程捕捉键盘上来的中断信号(即按ctrl+c键),当捕捉到中断信号后,父进程用系统调用kill( )向两个子进程发出信号,子进程捕捉到信号后,分别输出下列信息后终止: Child process 1 is killed by parent! Child process 2 is killed by parent!

父进程等待两个子进程终止后,输出以下信息后终止: Parent process is killed! <参考程序> #include #include #include #include #include int wait_mark;

void waiting(),stop(); void main() {int p1, p2;

signal(SIGINT,stop); while((p1=fork())==-1);

if(p1>0) {①

while((p2=fork())==-1);

If(p2>0) { ②

wait_mark=1; waiting(0); kill(p1,10); kill(p2,12); wait( );

/*在父进程中*/

/*在父进程中*/

21

wait( );

printf(\ exit(0); }

else /*在子进程2中*/ {

wait_mark=1; signal(12,stop); waiting(); lockf(1,1,0);

printf(\lockf(1,0,0); exit(0); }

}

else /*在子进程1中*/ {

wait_mark=1; signal(10,stop); waiting(); lockf(1,1,0);

printf(\ lockf(1,0,0); exit(0); } }

void waiting() {

while(wait_mark!=0); }

void stop() {

wait_mark=0; }

实验要求:

⑴、运行程序并分析结果。

⑵、如果把signal(SIGINT,stop)放在①号和②号位置,结果会怎样并分析原因。 ⑶、该程序段前面部分用了两个wait(0),为什么?

⑷、该程序段中每个进程退出时都用了语句exit(0),为什么?

2、修改上面的程序,增加语句signal(SIGINT,SIG_IGN)和语句signal(SIGQUIT,SIG_IGN),再观察程序执行时屏幕上出现的现象,并分析其原因。 <参考程序>

# include

22

# include # include main()

{ int pid1, pid2; int EndFlag=0; Pf1=0; Pf2=0;

void IntDelete() {

kill(pid1,10); kill(pid2,12); EndFlag=1; }

void Int1() {

printf(“child process 1 is killed by parent !\\n”); exit(0); }

void Int2() {

printf(“child process 2 is killed by parent !\\n”); exit(0); }

main() {

int exitcode;

signal(SIGINT,SIG_IGN); signal(SIGQUIT,SIG_IGN); while((pid1=fork())==-1); if(pid==0) {

signal(SIGUSER1,Int1); signal(SIGQUIT,SIG_IGN); pause(); exit(0); } else {

while((pid2=fork())= =-1); if(pid2==0) {

signal(SIGUSER1,Int1); signal(SIGQUIT,SIG_IGN); pause(); exit(0); } else

23

{

signal(SIGINT,IntDelete); waitpid(-1,&exitcode,0);

printf(“parent process is killed \\n”); exit(0); }

} }

实验要求:

运行程序并分析结果。

⑶司机售票员问题(选做题)

编程用fork()创建一个子进程代表售票员,司机在父进程中,再用系统调用signal()让父进程(司机)捕捉来自子进程(售票员)发出的中断信号,让子进程(售票员)捕捉来自(司机)发出的中断信号,以实现进程间的同步运行。

【实验报告】

1、列出调试通过程序的清单,分析运行结果。

2、给出必要的程序设计思路和方法(或列出流程图)。 3、总结上机调试过程中所遇到的问题和解决方法及感想。

【实验相关资料】

一、信号

1. 信号的基本概念

每个信号都对应一个正整数常量(称为signal number,即信号编号。定义在系统头文件中),代表同一用户的诸进程之间传送事先约定的信息的类型,用于通知某进程发生了某异常事件。每个进程在运行时,都要通过信号机制来检查是否有信号到达。若有,便中断正在执行的程序,转向与该信号相对应的处理程序,以完成对该事件的处理;处理结束后再返回到原来的断点继续执行。实质上,信号机制是对中断机制的一种模拟,故在早期的UNIX版本中又把它称为软中断。 ⑴ 信号与中断的相似点:

①采用了相同的异步通信方式;

②当检测出有信号或中断请求时,都暂停正在执行的程序而转去执行相应的处理程序; ③都在处理完毕后返回到原来的断点; ④对信号或中断都可进行屏蔽。 ⑵ 信号与中断的区别:

①中断有优先级,而信号没有优先级,所有的信号都是平等的;

②信号处理程序是在用户态下运行的,而中断处理程序是在核心态下运行; ③中断响应是及时的,而信号响应通常都有较大的时间延迟。 ⑶ 信号机制具有以下三方面的功能:

①发送信号。发送信号的程序用系统调用kill( )实现;

②预置对信号的处理方式。接收信号的程序用signal( )来实现对处理方式的预置;

24

③收受信号的进程按事先的规定完成对相应事件的处理。 2、信号的发送

信号的发送,是指由发送进程把信号送到指定进程的信号域的某一位上。如果目标进程正在一个可被中断的优先级上睡眠,核心便将它唤醒,发送进程就此结束。一个进程可能在其信号域中有多个位被置位,代表有多种类型的信号到达,但对于一类信号,进程却只能记住其中的某一个。进程用kill( )向一个进程或一组进程发送一个信号。 3、对信号的处理

当一个进程要进入或退出一个低优先级睡眠状态时,或一个进程即将从核心态返回用户态时,核心都要检查该进程是否已收到软中断。当进程处于核心态时,即使收到软中断也不予理睬;只有当它返回到用户态后,才处理软中断信号。对软中断信号的处理分三种情况进行:

①如果进程收到的软中断是一个已决定要忽略的信号(function=1),进程不做任何处理便立即返回;

②进程收到软中断后便退出(function=0); ③执行用户设置的软中断处理程序。 二、所涉及的中断调用 1、kill( )

系统调用格式

int kill(pid,sig) 参数定义

int pid,sig;

其中,pid是一个或一组进程的标识符,参数sig是要发送的软中断信号。 (1)pid>0时,核心将信号发送给进程pid。

(2)pid=0时,核心将信号发送给与发送进程同组的所有进程。

(3)pid=-1时,核心将信号发送给所有用户标识符真正等于发送进程的有效用户标识号的进程。 2、signal( )

预置对信号的处理方式,允许调用进程控制软中断信号。 系统调用格式

signal(sig,function) 头文件为

#include 参数定义

signal(sig,function) int sig;

void (*func) ( )

其中sig用于指定信号的类型,sig为0则表示没有收到任何信号,余者如下表: 值 名 字 01 SIGHUP

说 明 挂起(hangup) 25

02 SIGINT 03 SIGQUIT 04 SIGILL 05 SIGTRAP 06 SIGIOT 07 SIGEMT 08 SIGFPE 09 SIGKILL 10 SIGBUS 11 SIGSEGV 12 SIGSYS 13 SIGPIPE 14 SIGALRM 15 SIGTERM 16 SIGUSR1 17 SIGUSR2 18 SIGCLD 19 SIGPWR 中断,当用户从键盘按^c键或^break键时 退出,当用户从键盘按quit键时 非法指令 跟踪陷阱(trace trap),启动进程,跟踪代码的执行 IOT指令 EMT指令 浮点运算溢出 杀死、终止进程 总线错误 段违例(segmentation violation),进程试图去访问其虚地址空间以外的位置 系统调用中参数错,如系统调用号非法 向某个非读管道中写入数据 闹钟。当某进程希望在某时间后接收信号时发此信号 软件终止(software termination) 用户自定义信号1 用户自定义信号2 某个子进程死 电源故障

function:在该进程中的一个函数地址,在核心返回用户态时,它以软中断信号的序号作为参数调用该函数,对除了信号SIGKILL,SIGTRAP和SIGPWR以外的信号,核心自动地重新设置软中断信号处理程序的值为SIG_DFL,一个进程不能捕获SIGKILL信号。 function 的解释如下:

(1)function=1时,进程对sig类信号不予理睬,亦即屏蔽了该类信号; (2)function=0时,缺省值,进程在收到sig信号后应终止自己;

(3)function为非0,非1类整数时,function的值即作为信号处理程序的指针。

26

实验五 进程间通信

(二)进程的管道通信实验

【实验目的】

1、了解什么是管道

2、熟悉UNIX/LINUX支持的管道通信方式 【实验内容】

1、编制一段程序,实现进程的管道通信。使用pipe()建立一条管道线。两个子进程p1和p2分别向管道各写一句话:

Child 1 is sending message! Child 2 is sending message!

而父进程则从管道中读出来自于两个子进程的信息,显示在屏幕上。 <参考程序>

# include # include # include int pid1,pid2; main() {

int fd[2];

char OutPipe[100],InPipe[100]; pipe(fd);

while((pid1=fork())= = -1); if(pid1= =0) {

lockf(fd[1],1,0);

sprintf(OutPipe,” child 1 process is sending message!”); write(fd[1],OutPipe,50); sleep(5);

lockf(fd[1],0,0); exit(0); }

else {

while((pid2=fork())= = -1); if(pid2= =0) {

lockf(fd[1],1,0);

sprintf(OutPipe,” child 2 process is sending message!”); write(fd[1],OutPipe,50);

27

sleep(5);

lockf(fd[1],0,0); exit(0); } else {

wait(0);

read(fd[0],InPipe,50); printf(“%s\\n”,InPipe); wait(0);

read(fd[0],InPipe,50); printf(“%s\\n”,InPipe); exit(0); } } }

实验要求:运行程序并分析结果。

2.在父进程中用pipe()建立一条管道线,往管道里写一句话,两个子进程接收这句话。

【实验报告】

1、列出调试通过程序的清单,分析运行结果。

2、给出必要的程序设计思路和方法(或列出流程图)。 3、总结上机调试过程中所遇到的问题和解决方法及感想。

【实验相关资料】

一、什么是管道

UNIX系统在OS的发展上,最重要的贡献之一便是该系统首创了管道(pipe)。这也是UNIX系统的一大特色。 所谓管道,是指能够连接一个写进程和一个读进程的、并允许它们以生产者—消费者方式进行通信的一个共享文件,又称为pipe文件。由写进程从管道的写入端(句柄1)将数据写入管道,而读进程则从管道的读出端(句柄0)读出数据。

句柄fd[0]

句柄fd[1] 二、管道的类型: 1、有名管道

一个可以在文件系统中长期存在的、具有路径名的文件。用系统调用mknod( )建立。它克服无名管道使用上的局限性,可让更多的进程也能利用管道进行通信。因而其它进程可以知道它的存在,并能利用路径名来访问该文件。对有名管道的访问方式与访问其他文件一样,

28

读出端 写入端

需先用open( )打开。 2、无名管道

一个临时文件。利用pipe( )建立起来的无名文件(无路径名)。只用该系统调用所返回的文件描述符来标识该文件,故只有调用pipe( )的进程及其子孙进程才能识别此文件描述符,才能利用该文件(管道)进行通信。当这些进程不再使用此管道时,核心收回其索引结点。二种管道的读写方式是相同的,本文只讲无名管道。 3、pipe文件的建立

分配磁盘和内存索引结点、为读进程分配文件表项、为写进程分配文件表项、分配用户文件描述符 4、读/写进程互斥

内核为地址设置一个读指针和一个写指针,按先进先出顺序读、写。为使读、写进程互斥地访问pipe文件,需使各进程互斥地访问pipe文件索引结点中的直接地址项。因此,每次进程在访问pipe文件前,都需检查该索引文件是否已被上锁。若是,进程便睡眠等待,否则,将其上锁,进行读/写。操作结束后解锁,并唤醒因该索引结点上锁而睡眠的进程。 三、所涉及的系统调用 1、pipe( )

建立一无名管道。 系统调用格式

pipe(filedes) 参数定义

int pipe(filedes); int filedes[2];

其中,filedes[1]是写入端,filedes[0]是读出端。 该函数使用头文件如下: #include #inlcude #include 2、read( )

系统调用格式

read(fd,buf,nbyte)

功能:从fd所指示的文件中读出nbyte个字节的数据,并将它们送至由指针buf所指示的缓冲区中。如该文件被加锁,等待,直到锁打开为止。 参数定义

int read(fd,buf,nbyte); int fd; char *buf;

unsigned nbyte; 3、write( )

29

对于msgrcv系统调用,核心须完成下述工作:

(1)对消息队列的描述符和许可权等进行检查。若合法,就往下执行;否则返回; (2)根据type的不同分成三种情况处理:

type=0,接收该队列的第一个消息,并将它返回给调用者; type为正整数,接收类型type的第一个消息;

type为负整数,接收小于等于type绝对值的最低类型的第一个消息。

(3)当所返回消息大小等于或小于用户的请求时,核心便将消息正文拷贝到用户区,并从消息队列中删除此消息,然后唤醒睡眠的发送进程。但如果消息长度比用户要求的大时,则做出错返回。 4. msgctl( )

消息队列的操纵。读取消息队列的状态信息并进行修改,如查询消息队列描述符、修改它的许可权及删除该队列等。 系统调用格式:

msgctl(msgqid,cmd,buf); 本函数使用的头文件如下: #include #include #include 参数定义:

int msgctl(msgqid,cmd,buf); int msgqid,cmd;

struct msgqid_ds *buf;

其中,函数调用成功时返回0,不成功则返回-1。buf是用户缓冲区地址,供用户存放控制参数和查询结果;cmd是规定的命令。命令可分三类:

(1)IPC_STAT。查询有关消息队列情况的命令。如查询队列中的消息数目、队列中的最大字节数、最后一个发送消息的进程标识符、发送时间等;

(2)IPC_SET。按buf指向的结构中的值,设置和改变有关消息队列属性的命令。如改变消息队列的用户标识符、消息队列的许可权等; (3)IPC_RMID。消除消息队列的标识符。 msgqid_ds 结构定义如下:

struct msgqid_ds

{ struct ipc_perm msg_perm; /*许可权结构*/ short pad1[7]; /*由系统使用*/ ushort msg_qnum; /*队列上消息数*/

ushort msg_qbytes; /*队列上最大字节数*/ ushort msg_lspid; /*最后发送消息的PID*/ ushort msg_lrpid; /*最后接收消息的PID*/ time_t msg_stime; /*最后发送消息的时间*/ time_t msg_rtime; /*最后接收消息的时间*/ time_t msg_ctime; /*最后更改时间*/ };

struct ipc_perm

{ ushort uid; /*当前用户*/ ushort gid; /*当前进程组*/

35

ushort cuid; /*创建用户*/ ushort cgid; /*创建进程组*/ ushort mode; /*存取许可权*/

{ short pid1; long pad2;} /*由系统使用*/ }

36

实验五 进程通信

(四) 共享存储区通信

【实验目的】

1、了解和熟悉共享存储机制。

2、学会用共享存储区方法进行通信。

【实验内容】

1、编制一长度为1k的共享存储区发送和接收的程序。 <程序说明>

①为了便于操作和观察结果,用一个程序作为“引子“,先后fork()两个子进程,server和client,进行通信。

②server端建立一个key为75的共享区,并将第一个字节置为-1,作为数据空的标志。等待其他进程发来的消息。当该字节的值发生变化时,表示收到了信息,进行处理。然后再次把它的值设为-1,如果遇到的值为0,则视为为结束信号,取消该队列,并退出server。server每接收到一次数据后显示“(server)received”。

③client端建立一个key为75的共享区,当共享取得第一个字节为-1时,server端空闲,可发送请求。client随即填入9到0。期间等待 server 端的再次空闲。进行完这些操作后,client退出。client每发送一次数据后显示“(client)sent”。 ④父进程在server和client均退出后结束。 <参考程序>

#include #include #include #define SHMKEY 75

int shmid,i; int *addr;

void client( ) { int i;

shmid=shmget(SHMKEY,1024,0777); /*打开共享存储区*/ addr=shmat(shmid,0,0); /*获得共享存储区首地址*/ for (i=9;i>=0;i--)

{ while (*addr!=-1);

printf(\ *addr=i; }

exit(0); }

37

void server( ) {

shmid=shmget(SHMKEY,1024,0777|IPC_CREAT); /*创建共享存储区*/ addr=shmat(shmid,0,0); /*获取首地址*/ do {

*addr=-1;

while (*addr= =-1);

printf(\}while (*addr);

shmctl(shmid,IPC_RMID,0); /*撤消共享存储区,归还资源*/ exit(0); }

main( ) {

while ((i=fork( ))= =-1); if (!i) server( );

while ((i=fork( ))= =-1); if (!i) client( ); wait(0); wait(0); }

实验要求:运行程序,并分析运行结果。

2、 编程在主进程中创建两个子进程,在子进程shmw()中创建一个系统V共享内存区,并在其中写入格式化数据;在子进程shmr()中访问同一个系统V共享内存区,读出其中的格式化数据。

【实验报告】

1、列出调试通过程序的清单,分析运行结果。

2、总结上机调试过程中所遇到的问题和解决方法及感想。

【实验相关资料】

一、共享存储区

1、共享存储区机制的概念

共享存储区(Share Memory)是UNIX系统中通信速度最高的一种通信机制。该机制可使若干进程共享主存中的某一个区域,且使该区域出现(映射)在多个进程的虚地址空间中。另一方面,一个进程的虚地址空间中又可连接多个共享存储区,每个共享存储区都有自己的名字。当进程间欲利用共享存储区进行通信时,必须先在主存中建立一共享存储区,然后将它附接到自己的虚地址空间上。此后,进程对该区的访问操作,与对其虚地址空间的其它部分的操作完全相同。进程之间便可通过对共享存储区中数据的读、写来进行直接通信。图示1列出二个进程通过共享一个共享存储区来进行通信的例子。其中,进程A将建立的共享存储区附接到自己的AA’区域,进程B将它附接到自己的BB’区域。

Linux的2.2.x内核支持多种共享内存方式,如mmap()系统调用,Posix共享内存,以

38

及系统V共享内存。linux发行版本如Redhat 8.0支持mmap()系统调用及系统V共享内存,但还没实现Posix共享内存,这里主要介绍系统V共享内存API的原理及应用。

内存空间 进程B的虚空间 进程A的虚空间

正文 正文

数据 数据

B A 共享

存储区 B’ A’ 栈 栈

图1 共享存储区

2、系统V共享内存原理

进程间需要共享的数据被放在一个叫做IPC共享内存区域的地方,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去。系统V共享内存通过shmget获得或创建一个IPC共享内存区域,并返回相应的标识符。内核在保证shmget获得或创建一个共享内存区,初始化该共享内存区相应的shmid_kernel结构同时,还将在特殊文件系统shm中,创建并打开一个同名文件,并在内存中建立起该文件的相应dentry及inode结构,新打开的文件不属于任何一个进程(任何进程都可以访问该共享内存区)。所有这一切都是系统调用shmget完成的。

注:每一个共享内存区都有一个控制结构struct shmid_kernel,shmid_kernel是共享内存区域中非常重要的一个数据结构,它是存储管理和文件系统结合起来的桥梁,定义如下: struct shmid_kernel /* private to the kernel */ {

struct kern_ipc_perm shm_perm; struct file * shm_file; int id;

unsigned long shm_nattch; /*

unsigned long shm_segsz; /*共享内存的大小(bytes)*/ time_t shm_atim; /*最后一次attach此共享内存的时间*/ time_t shm_dtim; /*最后一次detach此共享内存的时间*/ time_t shm_ctim; /*最后一次更动此共享内存结构的时间*/ pid_t shm_cpid; /*建立此共享内存的进程识别码*/

pid_t shm_lpid; /*最后一个操作此共享内存的进程识别码*/ };

该结构中最重要的一个域应该是shm_file,它存储了将被映射文件的地址。每个共享内存区对象都对应特殊文件系统shm中的一个文件,一般情况下,特殊文件系统shm中的文件是不能用read()、write()等方法访问的,当采取共享内存的方式把其中的文件映射到进程地址空间后,可直接采用访问内存的方式对其访问。

39

这里我们采用[1]中的图表给出与系统V共享内存相关数据结构:

正如消息队列和信号灯一样,内核通过数据结构struct ipc_ids shm_ids维护系统中的所有共享内存区域。上图中的shm_ids.entries变量指向一个ipc_id结构数组,而每个ipc_id结构数组中有个指向kern_ipc_perm结构的指针。到这里读者应该很熟悉了,对于系统V共享内存区来说,kern_ipc_perm的宿主是shmid_kernel结构,shmid_kernel是用来描述一个共享内存区域的,这样内核就能够控制系统中所有的共享区域。同时,在shmid_kernel结构的file类型指针shm_file指向文件系统shm中相应的文件,这样,共享内存区域就与shm文件系统中的文件对应起来。

在创建了一个共享内存区域后,还要将它映射到进程地址空间,系统调用shmat()完成此项功能。由于在调用shmget()时,已经创建了文件系统shm中的一个同名文件与共享内存区域相对应,因此,调用shmat()的过程相当于映射文件系统shm中的同名文件过程,原理与mmap()大同小异。 3、系统V共享内存API

对于系统V共享内存,主要有以下几个API:shmget()、shmat()、shmdt()及shmctl()。 #include #include shmget()用来获得共享内存区域的ID,如果不存在指定的共享区域就创建相应的区域。shmat()把共享内存区域映射到调用进程的地址空间中去,这样,进程就可以方便地对共享区域进行访问操作。shmdt()调用用来解除进程对共享内存区域的映射。shmctl实现对共享内存区域的控制操作。

注:shmget的内部实现包含了许多重要的系统V共享内存机制;shmat在把共享内存区域映射到进程空间时,并不真正改变进程的页表。当进程第一次访问内存映射区域访问时,会因为没有物理页表的分配而导致一个缺页异常,然后内核会根据相应的存储管理机制为共享内存映射区域分配相应的页表。

应当指出,共享存储区机制只为进程提供了用于实现通信的共享存储区和对共享存储区进行操作的手段,然而并未提供对该区进行互斥访问及进程同步的措施。因而当用户需要使用该机制时,必须自己设置同步和互斥措施才能保证实现正确的通信。

40

二、涉及的系统调用

1、shmget( )

创建、获得一个共享存储区。 系统调用格式:

shmid=shmget(key,size,flag) 该函数使用头文件如下: #include #include #include 参数定义

int shmget(key,size,flag); key_t key; int size,flag;

其中,key是共享存储区的名字;size是其大小(以字节计);flag是用户设置的标志,如IPC_CREAT。IPC_CREAT表示若系统中尚无指名的共享存储区,则由核心建立一个共享存储区;若系统中已有共享存储区,便忽略IPC_CREAT。 附:

操作允许权 八进制数 用户可读 00400 用户可写 00200 小组可读 00040 小组可写 00020 其它可读 00004 其它可写 00002

控制命令 值 IPC_CREAT 0001000 IPC_EXCL 0002000

例:shmid=shmget(key,size,(IPC_CREAT|0400))

创建一个关键字为key,长度为size的共享存储区 2、shmat( )

共享存储区的附接。从逻辑上将一个共享存储区附接到进程的虚拟地址空间上。 系统调用格式:

virtaddr=shmat(shmid,addr,flag) 该函数使用头文件如下: #include #include #include 参数定义

char *shmat(shmid,addr,flag); int shmid,flag; char * addr;

其中,shmid是共享存储区的标识符;addr是用户给定的,将共享存储区附接到进程的虚地址空间;flag规定共享存储区的读、写权限,以及系统是否应对用户规定的地址做舍入操作。其值为SHM_RDONLY时,表示只能读;其值为0时,表示可读、可写;其值为SHM_RND

41

(取整)时,表示操作系统在必要时舍去这个地址。该系统调用的返回值是共享存储区所附接到的进程虚地址viraddr。 3、shmdt( )

把一个共享存储区从指定进程的虚地址空间断开。 系统调用格式:

shmdt(addr) 该函数使用头文件如下: #include #include #include 参数定义

int shmdt(addr); char addr;

其中,addr是要断开连接的虚地址,亦即以前由连接的系统调用shmat( )所返回的虚地址。调用成功时,返回0值,调用不成功,返回-1。 4、shmctl( )

共享存储区的控制,对其状态信息进行读取和修改。 系统调用格式:

shmctl(shmid,cmd,buf) 该函数使用头文件如下: #include #include #include 参数定义

int shmctl(shmid,cmd,buf); int shmid,cmd;

struct shmid_ds *buf;

其中,buf是用户缓冲区地址,cmd是操作命令。命令可分为多种类型:

(1)用于查询有关共享存储区的情况。如其长度、当前连接的进程数、共享区的创建者标识符等;

(2)用于设置或改变共享存储区的属性。如共享存储区的许可权、当前连接的进程计数等; (3)对共享存储区的加锁和解锁命令; (4)删除共享存储区标识符等。

上述的查询是将shmid所指示的数据结构中的有关成员,放入所指示的缓冲区中;而设置是用由buf所指示的缓冲区内容来设置由shmid所指示的数据结构中的相应成员。 cmd有下列几种数值:

IPC_STAT 把共享内存的

IPC_SET 将参数buf所指的shmid_ds 结构中的shm_perm.uid、shm_perm.gid和shm_perm.mode复制到共享内存的shmid_ds结构内。 IPC_RMID 删除共享内存和数据结构。 SHM_LOCK 不让此共享内存置换到swap。 SHM_UNLOCK 允许此gon共享内存置换到swap。

SHM_LOCK 和SHM_UNLOCK为LUNIX特有,且唯有超级用户(root)允许使用

42

实验六 死锁避免的算法

【实验目的】

1、 了解死锁避免的原理。

2、 研究银行家算法的实现方法。

【实验内容】

编程实现银行家算法。

【实验报告】

1、 列出调试通过程序的清单,并附上文档说明。

2、 总结上机调试过程中所遇到的问题和解决方法及感想。

【实验相关资料】

一、死锁概念

多个并发进程,每个进程占有部分资源,又都等待其它进程释放所占资源,造成均不能向前推进的现象。 二、死锁的避免

死锁避免原理就是使系统始终处于安全状态。

安全状态:所谓安全状态是指能够找到一个安全序列,系统能够按照这个安全序列依次为进程分配资源,使所有进程都能执行完毕,如果找不到这样的安全序列,系统就处于不安全状态。

三、银行家算法

银行家算法要求每个进程的最大资源需求,其基本思想是:始终保持系统处于安全状态,当进程提出资源请求时,系统先进行预分配,再判断系统分配后是否仍然处于安全状态。如果仍然处于安全状态,就进行实际分配;如果处于不安全状态,则拒绝该进程的资源请求。 四、银行家算法相关数据结构 1. 最大需求矩阵:

?d11d12...d1m??d21d22...d2md (t)=???????dd...d?n1n2nm???? ???其中,行表示进程,列表示资源,如:dij=5表示第 i个进程最多需要j类资源5个。 2. 资源分配矩阵:

43

??a11a12...a1m??a(t)=?a21a22...a?2m??????? ???an1an2...anm??元素aij=8表示分配给第 i进程8个j类资源。 3. 需求矩阵:

??b11b12...b1m?? b(t)=?b?21b22...b2m??????? ???bn1bn2...bnm?? 元素bij=3表示第i类进程还需要3个j类资源。 最大需求矩阵=分配矩阵+需求矩阵,即d(t)=a(t)+b(t)。 44

实验七 存储管理

一、常用页面置换算法

【实验目的】

通过模拟实现请求页式存储管理的几种基本页面置换算法,了解虚拟存储技术的特点,掌握虚拟存储请求页式存储管理中几种基本页面置换算法的基本思想和实现过程,并比较它们的效率。

【实验内容】

1、设计一个虚拟存储区和内存工作区,并使用下述算法计算访问命中率。 1)最佳淘汰算法(OPT) 2)先进先出的算法(FIFO) 3)最近最久未使用算法(LRU) 4)最不经常使用算法(LFU) 5)最近未使用算法(NUR)

命中率=1-页面失效次数/页地址流长度

2、 用系统提供的malloc函数和free函数模拟生成一些空闲区,用FF、BF算法组织形成空闲区队列,随机生成一个作业序列模拟其分配过程。(选做)

【实验准备】

本实验的程序设计基本上按照实验内容进行。即首先用srand( )和rand( )函数定义和产生指令序列,然后将指令序列变换成相应的页地址流,并针对不同的算法计算出相应的命中率。

(1)通过随机数产生一个指令序列,共320条指令。指令的地址按下述原则生成: A:50%的指令是顺序执行的

B:25%的指令是均匀分布在前地址部分 C:25%的指令是均匀分布在后地址部分 具体的实施方法是:

A:在[0,319]的指令地址之间随机选取一起点m B:顺序执行一条指令,即执行地址为m+1的指令

C:在前地址[0,m+1]中随机选取一条指令并执行,该指令的地址为m’ D:顺序执行一条指令,其地址为m’+1

E:在后地址[m’+2,319]中随机选取一条指令并执行 F:重复步骤A-E,直到320次指令 (2)将指令序列变换为页地址流 设:页面大小为1K;

45

用户内存容量4页到32页; 用户虚存容量为32K。 在用户虚存中,按每K存放10条指令排列虚存地址,即320条指令在虚存中的存放方式为: 第 0 条-第 9 条指令为第0页(对应虚存地址为[0,9]) 第10条-第19条指令为第1页(对应虚存地址为[10,19]) ????????????

第310条-第319条指令为第31页(对应虚存地址为[310,319]) 按以上方式,用户指令可组成32页。 <参考程序>

#define TRUE 1 #define FALSE 0 #define INVALID -1 #define NULL 0

#define total_instruction 320 /*指令流长*/ #define total_vp 32 /*虚页长*/ #define clear_period 50 /*清0周期*/

typedef struct /*页面结构*/ {

int pn,pfn,counter,time; }pl_type;

pl_type pl[total_vp]; /*页面结构数组*/

struct pfc_struct{ /*页面控制结构*/ int pn,pfn;

struct pfc_struct *next; };

typedef struct pfc_struct pfc_type;

pfc_type pfc[total_vp],*freepf_head,*busypf_head,*busypf_tail;

int diseffect, a[total_instruction];

int page[total_instruction], offset[total_instruction];

int initialize(int); int FIFO(int); int LRU(int); int LFU(int); int NUR(int); int OPT(int);

int main( ) {

int s,i,j;

46

srand(10*getpid()); /*由于每次运行时进程号不同,故可用来作为初始化随机数队列的“种子”*/

s=(float)319*rand( )/32767/32767/2+1; //

for(i=0;i

if(s<0||s>319) {

printf(\ exit(0); }

a[i]=s; /*任选一指令访问点m*/ a[i+1]=a[i]+1; /*顺序执行一条指令*/

a[i+2]=(float)a[i]*rand( )/32767/32767/2; /*执行前地址指令m' */ a[i+3]=a[i+2]+1; /*顺序执行一条指令*/

s=(float)(318-a[i+2])*rand( )/32767/32767/2+a[i+2]+2; if((a[i+2]>318)||(s>319))

printf(\ }

for (i=0;i

page[i]=a[i]/10; offset[i]=a[i]; }

for(i=4;i<=32;i++) /*用户内存工作区从4个页面到32个页面*/ {

printf(\ FIFO(i); LRU(i); LFU(i); NUR(i); OPT(i); }

return 0; }

int initialize(total_pf) /*初始化相关数据结构*/ int total_pf; /*用户进程的内存页面数*/ {int i;

diseffect=0;

for(i=0;i

pl[i].pn=i;

pl[i].pfn=INVALID; /*置页面控制结构中的页号,页面为空*/ pl[i].counter=0;

47

pl[i].time=-1; /*页面控制结构中的访问次数为0,时间为-1*/ }

for(i=0;i

pfc[i].next=&pfc[i+1]; pfc[i].pfn=i;

} /*建立pfc[i-1]和pfc[i]之间的链接*/ pfc[total_pf-1].next=NULL;

pfc[total_pf-1].pfn=total_pf-1;

freepf_head=&pfc[0]; /*空页面队列的头指针为pfc[0]*/

return 0; }

int FIFO(total_pf) /*先进先出算法*/

int total_pf; /*用户进程的内存页面数*/ {

int i,j; pfc_type *p;

initialize(total_pf); /*初始化相关页面控制用数据结构*/ busypf_head=busypf_tail=NULL; /*忙页面队列头,队列尾链接*/ for(i=0;i

if(pl[page[i]].pfn==INVALID) /*页面失效*/ {

diseffect+=1; /*失效次数*/ if(freepf_head==NULL) /*无空闲页面*/ {

p=busypf_head->next;

pl[busypf_head->pn].pfn=INVALID;

freepf_head=busypf_head; /*释放忙页面队列的第一个页面*/ freepf_head->next=NULL; busypf_head=p; }

p=freepf_head->next; /*按FIFO方式调新页面入内存页面*/ freepf_head->next=NULL; freepf_head->pn=page[i];

pl[page[i]].pfn=freepf_head->pfn;

if(busypf_tail==NULL)

busypf_head=busypf_tail=freepf_head; else {

busypf_tail->next=freepf_head; /*free页面减少一个*/ busypf_tail=freepf_head; }

48

freepf_head=p; } }

printf(\

return 0; }

int LRU (total_pf) /*最近最久未使用算法*/ int total_pf; {

int min,minj,i,j,present_time; initialize(total_pf); present_time=0;

for(i=0;i

if(pl[page[i]].pfn==INVALID) /*页面失效*/ {

diseffect++;

if(freepf_head==NULL) /*无空闲页面*/ {

min=32767;

for(j=0;jpl[j].time&&pl[j].pfn!=INVALID) {

min=pl[j].time; minj=j; }

freepf_head=&pfc[pl[minj].pfn]; //腾出一个单元 pl[minj].pfn=INVALID; pl[minj].time=-1;

freepf_head->next=NULL; }

pl[page[i]].pfn=freepf_head->pfn; //有空闲页面,改为有效 pl[page[i]].time=present_time;

freepf_head=freepf_head->next; //减少一个free 页面 } else

pl[page[i]].time=present_time; //命中则增加该单元的访问次数

present_time++; }

printf(\return 0; }

49

本文来源:https://www.bwwdw.com/article/jde8.html

Top