距离上次更新,已经过去了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);
}