操作系统_学习日志_虚拟内存

版权声明:我的码云地址:https://gitee.com/ZzYangHao https://blog.csdn.net/qq_21049875/article/details/82054315

  距离上次更新,已经过去了19天了,浪费了很多时间在买笔记本、玩游戏、吃吃吃,很后悔,因为快9月份了,又到了秋招了,书后面的内容我自己也大概看了一下,一些东西自己以前使用过,所以就怠慢了,今天来更新吧。
  这次的内容是,虚拟内存,以前不太清楚虚拟内存是什么,只是在玩游戏的过程中,因为内存不够用,或者其他原因去修改虚拟内存大小,而在写代码的过程中,也仅仅知道我们所声明的变量什么的都是虚拟地址,并不是物理地址。
  而虚拟内存章节的内容占书中比例也不小,内容很多,所以我就不打算怎么介绍了,推荐去看https://wdxtub.com/2016/04/16/thin-csapp-7/,我自己的话是结合书本和该网站上的教学去学习的。
  而我个人觉得对于我来说,学习内容分为:了解物理地址和虚拟地址的区别;理解虚拟内存在缓存、内存管理与保护中所扮演的角色;了解虚拟内存到物理内存的翻译机制;理解现代计算机系统中虚拟内存的应用;理解动态内存分配的基本概念;区别内部碎片与外部碎片;掌握管理动态内存分配的三种方法;了解垃圾回收的基本原理;了解内存使用中常见的错误。
  感觉我自己总结内容的话,要么重复别人的或者抄别人的也没多大意义,别人已经总结得很好了,我就没必要重复了,所以这章节我打算直接从实验写起吧,以及写实验的相关内容。
  所以我个人认为,如果不想了解太深,只需要大致弄懂这幅图就可以了,这幅图包含的内容很多,MMU的地址翻译过程(特别是虚拟地址翻译挺复杂的)、缺页后的异常处理机制。
这里写图片描述

PS:这个实验给我一种感受,C和指针,很强大!

  
  

虚拟内存实验

  这个实验有点类似于实现一个内存池,以前自己写过一个简单的内存池,因为要定义一个静态的全局变量来维护链表,导致了这个是线程不安全的管理方式。
  我选择了最简单的方式去做,简单!=容易,很多代码都是书上有的,也就是隐式空闲链表,我们需要补充的是place和find_fit这两个函数,如果想改善或者用其他方式做就可以改一下结构,例如改成显示空闲链表,或者优化寻找适配的空闲块算法,下一次适配或者最佳适配等等,再想改进可以改进realloc,调整再分配方式。
  实验文件:
  这里写图片描述
  实验目的:
  从readme文本文件中可以看到我们要修改的mm.c文件,如果不看书的话,可能不知道从哪下手,因为这个实验目的是我们要完成一个简单的分配器,我会在下面说明该怎么做,为了节约时间,我只实现一种分配器,隐式空闲链表管理内存块,查找采用首次适配。
必要知识:
  存储器实现的必要技术:

  针对空闲块的组织方法有以下三种:
    a.隐式空闲链表(implicit free list)
    b.显式空闲链表(explicit free list)
    c.分离空闲链表(segregated free list)

  查找空闲块的三个方法:
    a.首次适应(first fit)
    b.最佳适配(best fit)
    c.下一次适配(next fit)

  知道在什么时候合并空闲块:
    1、包含扩展堆的过程或者说是函数中合并。
    2、释放掉内存占用,即free


Main Files:
mm.{c,h}
Your solution malloc package. mm.c is the file that you
will be handing in, and is the only file you should modify.

  测试方法:

To build the driver, type “make” to the shell.
To run the driver on a tiny test trace:
  unix> mdriver -V -f short1-bal.rep
The -V option prints out helpful tracing and summary information.
To get a list of the driver flags:
  unix> mdriver -h

  
  
  开始实验:
  

//按8字节对齐
#define ALIGNMENT 8

//把size对齐到8的倍数
#define ALIGN(size) (((size) + (ALIGNMENT-1)) & ~0x7)

#define SIZE_T_SIZE (ALIGN(sizeof(size_t)))

//单双字
#define WSIZE 4
#define DSIZE 8           /*Double word size*/
//扩展堆大小
#define CHUNKSIZE (1<<12) /*the page size in bytes is 4K*/

#define MAX(x,y)    ((x)>(y)?(x):(y))

//头部29位文件size|3位用于表示是否alloc,因为是按8字节对齐
//alloc为1则为已占用,0为空闲
#define PACK(size,alloc)    ((size) | (alloc))

#define GET(p)  (*(unsigned int *)(p))
#define PUT(p,val)  (*(unsigned int *)(p) = (val))

//获得文件大小
#define GET_SIZE(p)  (GET(p) & ~0x7)
//获得分配位
#define GET_ALLOC(p)    (GET(p) & 0x1)
//头部
#define HDRP(bp)    ((char *)(bp)-WSIZE)
//脚部
#define FTRP(bp)    ((char *)(bp)+GET_SIZE(HDRP(bp))-DSIZE)
//下一个有效负荷,也就是内存块
#define NEXT_BLKP(bp)   ((char *)(bp)+GET_SIZE(((char *)(bp)-WSIZE)))
#define PREV_BLKP(bp)   ((char *)(bp)-GET_SIZE(((char *)(bp)-DSIZE)))

