内存池算法简单剖析

news2024/9/21 0:39:05

为什么要引入内存池算法?

  1. 我们知道C/C++ 语言中通过 malloc 调用 sbrk 和 mmap 这两个系统调用,向操作系统申请堆内存。但是,sbrk 和 mmap 这两个系统调用分配内存效率比较低,因为,执行系统调用是要进入内核态的,这样内核态又要转向用户态,运行态的切换会耗费不少时间。
  2. 至于为什么执行系统调用是要进入内核态?,可以参考我的这篇文章:Linux 系统调用的本质
  3. 为了解决这个问题,人们倾向于使用系统调用来分配大块内存,然后再把这块内存分割成更小的块,以方便程序员使用,这样可以提升分配的效率。在 C 语言的运行时库里,这个工作是由 malloc 函数负责的。但有时候 C 语言的原生malloc 实现还是不能满足特定应用的性能要求,这就需要程序员来实现符合自己应用要求的内存池,以便自己进行内存的分配和释放。
  4. malloc 实现的基本原理是先向操作系统申请一块比较大的内存,然后再通过各种优化手段让内存分配的效率最大化。在 glibc 的实现里,malloc 函数在向操作系统申请堆内存时,会使用 mmap,以 4K 的整数倍一次申请多个页。这样的话,mmap 的区域就会以页对齐,页与页之间的排列非常整齐,避免了出现内存碎片。

内存池算法逐步演进

空闲链表法

这种算法所使用的数据结构比较简单,算法也很直接,我们把这种算法称为简单算法(Naive Algorithm)。我们举个例子说明简单算法的运行过程,假如在算法开始时,内存的情况如下图所示。
在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>

int main(int argc,char* argv[])
{
    void* p1 = malloc(16);
    void* p2 = malloc(16);
    void* p3 = malloc(20);

    free(p2);

    void* p4 = malloc(16);
    void* p5 = malloc(16);

    free(p4);

    return 0;
}

申请了100字节的内存,执行完 main函数以后,内存的划分就如下所示:
在这里插入图片描述
100字节的内存,已经分割成 16、16、20、16、16、16 六个小的内存块。其中着色部分,也就是第一、第三和第五块内存是已经分配出去的,正在使
用的内存,而白色区域则是尚未分配的内存。图的上半部分代表空闲链表,每一块未分配的内存都会由一个空闲链表的节点进行管理。结点中记录了这块空闲内存区域的起始位置和长度。

缺点1: 如果此时,又到达了一个内存分配请求,要申请一个大小为 20 的内存区域,虽然所有空闲区域的大小之和是 48,是超过 20 的,但是由于这三块空闲区域并不连续,所以,我们已经无法从这 100 字节的内存中再分配出一块 20 字节的内存区域了,相对于这次请求,这三块 16 字节的空闲区域就是内存碎片。这就是我们所介绍的简单算法的第一个缺陷:会产
生内存碎片。
**缺点2:**每一次分配内存时,我们都需要遍历 free list,最差情况下的时间复杂度显然是 O(n)。如果是多线程同时分配的话,free list 会被多线程并发访问,为了保护它,就必须使用各种同步机制,比如锁。可见上述算法的第二个缺陷是分配效率一般,且多线程并发场景下性能还会恶化。

分桶式内存管理法

  1. 分桶式内存管理采用了多个链表,对于单个链表,它内部的所有结点所对应的内存区域的大小是相同的。换句话说,相同大小的区域会挂载到同一个链表上。
  2. 最常见的方式是以 4 字节为最小单位,把所有 4 字节的区域挂到同一个链表上,再把 8 字节的区域挂到一起,然后是 16 字节,32 字节,这样以 2 次幂向上增长。如下图所示:
    在这里插入图片描述
  3. 采用了新的数据结构以后,分配和回收的算法也相应地发生了变化。首先,分配的时候,我们要只要找到能满足这一次分配请求的最小区域,然后去相应的链表里把整块区域都取下来。比如,分配一个 7 字节的内存块时,我们就可以从 8 字节大小的空闲链表里直接取出链表头上的那块区域,分配给应用程序。由于从链表头上删除元素的时间复杂度是 O(1),所以我们分配内存的效率就大大提高了。
  4. 由于整个大块内存被提前分割成了整齐的小块(比如是以 4 字节对齐),所以整个区域里不存在块与块之间内存碎片。但是这种做法还是会产生区域内部的空间浪费,比如上面举的例子,当申请的内存大小是 7 时,按当前算法,只能分配给它大小为 8 的块,这就造成了一个字节的内部浪费,或者称之为内部碎片。
  5. 内部碎片带来的问题是内存使用率没有达到 100%,在最差情况下,可能只有 50%。但是内部碎片随着这一块区域的释放也就消失了,所以不会因为长时间运行而积累成严重的问题。
  6. 释放时,只需要把要释放的内存直接挂载到相应的链表里就可以了。 这个速度和分配是一样的,效率非常高。

