brk()系统调用能动态调整进程数据段大小吗?

2026-04-16 10:363阅读0评论SEO资源
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计1773个文字,预计阅读时间需要8分钟。

brk()系统调用能动态调整进程数据段大小吗?

malloc() 不是系统调用也不是运算符,而是 C 库中的函数,用于动态分配内存。在请求内存时,会以两种方式向操作系统申请堆内存:

方式一:通过 brk() 系统调用从 堆 中分配内存。

brk()系统调用能动态调整进程数据段大小吗?

我们知道malloc() 并不是系统调用,也不是运算符,而是 C 库里的函数,用于动态分配内存。

malloc 申请内存的时候,会有两种方式向操作系统申请堆内存:

  • 方式一:通过 brk() 系统调用从堆分配内存
  • 方式二:通过 mmap() 系统调用在文件映射区域分配内存;

一、brk()系统调用

1、brk()的申请方式

一般如果用户分配的内存小于 128 KB,则通过 brk() 申请内存。而brk()的实现的方式很简单,就是通过 brk() 函数将堆顶指针向高地址移动,获得新的内存空间。

malloc 通过brk()方式申请的内存,free 释放内存的时候,并不会把内存归还给操作系统,而是缓存在 malloc 的内存池中,待下次使用,这样就可以重复使用。

2、brk()系统调用的优缺点

所以使用brk()方式的点很明显:可以减少缺页异常的发生,提高内存访问效率。

但它的缺点也同样明显:由于申请的内存没有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。brk()方式之所以会产生内存碎片,是由于brk通过移动堆顶的位置来分配内存,并且使用完不会立即归还系统,重复使用,如果高地址的内存不释放,低地址的内存是得不到释放的。

正是由于使用brk()会出现内存碎片,所以在我们申请大块内存的时候才会使用mmap()方式,mmap()是以页为单位进行内存分配和管理的,释放后就直接归还系统了,所以不会出现这种小碎片的情况。

3、brk()系统调用的优化

一、Ptmalloc :malloc采用的是内存池的管理方式,Ptmalloc 采用边界标记法将内存划分成很多块,从而对内存的分配与回收进行管理。为了内存分配函数malloc的高效性,ptmalloc会预先向操作系统申请一块内存供用户使用,当我们申请和释放内存的时候,ptmalloc会将这些内存管理起来,并通过一些策略来判断是否将其回收给操作系统。这样做的最大好处就是,使用户申请和释放内存的时候更加高效,避免产生过多的内存碎片。

二、Tcmalloc:Ptmalloc在性能上还是存在一些问题的,比如不同分配区(arena)的内存不能交替使用,比如每个内存块分配都要浪费8字节内存等等,所以一般倾向于使用第三方的malloc。

Tcmalloc是Google gperftools里的组件之一。全名是 thread cache malloc(线程缓存分配器)其内存管理分为线程内存和中央堆两部分。

1.小块内部的分配:对于小块内存分配,其内部维护了60个不同大小的分配器(实际源码中看到的是86个),和ptmalloc不同的是,它的每个分配器的大小差是不同的,依此按8字节、16字节、32字节等间隔开。在内存分配的时候,会找到最小符合条件的,比如833字节到1024字节的内存分配请求都会分配一个1024大小的内存块。如果这些分配器的剩余内存不够了,会向中央堆申请一些内存,打碎以后填入对应分配器中。同样,如果中央堆也没内存了,就向中央内存分配器申请内存。

在线程缓存内的60个分配器分别维护了一个大小固定的自由空间链表,直接由这些链表分配内存的时候是不加锁的。但是中央堆是所有线程共享的,在由其分配内存的时候会加自旋锁(spin lock)。

2.大内存的分配:对于大内存分配(大于8个分页, 即32K),tcmalloc直接在中央堆里分配。中央堆的内存管理是以分页为单位的,同样按大小维护了256个空闲空间链表,前255个分别是1个分页、2个分页到255个分页的空闲空间,最后一个是更多分页的小的空间。这里的空间如果不够用,就会直接从系统申请了。

3.ptmalloc与tcmalloc的不足:都是针对小内存分配和管理;对大块内存还是直接用了系统调用。应该尽量避免大内存的malloc/new、free/delete操作。频繁分配小内存,例如:对bool、int、short进行new的时候,造成内存浪费。

三、Jemalloc: jemalloc 是由 Jason Evans 在 FreeBSD 项目中引入的新一代内存分配器。它是一个通用的malloc实现,侧重于减少内存碎片和提升高并发场景下内存的分配效率,其目标是能够替代 malloc。下面是Jemalloc的两个重要部分:

1.arena:arena 是 jemalloc 最重要的部分,内存由一定数量的 arenas 负责管理。每个用户线程都会被绑定到一个 arena 上,线程采用 round-robin 轮询的方式选择可用的 arena 进行内存分配,为了减少线程之间的锁竞争,默认每个 CPU 会分配 4 个 arena,各个 arena 所管理的内存相互独立。

