作者 | daydreamer
在互联网的服务中,C++常用于建立高性能、高并发、大流量、低延时的后端服务。怎么合理的分配内存满意体系高性能需求是一个高频且重要的话题,而且由于内存本身的特点和实践问题的复杂,组合出了诸多难题。
我们能够对内存进行多种类型的区分,从内存恳求巨细来看:
-
小目标分配:小于4倍内存页巨细的内存分配,在4KiB页巨细情况下,<16KiB算作小目标分配;
-
大目标分配:大于等于4倍内存页巨细的内存分配,在4KiB页巨细情况下,>=16KiB算作大目标分配。
从一块内存的被持有时长来看:
-
后端一次恳求内乃至更短时刻恳求和开释
-
任意时刻窗口内内存持有和更新
-
简直与应用进程等长的内存持有和更新
-
某个进程消亡后一段时刻内,由该进程恳求的仍具有意义的内存持有和开释
当然还能够按照内存恳求开释频率、读写频率进行进一步的分类。
内存办理服务于应用体系,意图是帮忙体系更好的处理瓶颈问题,比如关于『怎么下降后端呼应的延迟和提高稳定性』内存办理或许要考虑的是:
-
处理内存读写并发(读频频or写频频)下降呼应时刻和CPU消耗
-
应用层的内存的池化复用
-
底层内存向体系恳求的内存块巨细及内存碎片化
每一个问题展开或许都是一个比较大的话题,本文作为系列文章《探秘C++内存办理》的开篇,先介绍Linux C++程序内存办理的理论基础。后续会继续解密C++程序常用的内存办理库的完成原理,包含ptmalloc,jemalloc,tcmalloc等,介绍当前业界盛行的内存分配器怎么办理C++程序的内存。了解内存分配器原理,更有助于工程师在实践中下降处理内存运用问题的本钱,依据体系量身打造应用层的内存办理体系。
一、Linux内存办理
Linux自底向上大致能够被区分为:
-
硬件(Physical Hardware)
-
内核层(Kernel Space)
-
用户层(User Space)
△图1:Linux结构
内核模块在内核空间中运转,应用程序在用户空间中运转,二者的内存地址空间不重叠。这种办法确保在用户空间中运转的应用程序具有共同的硬件视图,而与硬件渠道无关。用户空间经过运用体系调用以可控的方法使内核服务,如:堕入内核态,处理缺页中止。
Linux的内存办理体系自底向上大致能够被区分为:
-
内核层内存办理 : 在 Linux 内核中 , 经过内存分配函数办理内存:
-
kmalloc()/__get_free_pages():恳求较小内存(kmalloc()以字节为单位,__get_free_pages()以一页128K为单位),恳求的内存坐落物理内存的映射区域,而且在物理上也是接连的,它们与实在的物理地址只要一个固定的偏移。
-
vmalloc():恳求较大内存,虚拟内存空间给出一块接连的内存区,但不确保物理内存接连,开销远大于__get_free_pages(),需求建立新的页表。
-
用户层内存办理:经过调用体系调用函数(brk、mmap等),完成常用的内存办理接口(malloc, free, realloc, calloc)办理内存;经典内存办理库ptmalloc2、tcmalloc、jemalloc。
-
应用程序经过内存办理库或直接调用体系内存办理函数分配内存,依据应用程序本身的程序特性进行运用,如:单个变量内存恳求和开释、内存池化复用等。
至此单个进程能够运用Linux供给的内存区分顺畅的运转,从用户程序来看Linux进程的内存模型大致如下所示:
△图2:Linux进程的内存模型
-
栈区(Stack):存储程序履行期间的本地变量和函数的参数,从高地址向低地址生长
-
堆区(Heap): 动态内存分配区域,经过malloc、new、free和delete等函数办理
在规范C库中,供给了malloc/free函数分配开释内存,这些函数的底层是依据brk/mmap这些体系调用完成的,对照图2来看:
-
brk(): 用于恳求和开释小内存。数据段的末尾,堆内存的开始,叫做brk(program break)。经过设置heap的结束地址,将该地址向高或低移动完成堆内存的扩张或缩短。低地址内存必须在高地址内存的开释之后才干得到的开释,被标记为闲暇区的低地址,无法被兼并,假如后续再来内存空间的恳求大于此闲暇区,这部分将成为内存空泛。默认情况下,当最高地址空间的闲暇内存超越128K(可由M_TRIM_THRESHOLD选项调理)时,履行内存紧缩操作(trim)。
-
mmap():用于恳求大内存。mmap(memory map)是一种内存映射文件的办法,即将一个文件或许其它目标映射到进程的虚拟地址空间中(堆和栈中间的文件映射区域 Memory Mapping Segment),完成文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映联系。完成这样的映射联系后,进程就能够选用指针的方法读写操作这一段内存,而体系会主动回写脏页面到对应的文件磁盘上,内核空间对这段区域的修改也直接反映用户空间,从而能够完成不同进程间的文件同享。大于 128 K 的内存,运用体系调用mmap()分配内存。与 brk() 分配内存不同的是,mmap() 分配的内存能够单独开释。
-
munmp():开释有mmap()创立的这段内存空间。
但在关于多个一起运转的进程,体系仍需处理有限的物理内存和增长的内存地址等问题。那么当Linux存在多个一起运转的进程时,一次内存的分配进程具体都经过哪些进程呢?现代Linux体系上内存的分配首要进程如下[1] :
-
应用程序经过调用内存分配函数,体系调用brk或许mmap进行内存分配,恳求虚拟内存地址空间。
-
虚拟内存至物理内存映射处理进程,经过恳求MMU分配单元,依据虚拟地址计算出该地址所属的页面,再依据页面映射表的起始地址计算出该页面映射表(PageTable)项所在的物理地址,依据物理地址在高速缓存的TLB中寻找该表项的内容,假如该表项不在TLB中,就从内存将其内容装载到TLB中。
△图3:Linux内存分配机制(虚拟+物理映射)
关于内存分配进程中涉及到东西进一步剖析:
-
虚拟内存(Virtual Memory):现代操作体系遍及运用的一种技术,每个进程有用独立的逻辑地址空间,内存被分为巨细相等的多个块,称为页(Page)。每个页都是一段接连的地址,对应物理内存上的一块称为页框,通常页和页框巨细相等。虚拟内存使得多个虚拟页面同享同一个物理页面,而内核和用户进程、不同用户进程阻隔。
-
MMU(Memory-Management Unit):内存办理单元,负责办理虚拟地址到物理地址的内存映射,完成各个用户进程都具有自己的独立的地址空间,供给硬件机制的内存拜访权限检查,维护每个进程所用的内存不会被其他的进程所破坏。
-
PageTable:虚拟内存至物理内存页面映射联系存储单元。
-
TLB(Translation Lookaside Buffer):高速虚拟地址映射缓存, 首要为了提升MMU地址映射处理功率,加了缓存机制,假如存在即可直接取出映射地址供运用。
这里要说到一个很重要的概念,内存的延迟分配,只要在真实拜访一个地址的时分才建立这个地址的物理映射,这是 Linux 内存办理的基本思想之一。Linux 内核在用户恳求内存的时分,只是分配了虚拟内存,并没有分配实践物理内存;当用户第一次运用这块内存的时分,内核会发生缺页中止,分配物理内存,建立虚拟内存和物理内存之间的映射联系。当一个进程发生缺页中止的时分,进程会堕入内核态,履行以下操作:
-
检查要拜访的虚拟地址是否合法
-
查找/分配一个物理页
-
填充物理页内容
-
建立映射联系(虚拟地址到物理地址)
-
重新履行触发缺页中止的指令
假如填充物理页的进程需求读取磁盘,那这次缺页中止是majflt,否则是minflt。我们需求要点重视majflt的值,由于majflt关于性能的危害是丧命的,随机读一次磁盘的耗时数量级在几个毫秒,而minflt只要在很多的时分才会对性能产生影响。
二、总结
经过对Linux内存办理的介绍,我们能够看到内存办理需求处理的问题:
-
调用体系供给的有限接口操作虚存读写
-
权衡单次分配较大内存和多次分配较少内存带来本钱:控制缺页中止(尤其是majflt)vs 进程占用过多内存
-
下降内存碎片
-
下降内存办理库本身带来的额外损耗
在接下来的几篇文章将就ptmalloc,jemalloc,tcmalloc几个经典内存办理库,与大家进一步讨论C++程序常用的内存办理库的完成原理。
———- END ———-
相关参考:
[1] 《Linux透明大页机制在云上大规模集群实践介绍》:mp.weixin.qq.com/s/hGjADS9td…
[2] Writing a Linux Kernel Module — Part 1: Introduction: derekmolloy.ie/writing-a-l…
[3] blog.csdn.net/alimingh/ar…
[4] zhuanlan.zhihu.com/p/469124915
[5] zhuanlan.zhihu.com/p/442426370
[6] blog.csdn.net/agonie20121…
推荐阅读【技术加油站】系列:
从零到一了解APP速度测评
百度工程师教你玩转设计形式(工厂形式)
揭秘百度智能测验在测验剖析范畴实践
百度用户产品流批一体的实时数仓实践
ffplay视频播映原理剖析
百度工程师眼中的云原生可观测性追踪技术