深入研究glibc内存管理器原理及优缺点

最近查清了线上内存占用过大和swap使用频繁的原因:由于linux使用的glibc使用内存池技术导致的堆外内存暴增,基于这个过程中学习和了解了glibc的内存管理原理,和大家分享,如有错误请及时指出。
争取成为一个全栈打工人

一、应用内存分布

从Linux操作系统层面来看,每个应用进程使用task_struct结构进行描述和管理,在task_struct的中,使用mm_struct对内存进行管理,如下图所示:
内存分布图

在mm_struct管理的虚拟内存中,主要包括:Kernel Space、MMAP segment、Stack、Heap、BSS segment、Data segment和Text segment,如下图所示,是32位操作系统的内存分布图:
在这里插入图片描述
从上图可以看到,栈至顶向下扩展,并且栈是有界的。堆至底向上扩展,mmap 映射区 域至顶向下扩展,mmap 映射区域和堆相对扩展,直至耗尽虚拟地址空间中的剩余区域,这 种结构便于 C 运行时库使用 mmap 映射区域和堆进行内存分配。上图的布局形式是在内核 2.6.7 以后才引入的,这是 32 位模式下进程的默认内存布局形式。
其中,Heap区是程序的动态内存区,同时也是C++内存泄漏的温床。malloc、free均发生在这个区域。

我们研究glibc是在了解进程的内存分布的前提下进行的。

二、内存管理器

平时我们应用申请内存,是通过内存分配器申请内存,整体的结构图如下所示:
在这里插入图片描述

我们经常看到的内存管理器主要有如下几种:

dlmalloc – General purpose allocator
**ptmalloc2 – glibc**
jemalloc – FreeBSD and Firefox
tcmalloc – Google
libumem – Solaris

本文主要研究ptmalloc2(即glibc)内存管理器,如无特别说明,以下我们将都用ptmalloc作为说明对象。

  • Linux 中 malloc 的早期版本是由 Doug Lea 实现的,它有一个重要问题就是在并行处理时多个线程共享进程的内存空间,各线程可能并发请求内存,在这种情况下应该如何保证分配和回收的正确和高效。
  • Wolfram Gloger 在 Doug Lea 的基础上改进使得 Glibc 的 malloc 可以支 持多线程——ptmalloc,在 glibc-2.3.x.中已经集成了 ptmalloc2,这就是我们平时使用的 malloc, 目前 ptmalloc 的最新版本 ptmalloc3。ptmalloc2 的性能略微比 ptmalloc3 要高一点点。
  • ptmalloc 实现了 malloc(),free()以及一组其它的函数. 以提供动态内存管理的支持。但是malloc本质上都是通过系统调用brk或者mmap实现的。
  • 分配器处在用户程序和内核之间,它响应用户的分配请求,向操作系统申请内存,然后将其返 回给用户程序,为了保持高效的分配,分配器一般都会预先分配一块大于用户请求的内存, 并通过某种算法管理这块内存。来满足用户的内存分配要求,用户释放掉的内存也并不是立即就返回给操作系统,同时这也造成了内存占用(RSS)过大、进程被Killer的风险。相反,分配器会管理这些被释放掉的空闲空间,以应对用户以后的内存分配要求。也就是说,分配器不但要管理已分配的内存块,还需要管理空闲的内存块,当响应用户分配要求时,分配器会首先在空闲空间中寻找一块合适的内存给用户,在空闲空间中找不到的情况下才分配一块新的内存。为实现一个高效的分配器,需要考虑很多的因素。 比如,分配器本身管理内存块所占用的内存空间必须很小,分配算法必须要足够的快。

三、内存分配与回收

在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。

brk()/sbrk() // 通过移动Heap堆顶指针brk,达到增加或者释放内存目的,能够提供连续的逻辑空间
mmap()/munmap() // 通过文件影射的方式,把文件映射到mmap区,或者释放空间,但是申请多个mmap区域 之间不一定连续
3.1 内存分配与释放过程示例

回到上面我们看到 的《32位操作系统的内存分布图》,用如下C代码进行内存分配与释放测试:

/* Per thread arena example. */
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
 
void* threadFunc(void* arg) {
        printf("Before malloc in thread 1\n");
        getchar();
        char* addr = (char*) malloc(1000);
        printf("After malloc and before free in thread 1\n");
        getchar();
        free(addr);
        printf("After free in thread 1\n");
        getchar();
}
 
int main() {
        pthread_t t1;
        void* s;
        int ret;
        char* addr;
 
        printf("Welcome to per thread arena example::%d\n",getpid());
        printf("Before malloc in main thread\n");
        getchar();
        addr = (char*) malloc(1000);
        printf("After malloc and before free in main thread\n");
        getchar();
        free(addr);
        printf("After free in main thread\n");
        getchar();
        ret = pthread_create(&t1, NULL, threadFunc, NULL);
        if(ret)
        {
                printf("Thread creation error\n");
                return -1;
        }
        ret = pthread_join(t1, &s);
        if(ret)
        {
                printf("Thread join error\n");
                return -1;
        }
        return 0;
}
1、在程序调用malloc之前程序进程中是没有heap segment的,并且在创建在创建线程前,也是没有线程堆栈的。Before malloc in main thread,如下所示:

在这里插入图片描述

2、在主线程中调用malloc之后,就会发现系统给程序分配了堆栈,且这个堆栈刚好在数据段之上,After malloc in main thread,如下所示:

在这里插入图片描述

  • 通过主线程malloc分配后,heap segment的开始地址(0804b000)刚好在数据段之上(0804a000-0804b000),说明是通过brk系统调用实现的。因为brk能分配连续的虚拟地址空间。
  • 虽然我们只申请了1000字节的数据,但是系统却分配了132KB大小的堆,这是因为初次分配堆栈的时候,会超过应用申请的内存空间,作为预分配,这个分配的区域叫做 Arena。主线程分配的叫做main arena,子线程分配的叫做thread arena。

arena通俗的名称叫做内存分配区,不等价于堆,它是一个空闲内存管理逻辑。

每个arena中含有多个chunk,chunk是内存分配的基本单位,这些chunk以链表的形式加以组织,在接下来的内容中,我们将详细介绍chunk。

由于132KB比1000字节大很多,所以主线程后续再声请堆空间的话,就会先从这132KB的剩余部分中申请,直到用完或不够用的时候,再通过增加program break location的方式来增加main arena的大小。同理,当main arena中有过多空闲内存的时候,也会通过减小program break location的方式来缩小main arena的大小。

3、主线程调用free(addr)之后,After free in main thread。

在主线程调用free之后:从内存布局可以看出程序的堆空间并没有被释放掉,原来调用free函数释放已经分配了的空间并非直接“返还”给系统,而是由glibc 的malloc库函数加以管理。它会将释放的chunk添加到main arenas的bin(这是一种用于存储同类型free chunk的双链表数据结构,后问会加以详细介绍)中。在这里,记录空闲空间的freelist数据结构称之为bins。之后当用户再次调用malloc申请堆空间的时候,glibc malloc会先尝试从bins中找到一个满足要求的chunk,如果没有才会向操作系统申请新的堆空间。如下图所示:
在这里插入图片描述

4、现在进入子线程的内存申请与分配阶段:Before malloc in thread1

在thread1调用malloc之前:从输出结果可以看出thread1中并没有heap segment,但是此时thread1自己的栈空间已经分配完毕了:

  • 8
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值