struct arena_s { atomic_u_t nthreads[2]; tsdn_t *last_thd; arena_stats_t stats; // arena的状态 ql_head(tcache_t) tcache_ql; ql_head(cache_bin_array_descriptor_t) cache_bin_array_descriptor_ql; malloc_mutex_t tcache_ql_mtx; prof_accum_t prof_accum; uint64_t prof_accumbytes; atomic_zu_t offset_state; atomic_zu_t extent_sn_next; // extent的序列号生成器状态 atomic_u_t dss_prec; atomic_zu_t nactive; // 激活的extents的page数量 extent_list_t large; // 存放 large extent 的 extents malloc_mutex_t large_mtx; // large extent的锁 extents_t extents_dirty; // 刚被释放后空闲 extent 位于的地方 extents_t extents_muzzy; // extents_dirty 进行 lazy purge 后位于的地方,dirty -> muzzy extents_t extents_retained; // extents_muzzy 进行 decommit 或 force purge 后 extent 位于的地方,muzzy -> retained arena_decay_t decay_dirty; // dirty --> muzzy arena_decay_t decay_muzzy; // muzzy --> retained pszind_t extent_grow_next; pszind_t retain_grow_limit; malloc_mutex_t extent_grow_mtx; extent_tree_t extent_avail; // heap,存放可用的 extent 元数据 malloc_mutex_t extent_avail_mtx; // extent_avail的锁 bin_t bins[NBINS]; // 所有用于分配小内存的 bin base_t *base; // 用于分配元数据的 base nstime_t create_time; // 创建时间 };

2.extent:管理 jemalloc 内存块(即用于用户分配的内存)的结构,每一个内存块大小可以是 N * page_size(4KB)(N >= 1)。每个 extent 有一个序列号(serial number)。一个 extent 可以用来分配一次 large_class 的内存申请,但可以用来分配多次 small_class 的内存申请。

struct extent_s { uint64_t e_bits; // 8字节长,记录多种信息 void *e_addr; // 管理的内存块的起始地址 union { size_t e_size_esn; // extent和序列号的大小 size_t e_bsize; // 基本extent的大小 }; union { /* * S位图,当此 extent 用于分配 small_class 内存时,用来记录这个 extent 的分配情况, * 此时每个 extent 的内的小内存称为 region */ arena_slab_data_t e_slab_data; atomic_p_t e_prof_tctx; // 一个计数器,用于large object }; }


本文共计1773个文字,预计阅读时间需要8分钟。

brk()系统调用能动态调整进程数据段大小吗?

malloc() 不是系统调用也不是运算符,而是 C 库中的函数,用于动态分配内存。在请求内存时,会以两种方式向操作系统申请堆内存:

方式一:通过 brk() 系统调用从 堆 中分配内存。

brk()系统调用能动态调整进程数据段大小吗?

我们知道malloc() 并不是系统调用,也不是运算符,而是 C 库里的函数,用于动态分配内存。

malloc 申请内存的时候,会有两种方式向操作系统申请堆内存:

