实验要求
现有若干进程,每个进程的页面访问顺序已经给出,并且这些进程交替地访问页面
设定一个工作集窗口Δ和内存页面数M
用一个数据结构维护每个进程的工作集,这个数据结构可以是数组或链表
根据进程访问页面的顺序,动态更新每个进程的工作集合和内存的空闲页面数
内存页面不足时,暂停某些进程。并在内存足够时,再将其唤醒
对给出的几个进程,利用工作集模型,进行内存的管理。
内存页面总数设为1000
工作集窗口初始可设为500左右,然后改变工作集窗口的大小,观察其对实验结果的影响
跟踪每个进程访问页面过程中页错误率的变化趋势,并将其记录到相应的文件中。
利用记录的数据生成折线图,然后做出分析。(生成折线图,可使用Excel工具)
分析
用到的数据结构
虽然是C语言的程序,不过这里先采用面向对象的思路进行分析。因为根据要求,我们会需要一些数据结构,最明显的:集合(Set),因为Set的特点是元素唯一性,所以可以使用Set保存当前工作集窗口下用到的页面。
既然是面向对象,那么把工作重心放到名词上,先抽出类。
每一个进程都有一个工作集合,在每一时刻工作集合下存着当前该进程访问的页面。另外,由于内存不足或充裕的变动,进程可能被暂停,所以保留一个标志位表明进程的运行状态。进程的访问页面顺序由题目给出,因此用一个数组保留所有访问顺序。
每一个工作集窗口对应一个进程,由于颠簸,在进程访问页面时会有访问错误,因此要记录访问错误次数。
考虑到方便起见,我先用高级语言实现,省去了自己封装Set还有处理指针的麻烦,由于我使用的是Xcode环境,所以用的是oc,代码在后面的附录中,仅供参考。接下来依旧会用C语言进行实现和讲解。
程序的实现思路
几个进程依次访问页面,对于每一个进程,每次将要访问的页存入工作集,同时将不再访问的页从工作集中移除,这样工作集中的页就是要调入到内存中的页。
下一时刻,当进程准备访问新的页时,先查看工作集中有没有该页,如果有,那么可以直接访问而不会发生错误,否则会发生页访问错误,需要将新的页装入工作集。
为了方便起见,我们假定页不动,将工作集看成窗口(也就是所谓的工作集窗口),每次访问时工作集窗口后移。起初窗口从起点开始,这时访问的页还不足以填充满这个窗口,直到窗口移到最后一个元素,说明进程执行完毕。
这样,我们就能基本上确定每个类需要的操作了。主要是窗口类,要有一个将窗口后移的函数
/**
* 工作窗口后移至某一值,将移出窗口并且不用的页从内存删除
*/
void window_moveTo(ModelSetWindow *window, int end);
内存不足时将进程暂停
/**
* 内存空间不够时将该进程暂停
*/
void window_stop(ModelSetWindow *window);
对进程而言,需要每次将进程用到的页调入内存
/**将该进程用到的新的页调入内存*/
void process_addPage(Process *process, int pageNumber);
实现
常量
本次实验最麻烦的就是这些难以理解、容易混淆的概念。这里理顺一下。
内存页面总数:整个内存能容纳的最大页面数,所有进程的所有工作集窗口内页面总数不允许超过该值。
工作集窗口大小:每个进程的工作集大小,例如上图中大小为10。(虽然某一时刻下由于相同页面的出现,工作集内元素不一定等于10,上图中分别是5和2)
将它们和其它常量定义一下
//CONST.h
//bool 类型
typedef int BOOL;
#define YES 1
#define NO 0
//数组最大长度
#define MAX_LENGTH 40000
//本次实验中以int表示一个页
typedef int ELEM_TYPE;
//获取较大值
#define MAX(a,b) (((a)>(b))?(a):(b))
/**
* 内存页面总数
*/
#define kMemoryPageCount 1000
/**
* 工作集窗口大小
*/
#define kModelSetWindowSize 400
第一个数据结构:集合(Set)
Set在数据结构上的定义是,无序的,不重复的collection,这里因为是用C语言写,所以用数组表示,在方法中限制。
/**集合*/
typedef struct Set {
ELEM_TYPE data[MAX_LENGTH];
int length;
} Set;
void set_init(Set *set);
/**为集合添加新元素,如果新元素已经在集合中存在,则不会添加*/
void set_add(Set *set, ELEM_TYPE elem);
/**判断当前集合是否包含某元素*/
BOOL set_containsObject(Set *set, ELEM_TYPE elem);
这个Set的实现非常简单,这里就不赘述了。
进程
按照之前的分析,不难写出进程的数据结构
/**进程*/
typedef struct Process {
/**进程id*/
int processID;
/**进程页面数*/
int processPageCount;
/**进程工作集合*/
Set *set;
/**是否正在执行*/
BOOL isRunning;
/**内存访问顺序*/
ELEM_TYPE sequence[MAX_LENGTH];
/**内存访问顺序的大小*/
int sequence_count;
} Process;
void process_initWithIDAndPageCount(Process *process, int processID, int count);
/**将该进程用到的新的页调入内存*/
void process_addPage(Process *process, int pageNumber);
/**从文件读取生成页面访问序列*/
void process_getSequence(Process *process);
addPage的做法就是调用该进程的set的set_add方法,保证工作集内页面唯一。这里看一下读文件的getSequence方法。
void process_getSequence(Process *process) {
FILE *fp;
char fileName[255];
sprintf(fileName, "%s/process_0%d", kFileAbsoluteLocation, process->processID);
fp = fopen(fileName, "r");
if (fp == NULL) {
printf("error occured when read file %s\n", fileName);
}
char buffer[255];
for (int i = 0; i < process->processPageCount; i++) {
if (feof(fp)) {
return;
}
char *number = fgets(buffer, 1000, fp);
process->sequence[i] = atoi(number);
process->sequence_count++;
}
}
1、C语言中的格式字符串生成:使用sprintf。sprintf是输出格式化字符串到一字符数组,规则和printf一样。当然有输出就有输入,由于实验中用不到,就不做介绍了。
2、按行读数据:使用fgets读入一行,第二个参数n表示读入的长度,由于每次要在读入的字符串后添加换行符,所以本质上只能读入n-1长度,当然如果读到n-1之前遇到了行末,则不再往下读取。
工作集窗口
/**工作集窗口*/
typedef struct ModelSetWindow{
/**该窗口对应的进程*/
Process *process;
/**错误次数,用于计算错误率*/
int wrongCount;
/**对应进程是否访问结束*/
BOOL hasfinished;
/**当前对象是否已经把结果打印出来了*/
BOOL hasLoggedResult;
/**当前访问到的步数*/
int currentStep;
}ModelSetWindow;
void window_initWithProcess(ModelSetWindow *window, Process *process);
/**
* 获取下一个页
*/
ELEM_TYPE window_getNextPage(ModelSetWindow *window, int end);
/**
* 工作窗口后移至某一值,将移出窗口并且不用的页从内存删除
*/
void window_moveTo(ModelSetWindow *window, int end);
/**
* 内存空间不够时将该进程暂停
*/
void window_stop(ModelSetWindow *window);
/**
* 工作集合窗口开始工作,后移一位,表示模拟页调用,返回当前该进程占用内存页面数
*/
int window_run(ModelSetWindow *window);
1、moveTo方法
理论上,对于每次被移除窗口的页面x,需要判断是否还应该存在于集合中。如果新进来的页面或未被移除的页面中包含x,那么x不用从集合中删除,否则应删除x,在实现上会非常复杂,所以这里干脆每次清空工作集,重新对当前窗口中页面依次加入。
还要注意开始阶段窗口未完全滑入时,元素不满的情况下起点问题。
/**
* 工作窗口后移至某一值,将移出窗口并且不用的页从内存删除
*/
void window_moveTo(ModelSetWindow *window, int end) {
if (window->process->set == NULL) {
window->process->set = (Set *)malloc(sizeof(Set));
set_init(window->process->set);
}
//有可能当前窗口还没完全移入
int start = MAX(0, end - kModelSetWindowSize + 1);
//把用到的加入内存
for (int i = start; i <= end; i++) {
process_addPage(window->process, window->process->sequence[i]);
}
}
2、run方法
从上面可以看到之前说的“清空”并没有体现,实际上主要的操作都在run方法中,run方法要先判断本次是否有页错误,如果有那么计数值要增加,另外关于是否被暂停、是否完成的操作也在run中,最后,为了方便在main方法中获取所有进程的占用页的总和,这里将单个进程的占用页数返回。
注:写博客的时候突然发现如果没有页错误,不需要进行“清空、重新增加”的操作。懒得改了:-(
/**
* 工作集合窗口开始工作,后移一位,表示模拟页调用,返回当前该进程占用内存页面数
*/
int window_run(ModelSetWindow *window) {
//如果新掉进来的页在原来中找不到,则说明此次出现页错误
if (window->process->set != NULL && set_containsObject(window->process->set, window_getNextPage(window, window->currentStep)) == NO) {
window->wrongCount++;
// printf("进程%d发生页错误,wrongCount = %d\n", window->process->processID, window->wrongCount);
}
//把全部的清除,表示把“不用的删除”,因为接下来会重新对窗口添加一遍
free(window->process->set);
window->process->set = NULL;
//如果当前进程被暂停
if (window->process->isRunning == NO) {
return 0;
}
//如果当前进程执行完毕
if (window->currentStep >= window->process->sequence_count) {
window->hasfinished = YES;
if (window->hasLoggedResult == NO) {
printf("进程%d执行完毕,页错误次数%d,页错误率%f\n", window->process->processID, window->wrongCount, (float)window->wrongCount / window->process->processPageCount);
window->hasLoggedResult = YES;
}
return 0;
}
window_moveTo(window, window->currentStep);
window->currentStep++;
// printf("currentStep = %d, window count = %d\n", window->currentStep, (int)window->process->set->length);
return (int)window->process->set->length;
}
Main方法
main方法中需要判断当前所有进程是否都结束,如果没有的话一直执行操作,同时监听超出内存容量的情况,如果进程占用页数超过内存页面总数,那么要将某个进程暂停,空闲时恢复。这里将空闲定义为有300(hard code)个空闲页。
int main(int argc, const char * argv[]) {
//准备生成随机数
srand((unsigned)time(NULL));
//第一个进程
Process p1;
process_initWithIDAndPageCount(&p1, 1, 40000);
//第二个进程
Process p2;
process_initWithIDAndPageCount(&p2, 2, 39000);
//第三个进程
Process p3;
process_initWithIDAndPageCount(&p3, 3, 38000);
//第四个进程
Process p4;
process_initWithIDAndPageCount(&p4, 4, 40000);
//四个进程对应的窗口
ModelSetWindow window1;
window_initWithProcess(&window1, &p1);
ModelSetWindow window2;
window_initWithProcess(&window2, &p2);
ModelSetWindow window3;
window_initWithProcess(&window3, &p3);
ModelSetWindow window4;
window_initWithProcess(&window4, &p4);
ModelSetWindow windows[kProcessCount] = {window1, window2, window3, window4};
while (1) {
//查看是否全部进程都结束了
BOOL hasFinish = YES;
for (int i = 0; i < kProcessCount; i++) {
ModelSetWindow *w = &windows[i];
if (w->hasfinished == NO) {
hasFinish = NO;
break;
}
hasFinish = YES;
}
if (hasFinish == NO) {
int count = 0;
for (int i = 0; i < kProcessCount; i++) {
ModelSetWindow *w = &windows[i];
count += window_run(w);
}
// printf("%d\n", count);
if (count > kMemoryPageCount) {
ModelSetWindow *selectedWindow;
//选一个正在运行的进程
do {
int random = rand() % kProcessCount;
selectedWindow = &windows[random];
} while (selectedWindow->process->isRunning != YES || selectedWindow->hasfinished == YES);
window_stop(selectedWindow);
printf("本次超出内存容量,暂停进程%d\n", selectedWindow->process->processID);
} else if (kMemoryPageCount - count > 500) {
//找一个暂停的进程继续运行
for (int i = 0; i < kProcessCount; i++) {
ModelSetWindow *w = &windows[i];
if (w->process->isRunning == NO) {
printf("内存空闲,将进程%d执行\n", w->process->processID);
w->process->isRunning = YES;
break;
}
}
}
} else {
break;
}
}
printf("执行完毕\n");
return 0;
}
实验结果
源代码
本文源码都可以在这里找到,其中MemoryManagement-OC是最开始用高级语言写的,其他的为C语言版,非Xcode环境可以找到其中所有的.h和.c文件,拷到对应的环境下运行。
上述代码在Xcode6.3.1 llvm编译环境下执行通过。
总结
1、个人感觉,对于这次实验而言,难点在于对题意和概念的理解,理解题意后操作上没有什么问题。毕竟我们不是模拟那些调度算法。
但是,一次实验彻底暴露了C功底。。。各种BAD_ACCESS各种崩有没有- -C语言不像高级语言,不仅体现在自己管理内存上(之前没有free掉set导致每次运行都要占用我的小air的2G+的内存),还体现在新手杀手——指针上。
总结两个经常崩的问题和解决:
①对于需要传指针的函数,直接声明一个指针后穿进去会crash,原因:野指针访问。
例如:
char *fileName;
sprintf(fileName, "...."); //crash~~fileName刚声明出来是野指针
//应改为
char *fileName[255];
sprintf(fileName, "....");
②原理同上,自定义“对象”的初始化,声明栈“对象”然后传地址
//错误的初始化
Process *p1;
process_initWithIDAndPageCount(p1, 1, 40000);
//正确的写法
Process p1;
process_initWithIDAndPageCount(&p1, 1, 40000);
2、面向对象的思维方式。
之前一直觉得所谓面向对象不过是在结构体中加方法,没有真正理解提出这一概念的意义。这次实验中能明显感觉出面向对象的好处,应该说,面向对象更多的是让我们切换一种思维方式,不再从算法入手,而是从对象入手。
曾经疑惑过,就算是面向对象也要自己在类中写方法,即“每个类中面向过程”,不过由于对象已经被抽象和封装,每个类只写自己的功能。以类为单元,这个程序就不再是零散的零件了。