缺点:

  1. 分桶式内存管理比简单算法无论是在算法效率方面,还是在碎片控制方面都有很大的提升。但它的缺陷也很明显:区域内部的使用率不够高和动态扩展能力不够好。
  2. 例如,4 字节的区域提前消耗完了,但 8 字节的空闲区域还有很多。如果这个时候应用程序需要很多 1,2,3,4字节内存的时候, 此时就会面临两难选择,如果直接分配 8 字节的区域,则区域内部浪费就比较多,如果不分配,则明明还有空闲区域,却无法成功分配。
  3. 为了解决上述两个问题,人们在分桶的基础上继续改进,让内存可以根据需求动态地决定小的内存区域和大的内存区域的比例。这种设计的典型就是伙伴系统,我们一起来看下。

伙伴系统算法

  1. 正如上面的例子所讲的,当系统中还有很多 8 字节的空闲块,而 4 字节的空闲块却已经耗尽,这时再有一个 4 字节的请求,则会出现 malloc 失败的情况。为了避免分配失败,我们其实还可以考虑将大块的内存做一次拆分。
  2. 如下图所示。分配一块 4 字节大小的空间,在 4 字节的 free list 上找不到空闲区域,系统就会往上找,假如 8 字节和 16 字节的 free list 中也没有空闲区域,就会一直向上找到 32字节的 free list。
    在这里插入图片描述
  3. 伙伴系统不会直接把 32 的空闲区域分配出去,因为这样做的话,会带来巨大的浪费。它会先把 32 字节分成两个 16 字节,把后边一个挂入到 16 字节的 free list 中。然后继续拆分前一半。前一半继续拆成两个 8 字节,再把后一半挂入到 8 字节的 free list;最后,前8 字节继续拆分成2个4字节,把后面4个字节加入 free list中,前面4个字节用于本次malloc分配。分配后的内存的状态如下所示:
    在这里插入图片描述
  4. 这种不断地把一块内存分割成更小的两块内存的做法,就是伙伴系统,这两块更小的内存就是伙伴。
  5. 它的好处是可以动态地根据分配请求将大的内存分割成小的内存。当释放内存时,如果系统发现与被释放的内存相邻的那个伙伴也是空闲的,就会把它们合并成一个更大的连续内存。通过这种拆分,系统就变得更加富有弹性。

malloc 的实现:
15. malloc 的实现,在历史上先后共有几十种策略,这些策略往往就是上述三种算法的组合。具体到 glibc 中的 malloc 实现,它就采用了分桶的策略,但是它的每个桶里的内存不是固定大小的,而是采用了将 1 ~ 4 字节的块挂到第一个链表里,将 5 ~ 8 字节的块挂到第二个链表里,将 9~16 字节的块挂到第三个链表里,依次类推。
16. 在单个链表内部则采用 naive 的分配方式,比如要分配 5 个字节的内存块,我们会先在 5~ 8 这个链表里查找,如果查找到的内存大小是 8 字节的,那就会将这个区域分割成 5 字节和 3 字节两个部分,其中 5 字节用于分配,剩余的 3 字节的空闲区域则会挂载到 1~4这个链表里。
17. 可见 malloc 的实现策略是比较灵活的,针对不同的场景,不同的分配策略的性能表现也是不一样的。很多公司的基础平台都选择自己实现内存池来提供 malloc 接口,这样可以更好地服务本公司的业务。最著名的例子就是 Google 公司实现的 Tcmalloc 库。
18. Tcmalloc 相比起其他的 malloc 实现,最大的改进是在多线程的情况下性能提升。我们知道,在多线程并发地分配内存时,每次分配都要对 free list 进行加锁以避免并发程序带来的问题,这就容易形成性能瓶颈。
为了解决这个问题,Tcmalloc 引入了线程本地缓存 (Thread Local Cache),每个线程在分配内存的时候都先在自己的本地缓存中寻找,如果找到就结束,只有找不到的情况才会继续向全局管理器申请一块大的空闲区域,然后按照伙伴系统的方式继续添加到本地缓存中去。

