18张图揭秘高性能Linux服务器内存池技术是如何实现的

news2024/12/26 16:11:11

大家生活中肯定都有这样的经验,那就是大众化的产品都比较便宜,但便宜的大众产品就是一个词,普通;而可以定制的产品一般都价位不凡,这种定制的产品注定不会在大众中普及,因此定制产品就是一个词,独特。

有的同学可能会有疑问,你不是要聊技术吗?怎么又说起消费了?

原来技术也有大众货以及定制品。

通用 VS 定制

作为程序员(C/C++)我们知道申请内存使用的是malloc,malloc其实就是一个通用的大众货,什么场景下都可以用,但是什么场景下都可以用就意味着什么场景下都不会有很高的性能

malloc性能不高的原因一在于其没有为特定场景做优化,除此之外还在于malloc看似简单,但是其调用过程是很复杂的,一次malloc的调用过程可能需要经过操作系统的配合才能完成。那么调用malloc时底层都发生了什么呢?简单来说会有这样典型的几个步骤:

  1. malloc开始搜索空闲内存块,如果能找到一块大小合适的就分配出去
  2. 如果malloc找不到一块合适的空闲内存,那么调用brk等系统调用扩大堆区从而获得更多的空闲内存
  3. malloc调用brk后开始转入内核态,此时操作系统中的虚拟内存系统开始工作,扩大进程的堆区,注意额外扩大的这一部分内存仅仅是虚拟内存,操作系统并没有为此分配真正的物理内存
  4. brk执行结束后返回到malloc,从内核态切换到用户态,malloc找到一块合适的空闲内存后返回

以上就是一次内存申请的完整过程,我们可以看到,一次内存申请过程其实是非常复杂的,既然每次分配内存都要经过这么复杂的过程,那么如果程序大量使用malloc申请内存那么该程序注定无法获得高性能。幸好,除了大众货的malloc,我们还可以私人定制,也就是针对特定场景自己来维护内存申请和分配,这就是高性能高并发必备的内存池技术

内存池技术有什么特殊的吗?

有的同学可能会说,等等,那malloc和这里提到的内存池技术有什么区别呢?第一个区别在于我们所说的malloc其实是标准库的一部分,位于标准库这一层;而内存池是应用程序的一部分。

其次在于定位,我们自己实现的malloc其实也是定位通用性的,通用性的内存分配器设计实现往往比较复杂,但是内存池技术就不一样了,内存池技术专用于某个特定场景,以此优化程序性能,但内存池技术的通用性是很差的,在一种场景下有很高性能的内存池基本上没有办法在其它场景也能获得高性能,甚至根本就不能用于其它场景,这就是内存池这种技术的定位。

那么内存池技术是怎样优化性能的呢?

内存池技术原理

简单来说,内存池技术一次性获取到大块内存,然后在其之上自己管理内存的申请和释放,这样就绕过了标准库以及操作系统

也就是说,通过内存池,一次内存的申请再也不用去绕一大圈了。除此之外,我们可以根据特定的使用模式来进一步优化,比如在服务器端,每次用户请求需要创建的对象可能就那几种,那么这时我们就可以在自己的内存池上提前创建出这些对象,当业务逻辑需要时就从内存池中申请已经创建好的对象,使用完毕后还回内存池。因此我们可以看到,这种为某些应用场景定制的内存池相比通用的比如malloc内存分配器会有大的优势。接下来我们就着手实现一个。

实现内存池的考虑

值得注意的是,内存池实际上有很多的实现方法,在这里我们还是以服务器端编程为例来说明。假设你的服务器程序非常简单,处理用户请求时只使用一种对象(数据结构),那么最简单的就是我们提前申请出一堆来,使用的时候拿出一个,使用完后还回去:

怎么样,足够简单吧!这样的内存池只能分配特定对象(数据结构),当然这样的内存池需要自己维护哪些对象是已经被分配出去的,哪些是还没有被使用的。但是,在这里我们可以实现一个稍微复杂一些的,那就是可以申请不同大小的内存,而且由于是服务器端编程,那么一次用户请求过程中我们只申请内存,只有当用户请求处理完毕后一次性释放所有内存,从而将内存申请释放的开销降低到最小。因此,你可以看到,内存池的设计都是针对特定场景的。现在,有了初步的设计,接下来就是细节了。

数据结构

为了能够分配大小可变的对象,显然我们需要管理空闲内存块,我们可以用一个链表把所有内存块链接起来,然后使用一个指针来记录当前空闲内存块的位置,如图所示:

