malloc函数分配c语言内存的规则是什么 ? 为什么我这段代码(stack- init函数)没有报错呢? 认为会报错的大致想法如下: 函数中,我先为结构体stack在堆中malloc出一片内存,内存大小应该为两个指针加上一个int的大小(可能因为内存对齐有所偏差)。 但我又malloc了一片大小为200✖️sizeof(space)的空间并把首地址赋值给了base,那岂不是说明base所掌管的空间从一个指针的空间存储大小变成了一大片空间存储大小。这样不会导致第一次malloc给stack的空间溢出吗? 这样的想法有什么错误的地方吗?
大家好,我是飞哥!今天我们来深入地了解一下malloc函数的内部工作原理。 在前面的文章中,我们介绍了 mmap、brk 等系统调用。但是这些系统调用在很多的时候,我们并不会直接使用。原因有以下两个系统调用管理的内存粒度太大。系统调用申请内存都是整页 4KB 起,但是我们平时编程的时候经常需要申请几十字节的小对象。如果使用 mmap 未免碎片率也太大了。频繁的系统调用的开销比较大。和函数调用比起来,系统的调用的开销非常的大。如果每次申请内存都发起系统调用,那么我们的应用程序将慢如牛。 所以,现代编程语言的做法都是自己在应用层实现了一个内存分配器。其思想都和内核自己用的 SLAB 内存分配器类似。都是内存分配器预先向操作系统申请一些内存,然后自己构造一个内存池。当我们申请内存的时候,直接由分配器从预先申请好的内存池里申请。当我们释放内存的时候,分配器会将这些内存管理起来,并通过一些策略来判断是否将其回收给操作系统。 通过这种方式既灵活地管理了各种不同大小的小对象,也避免了用户频率地调用 mmap 系统调用所造成的开销。常见的内存分配器有 glibc 中的 ptmalloc、Google 的 tcmalloc、Facebook 的 jemalloc 等等。我们在学校里学习 C 语言时候使用的 malloc 函数的底层就是 glibc 的 ptmalloc 内存分配器实现的。 我们今天就以最经(古)典(老)的 ptmalloc 内存分配器讲起,带大家深入地了解 malloc 函数的内部工作原理。最近我上线了很多技术视频,详情参见文件末尾~ 本文中需要用到 glibc 源码,下载地址是 http://ftp.gnu.org/gnu/glibc/ 。本文使用的源码版本是 2.12.1。 一、ptmalloc 内存分配器定义 1.1 分配区 arena 在 ptmalloc 中,使用分配区 arena 管理从操作系统中批量申请来的内存。 之所以要有多个分配区,原因是多线程在操作一个分配区的时候需要加锁。在线程比较多的时候,在锁上浪费的开销会比较多。为了降低锁开销,ptmalloc 支持多个分配区。这样在单个分配区上锁的竞争开销就会小很多。 在 ptmalloc 中存在一个全局的主分配区,是用静态变量的方式定义的。 分配区的数据类型是 struct malloc_state,其定义如下: 在分配区中,首先有一个锁。这是因为多个分配区只是能降低锁竞争的发生,但不能完全杜绝。所以还需要一个锁来应对多线程申请内存时的竞争问题。接下来就是分配区中内存管理的各种数据结构。这部分下个小节我们再详细看。 再看下 next 指针。通过这个指针,ptmalloc 把所有的分配区都以一个链表组织了起来,方便后面的遍历。
1.2 内存块 chunk 在每个 arena 中,最基本的内存分配的单位是 malloc_chunk,我们简称 chunk。它包含 header 和 body 两部分。这是 chunk 在 glibc 中的定义: 我们在开发中每次调用 malloc 申请内存的时候,分配器都会给我们分配一个大小合适的 chunk 出来,把 body 部分的 user data 的地址返回给我们。这样我们就可以向该地址写入和读取数据了。
如果我们在开发中调用 free 释放内存的话,其对应的 chunk 对象其实并不会归还给内核。而是由 glibc 又组织管理了起来。其 body 部分的 fd、bk 字段分别是指向上一个和下一个空闲的 chunk( chunk 在使用的时候是没有这两个字段的,这块内存在不同场景下的用途不同),用来当双向链表指针来使用。
1.3 空闲内存块链表 bins glibc 会将相似大小的空闲内存块 chunk 都串起来。这样等下次用户再来分配的时候,先找到链表,然后就可以从链表中取下一个素快速分配。这样的一个链表被称为一个 bin。ptmalloc 中根据管理的内存块的大小,总共有 fastbins、smallbins、largebins 和 unsortedbins 四类。 这四类 bins 分别定义在 struct malloc_state 的不同成员里。 fastbins 是用来管理尺寸最小空闲内存块的链表。其管理的内存块的最大大小是 MAX_FAST_SIZE。 SIZE_SZ 这个宏指的是指针的大小,在 32 位系统下,SIZE_SZ 等于 4 。在 64 位系统下,它等于 8。因为现在都是 64 位系统,所以本文中后面的例子中我们都是以 SIZE_SZ 为 8 来举例。所以在 64 位系统下,MAX_FAST_SIZE = 80 * 8 / 4 = 160 字节。 bins 是用来管理空闲内存块的主要链表数组。其链表总数为 2\ * NBINS 个,NBINS 的大小是 128,所以这里总共有 256 个空闲链表。 smallbins、largebins 和 unsortedbins 都使用的是这个数组。 另外top 成员是用来保存着特殊的 top chunk。当所有的空闲链表中都申请不到合适的大小的时候,会来这里申请。 1)fastbins 其中 fastbins 成员定义的是尺寸最小的素的链表。它存在的原因是,用户的应用程序中绝大多数的内存分配是小内存,这组 bin 是用于提高小内存的分配效率的。 fastbin 中有多个链表,每个 bin 链表管理的都是固定大小的 chunk 内存块。在 64 位系统下,每个链表管理的 chunk 素大小分别是 32 字节、48 字节、……、128 字节 等不同的大小。
glibc 中提供了 fastbin_index 函数可以快速地根据要申请的内存大小找到 fastbins 下对应的数组下标。 例如要申请的内存块大小是 32 字节,fastbin_index(32) 计算后可知应该到下标位 0 的空闲内存链表里去找。再比如要申请的内存块大小是 64 字节,fastbin_index(64) 计算后得知数组下标为 2。 2)smallbins smallbins 是在 malloc_state 下的 bins 成员中管理的。
smallbins 数组总共有 64 个链表指针,是由 NSMALLBINS 来定义的。 和 fastbin 一样,同一个 small bin 中的 chunk 具有相同的大小。small bin 在 64 位系统上,两个相邻的 small bin 中的 chunk 大小相差 16 字节(MALLOC_ALIGNMENT 是 2 个 SIZE_SZ,一个 SIZE_SZ 大小是 8)。 管理的 chunk 大小范围定义在 in_smallbin_range 中能看到。只要小于 MIN_LARGE_SIZE 的都属于 small bin 的管理范围。 通过上面的源码可以看出 MIN_LARGE_SIZE 的大小等于 64 * 16 = 1024。所以 small bin 管理的内存块大小是从 32 字节、48 字节、……、1008 字节。(在 glibc 64位系统中没有管理 16 字节的空闲内存,是 32 字节起的) 另外 glibc 也提供了根据申请的字节大小快速算出其在 small bin 中的下标的函数 smallbin_index。 例如要申请的内存块大小是 32 smallbin_index(32) 计算后可知应该到下标位 2 的空闲内存链表里去找。再比如要申请的内存块大小是 64 smallbin_index(64) 计算后得知数组下标为 3。 2)largebins largebins 和 smallbins 的区别是它管理的内存块比较大。其管理的内存是 1024 起的。 而且每两个相邻的 largebin 之间管理的内存块大小不再是固定的等差数列。这是为了用较少的链表数来更大块空闲内存的管理。 largebin_index_64 函数用来根据要申请的内存大小计算出其在 large bins 中的下标。 4)unsortedbins unsortedbins 比较特殊,它管理的内存块不再是和 smallbins 或 largebins 中那样是相同或者相近大小的。而是不固定,是被当做缓存区来用的。 当用户释放一个堆块之后,会先进入 unsortedbin。再次分配堆块时,ptmalloc 会优先检查这个链表中是否存在合适的堆块,如果找到了,就直接返回给用户(这个过程可能会对 unsortedbin 中的堆块进行切割)。若没有找到合适的,系统也会顺带清空这个链表上的素,把它放到合适的 smallbin 或者 large bin 中。 5)top chunk 另外还有有个独立于 fastbins、smallbins、largebins 和 unsortedbins 之外的一个特殊的 chunk 叫 top chunk。
如果没有空闲的 chunk 可用的时候,或者需要分配的 chunk 足够大,当各种 bins 都不满足需求,会从top chunk 中尝试分配。 二、malloc 的工作过程 在前面的小节里我们看到,glibc 在分配区 arena 中分别用 fastbins、bins(保存着 smallbins、largebins 和 unsortedbins)以及 top chunk 来管理着当前已经申请到的所有空闲内存块。 有了这些组织手段后,当用户要分配内存的时候,malloc 函数就可以根据其大小,从合适的 bins 中查找合适的 chunk。假如用户要申请 30 字节的内存,那就直接找到 32 字节这个 bin 链表,从链表头部摘下来一个 chunk 直接用。假如用户要申请 500 字节的内存,那就找到 512 字节的 bin 链表,摘下来一个 chunk 使用…… 当用户用完需要释放的时候,glibc 再根据其内存块大小,放到合适的 bin 下管理起来。下次再给用户申请时备用。 另外还有就是为 ptmalloc 管理的 chunk 可能会发生拆分或者合并。当需要申请小内存块,但是没有大小合适的时候,会将大的 chunk 拆成多个小 chunk。如果申请大内存块的时候,而系统中又存在大量的小 chunk 的时候,又会发生合并,以降低碎片率。 这样不管如何申请和释放,都不会导致严重的碎片问题发生。这就是 glibc 内存分配器的主要管理。了解了主要原理后,我们再来看下 malloc 函数的实现中,具体是怎么样来分配处理内存分配的。 malloc 在 glibc 中的实现函数名是 public_mALLOc。 在 public_mALLOc 中主要的逻辑就是选择分配区和锁操作,这是为了避免多线程冲突。真正的内存申请核心逻辑都在 _int_malloc 函数中。这个函数非常的长。为了清晰可见,我们把它的骨干逻辑列出来。 在一进入函数的时候,首先是调用 checked_request2size 对用户请求的字节数进行规范化。因为分配区中管理的都是 32、64 等对齐的字节数的内存。如果用户请求 30 字节,那么 ptmalloc 会对齐一下,然后按 32 字节为其申请。 接着是从分配区的各种 bins 中尝试为用户分配内存。总共包括以下几次的尝试。1 如果申请字节数小于 fast bins 管理的内存块最大字节数,则尝试从 fastbins 中申请内存,申请成功就返回2 如果申请字节数小于 small bins 管理的内存,则尝试从 smallbins 中申请内存,申请成功就返回3 尝试从 unsorted bins 中申请内存,申请成功就返回4 尝试从 large bins 中申请内存,申请成功就返回5 如果前面的步骤申请都没成功,尝试从 top chunk 中申请内存并返回6 从操作系统中使用 mmap 等系统调用申请内存 在这些分配尝试中,一旦某一步申请成功了,就会返回。后面的步骤就不需要进行了。 最后在 top chunk 中也没有足够的内存的时候,就会调用 sYSMALLOc 来向操作系统发起内存申请。 在 sYSMALLOc 中,是通过 mmap 等系统调用来申请内存的。 另外还有就是穿插在这些的尝试中间,可能会涉及到 chunk 的切分,将大块的 chunk 切分成较小的返回给用户。也可能涉及到多个小 chunk 的合并,用来降低内存碎片率。 以上就是 glibc 中的 ptmalloc 管理堆内存的基本原理。最近我上线了技术视频,具体内容包括硬件原理、内存管理、进程管理、文件系统、网络管理、容器底层原理、Golang 语言运行时、性能观测和性能优化共计九大部分。目前正在持续更新中,详情参见开发内功修炼视频上线。也欢迎大家「开发内功修炼」
2024最新激活全家桶教程,稳定运行到2099年,请移步至置顶文章:https://sigusoft.com/99576.html
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。 文章由激活谷谷主-小谷整理,转载请注明出处:https://sigusoft.com/57001.html