首页 > 编程知识 正文

malloc 构造函数,malloc函数编译后是怎么样的

时间:2023-05-06 16:42:38 阅读:49803 作者:4701

关于malloc函数,我想学过c语言的人很清楚,在malloc的底层做了什么,还有多少人知道?

1、关于malloc相关的几个函数

关于malloc进入Linux man时,得到以下结果。

也可以考虑如下。

扩展语音* malloc (unsigned intnum _ bytes; 头文件:

#includemalloc.h或#includealloc.h的内容完全相同,如果分配成功,则返回指向分配内存空间的指针

否则返回指针NULL

另外,如果内存不再使用,则必须使用free ()函数释放内存块。

:对于void*,表示未确定类型的指针,c、c规定void*可以强转换为任何类型的指针。 关于void,也有说法认为可以直接指派其他类型的值,无需强变换,但不能相反

malloc:

分配给malloc的内存大小至少是参数指定的字节数

malloc返回值是指向可用内存开头的指针,指向可用内存的开头地址。 即使多次调用malloc分配的地址,也不能有重复部分。 但是,除非某个malloc释放分配的地址,否则malloc必须尽快完成内存分配并返回。 (无法使用NP-hard的内存分配算法。 )实现mallloc时,必须同时实现内存大小调整和内存释放函数652

malloc和free是一对。 如果申请了也不释放的话就是内存泄漏。 如果被无故释放的话,就意味着什么都没做。 解放只能进行一次。 在一个空间中释放两次以上会发生错误。 (但是,释放空指针是一个例外,因为释放空指针等于什么也没做,所以可以释放几次。 )

2、malloc和new

new可以返回指定类型的指针,并自动计算所需的大小。

int *p; p=new int; //返回类型为int*,分配大小为sizeof(int ) p=newint(100 ); //返回类型为int*类型,分配大小为sizeof(int ) 100,malloc需要自己计算字节数,并在返回时强制转换为指定类型的指针。

int *p; p=(int* ) malloc ) sizeof (int ); (1) malloc的返回是void*,如果我们写) p=malloc(sizeof(int ) ); 间接说明了(将void转换为int*,这不合理) )。

) malloc的实际参考是sizeof(int ),表示整数数据所需的大小。 写p=(int* ) malloc )1),可以看出只是申请了1字节大小的空间。

)3) malloc只是分配内存,也不能将其初始化,所以在得到的新内存中,其值是随机的。 一般意义:我们习惯性地将其初始化为空。 当然也可以使用memset函数。

简单地说:

malloc函数实际上在内存中查找指定大小的区域,并将该区域的第一个地址传递给指针变量。 这里的指针变量可以是单独的指针,也可以是数组的起始地址,这取决于malloc函数参数size的具体内容。 这里,malloc分配的存储器空间在逻辑上连续,但也可以是物理上不连续的。 作为程序员,我们关注逻辑上的连续性。 其他操作系统可以处理。

看看malloc具体是怎么实现的。

首先,要了解操作系统知识:

虚拟和物理存储器地址

为简单起见,现代操作系统在处理物理内存地址时一般采用虚拟内存地址技术。 即,在汇编器级别,在涉及存储器地址的情况下,都是使用的虚拟存储器地址。 采用该技术后,每个进程看起来就像自己占用了2N字节的内存。 其中n是机器的位数。 例如,在64位CPU和64位操作系统中,每个进程的虚拟地址空间为264字节。

这种虚拟地址空间的作用主要是简化程序的编写,方便操作系统对进程间内存的隔离管理,实际进程不是很大的空间,实际可用空间的大小取决于物理内存的大小。

由于在机器语言级别采用了虚拟地址,因此如果实际的机器码程序涉及内存操作,则为了实现内存数据的操作,必须根据当前进程正在运行的实际上下文将虚拟地址转换为物理内存地址这种转换通常在名为MMU的硬件上进行。

页面和地址配置

在现代操作系统中,虚拟内存和物理内存都不是按字节管理的,而是按页面管理的。 内存页是一定大小的连续内存地址的总称,具体而言,在Linux上,典型的内存页大小为4096字节

因此,存储器地址可以分为页号和页内偏移。 以64位计算机、4G物理内存和4K页面大小为例,虚拟和物理内存地址配置如下:

上面是虚拟存储器地址,下面是物理存储器地址。 因为所有页面大小都是4k,所以所有页面内偏移都用低12位表示,其馀的高地址表示页码

MMU映射单位是页面而不是字节,该映射由驻留存储器中的坏数据结构页面表来实现。 当前,计算机的具体内存地址映射很复杂,为了加快速度,引入了一系列缓存和优化,如TLB等机制。 以下是简化的内存地址翻译示意图。

内存页面和磁盘页面

我们知道,通常将内存视为磁盘的缓存。 当MMU运行时,可以看到物理内存页中不存在具有页面表名称的内存页面。 这种情况下

会触发一个缺页异常,此时系统会到磁盘中相应的地方将磁盘页载入到内存中,然后重新执行由于缺页而失败的机器指令。关于这部分,因为可以看做对malloc实现是透明的,所以不再详述
真实地址翻译流程:

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); 12

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 24t_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;} #define BLOCK_SIZE 24void *first_block=NULL;void *mallloc(size_t size){ t_block b,last; size_t s; //对齐地址 s = align8(size); if(first_block) //查找适合block last = first_block; b = find_block(&last,s); if(b) { //如果可以则分裂 if((b->size-s)>=(BLOCK_SIZE + 8)) split_block(b,s); b->free = 0; } else { //没有合适的block,开辟一个新的 b=extend_heap(last,s); if(!b) { return NULL; } else { b=extend_heap(NULL,s); if(!b) { return NULL; } first_block = b; } } return b->data;}

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。