malloc函数的底层实现你是否清楚
说起malloc函数,每个人都能说出它的功能,而且我们经常会用到,那么今天我要说的是关于malloc函数在编译器的底层实现,如果你对它的实现已经很清楚了,那么你可以不往下看了,因为这篇博客只是就它的一些简单原理进行了整理,你可以等我的下一篇博客,对它的深层的一些函数在进行的一些讲述。
这篇博客对于深层的函数实现并没有解释,只是让我们明白了windows系统中的一些分配算法的原理。请读者多多指正,因为在空闲链表分配算法上我看到过不同的说法。
关于VirtualAlloc函数:
首先我们来看一下VirtualAlloc函数,它是WINDOWS系统提供给我们的一个API,它的功能就是向操作系统给我们批发内存。为什么叫批发呢,因为在操作系统中并不是我们所编写的应用程序直接向系统申请内存,在VirtualAlloc函数向内存申请内存的时候是申请内存的大小必须是4096字节的整数倍,这就相当于是一个水果贩子去大批发市场进货,人家批发市场是有起发的要求的,不是说你去买上几斤都行,所以我们都是从水果贩子的手上再去买水果。这样是有道理的,因为你一个程序假如只需要申请几字节的内存,那么系统给我们4096字节的大小是不是很浪费。
那么用VirtualAlloc申请的内存又是怎么拿给我们的程序来用的呢,在这里就牵扯到了分配算法。其实在windows中在对管理器上提供了几个函数用来创建、分配、释放和销毁堆空间(我们申请的空间是在堆上的),从而实现了分配算法。
HeapCreat:创建一个堆
HeapAlloc:在堆里分配空间
HeapFree:释放分配的内存
HeapDestroy:销毁一个堆
这里的HeapCreat便是创建一个堆空间它申请内存空间便是通过VirtualAlloc函数来实现的。HeapAlloc函数是在堆空间里面给用户分配小的内存空间,如果内存不足它也能通过VirtualAlloc向系统申请更多的内存。
Malloc函数:
说了前面那么多,终于说道我们的重点了,malloc函数其实是上面几个Heap操作函数的包装。
堆空间是程序申请出来的一大块空间,当我们用malloc函数申请空间的时候大小是不确定的,有可能是很小的一块空间也有可能是很大的一块空间,所以对堆空间也是需要一定的方法进行管理的,当你需要申请空间的时候按照你申请的大小以一定方法分配给你。
这就是分配算法,分配算法有很多种,对于不同的场合是有不同的分配算法的。在这里只简单的描述一下几种简单的算法。
1. 空闲链表
我们知道在一块堆空间中,能用的空间到时候是一块一块的分布着,空闲链表的方法就是把这些空闲的块用链表的方式进行连接,如果用户申请一块空间,就可以遍历这个空闲链表找到能够容纳你申请的大小的一个空闲块,然后拆分,把合适大小的一块返回给你;当用户释放一块空间时,在把这块空间链接在空闲链表上。下面来看一下它的结构:
这里画的是其中的一块,在这块空间的前面存放了两个指针,N代表next指针用来指向它的下一块空间,如果下一块为空,则指针指向NULL,P代表Prev指针,用来指向它的前一块空间,如果为NULL代表他就是空闲链表的第一块空间。
这里给出了一个只有三个块的空闲链表,它们的指针指向就是如图所指那样,此时我的块都是空闲的。假如我现在要申请一块空间,它是怎么实现的呢?
假设我要申请的空间大小刚好是第二块空间的大小,那么会把第二块空间的地址返回给我们,然后把这块空间从原来的空闲链表上删掉。此时链表如下图所示:
其实一般找到的空间都是比我们所申请的空间大的,然后把这块空间进行了拆分,一部分就是就是我们所申请的空间,另一部分为剩下的空间,它还是对应在原来的空心链表上。
这样就实现了一种简单的分配算法,其实在释放这块空间的时候,虽然知道指向这块空间的指针,但是堆并不知道这块空间的大小,那么它就不知道该释放多大的一块空间。所以其实在我们刚才说的分配算法里,在给用户分配空间的时候会多分配4个字节,作用就是用来存放这块空间的大小,这样的话在释放的时候找到这一块空间,就知道这块空间到底有多大,然后进行释放。
但是,我们知道有内存越界这种情况,就是在我们用内存的时候不小心用了它后面不属于它的空间,那么像空闲链表这种结构,可能就会把那块地方存放的两个指针进行了修改,这样不就破坏了我们的链表,然后整个空间就不能再使用了,这就是这种结构的缺点。
2. 位图
还有一种分配方式是位图,它是把我们的一块堆空间进行划分,划分成大小形同的一些块,当用户申请空间时,它会给我们分配整数个块以至于能存放你所申请的大小,第一个块是头,接下来的块都是用户申请的空间的主体,这样的话在位图中,我们的每一个快都有可能且只有可能有三种情况,就是头(Head)/主体(Body)/空闲(Free)。
下图就是一个例子,在下图中分配了两片内存,第一片占用了四个块,第二片占用的五个块。
这样做的话到底有什么好处呢,首先这样分配的话,对于这个堆的存储信息,它是记录在一个数组里。因为每一个小块是有三种可能状态,那么用二进制的两个位就能够表示了,假如设为00位空闲,10位主体,11为头。这样整个位图在一个数组中就能表示了。
这样每一个堆空间都可以用N个字节来表示,比如堆的大小为1Mb,分成1M/128=8000个块(每个块设为128字节),我们知道一个int是4个字节32位,那么能用8000/(32/2)=512个int来存储,所以一个大小为512int的数组就是一个完整的位图。
位图有它的优点:
比空闲链表更加稳定,因为可以对数组进行备份,而且就算某个块损坏,也不会影响整个位图其他的块空间。
速度比较快还容易管理。
同时也有它的缺点:
也容易造成块的浪费,因为毕竟它是整数倍的分配。
当堆比较大的时候,可能这个位图会很大,数组很庞大,可能效率也并不像想象那么快。