static void *extend_heap(size_t words);
static void *coalesce(void *bp);
static void *find_fit(size_t size);
static void place(void *bp,size_t asize);
//堆指针
static char *heap_listp = 0;

  在这个分配器中,我们是这样划分内存的,相当于用链表来维护内存的分配,为什么会有隐式和显示的区分呢?我也不太清楚,不过我从书中对比发现的是,这两者的区别就是,隐式是使用操作指针+偏移量来找到下一个或者上一个内存块,而显示就是通过修改我们链表中的节点信息。
  隐式空闲链表带边界标记的结构大概如下表示,当然实现的话不是下面这样写,这样写你就不太好控制size的大小,当然,有很多方法去控制,如使用C++模板,但那样就会限制了每个块的大小而且还占用栈,那么就毫无意义了,所以我们采用的是指针+偏移量去获取地址以及设置地址存储信息,还能够自由控制字节对齐的方式,以下的结构体只是展示其组成结构,block_size(node的大小)=各个成员的大小合(按8字节对齐)。

struct node{
    int msg_herp;  //前3位alloc位,后29位为size位
    char valid_date[size];
    int msg_ftrp; //和msg_herp一样
}

  显示空闲链表则多了两个指针,一个指向前,一个指向后。
  了解上面的信息,应该可以看懂上面的大部分代码,上面的许多宏都是为了方便编写代码而定义的,现在就来编写相关的函数。
  
  
mm_init:
  不看书的话,可能对sbrk这个函数,可以去百度了解一下作用以及返回值。
  注意的是,我们的链表是初始会分配一个起始块,到最后分配一个结尾块,而起始块也被称为序言,而结尾块被称为后记,序言块会占用两个字(在这里描述的一个字为8个字节),后记则占一个字。
  heap_listp永远指向序言块,当然也可以永远指向序言块后面的第一个块。
  (这里调用mem_sbrk后,返回old_brk指针给heap_listp,新的brk位置在old_brk+4*WSIZE处。)

int mm_init(void)
{
    if((heap_listp = mem_sbrk(4*WSIZE))==(void *)-1){
        return -1;
    }
    PUT(heap_listp,0);
    PUT(heap_listp+(1*WSIZE),PACK(DSIZE,1));
    PUT(heap_listp+(2*WSIZE),PACK(DSIZE,1));
    PUT(heap_listp+(3*WSIZE),PACK(0,1));
    heap_listp += (2*WSIZE);
    //扩展堆
    if(extend_heap(CHUNKSIZE/WSIZE)==NULL){
        return -1;
    }
    return 0;
}

extend_heap
  调用这个函数相当于开辟新的内存块,在init中调用相当于开辟了一块4k的空闲块,通过调用mem_sbrk,拓展堆大小,返回一个旧的brk,也就是紧跟在我们链表的结尾块头部后面,相当于是链表后面追加一个空闲节点,并把下一个空闲块设置为结尾块。
  然后调用coalesce函数合并空闲块,就比如我们在malloc的时候,该空闲链表找不到适合的位置存放数据,那么我们就需要调用extend_heap,而之前的链表可能存在小空闲块,如果不合并的话就造成内部碎片。

static void *extend_heap(size_t words){
    char *bp;
    size_t size;

    size = (words%2) ? (words+1)*WSIZE : words*WSIZE;
    if((long)(bp=mem_sbrk(size))==(void *)-1)
        return NULL;

    PUT(HDRP(bp),PACK(size,0));
    PUT(FTRP(bp),PACK(size,0));
    PUT(HDRP(NEXT_BLKP(bp)),PACK(0,1));

    return coalesce(bp);
}

  
coalesce
  该函数负责合并空闲块。
  主要分为4种情况合并:
    1、当前块的前面非空闲,后面块非空闲。
    2、当前块的前面空闲,后面块非空闲。
    3、当前块的前面非空闲,后面块空闲。
    4、当前块的前面空闲,后面块空闲。
  合并的方式也很简单,也就是修改头部和脚部信息,更新alloc状态,这样我们在遍历链表的时候能够准确遍历。

