实 验 报 告
实验课程: 操作系统原理 学生姓名: 高君宇 学 号: 2110505112 专业班级: 计算机15班
2013年12月29日
实验一:用户接口实验
一、
实验内容
1) 控制台命令接口实验
该实验是通过“几种操作系统的控制台命令”、“终端处理程序”、“命令解释程序” 和
“Linux 操作系统的 bash”来让实验者理解面向操作命令的接口 shell 和进行简单的 shell 编程。
A 查看 bash 版本。
A 编写 bash 脚本,统计/my 目录下 c 语言文件的个数 2) 系统调用实验
该实验是通过实验者对“Linux 操作系统的系统调用机制”的进一步了解来理解操作 系统调用的运行机制;同时通过“自己创建一个系统调用 mycall()”和“编程调用自己 创建的系统调用”进一步掌握创建和调用系统调用的方法。 A 编程调用一个系统调用 fork(),观察结果。
A 编程调用创建的系统调用 foo(),观察结果。
A 自己创建一个系统调用 mycall(),实现功能:显示字符串到屏幕上。
A 编程调用自己创建的系统调用。
二、实验准备
为了使用户通过操作系统完成各项管理任务,操作系统必须为用户提供各种接口来实现 人机交互。经典的操作系统理论将操作系统的接口分为控制台命令和系统调用两种。前者主 要提供给计算机的操作人员对计算机进行各种控制;而后者则提供个程序员,使他们可以方 便地使用计算机的各种资源。
三、源程序
1、控制台命令接口实验
(1)查看 bash 版本
在 shell 提示符下输入:
$echo $BASH_VERSION
我们的版本是 4.2.37(1)-release
(2)建立 bash 脚本,输出 Hello word
在编辑器中输入以下内容
#!/bin/bash echo Hello World!
执行脚本 使用指令:
$./text
(3)编写 bash 脚本:统计/my 目录下 c 语言文件的个数
通过 bash 脚本,可以有多种方式实现这个功能,而使用函数是其中个一个选择。
在使用函数之前,必须先定义函数。 进入自己的工作目录,编写名为 count 的文件 脚本程序:
#! /bin/bash function count {
echo –n \ls $1|wc –l }
将 count 文件复制到当前目录下,然后在当前目录下建立文件夹,在 my 目录下建立几
个 c 文件,以便用来进行测试 2、系统调用实验
#接收程序的第一个参数
#对子程序的第一个参数所在的目录进行操作
(1) 系统调用是操作系统为程序员提供的接口服务。使用系统调用,程序员可以更充分的利 用计算机资源,使编写的程序更加灵活,功能更加强大。程序员在对系统充分了解的情况下 甚至可以订做系统调用,实现那些非专业程序员所难以实现的功能。同时通过“自己创建一 个系统调用 mycall()”和“编程调用自己创建的系统调用”进一步掌握创建和调用系统调 用的方法。 a. 添加源代码
在/usr/src/linux/kernel/sys.c 文件中添加源代码,如下所示:
asmlinkage int sys_foo(int x)
{ printf(“%d\\n”,x); }
b.连接新的系统调用
进入目录/usr/src/linux/include/asm-i386/,打开文件 unistd.h。这个文
件包含
了系统调用的清单,用来给每个系统调用分配一个唯一的号码。 进
入/usr/src/linux/arche/ i386/kernel/,打开文件 entry.S。
重新编译内核
配置内核(建议使用默认配置,仅仅修改 local versions 即,可当然也可以把一些无用的
模块去掉)
make menuconfig
编译内核(-jN 中的 N 表示使用多少个线程编译,一般为处理器数目+1)
make -j5 bzImage
编译内核模块
make -j5 modules
安装内核模块(需要 root 权限)
提示:模块被安装在/lib/modules/versions/下(比如 2.6.31 内核在 config 时设定其的
local versions 为-myservice,那么 versions 为 2.6.31-myservice)
sudo make modules_install
安装内核
提示:在较新的 Linux 发行版中会自动生成 init 内存文件系统,并将配置文件、内核及 init
内存文件系统拷贝到/boot 目录下,最后更新引导。
重启系统
完成以上配置后,重新启动系统进入自己的新系统
(2 )编程调用一个系统调用 fork(),观察结果。
# include
int main()
{
int iUid;
iUid=fork();
if(iUid==0)
for(;;) { printf(\
sleep(1);
}
if(iUid>0)
for(;;) {
printf(\
sleep(1);
}
if(iUid<0) printf(\
return 0;
}
编程调用创建的系统调用 foo(),观察结果。
#include
#include
_syscall1(char*,foo,int,ret)
main()
{
int I,J; I=100; J=0; J=foo(I);
printf(\
printf(\
}
编程调用自己创建的系统调用。
#include
{
char *str;
char string[50];
str=string;
str=\
mycall(str);
return 0;
附:内核编译
}
编译内核的基本步骤
(1) 将vm和win共享的shared folders文件夹中的
linux-3.0.5.tar.bz2拷贝到/usr/src目录下更改用户权限 sudo –s a.进入shared folders : cd /mnt/hfgs/shared folders/ b. 拷贝: cp linux-3.0.5.tar.bz2 /usr/src/
(2) 解压linux源码:进入目录/usr/srctar xvzf linux-3.0.5.tar.bz2 (3) 进入目录linux-3.0.5,输入命令 make menuconfig a.fedora和Centos 通过yum install ncurses-devel解决
b.Ubuttu和Debian 通过 sudo apt-get install libncurses5-dev
(4)在图形界面中设置config,一般进入General Setup中修改一下内核名称即可
(5)编译内核:make –j4 bzImage (开4个线程编译,如果是单核的cpu就不用加-j4参数了),编译结束以后,出现Kernel: arch/x86/boot/bzImage is ready (#1)则说明内核编译成功 (6)编译模块:make –j4 modules(时间较长) (7)模块安装 :make modules_install (8)安装内核: make install
在高版本中,make install命令会自动生成启动文件,在低内核版本中,需要添加。所以,我们还需要看一下有没有生成启动文件,如果没有,则需要自己手动添加。
(9)查看内核是否添加: cat /boot/proc/proc.conf
2、 创建系统调用的基本步骤
(1) 在linux-3.2.11/kernel下创建mysyscall.c文件,内容如下: #include
asmlinkage long sys_mysyscall(void){
printk(KERN_ALERT \return 0; }
(2) 在linux-3.2.11/kernel/Makefile中加入: obj-y += mysyscall.o
(3) 在linux-3.2.11/include/linux/syscalls.h中加入: asmlinkage long sys_mysyscall(void);
(4) 在linux-3.2.11/arch/x86/kernel/syscall_table_32.S(如果是64位机器则32替换为64)中加入: .long sys_mysyscall
在linux-3.2.11/arch/x86/ia32/ia32entry.S中加入: .quad sys_mysyscall
(5) 在linux-3.2.11/arch/x86/include/asm/unistd_32.h中加入: #define __NR_mysyscall 349 并将#define NR_syscalls 349 替换为#define NR_syscalls 350
(这里根据实际情况,__NR_mysyscall为现有最大值,NR_syscalls加一即可)
(6) 重新编译、安装、重启 (7) 测试
查看/proc/kallsyms中是否有mysyscall,如果有,表示符号已经导出。 编译运行,输出0即为正确,-1为错误。
若运行正确,用dmesg查看,末尾有输出:This is my sys call! 编译内核成功截图:
2、编译模块成功截图:
实验中出现的问题:
在进行内核编译时,首先遇到的困难就是将缺少的程序补入,但因为本省 Ubantu 所带 的编辑器很不好用,在输入过程中就花费了非常大的时间。但最后经过学长的帮助顺利完成。 在编译期间有经历了一个多小时的时间,最后编译成功。在编写调用程序并进行验证的过程 中,只能通过自己在网上查找资料,并借鉴同学的作法,最终完成。
四、实验体会
通过本次实验,我们理解了面向操作命令的接口 Shell,学会了简单的 shell 编码,理解 了操作系统调用的运行机制,掌握了创建系统调用的方法。本次实验通过内核编译,将一组 源代码变成操作系统的内核,并由此重新引导系统,这让我们初步了解了操作系统的生成过 程。
实验二:进程管理实验
一、实验内容
1) 编制实现软中断通信的程序
使用系统调用 fork()创建两个子进程,再用系统调用 signal()让父进程捕捉键盘上发出 的中断信号(即按 delete 键),当父进程接收到这两个软中断的某一个后,父进程用系统调 用 kill()向两个子进程分别发出整数值为 16 和 17 软中断信号,子进程获得对应软中断信号, 然后分别输出下列信息后终止:
Child process 1 is killed by parent !!
Child process 2 is killed by parent !!
父进程调用 wait()函数等待两个子进程终止后,输入以下信息,结束进程执行: Parent process is killed!! 多运行几次编写的程序,简略分析出现不同结果的原因。
2) 编制实现进程的管道通信的程序
使用系统调用 pipe()建立一条管道线,两个子进程分别向管道写一句话:
Child process 1 is sending a message! Child process 2 is sending a message!
而父进程则从管道中读出来自于两个子进程的信息,显示在屏幕上。
要求:父进程先接收子进程 P1 发来的消息,然后再接收子进程 P2 发来的消息。
二、实验分析
1、进程软中断实现:
算法流程图:
相关函数:
wait()函数
wait()函数常用来控制父进程与子进程的同步。在父进程中调用 wait()函数,则父进 程被阻塞,进入等待队列,等待子进程结束。当子进程结束时,产生一个终止状态字, 系统会向父进程发出 SIGCHLD 信号。当接到信号后,父进程提取子进程的终止状态字,
从 wait()函数返回继续执行原来的程序。
其调用格式为:
#include
正确返回:大于 0,子进程的进程 ID 值。
等于 0,其他。
错误返回:等于-1,调用失败。
exit()函数
exit()函数是进程结束时最常调用的函数,在 main()函数中调用 return,最终也是调用 exit() 函数。这些都是进程的正常终止。在正常终止时,exit()函数返回进程结束状态。
其调用格式为:
#include
kill()函数
kill()函数用于删除执行中的程序或者任务。
其调用格式为:
kill(int PID,int IID);
其中,PID 是要被杀死的进程号,IID 为向将被杀死的进程发送中的中断号。
Signal()函数
signal()函数是允许调用进程控制软中断信号的处理。
其调用格式为:
#include
signal(sig,function);
2.管道实现:
管道,是指用于连接一个读进程和一个写进程,以实现它们之间信息的共享文件又称 pipe 文件。向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据 送入管道;而接收管道输送的接收进程(读进程),可以从管道中接收数据。
为了协调双方的通信,管道通信机制必须提供以下 3 方面的协调能力:
A 互斥。当一个进程正在对 pipe 进程读/写操作时,另一进程必须等待。程序中使用
lock(fd[1],1,0)函数实现对管道的加锁操作。用 lock(fd[1],0,0)解除管道的锁定
A 同步。当写进程把一定数量的数据写入 pipe 后,便去睡眠等待,直到读进程取走数 据
后,再把它唤醒。当读进程试图从一空管道中读取数据时,也应睡眠等待,直至 写进程将数据写入管道后,才将其唤醒。
A 判断对方是否存在。只有确定写进程和读进程都存在的情况下,才能通过管道进行 通
信。
在该程序中,使用父进程创建两个子进程,其中一个向管道中写数据,另一个从管道 中读数据,父进程控制读写的互斥和同步。利用管道锁机制控制两次写或两次读的互斥。 父进程创建子进程 1,使之向管道中写数据,写好后,结束子进程 1,并向父进程发中断, 父进程收到中断信号后,再创建子进程 2,子进程 2 从管道中读数据,读完后结束,并向父 进程发中断。
三、源程序
1、进程软中断:
#include
#include
int pid1, pid2;
// 定义两个进程号变量
// 或者 signal(14,stop);
// 若创建子进程 1 不成功,则空循环
signal(3,stop);
while((pid1 = fork( )) == -1);
// 子进程创建成功,pid1 为进程号 if(pid1 > 0) {
创建子进程 2 while((pid2 = fork( )) == -1); //
if(pid2 > 0) { wait_flag = 1;
sleep(5); kill(pid1,16); kill(pid2,17); wait(0); wait(0);
// 父进程等待 5 秒 // 杀死进程 1
// 杀死进程 2
// 等待第 1 个子进程 1 结束的信号
// 等待第 2 个子进程 2 结束的信号
printf(\exit(0); } else {
wait_flag = 1;
// 父进程结束
signal(17,stop); // 等待进程 2 被杀死的中断号
17 printf(\
exit(0); } } else {
wait_flag = 1; signal(16,stop);
// 等待进程 1 被杀死的中断号
16 printf(\
exit(0); } }
void stop( ) { wait_flag = 0; }
实验结果:
实验结果简要分析:
上述结果中“Child process 1 is killed by parent !!” 和“Child process 2 is killed by
parent !!”相继出现,当运行几次后,谁在前谁在后是随机的。这是因为:从进程调度的角 度看,子进程被创建后处于就绪态。此时,父进程和子进程作为两个独立的进程,共享同一 个代码段,分别参加调度、执行,直至进程结束。但是谁会先被调度程序选中执行,则与系 统的调度策略和系统当前的资源状态有关,是不确定的。因此,谁先从 fork()函数中返回继 续执行后面的语句也是不确定的。 2、管道实现
#include
int fd[2];
char OutPipe[100],InPipe[100]; pipe(fd);
while((pid1 = fork()) == -1); if(pid1 == 0) { lockf(fd[1],1,0);
sprintf(OutPipe,\
write(fd[1],OutPipe,50); sleep(5);
exit(0); } else {
wait(0);
while((pid2 = fork()) == -1); if(pid2 == 0) {
read(fd[0],InPipe,50); printf(\
} else { wait(0); exit(0);
printf(\lockf(fd[0],0,0); exit(0);
} } }
运行结果:
四、实验体会
通过本次实验,我们理解了进程的实质和进程管理的机制。进程是操作系统中最重要的 概念,是现代操作系统的关键。实验中我们在 Linux 系统下实现进程从创建到终止的全过程, 体会了进程的创建过程、父进程和子进程的关系、进程状态的变化、进程之间的同步机制、 进程调度的原理和以信号和管道为代表的进程间通信方式的实现。
实验三:存储器管理实验
一、实验内容 对比以下几种算法的命
中率:
1) 先进先出算法 FIFO(First In First Out)
2) 最近最少使用算法 LRU(Least Recently Used)
3) 最近未使用算法 NUR(Never Used Recently)
4) 最佳置换算法 OPT(Optimal Replacement)
二、实验分析
1.置换算法原理
* FIFO 原理简述
(1) 在分配内存页面数(AP)小天进程页面数(PP)时,当然是最先运行的 AP 个
页面放入内存;
(2) 这时又需要处理新的页面,则将原来放的内存中的 AP 个页中最先进入的调出
(FIFO),再将新页面放入;
(3) 以后如果再有新页面需要调入,则都按上述规则进行。
(4) 算法特点:所使用的内存页面构成一个队列。
* LRU 算法原理简述
(1) 当内存分配页面数(AP)小于进程页面数(PP)时,把最先执行的 AP 个页面
放入内存。
(2) 当需调页面进入内存,而当前分配的内存页面全部不空闲时,选择将其中最长
时间没有用到的那一页调出,以空出内存来放置新调入的页面(LRU) 。
算法特点:每个页面都有属性来表示有多长时间未被 CPU 使用的信息。
* NUR 算法原理简述
所谓“最近未使用”,首先是要对“近”做一个界定,比如 CLEAR_PERIOD=50,便是 指在 CPU 最近的 50 次进程页面处理工作中,都没有处理到的页面。那么可能会有以下几种 情况:
(1) 如果这样的页面只有一个,就将其换出,放入需要处理的新页面。
(2) 如果有这样的页面不止一个,就在这些页面中任取一个换出(可以是下标最小
的或者最小的),放入需要处理的页面。
如果没有一个这样的页面,就随意换出一个页面(可以是下标最小的或者最大的) 。
* OPT 原理简述
所谓的最佳算法是一种理想状况下的算法,它要求先遍历所有的 CPU 待处理的进程页面序 列。在这些页面中,如果有些已经在内存中,而 CPU 不再处理的,就将其换出;而有些页 面已在内存中而 CPU 即将处理,就从当前位置算起,取最后才会处理到的页面,将其换出。
2.OPT 算法实现:
为每个进程页面设一个“间隔”属性 cDistance 表示 CPU 将在第几步处理到该页面,如 果页面不再被 CPU 处理,可以被设为某个很大的值(如 32767),这样每次被换出的就是 cDistance 最大的那个页面。
(1) 初始化。设置两个数组 page[ap]和 pagecontrol[pp]分别表示进程页面数和内存分
配的页面数,并产生一个随机数序列 main[total_instruction(] 这个序列由 page[ap]
的下标随机构成)表示待处理的进程页面顺序,diseffect 置 0。然后扫描整个页
面访问序列,对 cDistance[TOTAL_VP]数组进行赋值,表示该页面将在第几步
被处理。
(2) 看序列 main[]中是否有下一个元素,如果有,就由 main[]中获取一个 CPU 待处 理
的页面号,并转(3),如果没有则转(6) 。
(3) 如果该页面已经在内存中了,就转(2),否则转(4),同时 diseffect 加 1。
(4) 看是否有空闲页面,如果有,就返回页面指针,并转到(5),否则,遍历所有 未
处理的进程页面序列,如果有位于内存中的页面而以后 CPU 不再处理的,首 将其换出,返回页面指针;如果没有这样的页面,找出 CPU 将最晚处理的页面, 将其换出,并返回该页面指针。
(5) 在内存页面和待处理的进程页面之间建立联系,转(2) 。
(6) 输出计算 1-diseffect / total_instruction*100%的结果,完成。
三、源程序
#include
const int TOTAL_INSTRUCTION(320); const int TOTAL_VP(32); const int CLEAR_PERIOD(50); #include \
#include \#include \int main()
{
int i; CMemory a; for(i=4;i<=32;i++)
{
a.FIFO(i);
a.LRU(i); a.NUR(i); a.OPT(i); cout<<\
}
} return 0;
#ifndef _MEMORY_H #define _MEMORY_H
class CMemory {
public:
CMemory();
void initialize(const int nTotal_pf); void FIFO(const int nTotal_pf); void LRU(const int nTotal_pf); void NUR(const int nTotal_pf); void OPT(const int nTotal_pf);
private:
vector
vector
CPageControl *_pFreepf_head,*_pBusypf_head,*_pBusypf_tail; vector
};
CMemory::CMemory():_vDiscPages(TOTAL_VP),
_vMemoryPages(TOTAL_VP), _vMain(TOTAL_INSTRUCTION), _vPage(TOTAL_INSTRUCTION), _vOffset(TOTAL_INSTRUCTION)
{
int S,i,nRand; srand(getpid()*10); nRand=rand()2767;
S=(float)319*nRand/32767+1;
for(i=0;i _vMain[i]=S; _vMain[i+1]=_vMain[i]+1; nRand=rand()2767; _vMain[i+2]=(float)_vMain[i]*nRand/32767; _vMain[i+3]=_vMain[i+2]+1; nRand=rand()2767; S=(float)nRand *(318-_vMain[i+2])/32767+_vMain[i+2]+2; } for(i=0;i _vPage[i]=_vMain[i]/10; _vOffset[i]=_vMain[i]; _vPage[i]%=32; } } } _vMemoryPages[nTotal_pf-1].m_pNext=NULL; _vMemoryPages[nTotal_pf-1].m_nPageFaceNumber=nTotal_pf-1; } for(ix=1;ix _vMemoryPages[ix-1].m_pNext=&_vMemoryPages[ix]; _vMemoryPages[ix-1].m_nPageFaceNumber=ix-1; _nDiseffect=0; for(ix=0;ix<_vDiscPages.size();ix++) { _vDiscPages[ix].m_nPageNumber=ix; _vDiscPages[ix].m_nPageFaceNumber=INVALID; _vDiscPages[ix].m_nCounter=0; _vDiscPages[ix].m_nTime=-1; void CMemory::initialize(const int nTotal_pf) { int ix; _pFreepf_head=&_vMemoryPages[0]; } void CMemory::FIFO(const int nTotal_pf) { int i; CPageControl *p; initialize(nTotal_pf); _pBusypf_head=_pBusypf_tail=NULL; for(i=0;i if(_vDiscPages[_vPage[i]].m_nPageFaceNumber==INVALID) { _nDiseffect+=1; if(_pFreepf_head==NULL) //no empty pages { p=_pBusypf_head->m_pNext; _vDiscPages[_pBusypf_head->m_nPageNumber].m_nPageFaceNumber=INVALID; _pFreepf_head=_pBusypf_head; _pFreepf_head->m_pNext=NULL; _pBusypf_head=p; } p=_pFreepf_head->m_pNext; _pFreepf_head->m_pNext=NULL; _pFreepf_head->m_nPageNumber=_vPage[i]; _vDiscPages[_vPage[i]].m_nPageFaceNumber=_pFreepf_head->m_nPageFaceNumber; if(_pBusypf_tail==NULL) _pBusypf_head=_pBusypf_tail=_pFreepf_head; else { _pBusypf_tail->m_pNext=_pFreepf_head; _pBusypf_tail=_pFreepf_head; } _pFreepf_head=p; } } cout<<\ } void CMemory::LRU(const int nTotal_pf) { int i,j,nMin,minj,nPresentTime(0); initialize(nTotal_pf); for(i=0;i { if(_vDiscPages[_vPage[i]].m_nPageFaceNumber==INVALID) { _nDiseffect++; if(_pFreepf_head==NULL) { nMin=32767; for(j=0;j if(nMin>_vDiscPages[j].m_nTime&&_vDiscPages[j].m_nPageFaceNumber!=INVALID) { nMin=_vDiscPages[j].m_nTime; minj=j; } _pFreepf_head=&_vMemoryPages[_vDiscPages[minj].m_nPageFaceNumber]; _vDiscPages[minj].m_nPageFaceNumber=INVALID; _vDiscPages[minj].m_nTime=-1; _pFreepf_head->m_pNext=NULL; } _vDiscPages[_vPage[i]].m_nPageFaceNumber=_pFreepf_head->m_nPageFaceNumber; _vDiscPages[_vPage[i]].m_nTime=nPresentTime; _pFreepf_head=_pFreepf_head->m_pNext; } else _vDiscPages[_vPage[i]].m_nTime=nPresentTime; nPresentTime++; } cout<<\ } void CMemory::NUR(const int nTotal_pf) { int i,j,nDiscPage,nOld_DiscPage; bool bCont_flag; initialize(nTotal_pf); nDiscPage=0; for(i=0;i if(_vDiscPages[_vPage[i]].m_nPageFaceNumber==INVALID) { _nDiseffect++; if(_pFreepf_head==NULL) { bCont_flag=true; nOld_DiscPage=nDiscPage; while(bCont_flag) { } } _pFreepf_head=&_vMemoryPages[_vDiscPages[nDiscPage].m_nPageFaceNumber]; _vDiscPages[nDiscPage].m_nPageFaceNumber=INVALID; _pFreepf_head->m_pNext=NULL; } bCont_flag=false; else { nDiscPage++; if(nDiscPage==TOTAL_VP) nDiscPage=0; if(nDiscPage==nOld_DiscPage) for(j=0;j if(_vDiscPages[nDiscPage].m_nCounter==0&&_vDiscPages[nDiscPage].m_nPageFaceNumber! =INVALID) } else _vDiscPages[_vPage[i]].m_nCounter=1; if(i%CLEAR_PERIOD==0) for(j=0;j _vDiscPages[_vPage[i]].m_nPageFaceNumber=_pFreepf_head->m_nPageFaceNumber; _pFreepf_head=_pFreepf_head->m_pNext; } cout<<\ } void CMemory::OPT(const int nTotal_pf) { int i,j,max,maxpage,nDistance,vDistance[TOTAL_VP]; { _nDiseffect++; if(_pFreepf_head==NULL) { for(j=0;j if(_vDiscPages[j].m_nPageFaceNumber!=INVALID) vDistance[j]=32767; else vDistance[j]=0; nDistance=1; initialize(nTotal_pf); for(i=0;i if(_vDiscPages[_vPage[i]].m_nPageFaceNumber==INVALID) for(j=i+1;j } if((_vDiscPages[_vPage[j]].m_nPageFaceNumber!=INVALID)&&(vDistance[_vPage[j]]==32767 )) vDistance[_vPage[j]]=nDistance; nDistance++; max=-1; for(j=0;j max=vDistance[j]; maxpage=j; } _pFreepf_head=&_vMemoryPages[_vDiscPages[maxpage].m_nPageFaceNumber]; _pFreepf_head->m_pNext=NULL; _vDiscPages[maxpage].m_nPageFaceNumber=INVALID; } _vDiscPages[_vPage[i]].m_nPageFaceNumber=_pFreepf_head->m_nPageFaceNumber; _pFreepf_head=_pFreepf_head->m_pNext; } } cout<<\ } 实验结果: 四、实验体会 通过本次实验,我们理解了内存页面调度的机制,掌握了几种理论页面置换算法的实现 方法,了解了 HASH 数据结构的使用。页面置换算法是虚拟存储管理实现的关键,FIFO、 LRU、NRU 和 OPT 是几种经典页面置换算法,我们通过实现这几种算法,比较了较各种页 面置换算法的效率及优缺点,从而了解了虚拟存储实现的过程。 实验四:文件系统实验 一、实验内容 2) 设计并实现一个一级(单用户)文件系统程序 a.提供以下操作: A 文件创建/删除接口命令 create/delete 目录创建/删除接口命令 mkdir/rmdir A A 显示目录内容命令 ls b. 创建的文件不要求格式和内容 3) 设计并实现一个二级文件系统程序 a.提供用户登录; b.文件、目录要有权限 二、实验分析 1.基本思路: 用一个文件(disk.txt)模拟一个物理硬盘, 通过对该文件的一系列操作,模拟 UNIX 文件系统中的文件操作。 2.理磁盘块的设计: 卷盘块数等于100块,每个磁盘块512字节,磁盘块之间用/n 隔开,总共是514字节。0# 表示超块,1#--10#放索引结点,每个索引结点占64字节,共80个索引结点。初始化是存在 根目录 root,占用了0#结点和11#盘块。 3.空闲磁盘块: 采用成组链接法管理。每组10块,12#--99#分为9组,每组的最后一个磁盘块里存放下 一组的磁盘号信息。最后一组只有8块,加上0作为结束标志。在超块中用一个一维数组(10 个元素)作为空闲磁盘块栈。放入第一组盘块。 4.空闲 I 结点 采用混合索引式文件结构。索引结点结构中文件物理地址为六项:四个直接块号,一 个一次间址,一个两次间址,其中一次间址和两次间址中一个磁盘块中存放16个磁盘号 。 在超块中也用一维数组(80个元素)作为空闲 I 结点栈,与空闲磁盘块管理不同的是这里不 用采用成组链接法,这一维数组中存放所有 I 结点编号,而且一直保持同一大小秩序。根目 录占0#索引结点,由于根目录不会删改,是一直占0#索引结点,所以我并未按实验指导所说, 把它写在超块里,不过写进去也无所谓的。 5.超级块,I 结点和目录结构的设计 struct SUPERBLOCK//超级块 { int fistack[80];//空闲结点号栈 int fiptr;//空闲结点栈指针(还有多少个) int fbstack[10];//空闲盘块号栈 int fbptr;//空闲结点栈指针 int inum;//空闲 i 结点总数 int bnum;//空闲盘块总数 }; struct INODE//i 结点(64B) 已保证了每两个数据之间有空格隔开 { int fsize;//文件大小 int fbnum;//文件盘块数 int addr[4];//四个直接盘块号(0 ~ 512*4==2048) int addr1;//一个一次间址() int addr2;//一个两次间址() char owner[6];//文件拥有者 char group[6];//文件所属组 char mode[11];// 文件类别及存储权限 char ctime[9];//最近修改时间 }; struct DIR//目录项(36B) { char fname[14];//文件名(当前目录) int index;//i 结点号 char parfname[14];//父目录名 int parindex;//父目录 i 结点号 }; 6.用户,密码和组 用户组信息是存放在另一个文件(user.txt)中的。 各个主要功能函数的说明 1.main.cpp: 是程序的主函数模块 主函数首先会根据控制文件 control.txt 的头一个位(初始化确定位)来确定是否进 行初始化。如果初始化,那么将只有一个根目录。因为一旦初始化后,初始化确定位就会置 为0,所以系统只会在第一次进入时初始化,以后都是在前面建立的文件结构上进行操作的。 如果一定要系统初始化时,可把 control.txt 的头一个位置为1,或者使用命令“reset”。 进入系统时,当前路径是根目录。然后是登陆请求。登陆后,超块读入内存(由一个 结构对象 superblock 模拟),进入命令解析层,对用户的不同命令执行不同的操作。退出系 统前,把内存的超块再写回 disk.txt。 2.head.h: 是程序的所有全局变量、结构、函数声明的模块。也包括了所有要用到的系统库。 3.blockinodesuperblock.h: 是定义有关超块、结点、盘块、目录项的底层操作函数的模块。 1.对结点的操作: int ialloc(void); 申请一个 i 结点 返回结点号 否则返回-1。 返回的是空闲结点号栈中最小的结点号,结点用完时返回-1,申请失败。 void ifree(int index); 指定一个结点号,回收一个 i 结点。 先清空结 点,然后插入栈中合适位置(必须保持结点号的有序性) 。void readinode(int index,INODE &inode); 读指定的 i 结点( n#结点,读指针应定位到514+64*n (64B)+2*(n/8) )到 INOE inode 寄存 便于对同一结点的大量操作 void writeinode(INODE inode,int index); 把 INODE inode 写回指定的 i 结点 2.对盘块的操作;用的是成组链接法。(n#盘块 读指针应定位到514*n) int balloc(void); 申请一个盘块 返回盘块号 否则返回-1 void bfree(int index); 指定一个盘块号,回收一个盘块 3.对超块的操作; void readsuper(void); 读超块到内存 SUPERBLOCK superblock; void writesuper(void); 内存 SUPERBLOCK superblock;写回超块 4.对目录项的操作( n#目录项读指针应定位514*index+36*n ) void readdir(INODE inode,int index,DIR &dir); 读指定目录项进临时对象 DIR dir; void writedir(INODE inode,DIR dir,int index); 目录项对象 DIR dir 写到指定目录项 4.initial.h: 是定义初始化函数的模块 其中包括了对用户组的初始化:定义三个用户 adm,cnj 和 jtq,其中 adm,cnj 的组为 adm, jtq 的组为 guest。用户的初始密码都为123。对超块的(0#盘块)初始化 、对根目录文件 结点(0#结点)的初始化 、对数据盘块的初始化。 5.userop.h: 定义了两个功能函数: 1.登陆 bool login(void); 要求输入用户信息,并判断是否合法。 2.改变用户密码 void changepassword(void); 改变当前用户的密码。 6. file.h: 定义了有关文件操作的四个函数 1.创建文件 void mk(char *dirname,char *content); 当前目录下创建一个数据文件 (规定目录文件只占1 ~ 4个盘块)。虽然不要求这个 函数,但我觉得很有必要。而且,因为我使用了两个参数,前一个表示文件名,后 一个表示文件内容,可以在文件拷贝里使用这个函数。 2.删除文件 void rm(char *filename); 当前目录下删除指定数据文件 3.文件拷贝 void cp(char*string); 给定一个路径,把那个文件拷贝到当前目录下。首先要使用 dir.h 里面根据路径找 到目标的函数(find(char*string))找到对应文件,如果是数据文件的话,记录文件 内容到一缓冲区,然后在当前目录下调用创建目录的函数,就完成了。 4.显示文件内容 void cat(char *filename); 显示当前目录下指定数据文件的内容。 7. dir.h: 定义两个底层函数 1.bool havesame(char *dirname,INODE inode,int &i,int &index2) 判断对象 inode 指向的目录文件盘块中有无该名(dirname)的目录项存在着,有 返回1无返回0。同时,有该目录项的话,则按引用调用的 i 为待删子目录目录项下标 index2为目录项中的待删子目录的结点号 2.bool find(char *string) 根据路径找到指定文件或目录<路径至少有一个‘/’以 root 开头,不以‘/’结 尾>。需要注意的是,使用此函数时当前路径跟着改掉了,所以使用前必须保存当前路 径。万一找不到目标的话,可以还原为当前路径。 定义了有关目录操作的四个功能函数, 1.void mkdir(char *dirname) 当前目录下创建目录(规定目录文件只占一个盘块。为了降低难度,已设定目录文 件只占一个盘块。 2.void rmdir(char *dirname,int index) 当前目录下删除目录。将要删除的目录可能非空。有两种策略:一、规定只能删 除空目录。二、递归地将非空目录的所有子目录删除,让后再删除自己。第一种实现 较简单,我使用了第二种策略。所以参数为 (子目录名,当前结点)。如果使用第一种 策略的话,参数为只要子目录名就可以了。 3.void ls(void) 显示当前结点的所有子目录 4.void cd(char *string) 改变当前目录。有四中处理过程: string 为“.”切换到当前目录;为“..” 切换到父目录 ;为“/”切换到根目录;如果为一路径的话,就调用 bool find(char *string)切换到指定目录。 8. chsome.h: 定义一个底层函数: bool havewpower(INODE inode) 判断当前用户对指定的结点有无写权限 定义四个功能函数: 1.void chmod(char *name) 改变当前目录下指定文件的文件权限 2.void chown(char *name) 改变当前目录下指定文件的文件拥有者(如拥有者在另一个组,那么组也要改掉) 3.void chgrp(char *name) 改变当前目录下指定文件的文件所属组 4.void chnam(char *name) 改变当前目录下指定文件的文件名 9.command.h: 命令解析层函数模块。接受用户命令,执行不同的操作。 实验结果如下: single文件系统管理: double文件管理 三、源程序 1. main.cpp #include \ #include \#include \#include \#include \#include \ #include \#include \ ///////////////////////////////////////////////////////////////////////////////////////// void main() { control.open(\ int i; control>>i; control.close(); if(i!=0)//不为0就初始化 { initial(); } control.open(\ control.seekp(0); control<<0;//默认是上次基础上继续下去不用再初始化 control.close(); strcpy(curname,\当前目录文件名为 root road[0]=0;//当前目录路径(存放从根目录到这里的结点号) num=1;//最后位 road[num-1]为当前目录文件 i 结点号 cout<<\请登陆系统\\n\while( !login() )//登陆为止 cout<<\login success\\ readsuper(); getcommand();//命令解析函数 writesuper(); } 2. blockinodesuperblock.h ///////////////////////////////////////////////////////////////////////////////////////// int ialloc()//申请一个 i 结点 返回结点号 否则返回-1 { if(superblock.fiptr>0) { int temp=superblock.fistack[80-superblock.fiptr];//当前可用 superblock.fistack[80-superblock.fiptr]=-1; superblock.fiptr--; return temp; } return -1; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// void ifree(int index)//指定一个结点号,回收一个 i 结点 { disk.open(\清空结点 disk.seekp(514+64*index+2*(index/8)); disk< disk.close(); for(int i=80-superblock.fiptr;i<80;i++)//结点号找到合适位置插入空闲结点 号栈 { if(superblock.fistack[i] superblock.fistack[i-1]=superblock.fistack[i]; } else//放在第一个大于它的结点号前面 { superblock.fistack[i-1]=index; break; } } superblock.fiptr++; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// /*成组链接法*/ int balloc()//申请一个盘块 返回盘块号 否则返回-1 { int temp=superblock.fbstack[10-superblock.fbptr]; if(superblock.fbptr==1)//是栈底了==>是记录盘块了 { //是最后记录盘块最后号0(保留作栈底 分配不成功) if(temp==0) { return -1; } superblock.fbstack[10-superblock.fbptr]=-1; superblock.fbptr=0; //盘块内容读入栈 for(int i=0;i<10;i++) { int id,num=0; disk.open(\ //先计算盘块内容个数 num(最多10),最后盘块可能不到10个 disk.seekg(514*temp); for(int i=0;i<10;i++) { disk>>id; num++; if(id==0) break; } disk.seekg(514*temp);//盘块内容读入栈 for(int j=10-num;j<10;j++) { disk>>id; superblock.fbstack[j]=id; } superblock.fbptr=num; disk.close(); } disk.open(\清空回收盘块 disk.seekp(514*temp); disk< return temp; } else//不是记录盘块==>盘块使用掉 { superblock.fbstack[10-superblock.fbptr]=-1; superblock.fbptr--; return temp; } } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// void bfree(int index)//指定一个盘块号,回收一个盘块 { disk.open(\清空回收盘块 disk.seekp(514*index); disk< disk.close(); if(superblock.fbptr==10)//栈已满栈中内容记入回收盘块 栈清空 { disk.open(\ disk.seekp(514*index); for(int i=0;i<10;i++) { disk< superblock.fbstack[i]=-1; } disk.close(); superblock.fbptr=0; }//回收盘块压栈 superblock.fbstack[10-superblock.fbptr-1]=index; superblock.fbptr++; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// void readsuper()//读超块到内存 { disk.open(\int i; for(i=0;i<80;i++)//读空闲结点号栈 { disk>>superblock.fistack[i]; } disk>>superblock.fiptr;//空闲结点号栈指针 for(i=0;i<10;i++)//读空闲盘块号栈 { disk>>superblock.fbstack[i]; } disk>>superblock.fbptr;//空闲盘块号栈指针 disk>>superblock.inum;//空闲结点号总数 disk>>superblock.bnum;//空闲盘块号总数 disk.close(); } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// void writesuper()//内存写回超块 { disk.open(\int i; for(i=0;i<80;i++)//写空闲结点号栈 { disk< } disk< disk< } disk< } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// void readinode(int index,INODE &inode)//读指定的 i 结点到 INOE inode 寄存 便 于对同一结点的大量操作 { disk.open(\disk.seekg(514+64*index+2*(index/8)); disk>>inode.fsize;//文件大小 disk>>inode.fbnum;//文件盘块数===> 目录文件为1数据文件1~4 都有 for(int i=0;i<4;i++)//四个直接盘块号 disk>>inode.addr[i]; disk>>inode.addr1;//一 个一次间址()===> 不使用了 disk>>inode.addr2;//一个两次间址()===> 不使用了 disk>>inode.owner;//文件拥有者 disk>>inode.group;//文件所属组 disk>>inode.mode;// 文件类别及存储权限 disk>>inode.ctime;//最近修改时间 disk.close(); } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// void writeinode(INODE inode,int index)//把 INODE inode 写回指定的 i 结点 { disk.open(\disk.seekp(514+64*index+2*(index/8)); disk< disk< 据文件1~4都有 for(int i=0;i<4;i++)//四个直接盘块号 disk< disk< disk< disk< disk.close(); } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// void readdir(INODE inode,int index,DIR &dir)//读指定目录项(inode 的盘块,下标 index)进临时对象+清空源 { disk.open(\ disk.seekg(514*inode.addr[0]+36*index); disk>>dir.fname; disk>>dir.index; disk>>dir.parfname; disk>>dir.parindex; disk.seekp(514*inode.addr[0]+36*index); disk< disk< disk.close(); } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// void writedir(INODE inode,DIR dir,int index)//目录项对象写到指定目录项 { disk.open(\ disk.seekp(514*inode.addr[0]+36*index); disk< disk< disk.close(); } ///////////////////////////////////////////////////////////////////////////////////////// 3.command.h ///////////////////////////////////////////////////////////////////////////////////////// void getcommand() //命令解析函数 { char commond[10]; bool have; for(;;)//接受并解释命令 { have=false;//记录命令接受否 cout<<'\\n'; printroad(); cout<<\ cin>>commond; if( !strcmp(commond,\ )// 改变当前目录****( 文件名 切换到子目 录 ..切换到父目录 /切换到根目录)须切换到目录文件 { have=true; char string[100];//<是路径的话 至少有一个‘/’以 root 开头,不以 ‘/’结尾> cin>>string; cd(string); } if( !strcmp(commond,\构建基本文件结构 { have=true; cout<<\ mkdir(\cout<<\ mkdir(\cout<<\mkdir(\cout<<\ mkdir(\cout<<\ mkdir(\cout<<\ mkdir(\} /*当前目录下的操作*/ if( !strcmp(commond,\ )//创建目录****(规定目录文件只占一个 盘块)当前目录下创建 { have=true; char dirname[14]; cin>>dirname; mkdir(dirname); } if( !strcmp(commond,\ )//删除目录****当前目录下删除<1.只能 删除空目录><2.提醒+删除所有子目录> { have=true; char dirname[14]; cin>>dirname; rmdir(dirname,road[num-1]); } if( !strcmp(commond,\)//创建数据文件****(规定目录文件只占1 ~ 4个盘块)当前目录下创建 { have=true; char filename[14]; cin>>filename;//理论上可输入1 ~ 4盘块的内容 char content[2048]; cout<<\请先输入文件内容(1 ~ 2048位):\mk(filename,content); } if( !strcmp(commond,\文件拷贝****(把指定目录下的指定文件拷 贝到当前目录下) { have=true; char string[100];//<路径至少有一个‘/’以 root 开头,不以‘/’结 尾> cin>>string; cp(string); } if( !strcmp(commond,\数据文 件删除****当前目录下删除 { have=true; char filename[14]; cin>>filename; rm(filename); } if( !strcmp(commond,\ )//显示文件内容(当前目录下指定 据文件) 数 { have=true; char filename[14]; cin>>filename; cat(filename); } if( !strcmp(commond,\改变文件权限 { have=true; char name[14]; cin>>name; chmod(name); } if( !strcmp(commond,\ )//改变文件拥有者(如拥有者在另 一个组,那么组也要改掉) { have=true; char name[14]; cin>>name; chown(name); } if( !strcmp(commond,\改变文件所属组(???) { have=true; char name[14]; cin>>name; chgrp(name); } if( !strcmp(commond,\改变文件名 { have=true; char name[14]; cin>>name; chnam(name); } ///////////////////////////////////////////////////////////////////////////////// /*对当前目录的操作*/ if( !strcmp(commond,\显示当前目录 { have=true; cou t<<\您的当前目录为:\} if( !strcmp(commond,\)//显示所有子目录(必须保证是目录文 件) { have=true; ls(); } ///////////////////////////////////////////////////////////////////////////////// /*用户组操作*/ if( !strcmp(commond,\用户登陆==切换用户 { have=true; cout<<\帐号\已注销\\n\ while( !login() ) cout<<\ success\\} if( !strcmp(commond,\改变用户口令 { have=true; changepassword(); } if( !strcmp(commond,\系统重置(调用初始化函数)前有 非正常退出时,一定要用 { have=true; initial(); cout<<\ 系统已重置\} if( !strcmp(commond,\退出 { have=t rue; return; } if( have==false )//都不被接受 { cout< } } } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// /**/ void printroad(void)//根据结点路径,输出路径 { cout<<\INODE inode; int nextindex; char name[14]; for(int i=0;i+1 readinode( road[i],inode ); disk.open(\ for(int j=0;j<(inode.fsize/36);j++)//遍历所有的目录项 { disk.seekg(514*inode.addr[0]+36*j); disk>>name; disk>>nextindex; if(nextindex==road[i+1]) { cout<<'/'; cout< } } disk.close(); } } ///////////////////////////////////////////////////////////////////////////////////////// /*bool isspace(char *str)//判断字符串是否全为空格 { bool aaa=true;//检验参数有否 检验是否全空格记录在 isspace 中( 能当参数 ) for(int i=0;str[i]!='\\0';i++) { if(str[i]!=' ') aaa=false; } return aaa; }*/ ///////////////////////////////////////////////////////////////////////////////////////// 4.chsome.h ///////////////////////////////////////////////////////////////////////////////////////// bool havewpower(INODE inode)//判断当前用户对指定的结点有无写权限 { if( !strcmp(auser,inode.owner) )//是文件拥有者 { if(inode.mode[2]=='w') return true; return false; } else { if( !strcmp(agroup,inode.owner) )//是组内 { if(inode.mode[5]=='w') return true; return false; } else//其他用户 空格不 { if(inode.mode[8]=='w') return true; return false; } } } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// void chmod(char *name)//改变文件权限 { INODE inode,inode2; readinode(road[num-1],inode);//当前结点写入结点对象 int i,index2;//i 为目录项下标 index2为目录项中结点号 if( havesame(name,inode,i,index2) ) { readinode(index2,inode2); if( havewpower(inode2) ) { char amode[3]; cout<<\表示拥有者4表示组内7表示其他用户 a 表示-wx 模式 b 表示 r-x 模式 c 表示 rwx 模式 \\n\请输入修 改方案(例如4c):\ if( amode[0]=='1' || amode[0]=='4' || amode[0]=='7' ) { //(int)amode[0]=49;强制类型转换是转成其 ASCII 码 if( amode[1]=='a' ) { inode2.mode[ (int)amode[0]-48 ]='-'; inode2.mode[ (int)amode[0]+1-48 ]='w'; writeinode(inode2,index2); cout<<\修改完毕\ } else { if( amode[1]=='b' ) {