2.2.2 Heap内存模型
一般来说,malloc所申请的内存主要从Heap区域分配(本文不考虑通过mmap申请大块内存的情况)。
由上文知道,进程所面对的虚拟内存地址空间,只有按页映射到物理内存地址,才能真正使用。受物理存储容量限制,整个堆虚拟内存空间不可能全部映射到实际的物理内存。Linux对堆的管理示意如下:
Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。
2.2.3 brk与sbrk
由上文知道,要增加一个进程实际的可用堆大小,就需要将break指针向高地址移动。Linux通过brk和sbrk系统调用操作break指针。两个系统调用的原型如下:
intbrk(void*addr);void*sbrk(intptr_tincrement);
brk将break指针直接设置为某个地址,而sbrk将break从当前位置移动increment所指定的增量。brk在执行成功时返回0,否则返回-1并设置errno为ENOMEM;sbrk成功时返回break移动之前所指向的地址,否则返回(void *)-1。
一个小技巧是,如果将increment设置为0,则可以获得当前break的地址。
另外需要注意的是,由于Linux是按页进行内存映射的,所以如果break被设置为没有按页大小对齐,则系统实际上会在最后映射一个完整的页,从而实际已映射的内存空间比break指向的地方要大一些。但是使用break之后的地址是很危险的(尽管也许break之后确实有一小块可用内存地址)。
2.2.4 资源限制与rlimit
系统对每一个进程所分配的资源不是无限的,包括可映射的内存空间,因此每个进程有一个rlimit表示当前进程可用的资源上限。这个限制可以通过getrlimit系统调用得到,下面代码获取当前进程虚拟内存空间的rlimit:
intmain(){structrlimit*limit=(structrlimit*)malloc(sizeof(structrlimit));getrlimit(RLIMIT_AS,limit);printf("softlimit:%ld,hardlimit:%ld\n",limit->rlim_cur,limit->rlim_max);}
其中rlimit是一个结构体:
structrlimit{rlim_trlim_cur;/*Softlimit*/rlim_trlim_max;/*Hardlimit(ceilingforrlim_cur)*/};
每种资源有软限制和硬限制,并且可以通过setrlimit对rlimit进行有条件设置。其中硬限制作为软限制的上限,非特权进程只能设置软限制,且不能超过硬限制。
3 实现malloc3.1 玩具实现
在正式开始讨论malloc的实现前,我们可以利用上述知识实现一个简单但几乎没法用于真实的玩具malloc,权当对上面知识的复习:
/*一个玩具malloc*/#include
这个malloc每次都在当前break的基础上增加size所指定的字节数,并将之前break的地址返回。这个malloc由于对所分配的内存缺乏记录,不便于内存释放,所以无法用于真实场景。
3.2 正式实现
下面严肃点讨论malloc的实现方案。
3.2.1 数据结构
首先我们要确定所采用的数据结构。一个简单可行方案是将堆内存空间以块(Block)的形式组织起来,每个块由meta区和数据区组成,meta区记录数据块的元信息(数据区大小、空闲标志位、指针等等),数据区是真实分配的内存区域,并且数据区的第一个字节地址即为malloc返回的地址。
可以用如下结构体定义一个block:
typedefstructs_block*t_block;structs_block{size_tsize;/*数据区大小*/t_blocknext;/*指向下个块的指针*/intfree;/*是否是空闲块*/intpadding;/*填充4字节,保证meta块长度为8的倍数*/chardata[1]/*这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta*/};
由于我们只考虑64位机器,为了方便,我们在结构体最后填充一个int,使得结构体本身的长度为8的倍数,以便内存对齐。示意图如下:
3.2.2 寻找合适的block
现在考虑如何在block链中查找合适的block。一般来说有两种查找算法:
First fit:从头开始,使用第一个数据区大小大于要求size的块所谓此次分配的块
Best fit:从头开始,遍历所有块,使用数据区大小大于size且差值最小的块作为此次分配的块
两种方法各有千秋,best fit具有较高的内存使用率(payload较高),而first fit具有更好的运行效率。这里我们采用first fit算法。
/*Firstfit*/t_blockfind_block(t_block*last,size_tsize){t_blockb=first_block;while(b&&!(b->free&&b->size>=size)){*last=b;b=b->next;}returnb;}
find_block从frist_block开始,查找第一个符合要求的block并返回block起始地址,如果找不到这返回NULL。这里在遍历时会更新一个叫last的指针,这个指针始终指向当前遍历的block。这是为了如果找不到合适的block而开辟新block使用的,具体会在接下来的一节用到。
3.2.3 开辟新的block
如果现有block都不能满足size的要求,则需要在链表最后开辟一个新的block。这里关键是如何只使用sbrk创建一个struct:
#defineBLOCK_SIZE24/*由于存在虚拟的data字段,sizeof不能正确计算meta长度,这里手工设置*/t_blockextend_heap(t_blocklast,size_ts){t_blockb;b=sbrk(0);if(sbrk(BLOCK_SIZE+s)==(void*)-1)returnNULL;b->size=s;b->next=NULL;if(last)last->next=b;b->free=0;returnb;}
3.2.4 分裂block
First fit有一个比较致命的缺点,就是可能会让很小的size占据很大的一块block,此时,为了提高payload,应该在剩余数据区足够大的情况下,将其分裂为一个新的block,示意如下:
实现代码:
voidsplit_block(t_blockb,size_ts){t_blocknew;new=b->data+s;new->size=b->size-s-BLOCK_SIZE;new->next=b->next;new->free=1;b->size=s;b->next=new;}
本文地址:百科问答频道 https://www.neebe.cn/wenda/903029_2.html,易企推百科一个免费的知识分享平台,本站部分文章来网络分享,本着互联网分享的精神,如有涉及到您的权益,请联系我们删除,谢谢!