【c 语⾔】malloc 函数详解
谈到malloc函数相信学过c语⾔的⼈都很熟悉,但是malloc底层到底做了什么⼜有多少⼈知道。
1、关于malloc相关的⼏个函数
关于malloc我们进⼊Linux man⼀下就会得到如下结果:
也可以这样认为(window下)原型:
头⽂件:
如果分配成功:则返回指向被分配内存空间的指针
不然返回指针NULL
同时,当内存不再使⽤的时候,应使⽤free()函数将内存块释放掉。
关于:void*,表⽰未确定类型的指针,c,c++规定void*可以强转为任何其他类型的指针,关于void还有⼀种说法就是其他任何类型都可以直接赋值给它,⽆需进⾏强转,但是反过来不可以
malloc:
malloc分配的内存⼤⼩⾄少为参数所指定的字节数
malloc的返回值是⼀个指针,指向⼀段可⽤内存的起始位置,指向⼀段可⽤内存的起始地址,多次调⽤malloc所分配的地址不能有重叠部分,除⾮某次malloc所分配的地址被释放掉malloc应该尽快完成内存分配并返回(不能使⽤NP-hard的内存分配算法)实现malloc时应同时实现内存⼤⼩调整和内存释放函数(realloc和free)
malloc和free是配对的,如果申请后不释放就是内存泄露,如果⽆故释放那就是什么也没做,释放只能释放⼀次,如果⼀块空间释放两次或者两次以上会出现错误(但是释放空指针例外,释放空指针也等于什么也没做,所以释放多少次都是可以的。)
2、malloc和new
new返回指定类型的指针,并且可以⾃动计算所需要的⼤⼩。
⽽malloc需要我们⾃⼰计算字节数,并且返回的时候要强转成指定类型的指针。
(1)malloc的返回是void*,如果我们写成了:p=malloc(sizeof(int));间接的说明了(将void转化给了int*,这不合理)
(2)malloc的实参是sizeof(int),⽤于指明⼀个整型数据需要的⼤⼩,如果我们写成p=(int*)malloc(1),那么可以看出:只是申请了⼀个⼀个字节⼤⼩的空间。
(3)malloc只管分配内存,并不能对其进⾏初始化,所以得到的⼀⽚新内存中,其值将是随机的。⼀般意义上:我们习惯性的将其初始化为NULL,当然也可以使⽤memset函数。
extern void *malloc(unsigned int num_bytes);
#include<malloc.h>或者#include<alloc.h>两者的内容是完全⼀样的
int *p;
p = new int ;//返回类型为int * ,分配的⼤⼩是sizeof(int )
p = new int [100];//返回类型是int *类型,分配的⼤⼩为sizeof(int )*100
int *p;
p = (int *)malloc(sizeof (int ));
简单的说:
malloc函数其实就是在内存中⼀⽚指定⼤⼩的空间,然后将这个空间的⾸地址给⼀个指针变量,这⾥的指针变量可以是⼀个单独的指针,也可以是⼀个数组的⾸地址,这要看malloc函数中参数size的具体内容。我们这⾥malloc分配的内存空间在逻辑上是连续的,⽽在物理上可以不连续。我们作为程序员,关注的是逻辑上的连续,其他的操作系统会帮着我们处理。
下⾯就来看看malloc具体是怎么实现的。
⾸先要了解操作系统相关的知识:
虚拟内存地址和物理内存地址
为了简单,现代操作系统在处理物理内存地址时,普遍采⽤虚拟内存地址技术。即在汇编程序层⾯,当涉及内存地址时,都是使⽤的虚拟内存地址。采⽤这种技术时,每个进程仿佛⾃⼰独享⼀⽚2N字节的内存,其中N是机器位数。例如在64位CPU和64位操作系统下每个进程的虚拟地址空间为264Byte。
这种虚拟地址空间的作⽤主要是简化程序的编写及⽅便操作系统对进程间内存的隔离管理,真实中的进程不太可能如此⼤的空间,实际能⽤到的空间⼤⼩取决于物理内存的⼤⼩。
由于在机器语⾔层⾯都是采⽤虚拟地址,当实际的机器码程序涉及到内存操作时,需要根据当前进程运⾏的实际上下⽂将虚拟地址转化为物理内存地址,才能实现对内存数据的操作。这个转换⼀般由⼀个叫MMU的硬件完成。
页与地址构成
在现代操作系统中,不论是虚拟内存还是物理内存,都不是以字节为单位进⾏管理的,⽽是以页为单位。⼀个内存页是⼀段固定⼤⼩的连续的连续内存地址的总称,具体到Linux中,典型的内存页⼤⼩为
4096 Byte
所以内存地址可以分为页号和页内偏移量。下⾯以64位机器,4G物理内存,4K页⼤⼩为例,虚拟内存地址和物理内存地址的组成如下:
上⾯是虚拟内存地址,下⾯是物理内存地址。由于页⼤⼩都是4k,所以页内偏移都是⽤低12位表⽰,⽽剩下的⾼地址表⽰页号
MMU映射单位并不是字节,⽽是页,这个映射通过差⼀个常驻内存的数据结构页表来实现。现在计算机具体的内存地址映射⽐较复杂,为了加快速度会引⼊⼀系列缓存和优化,例如TLB等机制,下⾯给出⼀个经过简化的内存地址翻译⽰意图:
内存页与磁盘页
我们知道⼀般将内存看做磁盘的缓存,有时MMU在⼯作时,会发现页表表名某个内存页不在物理内存页不在物理内存中,此时会触发⼀个缺页异常,此时系统会到磁盘中相应的地⽅将磁盘页载⼊到内存中,然后重新执⾏由于缺页⽽失败的机器指令。关于这部分,因为可以看做对malloc实现是透明的,所以不再详述
molloc函数
真实地址翻译流程:
Linux进程级内存管理
2.2.1内存排布
明⽩了虚拟内存和物理内存的关系及相关的映射机制,下⾯看⼀下具体在⼀个进程内是如何排布内存的。
以Linux 64位系统为例。理论上,64bit内存地址空间为0x0000000000000000-0xFFFFFFFFFFFFFFF,这是个相当庞⼤的空
间,Linux实际上只⽤了其中⼀⼩部分
具体分布如图所⽰:
对⽤户来说主要关⼼的是User Space。将User Space放⼤后,可以看到⾥⾯主要分成如下⼏段:
Code:这是整个⽤户空间的最低地址部分,存放的是指令(也就是程序所编译成的可执⾏机器码)
Data:这⾥存放的是初始化过的全局变量
BSS:这⾥存放的是未初始化的全局变量
Heap:堆,这是我们本⽂主要关注的地⽅,堆⾃底向上由低地址向⾼地址增长
Mapping Area:这⾥是与mmap系统调⽤相关的区域。⼤多数实际的malloc实现会考虑通过mmap分配较⼤块的内存空间,本⽂不考虑这种情况,这个区域由⾼地址像低地址增长
Stack:栈区域,⾃⾼地址像低地址增长
Heap内存模型:
⼀般来说,malloc所申请的内存主要从Heap区域分配,来看看Heap的结构是怎样的。
Linux维护⼀个break指针,这个指针执⾏堆空间的某个地址,从堆开始到break之间的地址空间为映射好的,可以供进程访问,⽽从break 往上,是未映射的地址空间,如果访问这段空间则程序会报错
brk与sbrk
由上⽂知道,要增加⼀个进程实际上的可⽤堆⼤⼩,就需要将break指针向⾼地址移动。Linux通过brk和sbrk系统调⽤操作break指针。两个系统调⽤的原型如下:
int brk(void *addr);
void *sbrk(inptr_t increment);
brk将break指针直接设置为某个地址,⽽sbrk将break从当前位置移动increment所指定的增量。brk在执⾏成功时返回0,否则返回-1并设置为errno为ENOMEM,sbrk成功时返回break移动之前所指向的地址,否则返回(void*)-1;
资源限制和rlimirt
系统为每⼀个进程所分配的资源不是⽆限的,包括可映射的空间,因此每个进程有⼀个rlimit表⽰当前进程可⽤的资源上限,这个限制可以通过getrlimit系统调⽤得到,下⾯代码获取当前进程虚拟内存空间的rlimit
其中rlimt是⼀个结构体
struct rlimit
{
rlimt_t rlim_cur;
rlim_t rlim_max;
};
每种资源有硬限制和软限制,并且可以通过setrlimit对rlimit进⾏有条件限制作为软限制的上限,⾮特权进程只能设置软限制,且不能超过硬限制
实现malloc
(1)数据结构
⾸先我们要确定所采⽤的数据结构。⼀个简单可⾏⽅案是将堆内存空间以块的形式组织起来,每个块由meta区和数据区组成,meta区记录数据块的元信息(数据区⼤⼩、空闲标志位、指针等等),数据区是真实分配的内存区域,并且数据区的第⼀个字节地址即为malloc返回的地址
可以使⽤如下结构体定义⼀个block
typedef struct s_block *t_block;
struck s_block{
size_t size;//数据区⼤⼩
t_block next;//指向下个块的指针
int free;//是否是空闲块
int padding;//填充4字节,保证meta块长度为8的倍数
char data[1];//这是⼀个虚拟字段,表⽰数据块的第⼀个字节,长度不应计⼊meta
};
(2)寻合适的block
现在考虑如何在block链中查合适的block。⼀般来说有两种查算法:
First fit:从头开始,使⽤第⼀个数据区⼤⼩⼤于要求size的块所谓此次分配的块
Best fit:从头开始,遍历所有块,使⽤数据区⼤⼩⼤于size且差值最⼩的块作为此次分配的块
两种⽅式各有千秋,best fit有较⾼的内存使⽤率(payload较⾼),⽽first fit具有较⾼的运⾏效率。这⾥我们采⽤first fit算法
t_block find_block(t_block *last,size_t size){
t_block b = first_block;
while(b&&b->size>=size)
{
*last = b;
b = b->next;
}
return b;
}
find_block从first_block开始,查第⼀个符合要求的block并返回block起始地址,如果不到这返回NULL,这⾥在遍历时会更新⼀个叫last的指针,这个指针始终指向当前遍历的block.这是为了如果不到合适的block⽽开辟新block使⽤的。
(3)开辟新的block
如果现有block都不能满⾜size的要求,则需要在链表最后开辟⼀个新的block。这⾥关键是如何只使⽤
sbrk创建⼀个struct:#define BLOCK_SIZE 24
t_block extend_heap{
t_block b;
b = sbrk(0);
if(sbrk(BLOCK_SIZE+s)==(void*)-1)
return NULL;
b->size = s;
b->next - NULL;
if(last)
last->next = b;
b->free = 0;
return b;
};
(4)分裂block
First fit有⼀个⽐较致命的缺点,就是可能会让更⼩的size占据很⼤的⼀块block,此时,为了提⾼payload,应该在剩余数据区⾜够⼤的情况下,将其分裂为⼀个新的block
void split_block(t_block b,size_t s)
{
t_block new;
new = b->data;
new->size = b->size-s-BLOCK_SIZE;
new->next = b->next;
new ->free = 1;
b->size = s;
b->next = new;
}
(5)malloc的实现
有了上⾯的代码,我们就可以实现⼀个简单的malloc.注意⾸先我们要定义个block链表的头first_block,初始化为NULL;另外,我们需要剩余空间⾄少有BLOCK_SIZE+8才执⾏分裂操作
由于我们需要malloc分配的数据区是按8字节对齐,所以size不为8的倍数时,我们需要将size调整为⼤于size的最⼩的8的倍数
size_t align8(size_t s)
{
if(s&0x7 == 0)
return s;
return ((s>>3)+1)<<3;
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论