内核内存管理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/135017.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

区块链知识系列 - Oracle预言机

Oracle 预言机 区块链外信息写入区块链内的机制&#xff0c;一般被称为预言机&#xff08;oracle mechanism&#xff09;。 借助预言机外界的数据得以灌入链内, 使得DApp的玩法更多样. 比如DApp倚重的随机数, 可以考虑让一个硬件产生真随机数, 通过Oracle,定时灌入, 这将更公…

单机Docker部署应用Kraft模式的Kafka集群

单机Docker部署应用Kraft模式的Kafka集群1 Docker镜像准备1.1 下载Kafka1.2 配置容器1.3 修改kafka配置2 部署Kafka集群2.1 启动节点容器2.2 生成一个 Cluster ID2.3 格式化存储目录2.4 启动kafka服务3 知识3.1 控制器服务器3.2 进程角色3.3 仲裁投票者3.4 Kafka存储工具3.5 缺…

久泰新材料在港上市申请失效:年亏损超2亿元,崔轶钧为董事长

近日&#xff0c;贝多财经从港交所了解到&#xff0c;内蒙古久泰新材料科技股份有限公司&#xff08;下称“久泰集团”或“久泰新材料”&#xff09;在港交所的上市申请材料&#xff08;招股书&#xff09;已“失效”&#xff0c;目前已经无法正常查看或下载。 其中&#xff0c…

Hack the Box CTF 网络流量分析 中等难度 Penetrated | Wireshark

这是一道Hack the Box网络流量分析题&#xff0c;中等难度&#xff0c;题目本身就是一个 pcap 包。 1. 题目&#xff1a; 原文件链接如下&#xff0c;有兴趣可以自己先看一看&#xff1a; 链接: https://pan.baidu.com/s/16KLwQuoYA1AfEwuK78bBWg 提取码: 8864 Flag 格式&am…

Nginx内存管理源码剖析注解

文章目录Nginx内存池总览内存池中变量类型定义创建内存池&#xff1a;ngx_create_pool内存池分配空间&#xff1a;ngx_palloc小块内存空间分配&#xff1a;ngx_palloc_small创建小块内存池&#xff1a;ngx_palloc_block大块内存空间分配&#xff1a;ngx_palloc_large<br /&g…