static void *coalesce(void *bp){
    size_t  prev_alloc = GET_ALLOC(FTRP(PREV_BLKP(bp)));
    size_t  next_alloc = GET_ALLOC(HDRP(NEXT_BLKP(bp)));
    size_t size = GET_SIZE(HDRP(bp));

    if(prev_alloc && next_alloc) {
        return bp;
    }else if(prev_alloc && !next_alloc){
            size += GET_SIZE(HDRP(NEXT_BLKP(bp)));
            PUT(HDRP(bp), PACK(size,0));
            PUT(FTRP(bp), PACK(size,0));
    }else if(!prev_alloc && next_alloc){
        size += GET_SIZE(HDRP(PREV_BLKP(bp)));
        PUT(FTRP(bp),PACK(size,0));
        PUT(HDRP(PREV_BLKP(bp)),PACK(size,0));
        bp = PREV_BLKP(bp);
    }else {
        size +=GET_SIZE(FTRP(NEXT_BLKP(bp)))+ GET_SIZE(HDRP(PREV_BLKP(bp)));
        PUT(FTRP(NEXT_BLKP(bp)),PACK(size,0));
        PUT(HDRP(PREV_BLKP(bp)),PACK(size,0));
        bp = PREV_BLKP(bp);
    }
    return bp;
}

  
mm_malloc
  接下来就是我们自己的malloc函数。
    1、把申请的size按8字节对齐(头部+size+脚部)。
    2、找到空闲链表中适合存放的位置,如果没有则进行步骤3对堆扩展。
    3、扩展堆大小。
  其中限定了每个块的大小至少是16字节,因为是按8字节对齐,不可能在空闲链表中存在只有头部和脚部而没有有效符合(数据块)的情况。
  这里有个计算,asize = (DSIZE)*((size+(DSIZE)+(DSIZE-1)) / (DSIZE));,刚开始还不太明白,分析了一下,感觉很奇妙,其实有点相当于asize = (size + (DSIZE)+DSIZE-1) & (~7);,加DSIZE-1只是为了给二进制上的前3位(2^3=8)做加法运算,看是否有进位到后面的位数,然后&预算达到向上取整的方法,这种方法一般只对偶数倍有效。

void *mm_malloc(size_t size)
{
    size_t asize;
    size_t extendsize;
    char *bp;
    if(size ==0) return NULL;

    if(size <= DSIZE){
        asize = 2*(DSIZE);
    }else{
        asize = (DSIZE)*((size+(DSIZE)+(DSIZE-1)) / (DSIZE));
    }
    if((bp = find_fit(asize))!= NULL){
        place(bp,asize);
        return bp;
    }
    extendsize = MAX(asize,CHUNKSIZE);
    if((bp = extend_heap(extendsize/WSIZE))==NULL){
        return NULL;
    }
    place(bp,asize);
    return bp;
}

  
find_fit
  选择了最简单的方式,首次适配,遍历空闲链表,发现空闲块&&s空闲块size能够满足我们malloc的size,那么就返回该空闲块的地址。

static void *find_fit(size_t size){
    void *bp;
    for(bp = heap_listp; GET_SIZE(HDRP(bp))>0; bp = NEXT_BLKP(bp)){
        if(!GET_ALLOC(HDRP(bp)) && (size <= GET_SIZE(HDRP(bp)))){
            return bp;
        }
    }
    return NULL;
}

  
place
  该怎么分割空闲块给我们申请的size呢?肯定是不能直接把整个空闲块都给我们申请的size,要是直接把空闲块给我们的申请的size,就可能会出现大量的内部碎片,所以我们要合理的去分割空闲块,空闲块足够大就分割处size的空间块,不够大的话就直接把整个空闲块给我们使用,这样就能大部分的减少内部碎片与外部碎片。

static void place(void *bp,size_t asize){
    size_t csize = GET_SIZE(HDRP(bp));
    if((csize-asize)>=(2*DSIZE)){
        PUT(HDRP(bp),PACK(asize,1));
        PUT(FTRP(bp),PACK(asize,1));
        bp = NEXT_BLKP(bp);
        PUT(HDRP(bp),PACK(csize-asize,0));
        PUT(FTRP(bp),PACK(csize-asize,0));
    }else{
        PUT(HDRP(bp),PACK(csize,1));
        PUT(FTRP(bp),PACK(csize,1));
    }
}

  
mm_realloc
  重新申请内存。

void *mm_realloc(void *ptr, size_t size)
{
    size_t oldsize;
    void *newptr;

    /* If size == 0 then this is just free, and we return NULL. */
    if(size == 0) {
    mm_free(ptr);
    return 0;
    }

    /* If oldptr is NULL, then this is just malloc. */
    if(ptr == NULL) {
    return mm_malloc(size);
    }

    newptr = mm_malloc(size);

    /* If realloc() fails the original block is left untouched  */
    if(!newptr) {
    return 0;
    }

    /* Copy the old data. */
    oldsize = GET_SIZE(HDRP(ptr));
    if(size < oldsize) oldsize = size;
    memcpy(newptr, ptr, oldsize);

    /* Free the old block. */
    mm_free(ptr);

    return newptr;
}

  
mm_free
  释放内存,更改标记,注意释放后的空闲块要合并。

void mm_free(void *bp)
{
/* $end mmfree */
    if(bp == 0)
    return;

/* $begin mmfree */
    size_t size = GET_SIZE(HDRP(bp));
/* $end mmfree */

/* $begin mmfree */

    PUT(HDRP(bp), PACK(size, 0));
    PUT(FTRP(bp), PACK(size, 0));
    coalesce(bp);
}

猜你喜欢

转载自blog.csdn.net/qq_21049875/article/details/82054315