从图中我们可以看到,有两个空闲内存块,空闲内存之间使用链表链接起来,每个内存块都是前一个的2倍,也就是说,当内存池中的空闲内存不足以分配时我们就向malloc申请内存,只不过其大小是前一个的2倍:

其次,我们有一个指针free_ptr,指向接下来的空闲内存块起始位置,当向内存池分配内存时找到free_ptr并判断当前内存池剩余空闲是否足够就可以了,有就分配出去并修改free_ptr,否则向malloc再次成倍申请内存。从这里的设计可以看出,我们的内存池其实是不会提供类似free这样的内存释放函数的,如果要释放内存,那么会一次性将整个内存池释放掉,这一点和通用的内存分配器是不一样。现在,我们可以分配内存了,还有一个问题是所有内存池设计不得不考虑的,那就是线程安全。

线程安全

显然,内存池不应该局限在单线程场景,那我们的内存池要怎样实现线程安全呢?有的同学可能会说这还不简单,直接给内存池一把锁保护就可以了。

这种方法是不是可行呢?还是那句话,It depends,要看情况。如果你的程序有大量线程申请释放内存,那么这种方案下锁的竞争将会非常激烈,线程这样的场景下使用该方案不会有很好的性能。那么还有没有一种更好的办法吗?答案是肯定的。

 资料直通车:Linux内核源码技术学习路线+视频教程内核源码

学习直通车:Linuxc/c++高级开发【直播公开课】

零声白金VIP体验卡:零声白金VIP体验卡(含基础架构/高性能存储/golang/QT/音视频/Linux内核)

线程局部存储

既然多线程使用线程池存在竞争问题,那么干脆我们为每个线程维护一个内存池就好了,这样多线程间就不存在竞争问题了。那么我们该怎样为每个线程维护一个内存池呢?线程局部存储,Thread Local Storage正是用于解决这一类问题的,什么是线程局部存储呢?简单说就是,我们可以创建一个全局变量,因此所有线程都可以使用该全局变量,但与此同时,我们将该全局变量声明为线程私有存储,那么这时虽然所有线程依然看似使用同一个全局变量,但该全局变量在每个线程中都有自己的副本,变量指向的值是线程私有的,相互之间不会干扰。

关于线程局部存储,可以参考这里。假设这个全局变量是一个整数,变量名字为global_value,初始值为100,那么当线程A将global_value修改为200时,线程B看到的global_value的值依然为100,只有线程A看到的global_value为200,这就是线程局部存储的作用。

线程局部存储+内存池

有了线程局部存储问题就简单了,我们可以将内存池声明为线程局部存储,这样每个线程都只会操作属于自己的内存池,这样就再也不会有锁竞争问题了。

注意,虽然这里给出了线程局部存储的设计,但并不是说加锁的方案就比不上线程局部存储方案,还是那句话,一切要看使用场景,如果加锁的方案够用,那么我们就没有必要绞尽脑汁的去用其它方案,因为加锁的方案更简单,代码也更容易维护。还需要提醒的是,这里只是给出了内存池的一种实现方法,并不是说所有内存池都要这么设计,内存池可以简单也可复杂,一切要看实际场景,这一点也需要注意。

其它内存池形式

到目前为止我们给出了两种内存池的设计方法,第一种是提前创建出一堆需要的对象(数据结构),自己维护好哪些对象(数据结构)可用哪些已被分配;第二种可以申请任意大小的内存空间,使用过程中只申请不释放,最后一次性释放。这两种内存池天然适用于服务器端编程。最后我们再来介绍一种内存池实现技术,这种内存池会提前申请出一大段内存,然后将这一大段内存切分为大小相同的小内存块:

然后我们自己来维护这些被切分出来的小内存块哪些是空闲的哪些是已经被分配的,比如我们可以使用栈这种数据结构,最初把所有空闲内存块地址push到栈中,分配内存是就pop出来一个,用户使用完毕后再push回栈里。

从这里的设计我们可以看出,这种内存池有一个限制,这个限制就是说程序申请的最大内存不能超过这里内存块的大小,否则不足以装下用户数据,这需要我们对程序所涉及的业务非常了解才可以。用户申请到内存后根据需要将其塑造成特定对象(数据结构)。关于线程安全的问题,可以同样采用线程局部存储的方式来实现:

一个有趣的问题

