1. malloc源码分析—_int_malloc
根据上一章的分析,malloc会调用__libc_malloc
分配内存,__libc_malloc
会调用malloc_hook_ini
进行初始化,然后回调__libc_malloc
函数,这时候会执行_int_malloc
开始分配内存,定义在malloc.c中,因为非常长,这里分段来看,
1.1 _int_malloc第一部分
|
|
首先调用checked_request2size
将需要分配的内存大小bytes转换为chunk的大小。checked_request2size
是个宏定义,主要调用request2size进行计算,
|
|
为了说明request2size,首先看一下ptmalloc中关于chunk的定义,
|
|
当一个chunk为空闲时,至少要有prev_size
、size
、fd
和bk
四个参数,因此MINSIZE就代表了这四个参数需要占用的内存大小;而当一个chunk被使用时,prev_size
可能会被前一个chunk用来存储,fd
和bk
也会被当作内存存储数据,因此当chunk被使用时,只剩下了size
参数需要设置,request2size
中的SIZE_SZ
就是INTERNAL_SIZE_T
类型的大小,因此至少需要req+SIZE_SZ
的内存大小。MALLOC_ALIGN_MASK
用来对齐,因此request2size就计算出了所需的chunk的大小。
传入的参数av是在上一章__libc_malloc
中调用arena_get
获得的分配去指针,如果为null,就表示没有分配区可用,这时候就直接调用sysmalloc
通过mmap获取chunk。
1.2 sysmalloc
sysmalloc
的代码很长,但只有前面一小部分是这里需要分析的,
|
|
首先,可以直接通过mmap分配chunk有两个前提条件,一是需要分配的内存大小大于实用mmap进行分配的阀值mp_.mmap_threshold
,二是通过mp_.n_mmaps
判断系统还可以有可以使用mmap分配的空间。
下面就要计算需要分配多少内存,在前面已经通过request2size
计算了需要分配的内存大小,这里为什么还要计算呢?这是因为通过使用mmap直接分配的chunk不需要添加到链表中,因此不存在前后关系,当一个chunk被使用时,不能借用后一个chunk的prev_size
字段,这里需要把该字段的长度SIZE_SZ加上。并且这里假设MALLOC_ALIGNMENT == 2 * SIZE_SZ
。
接下来判断需要分配的内存大小是否会溢出,然后就调用MMAP
分配内存,MMAP
是一个宏定义,最后就是通过系统调用来分配内存,后面来看这个函数。
再往下就是通过set_head
在chunk中的size参数里设置标志位,因为chunk是按8字节对齐的,而size标识chunk占用的字节数,所以最后三位是没有用的,ptmalloc将这三位用来作为标志位,这里便是设置其中一个标志位,用来标识该chunk是直接通过mmap分配的。
|
|
设置完标志位后,接下来就是设置全局变量_mp
,将mp_.n_mmaps
加1,表示当前进程通过mmap分配的chunk个数,对应的mp_.max_n_mmaps
表示最大chunk个数。mp_.mmapped_mem
标识已经通过mmap分配的内存大小,mp_.max_mmapped_mem
对应可分配内存的最大值。其中,atomic_exchange_and_add
b表示原子加,atomic_max
则是原子取最大值。
最后,通过chunk2mem返回chunk中内存的起始指针。
|
|
这里也可以知道,当chunk被使用时,用户是从结构体中的变量fd开始使用内存的。回到_int_malloc函数中,假设通过sysmalloc分配成功,接下来就需要调用alloc_perturb对刚刚分配的内存进行初始化,
|
|
刚函数没有什么实际意义,所以不管它。
1.3 MMAP
为了方便分析,这里贴一段调用MMAP的代码,
|
|
MMAP在glibc中为宏定义,其定义很长,这里简单将它改写,
|
|
__libc_do_syscall
是一段汇编代码,最后就是系统调用啦,这里就进入了linux内核中的代码,在arch/x86/entry/syscalls/syscall_32.tbl中有如下定义,
|
|
因此,MMAP最后调用linux内核中的sys_mmap_pgoff
函数,定义在mm/mmap.c中,
|
|
SYSCALL_DEFINE6
是个宏定义,就是将系统调用号和函数联系起来,这里其实就是定义了sys_mmap_pgoff
函数。根据前面传入的flags,这里直接跳过判断,因此下面主要就是执行vm_mmap_pgoff
函数,定义在mm/utils.c中,
|
|
这里首先获得进程的mm_struct
结构,该结构保存了虚拟内存和物理内存的映射关系,security_mmap_file
和linux安全有关,这里不关心,因此调用do_mmap_pgoff
执行主要的mmap内容,前后加了信号量。do_mmap_pgoff
定义在mm/mmap.c中,这里省略了很多不关键的代码,
1.4 do_mmap_pgoff
|
|
传入的flags没有MAP_FIXED
,表是映射的地址不固定(这里传入的addr
为0),由内核分配。接下来通过调用round_hint_to_min
和PAGE_ALIGN
对地址和长度进行页对齐,并且检查地址是否溢出或者太小。
下面调用get_unmapped_area
在进程的用户空间里查找已经分配的虚拟内存。
|
|
这里首先获取get_area
函数指针用来查找用户空间中已经分配的虚拟内存,这里根据mmap的方向可以获取到arch_get_unmapped_area_topdown
或者arch_get_unmapped_area
两个函数指针,其arch_get_unmapped_area_topdown
对应的mmap方向是从高地址往低地址方向扩展的,本章还是分析传统的从低地址往高地址拓展对应的arch_get_unmapped_area
函数,
|
|
首先,如果是固定地址映射,直接返回addr地址。本章分析的不是这种情况,省略的代码和一些随机映射有关,这里省略了不分析。这样就进入了底下的if语句里,对地址对齐后,就调用find_vma查找addr地址开始已经分配出去的虚拟内存vma,最后addr到addr+len这个地址范围内没有虚拟内存,就将地址返回。
|
|
这里就不往下继续看代码的,简单来说,进程已经分配的虚拟内存保存在一个红黑树中,红黑树简单的作用就是防止一个树结构不平衡,出现某个左子树严重大于右子树的情况。为了加快查找的速度,这里设立了缓存。通过观察while结构,这里就是查找第一个结束地址大于addr的已经分配的虚拟内存,然后返回。
回到do_mmap_pgoff
中,calc_vm_prot_bits
和calc_vm_flag_bits
用来将prot和flags中的标志位转化为vm的标志位,例如prot中的PROT_READ
转化为VM_READ
,flags中的MAP_GROWSDOWN
转化为VM_GROWSDOWN
。根据前面prot和flags中的值,这里转化后,vm_flags
为VM_READ|VM_WRITE|mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC
。最后就调用mmap_region构造一个vma用来保存刚刚获得的虚拟内存。
1.5 mmap_region
为了方便分析和查看,这里对mmap_region代码做了适当的删除和改写,
|
|
may_expand_vm
用于判断加上即将分配的虚拟内存,是否超过了系统的限制,如果超过了就需要进行相应的操作或者返回错误,这里假设不会超过系统限制,不管它。find_vma_links
的定义如下,
|
|
该函数做了两件事,第一件事是重新检查一遍即将分配的虚拟内存是否已经被使用,主要是其他进程可能在这期间分配了该虚拟内存,第二件事是确定即将插入红黑树中的位置,保存在prev
、rb_link
和rb_parent
中。prev
保存了虚拟内存结束地址小于即将分配的虚拟内存开始地址的红黑树节点,rb_link
一般为null,rb_parent
简单说就是保存了离即将分配的虚拟内存开始地址最近的红黑树节点。
再往下通过vma_merge函数查看是否有虚拟空间可以合并,如果有则合并并返回。
|
|
这里就不详细分析这个函数了,主要通过prev->vm_end == addr
判断即将分配的虚拟内存能否往前合并,通过end == next->vm_start
判断即将分配的虚拟内存能否往后合并。其中,合并函数为vma_adjust
。再往下就不分析了。
回到函数中,假设不能合并,就要通过slab构造一个vm_area_struct
结构体,并设置相应的信息,slab是linux内核中分配小块内存的框架。然后通过vma_link
插入到进程的红黑树中,
|
|
__vma_link
执行实际的插入操作,就是一些红黑树的操作,不往下看了。
回到mmap_region
中,最后通过vma_set_page_prot
继续设置一些标志位,然后就返回分配到的虚拟内存的起始地址addr了,该返回值一直向上返回,然后退出系统调用,返回到glibc中。
到这里简单总结一下MMAP,其实质就是通过mmap在进程的内存管理结构中的红黑树中分配一块没有使用的虚拟内存。
1.6 _int_malloc — fastbin
|
|
get_max_fast
返回fastbin可以存储内存的最大值,它在ptmalloc的初始化函数malloc_init_state
中定义,后面会分析这个函数。
如果需要分配的内存大小nb落在fastbin的范围内,首先调用fastbin_index
获得chunk大小nb
对应的fastbin索引。
|
|
减2是根据fastbin存储的内存最小值计算的,本章假设SIZE_SZ=4
,因此改写后idx = nb/8-2
。
获得索引idx后,就通过fastbin取出空闲chunk链表指针,mfastbinptr
其实就是malloc_chunk
指针,
|
|
下面的do、while循环又是一个CAS操作,其作用是从刚刚得到的空闲chunk链表指针中取出第一个空闲的chunk(victim),并将链表头设置为该空闲chunk的下一个chunk(victim->fd)。这里注意,fastbin中使用的是单链表,而后面smallbin使用的是双链表。
获得空闲chunk后,需要转换为可以存储的内存指针,chunk2mem
上一章分析过了,就是返回malloc_chunk
结构中fd所在的位置,因为当一个chunk被使用时,malloc_chunk
结构中fd
、bk
包括后面的变量都没有用了。最后调用alloc_perturb
对用户使用的内存进行初始化,然后就返回该内存的指针了。
假设fastbin中没有找到空闲chunk,或者fastbin根本没有初始化,或者其他原因,就进入下一步,从smallbin中获取内存,因此继续往下看.
1.7 _int_malloc — smallbin & largebin
|
|
首先
|
|
基于本章假设,MIN_LARGE_SIZE
经过换算后为512字节,因此低于512字节大小的内存块都归smallbin管理。
接下来通过bin_at
获得smallbin空闲chunk链表指针,
|
|
这里乘2,并且减去fd相对于malloc_chunk
中的位置是因为smallbin中存储的是fd和bk指针。last
定义为
|
|
该函数获得chunk的前一个chunk,由因为该chunk是smallbin的链表头,因此获得的是最后一个chunk,如果两者相等,表示对应的链表为空,什么都不做。
这里假设不相等,接下来有两种情况,第一种是victim=0
,表示smallbin还没有初始化,这里需要特别说明一下这里。smallbin初始化为malloc_chunk
指针数组,虽然定义为指针数组,但实际上存储的是fd和bk指针,如下所示
|fd|bk|fd|bk|…|fd|bk|
当smallbin还未初始化时,假设idx=1
,根据bin_at
取出的bin
是一个虚拟的malloc_chunk
指针,bin->fd
,是第二个fd,因此bin->bk
就是对应的bk,其值为0(bin->bk取出的不是地址,而是值)。因此当victim
为0时,可以断定smallbin未初始化,此时调用malloc_consolidate
进行初始化,
|
|
省略代码的if语句里是将fastbin中的chunk进行合并,然后添加到bins中,这里不分析,因为还未初始化,因此get_max_fast
返回0,后面的章节碰到了再分析。进入else部分,check_malloc_state
为空函数,malloc_init_state
就是主要的初始化函数,
|
|
该函数做了四件事情,第一是初始化malloc_state
中的bins
数组,初始化的结果是对bins
数组中的每一个fd
和对应的bk
,都初始化为fd
的地址,即fd=bk=&fd
;第二是设置fastbin可管理的内存块的最大值,即global_max_fast
,DEFAULT_MXFAST
定义为,
|
|
本章假设为64,set_max_fast
定义为
|
|
第三是设置一些标志位;第四是初始化分配去中的top chunk,就是一个malloc_chunk
指针,fd
保存在bins[0]
中(smallbin中不使用bins[0]
和bins[1]
)。
重新回到_int_malloc
中,假设victim
不为0,下面就从双向链表中取出victim
,设置其中的标志位,然后返回用户可分配的内存指针。
假设smallbin中没有空闲chunk可用,下面就要开始寻找largebin了,largebin_index
定义为
|
|
根据前面SIZE_SZ
的假设,这里largebin_index
对应的就是largebin_index_32
,定义为
|
|
这里就不多解释了,如果需要知道sz和索引的对应关系,可以自己计算一下。
再接下来have_fastchunks
根据标志位判断fastbin中是否有空闲chunk,如果有,就调用malloc_consolidate
将这些chunk和并,然后加入到unsortedbin中。
1.8 _int_malloc — 合并fastbin
下面重新看一下malloc_consolidate
函数。
|
|
因为ptmalloc前面已经初始化过了,这里直接进入if内部,首先通过clear_fastchunks
设置标志位表示fastbin中存在空闲chunk,
|
|
然后通过unsorted_chunks
获得bins数组中unsortedbin对应的malloc_chunk
指针(其fd
和bk
指针对应bins[0]
和bins[1]
)。
|
|
再往下,将fastbin中的最大和最小的chunk对应的malloc_chunk
指针赋值给maxfb
和fb
,然后通过do,while循环遍历fastbin中的每个chunk链表,atomic_exchange_acq
又是一个CAS操作,该函数取出fb
指针,并将原来的chunk链表头指针的值设为0,表示chunk链表空闲了。然后开始进入内层的循环,这里遍历的是每个chunk链表中的每个malloc_chunk
指针。
接下来首先去除chunk中的PREV_INUSE
和NON_MAIN_ARENA
标志,为了获得chunk的大小(size中的最低三位被用来作为标志位,并且fastbin中chunk的标志位IS_MMAPPED
默认为0)。然后通过chunk_at_offset
和chunksize
获得下一个chunk以及其大小,
|
|
再往下,如果chunk的前一个chunk没在使用中,就合并该chunk与前一个chunk,主要是重新计算malloc_chunk
的指针,并调用unlink
将前一个chunk从bins数组中删除,
|
|
简单来说,该宏定义就是将前一个chunk从两个双线链表中删除,fd
和bk
指针构成的双向链表存在于smallbin和largebin中,fd_nextsize
和bk_nextsize
指针构成的双向链表只存在于largebin中。
再往下,如果相邻的下一个chunk不是top chunk,并且下一个chunk不在使用中,就继续合并,否则,就清除下一个chunk的PREV_INUSE
,表示该chunk已经空闲了。
然后将刚刚合并完的chunk添加进unsorted_bin
中,unsorted_bin
也是一个双向链表。
如果合并完的chunk属于smallbin的大小,则需要清除fd_nextsize
和bk_nextsize
,因为smallbin中的chunk不会使用这两个指针。并且通过setHead
保证不会有相邻的两个chunk都空闲,并且通过setFoot
设置下一个chunk的prev_size
。
如果相邻的下一个chunk是top chunk,则将合并完的chunk继续合并到top chunk中。
至此,malloc_consolidate
就分析完了,总结一下,malloc_consolidate
就是遍历fastbin中每个chunk链表的每个malloc_chunk
指针,合并前一个不在使用中的chunk,如果后一个chunk是top chunk,则直接合并到top chunk中,如果后一个chunk不是top chunk,则合并后一个chunk并添加进unsorted_bin
中。
1.9 _int_malloc — unsortedbin
|
|
这部分代码的整体意思就是遍历unsortedbin,从中查找是否有符合用户要求大小的chunk并返回。
第一个while循环从尾到头依次取出unsortedbin中的所有chunk,将该chunk对应的前一个chunk保存在bck
中,并将大小保存在size
中。
如果用户需要分配的内存大小对应的chunk属于smallbin,unsortedbin中只有这一个chunk,并且该chunk属于last remainder chunk且其大小大于用户需要分配内存大小对应的chunk大小加上最小的chunk大小(保证可以拆开成两个chunk),就将该chunk拆开成两个chunk,分别为victim
和remainder
,进行相应的设置后,将用户需要的victim
返回。
如果不能拆开,就从unsortedbin中取出该chunk(victim
)。
再下来,如果刚刚从unsortedbin中取出的victim
正好是用户需要的大小nb
,就设置相应的标志位,直接返回该victim
。
继续往下看_int_malloc
函数,为了使整个代码结构清晰,这里保留了外层的for循环和while循环。
|
|
这部分代码的整体意思是如果从unsortedbin中取出的chunk不符合用户要求的大小,就将该chunk合并到smallbin或者largebin中。
首先如果取出的chunk(victim)属于smallbin,就通过smallbin_index
计算需要插入的位置victim_index
,然后获取smallbin中对应位置的链表头指针保存在bck
中,最后直接插入到smallbin中,由于smallbin中的chunk不使用fd_nextsize
和bk_nextsize
指针,插入操作只需要更新bk
和fd
指针,具体的插入操作在后面。这里需解释一下,fd_nextsize
指针指向的是chunk双向链表中下一个大小不同的chunk,bk_nextsize
指向的是chunk双向链表中前一个大小不同的chunk。
如果取出的chunk(victim)属于largebin,通过largebin_index
计算需要插入的位置victim_index
,然后获取largebin链表头指针保存在bck
中。
如果fwd
等于bck
,即bck->fd=bck
,则表示largebin中对应位置上的chunk双向链表为空,直接进入后面的else部分中,代码victim->fd_nextsize = victim->bk_nextsize = victim
表示插入到largebin中的victim是唯一的chunk,因此其fd_nextsize
和bk_nextsize
指针都指向自己。
如果fwd
不等于bck
,对应的chunk双向链表存在空闲chunk,这时就要在该链表中找到合适的位置插入了。因为largebin中的chunk链表是按照chunk大小从大到小排序的,如果victim
的size
小于bck->bk->size
即最后一个chunk的大小,则表示即将插入victim
的大小在largebin的chunk双向链表中是最小的,因此要把victim
插入到该链表的最后。在这种情况下,经过操作后的结果如下所示(因为我手边的画图软件有限,这里就用符号“|”隔出数组,然后从代码中摘除核心的fd_nextsize
和bk_nextsize
指针的修改操作,对照这两个指针的意思,很容易看懂),
|
|
如果要插入的victim
的size
不是最小的,就要通过一个while循环遍历找到合适的位置,这里是从双向链表头bck->fd
开始遍历,利用fd_nextsize
加快遍历的速度,找到第一个size>=fwd->size
的chunk。如果size=fwd->size
,就只是改变victim
以及前后相应chunk的bk
、fd
指针就行。这里要特别注意,先做一个假设,假设chunk双向链表中A、B、C是三个不同大小的chunk集合,A集合有A0=A1=…,B集合有B0=B1=…,C集合有C0=C1=…,然后构造chunk链表,
| A0 | A1 | A2 | B0 | B1 | B2 | C0 | C1 | C2 |
特别注意,在ptmalloc中,只有A0、B0、C0可以配置fd_nextsize
和bk_nextsize
指针,其他位置是不用配置这两个指针的。因此相同大小的chunk只有最低地址的chunk会设置fd_nextsize
和bk_nextsize
指针。根据这个假设,很容易知道当两个size相等时,为什么要执行fwd = fwd->fd
,因为要保证插入victim的位置是相同大小的chunk的右边,即高地址的地方。插入后的链表如下,
|
|
链表中所有chunk的fd_nextsize
和bk_nextsize
指针不变。
再往下,如果size
不相等,则直接插入在fwd
的左边,这样也能保证所有的fd_nextsize
和bk_nextsize
指针设置在相同chunk大小的最地地址处(最左边)。插入后的链表如下,
|
|
再往下,mark_bin
用来标识malloc_state
中的binmap
,标识相应位置的chunk空闲。然后就更改fd
、bk
指针,插入到双向链表中,这个插入操作同时适用于smallbin和largebin,因此放在这里。最后如果在unsortedbin中处理了超过10000个chunk,就直接退出循环,这里保证不会因为unsortedbin中chunk太多,处理的时间太长了。
在这部分代码中,有个size |= PREV_INUSE
是暂时让我比较费解的地方,经过分析后,暂时认为size |= PREV_INUSE
是没必要的,虽然不会产生错误,也不会影响largebin中的排序,并且注释说这行代码能加速排序,但没看出能加速的地方,经过分析这里反而会多出一个无效的指针赋值,希望往后看代码时能解决这里的问题,或者希望有人能解答这行代码的具体作用。
1.10 _int_malloc — largebin
回到_int_malloc
函数中,继续往下看,为了使整个代码结构清晰,这里继续保留了外层的for循环。
|
|
这部分代码的整体意思就是要尝试从largebin中取出对应的chunk了。
这里的idx
是在前面(上一章分析的_int_malloc
函数前面一部分中)根据宏largebin_index
计算的。接下来就要根据idx
获得largebin中的双向链表头指针bin
,然后从bin->bk
开始从尾到头(根据chunk大小从小到大)遍历整个双向链表,找到第一个大于用户需要分配的chunk大小nb
的chunk指针victim
。
找到victim
后,需要将其拆开成两部分,第一部分是要返回给用户的chunk,第二部分分为两种情况,如果其大小小于MINSIZE
,则不能构成一个最小chunk,这种情况下就将拆开前的整个victim
返回给用户;如果大于MINSIZE
,就将拆开后的第二部分remainder
插入到unsortedbin中,然后把第一部分victim
返回给用户。
继续往下看_int_malloc
函数,
|
|
这一部分的整体意思是,前面在largebin中寻找特定大小的空闲chunk,如果没找到,这里就要遍历largebin中的其他更大的chunk双向链表,继续寻找。
开头的++idx
就表示,这里要从largebin中下一个更大的chunk双向链表开始遍历。ptmalloc中用一个bit表示malloc_state
的bins
数组中对应的位置上是否有空闲chunk,bit为1表示有,为0则没有。ptmalloc通过4个block(一个block 4字节)一共128个bit管理bins
数组。因此,代码中计算的block表示对应的idx
属于哪一个block,map
就表是block对应的bit组成的二进制数字。
接下来进入for循环,如果bit > map
,表示该map
对应的整个block
里都没有大于bit
位置的空闲的chunk,因此就要找下一个block
。因为后面的block
只要不等于0,就肯定有空闲chunk,并且其大小大于bit
位置对应的chunk,下面就根据block
,取出block
对应的第一个双向链表的头指针。这里也可以看出,设置map
和block
也是为了加快查找的速度。如果遍历完所有block
都没有空闲chunk,这时只能从top chunk里分配chunk了,因此跳转到use_top
。
如果有空闲chunk,接下来就通过一个while循环依次比较找出到底在哪个双向链表里存在空闲chunk,最后获得空闲chunk所在的双向链表的头指针bin
和位置bit
。
接下来,如果找到的双向链表又为空,则继续前面的遍历,找到空闲chunk所在的双向链表的头指针bin
和位置bit
。如果找到的双向链表不为空,就和上面一部分再largebin中找到空闲chunk的操作一样了,这里就不继续分析了。
1.11 _int_malloc — topchunk
继续往下看_int_malloc
,
|
|
这里就是_int_malloc
的最后一部分了,这部分代码的整体意思分为三部分,首先从top chunk中尝试分配内存;如果失败,就检查fastbin中是否有空闲内存了(其他线程此时可能将释放的chunk放入fastbin中了),如果不空闲,就合并fastbin中的空闲chunk并放入smallbin或者largebin中,然后会回到_int_malloc
函数中最前面的for循环,重新开始查找空闲chunk;如果连fastbin中都没有空闲内存了,这时只能通过sysmalloc
从系统分配内存了,该函数前面几章里已经分析过了一部分了,下一章会再次进入这个函数进行分析。这部分代码很简单,关键函数前面几章都碰到过了,这里就不详细分析了。
2. 总结
下面简单总结一遍_int_malloc
函数的整体思路。
第一步:如果进程没有关联的分配区,就通过sysmalloc
从操作系统分配内存。
第二步:从fastbin查找对应大小的chunk并返回,如果失败进入第三步。
第三步:从smallbin查找对应大小的chunk并返回,或者将fastbin中的空闲chunk合并放入unsortedbin中,如果失败进入第四步。
第四步:遍历unsortedbin,从unsortedbin中查找对应大小的chunk并返回,根据大小将unsortedbin中的空闲chunk插入smallbin或者largebin中。进入第五步。
第五步:从largebin指定位置查找对应大小的chunk并返回,如果失败进入第六步。
第六步:从largebin中大于指定位置的双向链表中查找对应大小的chunk并返回,如果失败进入第七步。
第七步:从topchunk中分配对应大小的chunk并返回,topchunk中没有足够的空间,就查找fastbin中是否有空闲chunk,如果有,就合并fastbin中的chunk并加入到unsortedbin中,然后跳回第四步。如果fastbin中没有空闲chunk,就通过sysmalloc
从操作系统分配内存。