操作系统实验——工作集模型下的内存管理模拟

实验要求

现有若干进程,每个进程的页面访问顺序已经给出,并且这些进程交替地访问页面

设定一个工作集窗口Δ和内存页面数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、面向对象的思维方式。

之前一直觉得所谓面向对象不过是在结构体中加方法,没有真正理解提出这一概念的意义。这次实验中能明显感觉出面向对象的好处,应该说,面向对象更多的是让我们切换一种思维方式,不再从算法入手,而是从对象入手。

曾经疑惑过,就算是面向对象也要自己在类中写方法,即“每个类中面向过程”,不过由于对象已经被抽象和封装,每个类只写自己的功能。以类为单元,这个程序就不再是零散的零件了。

时间: 2024-10-05 17:30:05

操作系统实验——工作集模型下的内存管理模拟的相关文章

ARC下的内存管理

1.ARC下单对象内存管理 局部变量释放对象随之被释放 int main(int argc, const char * argv[]) { @autoreleasepool { Person *p = [[Person alloc] init]; } // 执行到这一行局部变量p释放 // 由于没有强指针指向对象, 所以对象也释放 return 0; } 清空指针对象随之被释放 int main(int argc, const char * argv[]) { @autoreleasepool

iOS: ARC &amp; MRC下string内存管理策略探究

ARC & MRC下string内存管理策略探究 前两天跟同事争论一个关于NSString执行copy操作以后是否会发生变化,两个人整了半天,最后写代码验证了一下,发现原来NSString操作没我们想的那么简单,下面就让我们一起看看NSString和NSMutableString在MRC下执行retain,copy,mutableCopy,以及ARC下不同的修饰__weak, __strong修饰赋值究竟发生了什么. 一.验证代码如下: - (void)testStringAddress { i

STL容器存储的内容动态分配情况下的内存管理

主要分两种情况:存储的内容是指针:存储的内容是实际对象. 看下面两段代码, typedef pair<VirObjTYPE, std::list<CheckID>*> VirObj_CheckID_pair; class LangChecker { public:     LangChecker();     ~LangChecker();         void Register(VirObjTYPE type, CheckID id); private:     std::m

内存管理模拟

内存管理模拟算法:首次适应算法.最佳适应算法.最坏适应算法 此程序是参考别人的,因此也没有什么好说的,感觉写的不错就贴上来了 代码如下: 1 #include<stdio.h> 2 #include<malloc.h> 3 #include<stdlib.h> 4 5 #define PROCESS_NAME_LEN 32 //进程名字长度 6 #define MIN_SLICE 10 //最小碎片大小 7 #define DEFAULT_MEM_SIZE 1024 /

glibc下的内存管理

在解码过程中我们也遇到了类似的问题,第一次解码的音频比较大60s,耗了3G的内存,reset之后内存并没有退还给操作系统,第二次即使解一个10s的音频 几周前我曾提到,我被项目组分配去做了一些探究linux下内存管理机制的活儿.因为我们的产品遇到了一些与之相关的“诡异”问题.这些问题以及相关情况可以概括如下: 先介绍一下相关的背景.由于我们是3D软件,所以用户经常会有“导入/导出”各种geometry的需求.而一个存储这些数据的文件,可能含有不止一个geometry,而且每个geometry中也

操作系统实验二-ubuntu下安装配置pintos

Lucene 4.0版本的DocIdSetIterator中没有cost方法,而4.7.0则有这个方法,表示遍历整个DocIdSet的代价,对于DocsEnum就是其长度了,对于Scorer就可以是符合查询的个数了.ConjunctionScorer可以取其中cost最小的那个scorer,做and操作,而4.0中则是猜测,认为第一个doc最大的那个应该最稀疏,从那个scorer开始做.

FZU 1893 内存管理 模拟

比赛的时候队友要做这道题…… 他没做出来自己也被误导了…… 也算是个教训 自己还是要有自己的思路…… 又是模拟题…… 网上都是用vector做的 我最近才会stl 怎么会用那么高大上的的东西…… 强力模拟一波内存…… 反正只有100的大小 随意遍历…… 模拟都做不出来 只能说明真的方了…… 只要心态稳定 模拟都是水题…… 1 #include<stdio.h> 2 #include<string.h> 3 int memory[102]; 4 int room[102]; 5 in

自制操作系统(九) 内存管理

2016.07.04  2016.07.05 操作系统本质是一个程序,一个大型软件工程(商业化os的情况).而程序的本质---一方面是所谓的“数据结构+算法”,另一方面则是 封装+抽象.操作系统作为一个出现,一方面是控制硬件启动开机,并且作为第一个在计算机上运行的软件,另一方面,操作系统负责管理计算机的资源(内存管理,文件管理,I\O),协助用户要运行的程序在计算机上运行,甚至是多个程序同步运行(进程管理).所以你可看到,操作系统本质上和那些b\s模式的企业管理网站本质没有任何区别,都是管理.只

操作系统之内存管理1

1. 2.内存管理需要达到的目的? 1)地址保护:多道程序之间互不干扰,一个进程不能随便访问另外一个进程的地址空间. 2)地址独立:程序发出的地址与具体机器的物理主存地址是独立的. 3.为什么提出了虚拟内存? 在计算机中,一个程序要运行,必须加载到物理主存中,但是物理主存的容量是非常有限的,因此我们要把一个程序全部加载到主存,我们的每一个程序大小就要限制.另外,即使我们编写的每一个程序的大小都小于物理主存的容量,但主存能够存放的程序数量也是有限的.这将大大限制多道编程的发展,所以我们发明了虚拟内