除了线程安全,这里还有一个非常有趣的问题,那就是如果线程A申请的对象被线程B拿去释放,我们的内存池该怎么处理呢?这个问题之所以有趣是因为我们必须知道该内存属于哪个线程的局部存储,但申请的内存本身并不能告诉你这样的信息。有的同学可能会说这还不简单,不就是一个指针到另一个指针的映射吗,直接用map之类存起来就好了,但问题并没有这么简单,原因就在于如果我们切分的内存块很小,那么会存在大量内存块,这就需要存储大量的映射关系,有没有办法改进呢?改进方法是这样的,一般来说,我们申请到的大段内存其实是会按照特定大小进行内存对齐,我们假设总是按照4K字节对齐,那么该大段内存的起始地址后12个bit(4K = 2^12)为总是0,比如地址0x9abcd000,同时我们也假设申请到的大段内存大小也是4K:

那么我们就能知道该大段内存中的各个小内存块起始地址除了后12个bit位外都是一样的:

这样拿到任意一个内存的地址我们就能知道对应的大段内存的起始地址,只需要简单的将后12个bit置为0即可,有了大段内存的起始地址剩下的就简单了,我们可以在大段内存中的最后保存对应的线程局部存储信息:

这样我们对任意一个内存块地址进行简单的位运算就可以得到对应的线程局部存储信息,大大减少了维护映射信息对内存的占用。

总结

内存池是高性能服务器中常见的一种优化技术,在这里我们介绍了三种实现方法,值得注意的是,内存池实现没有统一标准,一切都要根据具体场景定制,因此我们可以看到内存池设计是有针对性的,当然其反面就是不具备通用性。

原文作者:码农的荒岛求生

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

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

相关文章

java基础练习(使用java实现跨库数据调度ETL)

简介 本文写一篇关于java库与库之间的数据传输,现实生产中可能是通过其他方式完成,在没有架构的基础上使用java实现跨库的数据传送,非常不便利。但是作为练习我觉得确实非常有用,涉及的java知识点相对较多。本文以一个实列讲解&am…

双十一越来越早,公域电商何去何从——深度解析

为什么双十一越来越早?传统的公域电商越来越饱和,某淘某东甚至和某79元网红打起了价格战 ,市面上标品越来越多,商家越来越多,买东西的变少了,商家越卖越便宜。 公域电商: 就像一个大市场。里面摆满了各种商…

1015. 摘花生

题目&#xff1a; 1015. 摘花生 - AcWing题库 思路&#xff1a;dp 代码&#xff1a; #include<iostream> #include<cstdio> #include<cmath> using namespace std; const int N 110; typedef long long ll; int T, r, c; int num[N][N]; ll dp[N][N];//dp…

性能优化之懒加载 - 基于观察者模式和单例模式的实现

一、引入 在前端性能优化中&#xff0c;关于图片/视频等内容的懒加载一直都是优化利器。当用户看到对应的视图模块时&#xff0c;才去请求加载对应的图像。 原理也很简单&#xff0c;通过浏览器提供的 IntersectionObserver - Web API 接口参考 | MDN (mozilla.org)&#xff0c…

Express框架开发接口之书城商店原型图和数据库设计

1.原型图 这是利用Axure画的&#xff0c;简单画一下原型图&#xff0c;根据他们的业务逻辑我们完成书城商店API开发 首页 分类 商品详情 购物车 个人中心 2.数据库设计 首页 首页导航 导航栏产品 通过点击导航栏对应id和产品id关联获取产品内容 设置外建 sql语句 CR…

CRM系统数据库是如何影响客户体验的?

CRM客户关系管理由概念到软件实体&#xff0c;已经有几十年的时间&#xff0c;随着信息技术的进步&#xff0c;数字化让CRM软件乘上快车&#xff0c;迅速成为各类企业的数字化管理工具。CRM客户管理系统的一个重要功能便是改善并提升客户体验&#xff0c;且CRM数据库是与客户体…

朝夕光年游戏高光全套解决方案

作为全球用户与开发者的游戏研发与发行业务品牌——朝夕光年&#xff0c;一直致力于服务全球玩家&#xff0c;帮助玩家在令人惊叹的虚拟世界中一起玩耍与创造。 游戏研发的过程中遇到很多玩家痛点和提效、拉新的问题&#xff0c;如何帮助玩家更好地记忆、记录、分享和传播自己的…

【Redis】String字符串类型-内部编码使用场景

文章目录 内部编码使用场景缓存功能计数功能共享会话手机验证码 内部编码 字符串类型的内部编码有3种&#xff1a; int&#xff1a;8个字节&#xff08;64位&#xff09;的⻓整型&#xff0c;存储整数embstr&#xff1a;压缩字符串&#xff0c;适用于表示较短的字符串raw&…