【电动车】主动配电网多源协同运行优化研究——大规模电动汽车的蒙特卡洛模拟(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

TiDB问题排查

TiDB 集群问题导图 1. 服务不可用 1.1 客户端报 "Region is Unavailable" 错误 1.1.1 "Region is Unavailable" 一般是由于 region 在一段时间不可用导致&#xff08;可能会遇到 "TiKV server is busy" 或者发送给 TiKV 的请求由于 not leader…

JMM内存模型

借鉴&#xff1a; 一文带你搞懂JMM内存模型和JVM内存区域_Apple_Web的博客-CSDN博客_jmm内存模型和jvm内存模型的区别 面试官问我什么是JMM_java技术爱好者_R的博客-CSDN博客_jmm Java内存模型 概述 Java内存模型(即Java Memory Model&#xff0c;简称JMM)本身是一种抽象的…

箭头函数带来的this变化实例

1.不使用箭头函数时 let Lesson {site: 后盾人,lists:[js,css,mysql],show: function (param) { console.log(this);// {site: 后盾人, lists: Array(3), show: ƒ}return this.lists.map(function(title){console.log(this);// Window {window: Window, self: Window, docume…

17. 老板让我手动控制网页渲染速度,说这能反爬虫?我信了。

手动数据延迟加载&#xff0c;真的可以反爬虫 爬虫训练场项目&#xff0c;加速更新中&#xff0c;专栏清单参考 pachong.vip 本次案例需要的代码量特别小&#xff0c;所以咱们再 Nginx 中也进行一下相关配置 文章目录页面逻辑实现接口逻辑实现延迟实现&#xff0c;time.sleep()…

2022年需求最大8种编程语言!(详细解读)

DevJobsScanner 在过去的 14 个月&#xff08;从 2021 年 10 月到 2022 年 11 月&#xff09;中分析了超过 1200 万个开发人员职位需求&#xff0c;并从其中挑选了明确需要编程语言的工作机会&#xff0c;得到了 2022 年最受欢迎的 8 种编程语言。 目前市场中需求最高的前八位…

数学建模学习笔记-算法(线性规划模型)-上

目录 线性规划问题 线性规划的matlab标准形式 解析 目标函数 约束条件 使用matlab的linprog函数来进行求解 线性规划问题 数学规划&#xff1a;安排现有资源安排生产&#xff0c;以取得最大效益的问题。 线性规划&#xff1a;目标函数和约束条件均为线性函数 在一组线性…

2023/1/2总结

今天AC了三个有关二叉树的题目&#xff1a; P1827 [USACO3.4] 美国血统 American Heritage_lxh0113的博客-CSDN博客 https://blog.csdn.net/lxh0113/article/details/128522831?spm1001.2014.3001.5502 P1030 [NOIP2001 普及组] 求先序排列_lxh0113的博客-CSDN博客 然后学…

web基础标签

标签分类&#xff1a; 文本标签&#xff1a; 文本标题标签&#xff1a;h1---h6 段落标签&#xff1a; p 水平线&#xff1a; <hr/> 换行符&#xff1a; <br/> 转义字符&#xff1a; 注释标签&#xff1a; <!--注释内容--> 无语义标签&#xff1a; 语义标签…

educoder数据结构与算法 线性表 第1关:实现一个顺序存储的线性表

本文已收录于专栏 &#x1f332;《educoder数据结构与算法_大耳朵宋宋的博客-CSDN博客》&#x1f332; &#x1f350;任务描述&#x1f350; 本关任务&#xff1a;实现 step1/Seqlist.cpp 中的SL_InsAt、SL_DelAt和SL_DelValue三个操作函数&#xff0c;以实现线性表中数据的插…

TypeScript中abstract抽象类、抽象成员

TypeScript也支持定义抽象类和抽象类成员。抽象类和抽象类成员都使用abstract关键字来定义 抽象类可以不包含抽象方法&#xff0c;但抽象方法必须存在于抽象类中抽象方法只能定义&#xff0c;不能实现&#xff0c;即没有函数体抽象类不能被直接使用&#xff0c;只能被继承&…

Spring Boot学习篇(五)

Spring Boot学习篇(五) mybatis-plus使用 1.1 配置pom.xml文件 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:s…

《设计模式》代理模式

《设计模式》设计模式的基本原则 《设计模式》单例模式 《设计模式》工厂模式 《设计模式》原型模式 《设计模式》建造者模式 《设计模式》适配器模式 《设计模式》桥接模式 《设计模式》装饰者模式 《设计模式》组合模式 《设计模式》外观模式 《设计模式》享元模式 《设计模式…

HTML5和CSS3 WEB技术开发

HTML5和CSS3 WEB技术开发 B站视频参考&#xff1a;https://www.bilibili.com/video/BV1H44y1k7ze/ 课程目标&#xff1a; 使用HTML5进行网站布局使用CSS3进行网站美化开发精美的商业网站 第一章 HTML5基础 概念&#xff1a; ​ 网页 &#xff1a;互联网的基础&#xff0c;网…

requests请求库(爬取)

文章目录requests模块链接拼接&#xff08;params参数&#xff09;UA伪装&#xff08;headers参数&#xff09;POST请求页面局部信息爬取&#xff08;GET&#xff09;爬取国家药品监督管理监督总局中基于中华人民共和国化妆品生产许可证相关数据爬取图片爬虫分类通用爬虫&#…