  • 方式一:通过 brk() 系统调用从堆分配内存
  • 方式二:通过 mmap() 系统调用在文件映射区域分配内存;

一、brk()系统调用

1、brk()的申请方式

一般如果用户分配的内存小于 128 KB,则通过 brk() 申请内存。而brk()的实现的方式很简单,就是通过 brk() 函数将堆顶指针向高地址移动,获得新的内存空间。

malloc 通过brk()方式申请的内存,free 释放内存的时候,并不会把内存归还给操作系统,而是缓存在 malloc 的内存池中,待下次使用,这样就可以重复使用。

2、brk()系统调用的优缺点

所以使用brk()方式的点很明显:可以减少缺页异常的发生,提高内存访问效率。

但它的缺点也同样明显:由于申请的内存没有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。brk()方式之所以会产生内存碎片,是由于brk通过移动堆顶的位置来分配内存,并且使用完不会立即归还系统,重复使用,如果高地址的内存不释放,低地址的内存是得不到释放的。

正是由于使用brk()会出现内存碎片,所以在我们申请大块内存的时候才会使用mmap()方式,mmap()是以页为单位进行内存分配和管理的,释放后就直接归还系统了,所以不会出现这种小碎片的情况。

3、brk()系统调用的优化

一、Ptmalloc :malloc采用的是内存池的管理方式,Ptmalloc 采用边界标记法将内存划分成很多块,从而对内存的分配与回收进行管理。为了内存分配函数malloc的高效性,ptmalloc会预先向操作系统申请一块内存供用户使用,当我们申请和释放内存的时候,ptmalloc会将这些内存管理起来,并通过一些策略来判断是否将其回收给操作系统。这样做的最大好处就是,使用户申请和释放内存的时候更加高效,避免产生过多的内存碎片。

二、Tcmalloc:Ptmalloc在性能上还是存在一些问题的,比如不同分配区(arena)的内存不能交替使用,比如每个内存块分配都要浪费8字节内存等等,所以一般倾向于使用第三方的malloc。

Tcmalloc是Google gperftools里的组件之一。全名是 thread cache malloc(线程缓存分配器)其内存管理分为线程内存和中央堆两部分。

1.小块内部的分配:对于小块内存分配,其内部维护了60个不同大小的分配器(实际源码中看到的是86个),和ptmalloc不同的是,它的每个分配器的大小差是不同的,依此按8字节、16字节、32字节等间隔开。在内存分配的时候,会找到最小符合条件的,比如833字节到1024字节的内存分配请求都会分配一个1024大小的内存块。如果这些分配器的剩余内存不够了,会向中央堆申请一些内存,打碎以后填入对应分配器中。同样,如果中央堆也没内存了,就向中央内存分配器申请内存。

在线程缓存内的60个分配器分别维护了一个大小固定的自由空间链表,直接由这些链表分配内存的时候是不加锁的。但是中央堆是所有线程共享的,在由其分配内存的时候会加自旋锁(spin lock)。

2.大内存的分配:对于大内存分配(大于8个分页, 即32K),tcmalloc直接在中央堆里分配。中央堆的内存管理是以分页为单位的,同样按大小维护了256个空闲空间链表,前255个分别是1个分页、2个分页到255个分页的空闲空间,最后一个是更多分页的小的空间。这里的空间如果不够用,就会直接从系统申请了。

3.ptmalloc与tcmalloc的不足:都是针对小内存分配和管理;对大块内存还是直接用了系统调用。应该尽量避免大内存的malloc/new、free/delete操作。频繁分配小内存,例如:对bool、int、short进行new的时候,造成内存浪费。

三、Jemalloc: jemalloc 是由 Jason Evans 在 FreeBSD 项目中引入的新一代内存分配器。它是一个通用的malloc实现,侧重于减少内存碎片和提升高并发场景下内存的分配效率,其目标是能够替代 malloc。下面是Jemalloc的两个重要部分:

1.arena:arena 是 jemalloc 最重要的部分,内存由一定数量的 arenas 负责管理。每个用户线程都会被绑定到一个 arena 上,线程采用 round-robin 轮询的方式选择可用的 arena 进行内存分配,为了减少线程之间的锁竞争,默认每个 CPU 会分配 4 个 arena,各个 arena 所管理的内存相互独立。

struct arena_s { atomic_u_t nthreads[2]; tsdn_t *last_thd; arena_stats_t stats; // arena的状态 ql_head(tcache_t) tcache_ql; ql_head(cache_bin_array_descriptor_t) cache_bin_array_descriptor_ql; malloc_mutex_t tcache_ql_mtx; prof_accum_t prof_accum; uint64_t prof_accumbytes; atomic_zu_t offset_state; atomic_zu_t extent_sn_next; // extent的序列号生成器状态 atomic_u_t dss_prec; atomic_zu_t nactive; // 激活的extents的page数量 extent_list_t large; // 存放 large extent 的 extents malloc_mutex_t large_mtx; // large extent的锁 extents_t extents_dirty; // 刚被释放后空闲 extent 位于的地方 extents_t extents_muzzy; // extents_dirty 进行 lazy purge 后位于的地方,dirty -> muzzy extents_t extents_retained; // extents_muzzy 进行 decommit 或 force purge 后 extent 位于的地方,muzzy -> retained arena_decay_t decay_dirty; // dirty --> muzzy arena_decay_t decay_muzzy; // muzzy --> retained pszind_t extent_grow_next; pszind_t retain_grow_limit; malloc_mutex_t extent_grow_mtx; extent_tree_t extent_avail; // heap,存放可用的 extent 元数据 malloc_mutex_t extent_avail_mtx; // extent_avail的锁 bin_t bins[NBINS]; // 所有用于分配小内存的 bin base_t *base; // 用于分配元数据的 base nstime_t create_time; // 创建时间 };

2.extent:管理 jemalloc 内存块(即用于用户分配的内存)的结构,每一个内存块大小可以是 N * page_size(4KB)(N >= 1)。每个 extent 有一个序列号(serial number)。一个 extent 可以用来分配一次 large_class 的内存申请,但可以用来分配多次 small_class 的内存申请。

struct extent_s { uint64_t e_bits; // 8字节长,记录多种信息 void *e_addr; // 管理的内存块的起始地址 union { size_t e_size_esn; // extent和序列号的大小 size_t e_bsize; // 基本extent的大小 }; union { /* * S位图,当此 extent 用于分配 small_class 内存时,用来记录这个 extent 的分配情况, * 此时每个 extent 的内的小内存称为 region */ arena_slab_data_t e_slab_data; atomic_p_t e_prof_tctx; // 一个计数器,用于large object }; }