工业5G路由器;小体积 千兆高速通信组网

计讯物联工业路由器TR232&#xff0c;5G高速网络&#xff0c;超低时延、高可靠性&#xff0c;小体积、易安装、强兼容&#xff0c;串口/网口多设备接入联网&#xff0c;为用户提供高速稳定的数据传输通道 。    小体积5G工业路由器TR323&#xff0c;外形1047824mm&#xff0…

el-popover触发元素位置改变后更新弹出框的偏移位置

el-popover的使用如下&#xff1a;包含一个触发元素和一个弹出框元素 但是如果触发元素位置发生变化时&#xff0c;如根据弹框选择内容&#xff0c;会显示或隐藏对应的元素&#xff0c;从而导致弹出框触发元素的位置的变化&#xff0c;此时触发元素位置变化了&#xff0c;但是…

C++ 初阶 类和对象(中)

前言&#xff1a;C初阶系列&#xff0c;每一期博主都会使用简单朴素的语言将对应的知识分享给大家&#xff0c;争取让所有人都可以听懂&#xff0c;C初阶系列会持续更新&#xff0c;上学期间将不定时更新&#xff0c;但总会更的 目录 一、构造函数 1.1为什么要有构造函数&…

树状数组 and 线段树

目录 &#x1f996;解释 -- 树状数组 &#xff08;一&#xff09;公式 &#xff08;二&#xff09;操作 (1) 求前缀和 (2) 某个位置上的数更新 &#x1f996;解释 -- 线段树 &#x1f33c;1264. 动态求连续区间和 AC 树状 AC 线段树 &#x1f33c;1265. 数星星 暴…

数字化时代,数据仓库是什么?有什么用?

在激烈的市场竞争和全新的数字经济共同作用下&#xff0c;数字化转型成为了大多数企业的共识&#xff0c;也是获取数字经济的最佳方式。在整个数据价值生产链路中&#xff0c;数据仓库的主要作用就是中心化分发&#xff0c;将原始数据与数据价值挖掘活动隔离。 所有的原始数据…

大数据毕业设计选题推荐-自媒体舆情分析平台-Hadoop-Spark-Hive

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

Docker学习路线3:安装设置

Docker提供了一个名为Docker Desktop的桌面应用程序&#xff0c;简化了安装和设置过程。还有另一个选项可以使用Docker引擎进行安装。 Docker Desktop网站Docker引擎 Docker Desktop Docker Desktop是一款易于安装的应用程序&#xff0c;可使开发人员快速在其台式机上设置Do…

IntelliJ IDEA Services工具栏运行不显示端口问题解决

问题 如Spring Boot服务启动时&#xff0c;端口不显示。 解决 1、 清理所有缓存 2、 关闭IntelliJ IDEA后&#xff0c;到C:\Users\&#xff08;你自己的用户名&#xff09;\AppData\Local\Temp路径把所有文件都删除&#xff0c;因为时一个缓存&#xff0c;不影响其他软件…

世微 DC-DC平均电流双路降压恒流驱动器 LED车灯AP2813

产品描述 AP2813 是一款双路降压恒流驱动器,高效率、外 围简单、内置功率管&#xff0c;适用于 5-80V 输入的高精度降 压 LED 恒流驱动芯片。内置功率管输出最大功率可达 12W&#xff0c;最大电流 1.2A。 AP2813 一路直亮&#xff0c;另外一路通过 MODE1 切换 全亮&#xff0c…

推荐PHP付费进群源码

PHP付费进群源码带自动定位基于ThinkPHP框架开发的&#xff0c;可以快速搭建知识付费粉丝进群。 更新&#xff1a; 1.首页付款轮播 2.城市定位功能 3.更新及优化域名库及支付设置 4.新增一张图模板设置模式&#xff0c;简化后台模板设置 5.前后台其他优化 演示地址&#xff1a…

【Linux进程】环境变量

目录 Linux环境变量 基本概念 常见环境变量 查看环境变量方法 测试PATH 测试HOME 和环境变量相关的命令 环境变量的组织方式 通过代码如何获取环境变量 命令行参数 命令行第三个参数 通过第三方变量environ获取 通过系统调用获取或设置环境变量 ​编辑环境变量通…

【ML】分类问题

分类问题 classification&#xff1a;根据已知样本特征&#xff0c;判断输入样本属于哪种已知样本类。 常用入门案例&#xff1a;垃圾邮件检测、图像分类、手写数字识别、考试通过预测。 分类问题和回归问题的明显区别&#xff1a; 分类问题的结果是非连续型标签&#xff0c…