操作系统原理:动态内存分配

动态内存分配背后的机制深刻的体现了计算机科学中的这句名言:

All problem in CS can be solved by another level of indirection. — Butler Lampson

用户层

malloc的实现

malloc的底层调用sbrk和mmap

malloc是C语言标准库函数,是在用户层实现的。在Linux里,malloc编译好,是在run-time的动态库so中,通过标准库头文件把api声明给调用者。mallocreallocfree是C语言的用户层的标准库函数,底层调用的是mmapsbrk函数。大致如下图:
操作系统原理:动态内存分配_第1张图片
malloc 底层调用的 sbrk 和 mmap. 申请内存小的时候用sbrk,增量扩展heap段;申请内存大的时候是调 mmap,进程重新开一块VMA(后面会介绍)。这些都是可配置的,malloc 的默认触发值是128k,可以看标准的源代码。

/* malloc.c */
#ifndef DEFAULT_MMAP_THRESHOLD_MIN
#define DEFAULT_MMAP_THRESHOLD_MIN (128 * 1024)
#endif

#ifndef DEFAULT_MMAP_THRESHOLD
#define DEFAULT_MMAP_THRESHOLD DEFAULT_MMAP_THRESHOLD_MIN
#endif

malloc 一般实现所用的数据结构 – 双向链表

本质上,malloc就是管理一块连续的可读写的进程虚拟内存。作为一个内存分配器,要做到: (1)最大化吞吐效率; (2)最大化内存利用率。同时不可避免的就会有所限制,即申请过的内存块不能被修改和移动。管理申请的内存块,在具体实现,上要考虑下面几点:

  • 组织:如何记录空闲块;
  • 选择:如何选择一个合适的空闲块来作为一个新分配的内存块;
  • 分割:如何处理一个空闲快分配过内存后剩下的部分;
  • 合并:如何处理一个刚刚被释放的块;

下面介绍两种malloc实现常用的数据结构:

隐含链表方式 – 隐含链表方式即在每一块空闲或被分配的内存块中使用一个字的空间来保存此块大小信息和标记是否被占用。根据内存块大小信息可以索引到下一个内存块,这样就形成了一个隐含链表,通过查表进行内存分配。优点是简单,缺点就是慢,需要遍历所有。meta-data如图:
操作系统原理:动态内存分配_第2张图片
显示空闲链表 – 显示空闲链表的方法,和隐含链表方式相似,唯一不同就是在空闲内存块中增加两个指针,指向前后的空闲内存块。相比显示链表,就是分配时只需要顺序遍历空闲块。meta-data如图:
操作系统原理:动态内存分配_第3张图片
还有一些其他的方法不一一介绍。

内核层

brk系统调用的实现原理

Linux kernel通过 VMA : vm_area_struct来分块管理进程地址空间。每个进程的进程控制块 task_struct 里都有一个 mm_struct指向了进程可使用的所有的VMA。sys_brk 操作系统调用本质就是调整这些VMA, 是mmap家族的一个特例,sbrk则是glibc针对brk的基础上实现的。详细可以看Linux kernel和glibc的的源码。

/* 大致的结构 */
struct mm_struct {
         struct vm_area_struct * mmap;  /* 指向虚拟区间(VMA)链表 */
         rb_root_t mm_rb;         /* 指向red_black树 */
         struct vm_area_struct * mmap_cache;     /* 指向最近找到的虚拟区间*/
         pgd_t * pgd;             /* 指向进程的页目录 */
         atomic_t mm_users;                   /* 用户空间中的有多少用户*/
         atomic_t mm_count;               /* 对"struct mm_struct"有多少引用*/
         int map_count;                        /* 虚拟区间的个数*/
         struct rw_semaphore mmap_sem;
         spinlock_t page_table_lock;        /* 保护任务页表和 mm->rss */
         struct list_head mmlist;            /*所有活动(active)mm的链表 */
         unsigned long start_code, end_code, start_data, end_data;
         unsigned long start_brk, brk, start_stack; /*堆栈相关*/
         unsigned long arg_start, arg_end, env_start, env_end;
         unsigned long rss, total_vm, locked_vm;
         unsigned long def_flags;
         unsigned long cpu_vm_mask;
         unsigned long swap_address;
         unsigned dumpable:1;
         /* Architecture-specific MM context */
         mm_context_t context;
};

下面是一个 mm_struct 管理进程内存的示意图:
操作系统原理:动态内存分配_第4张图片

Linux进程VMA的管理 - 红黑树

每个VMA包括VMA的起始和结束地址, 访问权限等. 其中的 vm_file 字段表示了该区域映射的文件(如果有的话)。有些不映射文件的VMA是匿名的,比如的heap, stack都分别对应于一个单独的匿名的VMA. 进程的VMA存放在一个List和一个rb_tree中, 该List根据VMA的起始地址排序。红黑树结构是为了加快查找速度,快速查找某一地址是否在进程的某一个VMA中. 通过命令读取/proc/pid/maps文件查看进程的内存映射, 这个实现也是通过查存放VMA的List打印出来的。
操作系统原理:动态内存分配_第5张图片

物理页内存管理-伙伴算法

操作系统是按页来管理物理内存的。伙伴算法每次只能分配2的幂次页的空间,比如一次分配1页,2页,4页,…, 1024页等。伙伴的定义:

  1. 两个块大小相同;
  2. 两个块地址连续;
  3. 两个块必须是同一个大块中分离出来的;

分配内存:

  1. 寻找大小合适的内存块(大于等于所需大小并且最接近2的幂,比如需要27,实际分配32)。如果找到了,分配给应用程序,没有执行下一步;
  2. 对半分离出高于所需大小的空闲内存块;
  3. 如果分到最低限度,分配这个大小;
  4. 回到步骤1,寻找合适大小的块;
  5. 重复该步骤直到一个合适的块;

释放内存:

  1. 释放该内存块;
  2. 寻找相邻的块,看其是否释放了;
  3. 如果相邻块也释放了,合并这两个块,重复上述步骤直到遇上未释放的相邻块,或者达到最高上限(即所有内存都释放了);

参考

  • 如何实现一个malloc
  • A Malloc Tutorial
  • Glibc内存管理–ptmalloc2源代码分析(九)
  • 内存动态分配函数malloc的基本实现原理
  • How are sbrk/brk implemented in Linux?
  • 频繁分配释放内存导致的性能问题的分析
  • 认真分析mmap:是什么 为什么 怎么用
  • Inside memory management
  • Computer Systems: A Programmer’s Perspective, 3/E
  • How the Kernel Manages Your Memory
  • glibc内存管理ptmalloc源代码分析1.pdf
  • Operating Systems: Three Easy Pieces
  • 伙伴分配器的一个极简实现

你可能感兴趣的:(OS/Linux)