一、进程与内存
当一个进程启动时,它需要获取系统分配给它的内存空间,并且设置好必要的数据结构和寄存器值,以便开始执行。这样,进程就可以在自己的独立地址空间中运行,访问所需的代码和数据:
- 创建进程控制块(Process Control Block,PCB):操作系统会为新进程创建一个数据结构,称为进程控制块(PCB),用于管理和跟踪进程的信息。PCB存储了进程的标识符、状态、寄存器值和其他与进程相关的信息。
- 分配虚拟地址空间:操作系统为新进程分配一个独立的虚拟地址空间。该地址空间是进程独立的逻辑地址范围,用于访问进程的代码、数据和堆栈。
- 加载可执行文件:操作系统将进程的可执行文件从磁盘加载到进程的虚拟地址空间中。这包括将程序代码、静态数据和其他必要的资源加载到适当的虚拟内存地址中。
- 分配物理内存:当可执行文件加载到虚拟地址空间后,操作系统需要为进程分配物理内存。通常,物理内存是以页面(通常是4KB)为单位进行分配的。
- 设置页表:操作系统通过页表将进程的虚拟地址映射到物理内存中的页面。页表是一种数据结构,记录了虚拟页和物理页之间的对应关系。操作系统会根据进程的内存需求和访问模式来设置页表。
- 初始化堆和栈:操作系统根据进程的需求,在虚拟地址空间中分配堆和栈空间。堆用于动态分配内存,而栈用于存储局部变量和函数调用信息。
- 设置程序计数器:操作系统将程序计数器(Program Counter,PC)设置为可执行文件的入口点,使得进程可以从正确的位置开始执行。
当一个进程结束时,其所占用的内存资源会被操作系统回收和释放,确保系统的内存得到有效管理,避免资源浪费和冲突,同时为其他进程提供更多可用的内存空间:
- 释放物理内存:操作系统会将进程所使用的物理内存空间标记为可用,以便后续可以重新分配给其他进程使用。这涉及更新操作系统的内存管理数据结构,如空闲内存列表或位图,以反映已释放的内存块。
- 清理页表:操作系统会清理进程的页表,将与该进程相关的页表项标记为无效。这样,其他进程在访问这些页时将引发页面错误,操作系统可以及时处理这些错误。
- 释放其他资源:除了物理内存,进程结束时还可能涉及释放其他资源,如打开的文件、网络连接、共享内存等。操作系统会关闭文件描述符、释放网络连接和共享内存区域,并执行其他必要的清理操作,以确保系统资源得到正确释放。
- 回收进程控制块:进程控制块(PCB)是操作系统用于管理和跟踪进程信息的数据结构。当进程结束时,操作系统会回收该进程的PCB,以便可以被重用或释放给其他进程使用。
这里有关内存的一系列操作,正是由内存管理机制完成的,这也是本文所要叙述的内容。
二、内存管理
2.1 内存管理概述
Linux内存管理是指对系统内存的分配、释放、映射、管理、交换、压缩等一系列操作的管理。
在Linux中,内存被划分为多个区域,每个区域有不同的作用,包括内核空间、用户空间、缓存、交换分区等。
Linux内存管理的目标是提高内存利用率,减少内存碎片,最大限度地利用可用内存,同时保证系统的稳定和可靠性。
2.2 原始内存管理
最初的机器没有那么复杂的内存管理机制,内核和进程运行在一个空间中,内核与进程之间没有做隔离,进程可以随意访问(干扰、窃取)内核的数据。而且进程和内核没有权限的区分,进程可以随意做一些敏感操作
而且当时的物理内存比较小,内存寻址是直接访问物理地址的形式,故程序通过内存寻址,是直接访问的物理内存;当时的内存分配也是直接分配一段连续的内存空间,这样就导致机器能同时运行的进程比较小,运行进程的吞吐量也比较小
- 物理内存:计算机硬件中用于存储程序和数据的实际内存芯片,也称为主存储器(Main Memory)。物理内存由许多存储单元组成,每个存储单元都有一个唯一的地址,用于存储数据。
- 物理(内存)地址:内存有一个最小的存储单位,大多数都是一个字节,内存地址来为每个字节的数据顺序编号,从0开始,每次增加1,因此说明了数据在内存中的位置,用十六进制数来表示内存地址,比如0x00000003、0x1A010CB0,这里的“0x”用来表示十六进制。“0x”后面跟着的,就是作为内存地址的十六进制数
- 内存寻址:Linux操作系统在内存中定位和访问数据的方式,Linux使用虚拟内存地址来管理物理内存,通过地址映射机制将虚拟地址映射到物理地址,这个过程称之为内存寻址。
为了提高内存的利用率和实现内存保护,第二代内存管理机制:分段内存管理诞生;并且引用了虚拟内存的概念。
2.3 内存分段机制与虚拟内存
2.3.1 虚拟内存
虚拟内存:是一种计算机系统的内存管理技术,它扩展了系统可用内存的容量,并为每个进程提供了独立的地址空间。虚拟内存将物理内存和磁盘空间结合使用,使得进程能够访问超出物理内存限制的数据。
后文再详细介绍
2.3.2 内存分段机制
内存分段机制是计算机中一种内存管理方式,它将内存(物理内存与虚拟内存)划分为若干个逻辑段,每个段的大小可以不同。
每个段由程序中的一个段表项来描述,包括段的起始地址、段的长度以及段的访问权限等。
- 段基地址:指定段在线性地址空间中的开始地址,基地址是线性地址对应于段中偏移0处
- 段限长:是虚拟地址空间中段内最大可用偏移地址,定义了段的长度
- 段属性:指定段的特性,如该段是否可读,可写或可执行,段的特权级等
段特权级:有特权(内核权限)与无特权(用户权限),内核的代码段和数据段都设置为特权段,进程的代码段和数据段都设置为用户段,这样进程就不能随意访问内核了。
当CPU执行特权段代码的时候会把自己设置为特权模式,此时CPU可以执行所以的指令。当CPU执行用户段代码的时候会把自己设置为用户模式,此时CPU只能执行普通指令,不能执行敏感指令。
通过这种方式,可以实现程序的隔离和保护,防止不同程序之间的相互干扰和破坏。
段寄存器:是用于存储程序中段(segment)的基地址的寄存器。段寄存器用于存储对应段的基地址,通过将段内的偏移量与段寄存器中的基地址相加,可以得到完整的物理地址
在实模式下,段寄存器通常用于访问内存中的不同段。而在保护模式下,段寄存器则用于实现内存保护和虚拟内存等高级功能。通过将段寄存器与段选择子(Segment Selector)等机制结合使用,可以实现更复杂的内存管理功能。
这里的实模式与保护模式指cpu的工作模式,规定了内存能够寻址的地址空间,现代多数的x86处理器操作系统都运行在保护模式下,只有在开机时是实模式
在分段机制下的虚拟地址由两部分组成,段选择因子和段内偏移量:
- 段选择子就保存在段寄存器里面。段选择子里面最重要的是段号,用作段表的索引。段表里面保存的是这个段的基地址、段的界限和特权等级等。
- 虚拟地址中的段内偏移量应该位于 0 和段界限之间,如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。
此时的内存寻址变为进程访问虚拟地址,再直接映射到物理地址上:
虚拟地址:由程序产生的由段选择符和段内偏移地址组成的地址
逻辑地址:由程序产生的段内偏移地址,逻辑地址与虚拟地址二者之间没有明确的界限
线性地址:指虚拟地址到物理地址变换的中间层,是处理器可寻址的内存空间(称为线性地址空间)中的地址。程序代码会产生逻辑地址,加上相应段基址就成了一个线性地址。如果启用了分页机制,那么线性地址再经过变换产生物理地址。若是没有采用分页机制,那么线性地址就是物理地址。
2.3.3 分段机制的优缺点
优点:
- 保护:每个段都有自己的访问权限,这样可以防止某些段被恶意软件或错误的程序修改
- 模块化:可以将程序的不同部分放在不同的段中,这样可以更容易地进行代码重用和模块化设计
- 隔离:将进程与内核隔离,进程无法随意访问内核,提高安全性
- 内存使用效率增大:分段机制中,程序的内存空间被划分为多个段,每个段对应程序的一部分,如代码段、数据段等。这样,当内存不足时,只需要将部分段换出到磁盘,而不是整个程序,减少了磁盘访问操作,并提高了程序的运行速度
缺点:
- 管理开销:需要管理多个段,这可能会增加内存管理的开销
- 碎片化:如果频繁地分配和释放不同大小的内存段,可能会导致内存碎片化,降低内存的利用率
- 不连续的物理内存:由于分段机制,物理内存可能不连续,这可能会影响某些性能敏感的应用程序
- 内存容量限制:分段机制受限于段寄存器的数量
- 内存保护有限:分段机制的内存保护只能针对整个段,如果一个程序要访问另一个程序的段,则无法阻止
这里补充下内存交换的几个概念:
- 内存交换(swap):将暂时不用的内存页面保存到磁盘中,然后再将物理内存中需要的页面重新加载到内存中;内存交换技术是在多道程序环境下用来扩充内存的两种方法之一 (另一种就是直接扩物理内存了)
- 换入(Page In):当应用程序或内核需要访问一个不在物理内存中的页面时,会发生换入,这时,系统会从磁盘读取该页面(通常是文件系统中的数据)到物理内存中,这可能会导致CPU的使用率增加,因为它涉及到磁盘I/O操作
- 换出(Page out):当物理内存紧张时,内核可能会决定将一些不太常用或暂时不需要的页面换出到磁盘上,以便为其他进程或应用程序释放内存空间,换出操作通常是异步的,这意味着系统不会立即执行该操作,而是在后台进行,换出操作通常涉及将页面数据写入磁盘并从物理内存中删除该页面;在Linux中,内核使用页面置换算法来决定哪些页面应该被换出。常见的页面置换算法包括FIFO(先进先出)、LRU(最近最少使用)等。
为了扩大内存容量限制,解决分段机制的内存碎片化与内存保护问题,又出现了内存分页机制。它兼容了分段机制,现在大多数系统都使用的分页机制,或者是段页式机制。
2.4 内存分页机制
分页机制把虚拟内存空间划分成若干大小相等的片,称为页(Page),并给各页加以编号,从0开始,如第0页、第1页等,通常每一页的大小为 4KB
把物理地址空间分成与页大小相等的若干个存储块,称为物理块或物理页面;分页管理时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧(也叫页框),以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号
2.4.1 页表
在分页机制下,虚拟地址与物理地址之间的映射,通过页表来实现,页表是存储在内存里的,内存管理单元 (MMU)就做将虚拟内存地址转换成物理地址的工作
一个页表的大小也是一个页面,4K大小,页表的内容可以看做是页表项的数组,一个页表项是一个物理地址,指向一个物理页帧
在32位系统上,物理地址是32位也就是4个字节,所以一个页表有4K/4=1024项,每一项指向一个物理页帧,大小是4K,所以一个页表可以表达4M的虚拟内存,要想表达4G的虚拟内存空间,需要有1024个页表才行,每个页表4K,一共需要4M的物理内存
下图为简略的页表映射图,仅做理解
4M的物理内存看起来好像不大,但是每个进程都需要有4M的物理内存做页表,如果有100个进程,那就需要有400M物理内存,这就太浪费物理内存了,而且大部分时候,一个进程的大部分虚拟内存空间并没有使用
因此引入了二级页表
2.4.2 二级页表
一级页表还是一个页面,4K大小,每个页表项还是4个字节,一共有1024项,一级页表的页表项是二级页表的物理地址,指向二级页表,二级页表的内容和前面一样。
一级页表只有一个,4K,有1024项,指向1024个二级页表,一个一级页表项也就是一个二级页表可以表达4M虚拟内存,一级页表总共能表达4G虚拟内存,此时所有页表占用的物理内存是4M加4K
虽然看起来使用二级页表好像还多用了4K内存,但是在大多数情况下,很多二级页表都用不上,所以不用分配内存,如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表。
比如一个进程只用了8M物理内存,那么它只需要一个一级页表和两个二级页表就行了,一级页表中只需要使用两项指向两个二级页表,两个二级页表填充满,就可以表达8M虚拟内存映射了,此时总共用了3个页表,12K物理内存,页表的内存占用大大减少了
而为什么不分级的页表就做不到这样节约内存呢?
因为从页表的性质来看,保存在内存中的页表承担的职责是将虚拟地址翻译成物理地址。假如虚拟地址在页表中找不到对应的页表项,就会报错。所以页表一定要覆盖全部虚拟地址空间,不分级的页表就需要有 100 多万个页来映射,而二级分页则只需要 1024 个页表项
2.4.2.1 页内偏移量
页内偏移量是指在虚拟地址或物理地址中的偏移量,用于指定该地址中的具体位置。在虚拟内存中,每个页面都由多个字节组成,页内偏移量就是用来标识这些字节的偏移量。通过将虚拟地址或物理地址与页内偏移量结合起来,操作系统可以确定一个特定的字节位置。存储在页表中的对应块号和页号对应的位置
在虚拟内存管理中,页内偏移量用于将虚拟地址转换为物理地址。当CPU访问虚拟内存时,MMU会根据当前的页目录和页表,将虚拟地址转换为相应的物理地址。在这个过程中,页内偏移量用于确定该地址在物理页面中的具体位置。
页内偏移量的长度通常与页面的大小有关。例如,如果页面大小为4KB,那么页内偏移量的长度为12位,因为4KB等于2^12字节。通过将页内偏移量与物理页面的起始地址相加,操作系统可以计算出实际的物理地址。
2.4.2.2 页表寄存器
页表寄存器是计算机中用于存储页表信息的寄存器。在虚拟内存管理中,页表寄存器用于将虚拟地址转换为物理地址。每个进程都有自己的页表,而页表寄存器则指向该进程的页表的起始地址。通过页表,操作系统能够实现内存的动态分配和管理,提高内存利用率并保护各个进程的内存空间。
在计算机系统中,页表寄存器的具体实现方式可能因操作系统和硬件架构的不同而有所差异。在一些系统中,页表寄存器可能是一个专门的硬件寄存器,用于存储页表的物理地址。而在其他系统中,页表寄存器可能是一个软件变量,通过操作系统进行管理。
无论采用哪种实现方式,页表寄存器都是虚拟内存管理中不可或缺的一部分,它使得计算机系统能够实现高效的内存管理,提高系统的性能和稳定性。
2.4.2.3 二级页表下的虚拟内存映射
2.4.3 四级页表
二级页表能解决32位操作系统的需求,但对于如今普遍的64位操作系统而言,还是不够的(64位虚拟空间内存大小为16EB,用户空间与内核空间大小均为128TB),这时就衍生出了四级页表,分为:
- 全局目录页 PGD(Page Global Directory)
- 上级目录页 PUD(Page Upper Directory)
- 中间目录页 PMD(Page Middle Directory)
- 页表(Page Table Entry):其中每一个页表项指向一个页框
2.4.4 TLB
多级页表虽然解决了空间上的问题,但是虚拟地址到物理地址的转换就多了几道转换的工序,这显然就降低了地址转换的速度,也就是带来了时间上的开销。
而程序是有局部性的,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。
利用这一特性,可以把最常访问的几个页表项存储到访问速度更快的硬件中,这个硬件就是TLB,通常称为页表缓存、转址旁路缓存、快表等
在 CPU 芯片里面,封装了内存管理单元(MMU)芯片,它用来完成地址转换和 TLB 的访问与交互
有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表
TLB 的命中率其实是很高的,因为程序最常访问的页就那么几个。
2.4.5 分页机制的优缺点
优点:
- 没有外部碎片,最后一页可能有内碎片,但不大
- 程序不必连续存放,便于改变程序占用空间大小
缺点:
- 程序仍需要全部装入内存
2.4.6 内存碎片
内存碎片分为两种类型:外部碎片和内部碎片
- 外部碎片:是指由于动态内存分配和释放过程中,导致剩余的未分配内存块被零散占据,无法满足大块内存的需求。虽然总的空闲内存足够,但无法分配连续的内存空间
- 内部碎片:是指已经分配给进程的内存块中,存在着未被充分利用的空间。例如,当为一个固定大小的数据结构分配内存,但实际使用的空间小于分配的大小时,就会产生内部碎片
内存碎片产生的常见原因:
- 频繁的内存分配和释放:过度频繁的内存分配和释放操作会导致内存块的零散分布,增加外部碎片的概率
- 内存对齐要求:某些系统和硬件要求内存地址对齐,导致分配的内存块大小超过实际需要,产生内部碎片
- 不合理的数据结构设计:设计过大的数据结构、不合理的内存分配策略等都可能导致内存碎片
- 内存泄漏(已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费):未释放的内存占用会导致内存的不连续分布,增加外部碎片
解决内存碎片常见的方法:
- 使用更高效的内存管理算法(后文介绍):例如,伙伴系统算法、Slab分配器等,根据不同的场景和需求来选择最合适的内存分配方式,以减少内存碎片的产生
- 定期清理内存:定期清理不再使用的内存页,可以减少内存碎片的产生。例如,可以定期执行
free
命令来释放不再使用的内存空间 - 使用更高效的磁盘文件系统:例如,XFS或EXT4等,可以减少磁盘空间的碎片化,从而减少内存碎片的产生
- 使用基于页面的内存管理方式(分页机制):将内存划分为固定大小的页面,以页面为单位进行内存分配和回收,可以减少内存碎片的产生
2.4.7 缺页异常
当应用程序尝试访问的内存页不存在或不能被映射到物理内存时,就会发生缺页异常。
原因可能是程序尝试访问的页面不存在,程序尝试写入只读页面,或者是页面被交换出内存
处理:
- 当发生缺页异常时,硬件会触发一个中断。Linux内核中的中断处理程序会响应这个中断,并检查异常的类型。
- 内核决定是否要加载该页面到内存中,这通常涉及从磁盘读取数据(如果是文件系统中的数据)。
- 如果页面已经在内存中,但因为某种原因不能被映射到物理内存(例如,它已经被锁定或正在被其他进程使用),内核将尝试找到一个可以映射该页面的空闲物理页。
影响:
- 如果频繁发生缺页异常,可能会导致系统性能下降,因为每次都需要从磁盘读取数据。
- 如果系统没有足够的内存来满足请求,那么会发生OOM(Out of Memory)错误,可能导致进程被终止。
调式及优化:
- 使用工具如
pmap
可以查看进程的内存映射。 - 使用
vmstat
、top
和htop
等工具可以帮助监控系统的内存使用情况。 - 如果发现频繁的缺页异常,可能需要优化应用程序的内存使用,或者增加系统的物理内存。
与页面置换算法:
- 当物理内存不足时,内核使用页面置换算法(如FIFO、LRU等)来决定哪个页面可以被替换或移除,从而为需要的页面腾出空间。
- 这些算法对缺页异常的发生频率有直接影响。
2.4.8 大页内存:标准大页,透明大页
Linux大页内存指的是一种内存管理方式,它允许系统管理更大的内存页面(最大可定义1GB的页大小),以适应越来越大的系统内存,让操作系统可以支持现代硬件架构的大页面容量功能。对大于4k的页,统称为 “大页(huge pages”)
使用大页的好处:
- 在TLB容量固定的情况下,可以提高TLB的命中率。例如果采用大小为 2MB 的大页,一个大页只需要对应一项 TLB 条目;同样大小的内存,换成使用 4KB 大小的页,则需要 512 项 TLB 条目。故可以使得程序运行得更快
- 减少了页表级数,也可以减少查找页表的时间。大页使得页表级数减少,例如原来从多达4级的页表可以减少到2级。查找页表的时间会明显改善。
- 减少缺页异常(page fault)的发生次数。假设应用程序需要 2MB 的内存,如果操作系统以 4KB 作为分页的单位,则需要512 次缺页异常才能将 2MB 应用程序空间全部映射到物理内存;然而当采用 2MB 作为分页时,只需要一次缺页异常就能完成。
缺点:
- 程序使用内存小,却申请了大页内存,会造成内存浪费,因为内存分配最小单位是页
大页分类:
- 标准大页(Huge pages):从Linux Kernel 2.6 后被引入,以适应越来越大的系统内存,默认大小为2M,且不可以被swap到磁盘
- 透明大页(Transparent Huge pages):从RHEL 6开始引入,缩写为THP。是Linux内核的一种内存管理特性,它允许系统自动地根据需要将内存页面合并成更大的页面
标准大页的优点是预先分配大页, 进程申请大页的时候到大页内存池中分配,成功概率很高。缺点应用程序需要特定适配使用文件系统编程接口
透明大页的内存页默认是2M大小,需要使用 swap 的时候,内存被分割为4k大小,对应用程序透明,不需要做特殊配置,但为动态分配内存(在运行时由 khugepaged 进程动态的分配),内存碎片化严重的时候,申请成功率不高,影响内存分配,造成性能问题
从RedHat 6,OEL 6,SLES 11 and UEK2 kernels 开始,系统缺省会启用 Transparent HugePages ,用来提高内存管理的性能。
注意:透明大页与标准大页联用会出现一些问题,导致性能问题和系统重启
以Oracle数据库为例:Oracle 数据库在内存中分配了大量的连续空间,而透明大页的合并可能会导致这些连续空间被中断,从而破坏了 Oracle 数据库的内存结构。此外,透明大页的使用还可能导致 Oracle 数据库的性能下降,因为合并后的大页需要更多的时间来访问。因此,为了避免 Oracle 数据库的节点意外重启,建议在使用 Oracle 数据库时禁用透明大页。
2.4.8.1 查看是否开启标准大页
#查看标准大页的页面大小
grep Hugepagesize /proc/meminfo
Hugepagesize: 2048 kB
#确认标准大页是否配置,并在使用
cat /proc/sys/vm/nr_hugepages
0 #0代表没有设置使用
grep -i HugePages_Total /proc/meminfo
HugePages_Total: 0 #0代表没有设置使用
2.4.8.2 查看是否开启透明大页
#[always] 表示已开启
#[madvise] 表示只在MADV_HUGEPAGE标志的VMA中使用
#[never] 表示禁用
cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never
2.4.8.3 关闭透明大页(修改后要重启程序)
#临时关闭
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
#永久关闭
echo 'echo never > /sys/kernel/mm/transparent_hugepage/defrag' >> /etc/rc.d/rc.local
echo 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' >> /etc/rc.d/rc.local
三、虚拟内存
3.1 概述
前面提到,虚拟内存是一种计算机系统的内存管理技术,它扩展了系统可用内存的容量,并为每个进程提供了独立的地址空间。虚拟内存将物理内存和磁盘空间结合使用,使得进程能够访问超出物理内存限制的数据。
以下是虚拟内存的几个关键概念:
- 虚拟地址空间(也叫虚拟内存空间):每个运行的进程都有自己的虚拟地址空间,它是一个连续的地址范围。虚拟地址空间的大小取决于所用的体系结构,可以远远超过物理内存的大小,从而提供了更大的地址范围和更高的灵活性;例如32位系统通常有4GB的虚拟地址空间,而64位系统则可以支持更大的地址空间。
- 虚拟地址:是在进程的上下文中使用的地址,也是程序所使用的地址,每个进程都有自己的虚拟地址空间,其中的地址对于该进程是唯一的。虚拟地址由两部分组成:页号(Page Number)和页内偏移(Page Offset)。在使用分页机制的系统中,虚拟地址被划分为固定大小的页面(通常是4KB)
- 分页(即前面提到的分页机制):虚拟内存将虚拟地址空间划分为固定大小的页(通常是4KB)。物理内存也被划分成与虚拟页相同大小的物理页帧。
- 页表:每个进程都有一个页表,用于将虚拟页映射到物理页帧。页表记录了虚拟页和物理页帧之间的映射关系。
- 页面置换:当物理内存不足时,操作系统可以将不常用的页从物理内存移动到磁盘上,以便为其他进程腾出空间。这个过程称为页面置换或页面交换。
- 页面错误:当进程访问一个尚未加载到物理内存的虚拟页时,会发生页面错误。操作系统会捕获这个错误,并将相应的页从磁盘加载到内存中,然后重新执行引发错误的指令。
3.1.1 虚拟内存的优缺点
优点:
- 扩展地址空间:虚拟内存允许每个进程具有比物理内存更大的地址空间。这使得进程可以处理比实际可用内存更大的数据集和程序代码。进程可以使用虚拟地址来访问数据,而不需要全部加载到物理内存中。
- 内存隔离:每个进程都有自己的虚拟地址空间,相互之间是隔离的。这意味着一个进程无法直接访问其他进程的内存,从而提供了安全性和稳定性。如果一个进程出现错误或崩溃,它不会影响其他进程或操作系统。
- 灵活的内存分配:虚拟内存允许操作系统根据需要动态地将页面从磁盘加载到物理内存,或者将不常用的页面换出到磁盘上的页面文件。这样,操作系统可以灵活地管理内存资源,并优化内存的使用效率。
- 共享内存:虚拟内存提供了共享内存的机制,允许多个进程共享同一块物理内存区域。这对于进程间通信和数据共享非常有用,可以减少数据复制和数据传输的开销,提高系统的性能和效率。
缺点:
- 性能开销:使用虚拟内存会引入一定的性能开销。虚拟地址到物理地址的转换需要额外的硬件支持和操作系统的管理。此外,页面的加载和换出也会涉及磁盘I/O操作,可能引起额外的延迟。
- 页面错误(Page Fault):当进程访问的页面不在物理内存中时,会发生页面错误,需要操作系统将页面从磁盘加载到内存中。页面错误的处理会导致一定的延迟和系统开销。
- 硬盘空间占用:虚拟内存需要使用一部分磁盘空间作为页面文件,以存储被换出的页面。这会占用一定的磁盘空间,并可能导致磁盘碎片化。
- 不确定性:由于虚拟内存的存在,进程无法准确地知道其实际可用的物理内存大小。这可能导致进程在分配内存时出现过度分配或不足分配的情况,从而影响系统的性能和稳定性。
3.1.2 虚拟内存映射过程
- 分页单元:虚拟内存和物理内存都被划分为固定大小的页(通常是4KB)。每个页都有一个唯一的标识符,称为页号。
- 页表:操作系统维护一个页表,它是一个数据结构,用于记录虚拟页和物理页之间的映射关系。
- 虚拟地址转换:当进程访问虚拟地址时,操作系统将虚拟地址分割为页号和页内偏移。
- 页表查找:操作系统使用进程的页表,根据页号查找对应的页表项(Page Table Entry)。
- 映射到物理页框:页表项中记录了该虚拟页对应的物理页框号(Physical Page Frame Number)。操作系统使用物理页框号和页内偏移组合成物理地址。
- 物理内存访问:使用物理地址进行实际的内存访问,读取或写入数据。
需要注意的是,由于虚拟内存的存在,不是所有的虚拟页都会被加载到物理内存中。只有在进程访问虚拟页时,才会触发页面错误(Page Fault),操作系统根据需要将虚拟页从磁盘上的页面文件加载到物理内存中。这样就实现了根据需求动态地将虚拟内存映射到物理内存的机制。
3.2 虚拟地址空间分布
虚拟地址空间通常在操作系统中被划分为多个部分,用于存储不同类型的数据和执行不同的任务;通常分为内核空间与用户空间
- 内核空间(Kernel Space):内核空间是操作系统内核的专用区域,用于执行操作系统的核心功能和管理系统资源。它通常位于虚拟地址空间的顶部或底部,并具有较高的地址范围。只有操作系统内核可以直接访问和操作内核空间。
- 用户空间(User Space):用户空间是给用户程序使用的区域,用于执行应用程序和用户任务。它通常位于虚拟地址空间的中间部分,并具有较低的地址范围。用户程序可以在用户空间中运行,并通过系统调用等方式与内核空间进行通信。
每个进程都有自己独立的用户空间,但它们共享同一个内核空间。内核空间包含操作系统的核心代码、驱动程序和系统资源管理等,它提供了操作系统的功能和服务,例如进程调度、内存管理、文件系统等。
当进程需要与内核进行交互,例如进行系统调用、请求内存分配或进行设备访问时,它会切换到内核模式,并在内核空间执行相关的操作。这样,不同的进程可以通过访问共享的内核空间来请求操作系统提供的服务和功能。
内核空间的共享性有以下几个优势:
- 资源共享:所有进程共享同一个内核空间,这意味着它们可以共享系统资源,如设备驱动程序、文件系统、网络协议栈等。这样可以避免每个进程都需要独立拥有和管理这些资源,减少资源的冗余占用和系统开销。
- 系统一致性:内核空间的共享确保了系统中的所有进程都使用相同版本的内核代码和功能。这有助于保持系统的一致性和稳定性,简化了操作系统的维护和更新。
- 安全性:内核空间的共享可以提供更严格的访问控制和权限管理。由于内核空间的代码和数据是受保护的,进程无法直接修改或访问内核空间的内容,从而增强了系统的安全性和稳定性。
虽然内核空间是共享的,但每个进程在用户空间中有自己独立的地址空间,这样可以保证进程之间的隔离性和安全性。进程之间无法直接访问彼此的用户空间,只能通过系统调用等方式通过内核空间进行通信和交互。
3.3 内核空间
内核空间通常位于虚拟地址空间的高地址部分,从最高位开始的一个连续区域。它包含了操作系统的核心代码、驱动程序和系统资源管理等。内核空间是操作系统的特权区域,只有在内核模式下运行的代码才能够访问和执行内核空间中的内容。
内核空间的主要功能包括:
- 内核代码:内核空间包含操作系统的核心代码,实现了操作系统的功能和服务,如进程管理、内存管理、文件系统、网络协议栈等。这些代码由操作系统开发人员编写,并在内核模式下执行,以提供系统级的操作和服务。
- 驱动程序:内核空间还包含设备驱动程序,用于管理和控制计算机硬件设备,如硬盘驱动器、网络适配器、显卡等。驱动程序在内核空间中运行,可以与硬件设备进行交互,并向用户空间提供访问硬件设备的接口。
- 系统资源管理:内核空间负责管理系统资源,包括内存管理、进程调度、文件系统管理、网络连接管理等。它维护着全局的数据结构和状态信息,确保不同进程之间的资源使用和访问的合理性和安全性。
内核空间的特点和作用包括:
- 特权执行:只有在内核模式下运行的代码才能够访问和执行内核空间中的代码和数据。用户模式下运行的应用程序无法直接访问内核空间,这样可以保证系统的安全性和稳定性。
- 共享性:所有进程共享同一个内核空间,这意味着它们可以共享系统资源和服务。这样可以避免每个进程都需要独立拥有和管理这些资源,减少资源的冗余占用和系统开销。
- 隔离性:虽然进程共享内核空间,但每个进程的用户空间是独立的,进程之间无法直接访问彼此的用户空间。内核空间提供了一层保护,确保不同进程之间的数据和操作相互隔离,增强了系统的安全性和稳定性。
空间分布
在x86_32上,内核空间只有1G,而64位的内核空间有128T(只使用了其中的低 48 位来表示虚拟内存地址),空间分布如下:
- 代码段:存放操作系统的核心代码,用于实现操作系统的功能和服务;如内核启动代码,调度器,系统调用接口等。
- vmemmap 映射区:虚拟内存映射区,提供了内核访问和管理物理内存、数据结构、模块和驱动程序、堆栈等的能力,它为内核提供了一个方便而安全的方式来执行操作系统的核心功能和服务;间接映射机制,用于将物理内存映射到非连续的虚拟地址空间中
- vmalloc 映射区:动态分配可变大小的内存块,vmalloc是一种内核提供的函数,用于在内核空间动态地分配大块的虚拟内存
- 直接映射区(Direct Mapping Region):用于将物理内存直接映射到内核空间的虚拟地址空间,提供了内核对物理内存的快速访问能力,存储和管理内核数据结构,执行内核代码,并提供了虚拟内存管理的基础;因为64T足够映射当下系统全部的物理内存,故64位操作系统没有高端内存这一说
高端内存:指位于4GB物理内存以上的部分内存空间。在32位x86架构中,由于寻址空间限制,操作系统和应用程序只能直接访问低于4GB的物理内存,而超过4GB的物理内存则无法直接寻址。这部分超过4GB的物理内存被称为高端内存。
3.4 用户空间
用户空间是进程创建时动态创建的,对于用户空间,物理内存的分配和虚拟内存的分配是割裂的,用户空间总是先分配虚拟内存不分配物理内存,物理内存总是拖到最后一刻才去分配。
而且对于进程本身来说,它只能分配虚拟内存,物理内存的分配对它来说是不可见的,或者说是透明的。
当进程去使用某一个虚拟内存时如果发现还没有分配物理内存则会触发缺页异常,此时才会去分配物理内存并映射上,然后再去重新执行刚才的指令,这一切对进程来说都是透明的,进程感知不到
空间分布
- 代码段(Text Segment):代码段存放可执行程序的指令,通常是只读的。以确保程序的指令在运行时不会被修改。代码段是所有进程共享的,这样可以节省内存空间。当程序被加载到内存并执行时,处理器从代码段中读取指令并执行。代码段的大小和位置是固定的。
- 数据段(Data Segment):用于存放已经初始化的全局变量和静态变量,这些变量在编译和链接过程中被赋予了初始值。数据段在可执行文件中占据一定的空间,并且在程序加载时,这些变量的初始值会被拷贝到对应的内存位置。
- BSS段(Block Started by Symbol):用于存放未初始化的全局变量和静态变量。这些变量在编译和链接过程中被标记为BSS段,但在可执行文件中并不占据实际的存储空间。在程序加载时,操作系统会为BSS段分配内存,并将其内容初始化为零值或空值。
- 堆(Heap):堆是用于动态分配内存的区域。在堆中,程序可以使用诸如malloc()和free()等函数来请求和释放内存。堆的大小是不固定的,可以根据需要进行扩展或收缩,从低地址开始向上增长
- 共享库和映射文件:也称mmap内存映射区。共享库是可重用的代码和数据的集合,多个进程可以共享同一个共享库,以减少内存占用和提高系统效率。映射文件是将磁盘上的文件映射到内存中,允许进程通过内存访问文件内容,内存的换入换出就是在此区域进行的。
- 栈(Stack):栈用于存放函数的局部变量、函数调用的上下文信息和临时数据。每当一个函数被调用时,一个新的栈帧被压入栈中,用于存储函数执行期间的相关数据。栈是一种后进先出(LIFO)的数据结构,从高地址开始向下增长
这些段的划分使得用户空间的虚拟地址空间具有结构化和层次化的特性,方便程序的管理和访问。不同的段用于存放不同类型的数据,并且具有不同的访问权限和行为。这种分段的机制有助于提高内存的使用效率和系统的安全性。
mmap映射区:
- mmap用来存放malloc函数申请一大块内存区域,mmap不仅仅可以用来映射物理内存同时也可以用来映射文件,所谓映射文件就时将文件数据拷贝到物理内存中某个区域,这块区域通过页表映射到用户mmap映射区中。这样用户可以像访问内存一样访问文件。
- 当mmap区域映射到文件时我们称为文件映射,非文件时称为匿名映射。
- 当内存空间不足时可以将内存数据写入磁盘中的某个文件,这个过程称为换出,当进程访问这些内存时,再从磁盘读取这些数据到内存中这个过程称为换入。而这个写入的文件被称为swap文件
四、物理内存管理
了解完内存分段、分页机制和虚拟内存,再来看下物理内存的管理机制,进程最终还是要落到物理内存上运行的,而物理内存管理是指操作系统管理和分配系统中物理内存资源的过程。它负责跟踪可用内存的状态、分配和释放内存,以及处理内存的页表和页面调度等任务。
4.1 物理内存架构
两种常见的多处理器系统的内存访问架构:UMA(Uniform Memory Access)架构,NUMA(Non-Uniform Memory Access)架构
多处理器系统:是指由多个处理器(即CPU,中央处理器)组成的计算机系统。在多处理器系统中,多个处理器可以同时执行指令和处理任务,共同完成计算任务。这些处理器可以是物理上独立的芯片,也可以是在同一芯片上的多个核心。
4.1.1 UMA(一致性内存访问架构)
在 Linux 中,UMA(Uniform Memory Access)是一种计算机体系结构,也称为对称多处理架构(Symmetric Multiprocessing Architecture,SMP)其中所有处理器(CPU)以相同的延迟访问共享的物理内存。在UMA系统中,每个处理器通过相同的总线或互联网络连接到内存,无论处理器的位置如何,访问内存的时间是相同的。这意味着无论处理器位于系统中的哪个位置,对内存的访问延迟都是一致的。
总线bus:计算机系统中用于传输数据、地址和控制信号的一组电子线路或导线集合。它连接了计算机系统中的各个组件,如中央处理器(CPU)、内存、输入/输出设备和其他外部设备。
优点:
- 简单性:UMA相对简单,因为所有处理器共享同一物理内存。这简化了系统设计和编程模型,降低了系统管理的复杂性。
- 延迟一致性:在UMA系统中,每个处理器对内存的访问延迟是相同的。这意味着无论处理器位于系统中的哪个位置,对内存的访问时间是均等的,因此可以更容易地预测和管理内存访问的性能。
- 公平性:UMA模型保证了处理器之间的公平性。每个处理器具有相同的访问特权,无论其位置如何,都可以获得相同的内存访问权益。
缺点:
- 内存带宽瓶颈:在UMA系统中,所有处理器共享同一条总线或交叉连接网络来访问内存。这可能导致内存带宽成为系统性能的瓶颈,因为所有处理器必须通过相同的通信通道来竞争访问内存。
- 可扩展性限制:由于所有处理器共享同一内存,UMA模型在大规模系统中的可扩展性受到限制。随着处理器数量的增加,内存访问的竞争和争用可能会增加,导致性能下降。
- 非局部性访问:在UMA系统中,处理器对内存的访问延迟是一致的,但访问远程内存可能会导致较高的延迟。这对于具有较高内存局部性的应用程序来说可能是一个问题,因为它们可能需要频繁地访问远程内存,从而降低性能。
适用场景:UMA适用于对内存访问延迟要求不高、处理器数量较少的应用场景,其中所有处理器具有相同的访问特权,且内存访问延迟相对较低。
4.1.2 NUMA(非一致性内存访问架构)
在NUMA架构中,系统中的内存分布在多个节点上,每个节点附加了一组处理器,每个节点都有自己的本地内存和处理器(物理cpu),而且节点之间通过高速互连网络连接,每个节点上的处理器访问本地内存的延迟较低,而访问远程节点上的内存的延迟较高。
优点:
- 低延迟访问本地内存:在NUMA系统中,每个处理器与一部分本地内存直接连接,因此可以实现较低延迟的本地内存访问。这对于具有较高内存局部性的应用程序来说是有利的,可以提高访问速度和性能。
- 可扩展性:NUMA支持在系统中添加更多的处理器和内存节点,以提供更高的可扩展性。不同的处理器和内存节点可以通过高速互连网络连接,形成一个大规模的系统。这使得NUMA系统适用于需要大量并行计算的应用场景。
- 内存带宽增加:由于NUMA系统中的内存分布在多个节点上,并且节点之间通过互连网络连接,可以实现更高的内存带宽。这对于需要大量数据传输和通信的应用程序来说是有益的。
缺点:
- 高延迟访问远程内存:在NUMA系统中,访问远程节点的内存比访问本地内存具有更高的延迟。当处理器需要访问远程内存时,延迟增加可能导致性能下降。
- 内存访问不均衡:在NUMA系统中,不同的处理器有不同的访问延迟和带宽,这可能导致内存访问的不均衡。如果任务分布不合理,一些处理器可能需要频繁地访问远程内存,从而降低性能。
- 软件优化挑战:NUMA系统对软件优化提出了一些挑战。合理地分配任务和数据,使用本地内存和减少远程内存访问,需要进行复杂的软件优化和编程模型设计。
适用场景:NUMA适用于需要大规模并行处理和高内存带宽的应用场景,它可以提供较低延迟的本地内存访问和较高的可扩展性。
每个节点(node)中,内存还被分为区域(zone),用于表示不同范围的内存,映射不同的虚拟内存;区域中管理的最小单位为页(page),即前面所说的分页机制下的页帧或页框;一个节点中有一个或多个zone
- ZONE_DMA:管理的是可用于 DMA(Direct Memory Access,直接内存访问)设备的内存区域,这些设备通常需要在内存中读写数据,因此需要对这些内存区域进行特殊的管理。由于 DMA 控制器只有 24 位的地址线,因此它只能访问低于 4GB 的内存地址空间,因此因此在64位Linux中DMA 区域通常是位于系统的低端,大小为 0~16MB 左右
- ZONE_DMA32:可以访问更高物理地址的设备(例如高性能网卡),必须使用 64 位物理地址来进行访问。而 DMA32 区域的大小通常为 16MB~4GB,可以支持一些需要更高地址空间的设备内存空间。在 64 位系统中,由于物理地址空间非常大,一般不再需要显式地划分 DMA 区和 DMA32 区
- ZONE_NORMAL:常规内存,管理的是一般的系统内存,它是最常用的 zone。(在64位中4G以上为ZONE_NORMAL区域管理)
- ZONE_HIGHMEM:这个 zone 管理的是高端内存(High Memory),只会出现在32位系统内,这时由于在32位系统中,物理内存最多能够直接映射到内核中896M内存,但是为了兼容大于896M内存系统,将大于896M的内存映射到高端内存以弥补地址空间不足的问题,注意高端内存映射是在使用时候才映射。在64位系统中由于地址空间使用足够,因此不需要ZONE_HIGHMEM。
- ZONE_MOVABLE:可移动内存,无配置项控制,必然存在,用于可热插拔的内存,ZONE_MOVABLE的页面可以被移动到其他区域,或者被释放掉,而不会影响系统的正常运行。:ZONE_MOVABLE一般称为伪ZONE,所管理的物理内存来自于ZONE_NORMAL或者ZONE_HIGHMEM
- ZONE_DEVICE:设备内存,由配置项CONFIG_ZONE_DEVICE决定是否存在,用于放置持久内存(也就是掉电后内容不会消失的内存)。一般的计算机中没有这种内存,默认的内存分配也不会从这里分配内存。持久内存可用于内核崩溃时保存相关的调试信息
linux上查看NUMA相关信息
使用 numactl -H
在node distances一栏,表示不同node之间发访问距离(跳数),如[0,0] 表示node 0的本地内存访问距离为10。
跳数(hops):表示从一个节点到达另一个节点所需的内存访问步骤数,并不是一个精确的时间或距离单位,而是一种相对指标
内存分配策略
在 NUMA架构中,为了最大程度地减少远程内存访问的开销,操作系统和应用程序可以采用以下策略来进行内存分配:
- 本地性原则(Locality Principle):根据任务和数据的本地性原则,将任务和数据分配到就近的 NUMA 节点上。这可以最小化远程内存访问的延迟和带宽开销,提高系统性能。操作系统可以使用与 NUMA 相关的调度算法和策略来实现本地性原则。
- NUMA 感知的内存分配(NUMA-Aware Memory Allocation):操作系统和应用程序可以使用 NUMA 感知的内存分配机制,将内存分配限制在本地节点上。这样可以确保任务和数据在就近的节点上分布,最小化远程内存访问。在 Linux 中,可以使用相关的系统调用(如 numa_alloc_*)或库函数(如 libnuma)来进行 NUMA 感知的内存分配。
- NUMA 平衡(NUMA Balancing):操作系统可以动态地将任务和数据从高负载的节点迁移到低负载的节点,以实现系统负载均衡和内存使用均衡。这可以通过 NUMA 感知的调度算法和策略来实现。在 Linux 中,通过配置相关的内核参数,如 numa_balancing,默认情况下可以启用 NUMA 平衡。
- 绑定(Binding):操作系统和应用程序可以使用绑定机制将任务和线程绑定到特定的 NUMA 节点上。这样可以确保任务和线程在执行期间只访问本地节点的内存,避免远程内存访问的开销。在 Linux 中,可以使用工具和接口,如 numactl 和 sched_setaffinity,进行绑定操作。
4.2 物理内存模型
计算机中有很多名称叫做内存模型的概念,它们的含义并不相同,要注意区分。本章节所讲的内存模型是Linux对物理内存地址空间连续性的抽象,用来表示物理内存的地址空间是否有空洞以及该如何处理空洞,故分为连续内存模型和非连续内存模型。
在前面的章节提到,内核是以页为基本单位对物理内存进行管理的,通过将物理内存划分为一页一页的内存块,每页大小为 4K。一页大小的内存块在内核中用 struct page 结构体来进行管理,struct page 中封装了每页内存块的状态信息,比如:组织结构,使用信息,统计信息,以及与其他结构的关联映射信息等。
而为了快速索引到具体的物理内存页,内核为每个物理页 struct page 结构体定义了一个索引编号(页帧号):PFN(Page Frame Number)。PFN 与 struct page 是一一对应的关系。
4.2.1 FLAT MEM 平坦内存模型
平坦内存模型也称为线性内存模型,是早期使用的内存模型。此模型下,把物理内存想象成是一片地址连续的存储空间,内核将这块内存空间分为一页一页的内存块 struct page ,这些内存块大小固定且连续,内核中使用了一个 mem_map 的全局数组用来组织所有划分出来的物理内存页,mem_map 全局数组的下标就是相应物理页对应的 PFN 。
在此模型下,内存CPU可以直接线性寻址所有可用的内存位置,系统对所有物理内存空间做映射和绑定,包括空洞区域和实际的物理内存;常用于UMA架构
优点:
- 简单性:FLAT MEM 模型相对简单,因为它将内存视为连续的地址空间,不需要考虑物理内存的分布和访问延迟。这简化了内存管理和编程模型,并减少了开发和调试的复杂性。
- 透明性:在 FLAT MEM 模型中,应用程序可以将所有的内存地址视为可寻址的,并且可以自由地访问任何内存位置。这提供了更大的灵活性和透明性,应用程序无需关心具体的内存分布和访问特性。
- 低延迟访问:由于 FLAT MEM 模型不考虑物理内存的分布和访问延迟,应用程序可以更快地访问内存,因为不需要进行额外的寻址和跨节点的数据传输。
缺点:
- NUMA 不适配:FLAT MEM 模型不适用于 NUMA 架构,因为它忽略了不同 NUMA 节点之间的内存访问延迟和带宽差异。在 NUMA 架构中,优化内存访问模式和分配策略是至关重要的,而 FLAT MEM 模型无法提供这种优化。
- 性能受限:在具有高度分布的内存访问模式的情况下,FLAT MEM 模型可能无法有效地利用系统中的内存资源,导致性能受限。这是因为 FLAT MEM 模型不考虑物理内存的分布和访问延迟,无法充分利用 NUMA 架构中的本地内存和减少远程内存访问。
- 可扩展性受限:在大规模系统中,FLAT MEM 模型可能面临可扩展性的挑战。由于 FLAT MEM 模型不考虑物理内存的分布,它可能无法有效地扩展到多个节点和大量的内存资源。
- 资源浪费:对于多块非连续的物理内存来说使用 FLATMEM 平坦内存模型进行管理会造成很大的内存空间浪费,因为物理内存存在大量不连续的内存地址区间,这些不连续的内存地址区间形成了内存空洞。
4.2.2 DISCONTIG MEM 非连续内存模型
Discontiguous Memory是一种非连续内存模型,与 NUMA(Non-Uniform Memory Access)架构相关。它是FLATMEM平坦内存模型的一种扩展,可以有效避免平坦内存模型所造成的内存空洞。
在 NUMA 架构中,不同的 NUMA 节点具有本地内存和远程内存之分。为了最大程度地减少远程内存访问的开销,操作系统会尽可能地将任务和数据分配到就近的节点上。这导致了内存的非连续分布,即不同节点上的内存不是连续的,但同个节点内的内存还是连续的。
优点:
- 本地性优化:DISCONTIG MEM 模型允许操作系统和应用程序根据 NUMA 架构中节点之间的内存访问延迟和带宽差异,优化任务和数据的分配,将其尽可能地分配到本地节点上。这可以减少远程内存访问的开销,提高系统性能。
- 内存资源利用:DISCONTIG MEM 模型可以更有效地利用 NUMA 架构中的内存资源。通过将任务和数据分配到本地节点上,可以减少远程内存访问,减少带宽压力,提高内存访问效率。
- 可扩展性:在大规模 NUMA 系统中,DISCONTIG MEM 模型可以提供更好的可扩展性。通过合理地分配任务和数据到不同的 NUMA 节点上,可以平衡系统负载,避免单个节点上的资源瓶颈,支持更大规模的系统扩展。
缺点:
- 复杂性增加:相对于传统的连续内存模型,DISCONTIG MEM 模型在内存管理和分配方面更加复杂。操作系统和应用程序需要考虑节点间的内存访问代价,并采取相应的策略来优化任务和数据的分配。这增加了系统设计和调优的复杂性。
- 额外的开销:DISCONTIG MEM 模型可能会引入额外的开销。例如,为了跟踪和管理 NUMA 架构中的非连续内存分布,操作系统需要维护额外的数据结构和算法。这些开销可能会占用一定的内存和计算资源。
- 编程挑战:在使用 DISCONTIG MEM 模型的系统中,编写优化的、可扩展的代码可能具有一定的挑战性。开发人员需要理解 NUMA 架构和 DISCONTIG MEM 模型的特点,并采用相应的编程技术来最大程度地发挥系统性能。
4.2.3 SPARSE MEM 稀疏内存模型
随着大内存和内存热插拔技术(下一章节介绍)的发展,物理内存的不连续就变为常态了,即node 节点内的内存也是不连续的,这就促进了内核管理物理内存的技术也随之发展,故衍生出了SPARSE MEM 稀疏内存模型
在 SPARSE MEM(稀疏内存模型)中,内存空间被划分为多个段(sections),其中只有一部分段被实际分配和使用,而其他段则被标记为未使用。每个段可以是固定大小或可变大小,具体取决于实现。通常物理页大小为4K时,sections大小为128M
在 SPARSE MEM 模型中,段(sections)是内存的基本单位,用于划分和管理内存空间。每个段都有一个状态标记,表示该段是已分配还是未分配。已分配的段表示实际使用的内存区域,而未分配的段表示空闲或未使用的内存区域。
稀疏内存模型的核心思想就是对粒度更小的连续内存块进行精细的管理
通过使用段(sections),SPARSE MEM 稀疏内存模型可以实现以下功能:
- 动态分配:根据需要,可以动态地分配未使用的段来满足内存需求。当需要分配内存时,可以从未分配的段中选择一个合适的段进行分配。
- 内存回收:当不再需要某个段时,可以将已分配的段标记为未分配,以便再次使用。这种内存回收机制可以确保内存资源的高效利用。
- 内存管理:通过精确分配和释放段,可以简化内存管理的复杂性。不需要维护整个内存空间的状态,而只需管理实际分配的段即可。
优点:
- 内存节省:SPARSE MEM 可以节省内存空间,因为它只分配实际需要的内存段,而不是整个内存空间。特别是在存在大量未使用或稀疏分布的内存区域时,可以显著降低内存的占用。
- 灵活性:SPARSE MEM 允许动态地分配和释放内存段,根据需求调整内存的大小和布局。这种灵活性可以在不同的应用场景下适应变化的内存需求。
- 简化内存管理:通过精确分配和释放内存段,可以简化内存管理的复杂性。不需要维护整个内存空间的状态,而只需管理实际分配的内存段即可。这可以减少内存管理的开销和复杂性。
- 高效的内存使用:SPARSE MEM 可以减少内存碎片化的问题。由于只分配实际需要的内存段,可以避免出现大量未使用的碎片化内存,提高内存的利用率。
缺点:
- 内存访问开销:由于内存段是离散的,可能存在跨段的内存访问。这可能导致额外的内存访问开销和延迟,特别是当需要远程访问其他段的内存时。
- 内存分配复杂性:SPARSE MEM 可能会引入内存分配的复杂性。由于内存段是离散的,需要进行动态分配和管理,可能需要更复杂的算法和数据结构来管理内存段的状态。
- 特定应用需求限制:某些应用程序可能对连续的内存空间有特殊要求,而 SPARSE MEM 可能无法满足这些要求。在这种情况下,可能需要使用其他内存管理模型。
现代的操作系统,基本都是用的SPARSE MEM 稀疏内存模型
4.2.4 物理内存热插拔
物理内存热插拔是指在系统运行时,将内存硬件插入或拔出主板的过程,可实现集群机器物理内存容量的动态增减,依赖于SPARSE MEM 稀疏内存模型
物理内存热插拔涉及以下步骤:
- 检测:操作系统或硬件平台会检测新插入或移除的内存模块。这可以通过硬件事件、总线扫描或操作系统的内存管理功能实现。
- 识别和初始化:系统会识别新插入的内存模块,并进行必要的初始化和配置。这可能包括分配地址空间、设置内存映射和初始化内存控制器等操作。
- 内存管理:一旦新的内存模块被识别和初始化,操作系统会将其纳入内存管理中。这意味着新的内存将被用于进程的分配和访问,从而增加系统的可用内存容量。
- 移除和回收:如果需要移除内存模块,操作系统会相应地调整内存管理,并将移除的内存模块标记为不可用。这可以通过内存页面迁移、重新分配和回收等技术来实现。
内存热插拔有个难点是并非所有的物理页都可以迁移,因为迁移意味着物理内存地址的变化,而内存的热插拔应该对进程来说是透明的,所以这些迁移后的物理页映射的虚拟内存地址是不能变化的
在虚拟内核空间中,有一段直接映射区,在这段虚拟内存区域中虚拟地址与物理地址是直接映射的关系,虚拟内存地址直接减去一个固定的偏移量(0xC000 0000 ) 就得到了物理内存地址
直接映射区中的物理页的虚拟地址会随着物理内存地址变动而变动, 因此这部分物理页是无法轻易迁移的,然而不可迁移的页会导致内存无法被拔除
为了解决这个问题,又将内存按照物理页是否可迁移,划分为不可迁移页,可回收页,可迁移页
然后在这些可能会被拔出的内存中只分配那些可迁移的内存页,这些信息会在内存初始化的时候被设置,这样一来那些不可迁移的页就不会包含在可能会拔出的内存中,当我们需要将这块内存热拔出时, 因为里边的内存页全部是可迁移的, 从而使内存可以被拔除
- 不可迁移页(Non-Movable Pages):不可迁移页是指操作系统不允许将其从一个物理内存位置迁移到另一个位置的页面。这通常包括操作系统核心代码、设备驱动程序和其他关键系统数据结构等。这些页通常需要在特定的物理内存位置上进行固定的分配和驻留,以确保系统的稳定性和可靠性。
- 可回收页(Reclaimable Pages):可回收页是指操作系统可以根据需要将其回收或释放的页面。这些页面包括已分配但当前未被使用的内存页面,如未使用的用户空间页或内核缓冲区。当系统需要更多内存时,操作系统可以回收这些页并重新分配给其他进程或任务。
- 可迁移页(Movable Pages):可迁移页是指操作系统允许将其从一个物理内存位置迁移到另一个位置的页面。这些页通常包括用户空间的数据和堆栈页。可迁移页的迁移可以用于内存优化、负载平衡或处理内存故障的目的。通过动态迁移可迁移页,操作系统可以重新组织内存布局,提高内存利用率和性能。
4.3 物理内存分配
除了前面提到的分段,分页机制来分配物理内存,内核还引入了几种内存分配算法,来更高效地管理和分配物理内存,以及避免与减少内存碎片。
4.3.1 伙伴系统(Buddy System)
伙伴系统(Buddy System)是一种常见的内存分配算法,用于管理物理内存的分配和释放。它的主要目标是在物理内存中分配连续的内存块,并通过合并空闲块来减少内存碎片。
伙伴系统并不直接管理页面,初始时将整个可用的物理内存视为一个大小为 2^N 的块,其中 N 是内存的总大小所能表示的最大幂次,这个块被视为根节点。随着内存的分配和释放,根节点会被分割为更小的伙伴块,并且这个过程会在树的不同层级上进行,最小的块为2^0,即一个页面。
注意:伙伴系统的块中不包括非空闲页
4.3.1.1 空闲块链表
在伙伴系统中,空闲块链表是用于管理可用的空闲内存块的数据结构。空闲块链表存储了各个不同大小的空闲块,并提供了高效的内存分配和回收。
空闲块链表通常是一个数组或链表的集合,每个索引或元素对应着一种特定大小的空闲块。索引或元素的位置与内存块的大小相关,通常从最小的块开始,以2的幂次递增。
每个空闲块链表中的节点包含了同一大小的空闲块的地址信息,通常使用指针或索引进行连接。链表节点的结构可能包含以下信息:
- 指向空闲块的起始地址的指针
- 指向下一个空闲块链表节点的指针
阶: 伙伴系统的阶(Order)是指内存块的大小级别,每个块的大小是2的阶次幂
4.3.1.2 内存分配与回收
- 内存分配过程:
- 当收到一个内存分配请求时,伙伴系统会根据请求的内存大小找到合适的块。通常会从最小的块开始搜索,直到找到一个大小能够满足请求的空闲块。
- 如果找到的空闲块比请求的内存稍大,系统会将该块分割成两个相等大小的伙伴块。其中一个块将被分配给请求的进程,而另一个块将视情况决定是否分配,或者继续保留为空闲块。
- 如果找到的空闲块大小正好与请求的内存大小相等,系统会将该块标记为已分配,并返回给请求的进程使用。
- 内存回收过程:
- 当一个进程释放已分配的内存块时,伙伴系统会将该内存块标记为未分配状态。
- 然后,系统会检查该释放的内存块的伙伴块是否也处于未分配状态。如果是,系统会将两个伙伴块合并成一个更大的块。
- 合并后的块继续进行合并检查,直到达到最大的块大小或者找到了一个已分配的伙伴块为止。
- 最终形成的合并后的块将被标记为未分配状态,并加入到空闲块链表中,以备后续的内存分配使用。
伙伴块:是指在伙伴系统中成对出现的内存块,将可用的内存划分为一系列大小相等的内存块,并将它们组织成伙伴块对,每个伙伴块对中的两个伙伴块具有相同的大小
4.3.1.3 优缺点
优点:
- 内存分配效率高:伙伴系统通过将物理内存划分为多个不同大小的页框,并使用二进制伙伴算法来分配和管理这些页框,以提高内存分配的效率。它可以快速且高效地满足不同大小的内存分配请求。
- 内存释放效率高:伙伴系统可以合并相邻的空闲页框,形成更大的页框,以减少内存碎片。这样,当有内存释放时,它可以快速合并并回收空闲页框,以供后续的内存分配使用。
- 管理简单直观:伙伴系统的算法相对简单,易于实现和理解。它采用二进制伙伴算法来管理内存的分配和释放,没有复杂的数据结构和算法要求。
- 可预测的性能:伙伴系统的内存分配和释放操作的时间复杂度是可预测的,不会随着内存使用量的增加而导致性能下降。这使得伙伴系统适用于对性能和可预测性要求较高的应用场景。
缺点:
- 内部碎片:伙伴系统可能会导致内部碎片的产生。当请求的内存大小不是2的幂时,会产生不可避免的内部碎片,因为伙伴系统只能分配固定大小的页框。
- 外部碎片:伙伴系统无法解决外部碎片问题。当有多个不同大小的内存分配和释放操作交替进行时,可能会导致外部碎片的产生,使得可用内存变得不连续,从而限制了大块连续内存的分配。
- 内存浪费:伙伴系统分配的内存大小必须是2的幂,这可能导致一些浪费,因为不同大小的内存请求可能只能被分配到比实际需求更大的页框中。
- 复杂的内存管理:虽然伙伴系统的算法本身相对简单,但在实际实现中,需要考虑内存分配算法的细节,例如如何处理边界情况、如何处理分配失败等。这些额外的实现细节可能增加实现的复杂性。
4.3.2 Slab分配器
在Linux中,伙伴分配器(buddy allocator)是以页为单位管理和分配内存。但在内核中的需求却以字节为单位(在内核中面临频繁的结构体内存分配问题)。假如需要动态申请一个内核结构体(占 20 字节),若仍然分配一页内存,这将严重浪费内存。
slab (Sequential Locally Allocated Buffers)分配器专为小内存分配而生,slab分配器分配内存以字节为单位,基于伙伴分配器的大内存进一步细分成小内存分配。换句话说,slab 分配器仍然从 Buddy 分配器中申请内存,之后自己对申请来的内存细分管理。
除了提供小内存外,slab 分配器的第二个任务是维护常用对象的缓存。对于内核中使用的许多结构,初始化对象所需的时间可等于或超过为其分配空间的成本。当创建一个新的slab 时,许多对象将被打包到其中并使用构造函数(如果有)进行初始化。释放对象后,它会保持其初始化状态,这样可以快速分配对象。
SLAB分配器的最后一项任务是提高CPU硬件缓存的利用率。 如果将对象包装到SLAB中后仍有剩余空间,则将剩余空间用于为SLAB着色。 SLAB着色是一种尝试使不同SLAB中的对象使用CPU硬件缓存中不同行的方案。 通过将对象放置在SLAB中的不同起始偏移处,对象可能会在CPU缓存中使用不同的行,从而有助于确保来自同一SLAB缓存的对象不太可能相互刷新。 通过这种方案,原本被浪费掉的空间可以实现一项新功能。
SLAB分配器通过将内存划分为一系列的SLAB缓存(也称内存池)来管理对象的分配。每个SLAB缓存是一个固定大小的内存区域(伙伴系统中提供的物理内存),用于存储相同类型的对象。每个SLAB缓存又由三个区域组成:对象区域、空闲列表和高速缓存。
- 对象区域是存储实际对象的内存区域,其中包含一定数量的对象实例。
- 空闲列表用于跟踪哪些对象当前是空闲的,可以立即分配给请求者。
- 高速缓存是用于提高分配和释放操作性能的预分配内存区域。
SLAB分配器中的内存块(SLAB)可以存在三种状态,分别是:
- Full(完全分配):SLAB处于完全分配状态表示所有的内存块都已经被分配给了对象。这意味着没有可用的内存块供新的对象分配使用。当应用程序释放一个对象时,该对象所在的SLAB可能从完全分配状态转变为部分分配状态或空闲状态。
- Partial(部分分配):SLAB处于部分分配状态表示部分内存块已经被分配给了对象,但仍有一些内存块是空闲的。在部分分配状态下,SLAB可以继续分配剩余的内存块给新的对象,而无需从内存池中获取新的SLAB。
- Free(空闲):SLAB处于空闲状态表示所有的内存块都是空闲的,没有被分配给任何对象。空闲状态的SLAB可以直接分配给新的对象使用。当应用程序释放一个对象时,该对象所在的SLAB可能从部分分配状态或完全分配状态转变为空闲状态。
这三种状态的SLAB在SLAB分配器中的管理和转换,可以提高内存分配和释放的效率。当SLAB处于完全分配状态时,新的对象分配会从部分分配或空闲状态的SLAB获取,从而避免了从内存池中获取新的SLAB的开销。而当对象被释放时,SLAB可以从完全分配或部分分配状态转变为空闲状态,以便后续的对象分配重用。这种状态管理策略可以减少内存碎片和提高内存利用率。
4.3.2.1 slab节点
在SLAB分配器中,SLAB缓存节点(SLAB Cache Node)是用于管理SLAB的数据结构。每个SLAB缓存节点代表一个SLAB,用于跟踪SLAB的状态和管理内存块。
SLAB缓存节点通常包含以下信息:
- SLAB地址:指向SLAB在内存池中的起始地址。
- SLAB状态:表示SLAB的当前状态,包括完全分配、部分分配或空闲。
- 空闲列表:记录SLAB中可用的空闲内存块的位置和状态。
- 对象计数器:记录SLAB中已分配的对象数量,用于判断SLAB是否完全分配。
- 其他控制信息:可能包括用于管理SLAB状态变化和内存块分配释放的其他控制信息。
SLAB缓存节点通过这些信息来管理SLAB的状态和内存块的分配释放。当SLAB节点表示的SLAB完全分配时,SLAB缓存节点会更新状态信息,标记SLAB为完全分配状态。当对象被释放时,SLAB缓存节点会更新空闲列表,将内存块添加到空闲列表中,以便后续的对象分配重用。
SLAB缓存节点通常通过链表或其他数据结构组织在一起,以便SLAB分配器能够快速访问和管理多个SLAB。这些节点的组织方式可能因具体的SLAB分配器实现而异。
4.3.2.2 内存分配与回收
内存分配过程:
- 当应用程序请求内存分配时,SLAB分配器首先检查高速缓存(Cache)中是否有可用的空闲对象。如果有,它会直接从高速缓存中分配一个对象给应用程序,跳转到步骤5。
- 如果高速缓存为空,SLAB分配器会检查空闲列表(Free List)中是否有空闲对象。空闲列表记录了之前释放的对象,可以被重用。如果有空闲对象,它会从空闲列表中分配一个对象给应用程序,跳转到步骤5。
- 如果空闲列表也为空,SLAB分配器会检查对象区域(Object Area)是否有可用的SLAB(一块预分配的内存区域)。
- 如果对象区域中有可用的SLAB,SLAB分配器会从SLAB中分配一个对象给应用程序,并将对象从SLAB中移出,更新空闲列表。如果SLAB中的所有对象都已经被分配,SLAB会被标记为完全分配,并从对象区域中移出。
- 分配完成,SLAB分配器将分配的对象返回给应用程序使用。
内存回收过程:
- 当应用程序释放一个对象时,对象会被标记为空闲,并添加到对应的空闲列表中。
- 如果释放对象所在的SLAB变成了完全空闲状态,SLAB会被移回对象区域,并重新添加到空闲列表中。这样,SLAB中的对象可以被重用,避免频繁的内存分配。
- 如果空闲列表中的对象过多,SLAB分配器可能会执行一些策略,例如合并相邻的空闲对象,以减少空闲列表的碎片化。
4.3.2.3 优缺点
优点:
- 内存分配效率高:SLAB分配器通过预先分配一定数量的内存块(SLAB)来满足内存分配请求。这消除了每次分配都需要动态管理内存的开销,提高了内存分配的效率。
- 内存释放效率高:SLAB分配器通过将释放的内存块缓存在空闲列表中,可以快速回收和重用内存块。这样可以减少频繁的内存分配和释放操作,提高了内存的利用率和系统性能。
- 减少内存碎片:SLAB分配器使用同一大小的内存块(SLAB)来分配对象,避免了内存碎片的产生。这是因为对象的大小固定且相同,不会导致不同大小的内存碎片。
- 管理简单直观:SLAB分配器的算法相对简单,易于实现和理解。它使用空闲列表来管理可用的内存块,没有复杂的数据结构和算法要求。
缺点:
- 内存浪费:SLAB分配器为每个对象类型预分配一定数量的内存块(SLAB),无论实际需要多少内存。这可能导致一些内存的浪费,特别是当对象类型的使用模式发生变化时。
- 难以处理不同大小的对象:SLAB分配器适用于固定大小的对象分配。如果应用程序需要分配不同大小的对象,需要为每种大小的对象类型创建单独的SLAB分配器。
- 内存分配延迟:当空闲列表中没有可用的内存块时,SLAB分配器需要从内存池中获取新的SLAB,这可能会导致一定的内存分配延迟。
- 不适用于大对象:由于SLAB分配器需要预分配一定数量的内存块,不适合分配大对象。对于大对象的分配,SLAB分配器的效率会降低,并可能造成内存的浪费。
4.3.3 CMA分配器
CMA(Contiguous Memory Allocator)分配器是一种用于在Linux内核中管理连续物理内存的机制。它主要用于满足某些设备驱动程序对连续物理内存的需求,例如某些图形、网络或多媒体设备。
在某些设备中,要求物理内存是连续的,这对于传输大块数据或直接内存访问(DMA)操作是必要的。但是,由于内存碎片化和其他因素,连续的物理内存可能难以获得。这时,CMA分配器就发挥作用了。
CMA分配器通过预留一部分物理内存,将其划分为固定大小的连续内存区域。这些内存区域可以由设备驱动程序使用,并保证是连续的。设备驱动程序可以使用CMA分配器来请求连续内存,以满足设备的要求。
CMA分配器的工作方式类似于伙伴系统分配器。它将预留的物理内存划分为不同的大小等级,并使用伙伴分配算法来管理和分配连续内存区域。设备驱动程序可以通过相应的API接口请求CMA分配器来获取连续内存块。
需要注意的是,CMA分配器只在某些特定的设备驱动程序中使用,而不是所有的内存分配场景。它主要针对对连续物理内存有需求的设备和应用,以提供更高效的数据传输和访问能力。
4.4 物理内存回收
物理内存是有限的,为了确保系统中可用的内存尽可能多地被应用程序使用,linux会进行一系列的内存回收动作。
内存回收按照回收时机可以分为同步回收和异步回收,同步回收是指在分配内存的时候发现无法分配到内存就进行回收,异步回收是指有专门的线程定期进行检测,如果发现内存不足就进行回收。
内存回收的类型有两种,一是内存规整,也就是内存碎片整理,它不会增加可用内存的总量,但是会增加连续可用内存的量,二是页帧回收,它会把物理页帧的内容写入到外存中去,然后解除其与虚拟内存的映射,这样可用物理内存的量就增加了。
内存回收的时机和类型是正交关系,同步回收中会使用内存规整和页帧回收,异步回收中也会使用内存规整和页帧回收。
在异步回收中,内存规整有单独的线程kcompactd,此类线程一个node一个,线程名是[kcompactd/nodeid],页帧回收也有单独的线程kswapd,此类线程也是一个node一个,线程名是[kswapd/nodeid]。
在同步回收中,还有一个最终手段,那就是OOM Killer,如果直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请,那么就会触发OOM Killer:根据算法选择一个占用物理内存较高的进程,然后将其杀死,以便释放内存资源,如果物理内存依然不足,OOM Killer 会继续杀死占用物理内存较高的进程,直到释放足够的内存位置
4.4.1 内存规整
内存规整是一种内存管理技术,其目的是将内存中的碎片整理成连续的内存区域,以便重新分配给需要大块连续内存的进程或任务。
在Linux系统中,内存规整通常通过页迁移来实现。页迁移是指将一个页从一个位置移动到另一个位置的过程。在内存规整过程中,页迁移被用于将可移动的页面移动到一起,腾出更多的连续内存空间。
内存规整的触发时机和规整范围可以根据需要进行调整。在Linux系统中,内存规整通常以zone为单位进行,并封装了compact_zone接口,作为内存规整的核心接口。
总之,内存规整是一种有效的内存管理技术,可以解决内存碎片问题,提高内存利用率和系统性能。
4.4.2 页帧回收
内存规整只是增加了连续内存的量,但是可用内存的量并没有增加,当可用内存量不足的时候就要进行页帧回收
在操作系统中,页帧是物理内存的最小分配单位,通常与页面大小相对应,页帧回收通常由内核的内存管理子系统执行,常见的页帧回收技术:
- 页面置换(Page Swapping):当系统内存不足时,内核可以将不常用的页面(页帧)交换到磁盘的交换空间(Swap Space)中,以释放物理内存。这个过程称为页面置换。被置换的页面可以在需要时再次从交换空间中加载到物理内存中。
- 页面回写(Page Writeback):当系统内存不足时,内核可以将修改过的页面(脏页面)回写到磁盘,以释放物理内存。这个过程称为页面回写。回写过程可以通过内核的脏页面回写(Dirty Page Writeback)机制进行异步或同步地完成。
- 匿名页面回收(Anonymous Page Reclaim):匿名页面是指不与文件关联的页面,通常用于进程的堆(Heap)和栈(Stack)等。当系统内存不足时,内核可以选择回收不使用的匿名页面来释放物理内存。回收匿名页面可能会导致相关进程的页面错误(Page Fault),需要重新加载页面。
- 内存压缩(Memory Compression):一些操作系统引入了内存压缩技术,可以将部分内存页进行压缩存储,从而减少内存占用。当系统内存不足时,内核可以通过解压缩压缩的页面来释放物理内存。
按类型分的页:
- 文件页(File Pages):文件页是与文件关联的页面,通常用于映射文件到进程的虚拟内存空间。当进程需要访问文件的内容时,文件页会从文件系统中加载到物理内存中,供进程读取和写入。文件页的内容可以被持久化到磁盘上的文件,以保证数据的持久性。
- 匿名页(Anonymous Pages):匿名页是不与文件关联的页面,通常用于进程的堆(Heap)和栈(Stack)等。匿名页通常用于存储进程的临时数据、动态分配的内存和函数调用的栈帧等。匿名页的内容不会被持久化到磁盘上的文件,它们只存在于进程的虚拟内存和物理内存中。
- 保留页(Reserved Pages):保留页是指已经被分配但尚未被使用的页面。这些页面可能被指定为特定的用途,如内核保留的页面、设备驱动程序使用的页面等。
- 只读页(Read-only Pages):只读页是指被标记为只读权限的页面,进程无法对其进行写入操作。这种页面常用于共享库、代码段等只读的数据。
- 受保护页(Guard Pages):受保护页是一种特殊的页面,用于保护内存区域的边界。当进程越界访问受保护页时,会触发异常或错误,以提醒程序员或调试器出现内存访问错误。
按状态分的页:
- 干净页(Clean Pages):干净页是指未被修改过或已经与磁盘上的文件内容保持一致的页面。这些页面不需要被写回磁盘,因为它们的内容与磁盘上的文件一致。
- 脏页(Dirty Pages):脏页是指被修改过但尚未写回磁盘的页面。当进程对页面进行写入操作时,该页面被标记为脏页,表示其与磁盘上的文件内容不一致。脏页通常需要被回写到文件系统中,以保持数据的一致性。内核会定期或在特定条件下将脏页写回磁盘,或者使用延迟写回策略,将脏页缓存在内存中,以提高写入效率。
- 零页(Zero Pages):零页是一种特殊的页面,其内容全部为零。零页通常用于初始化新分配的页面或作为未使用的页面的初始内容。
4.4.2.1 kswapd进程
页帧回收触发时机主要有两种,一种是内存不足时,被动触发,一种是通过kswapd进程定期检查系统的内存使用情况,当达到一定的阈值时,kswapd会被唤醒,主动进行页帧回收。
kswapd(Kernel Swap Daemon)是Linux内核中的一个守护进程,用于管理页面交换(Page Swapping)和内存回收。它的主要任务是在内存不足时,通过将不常用的页面交换到磁盘上的交换空间,从而释放物理内存。
在NUMA架构中,每个节点都有一个kswapd的进程
内存水位
在NUMA架构中,每个node上的zone定义了三个内存水位(zone_watermark),用来表示内存资源的可用性和使用情况,并根据预定义的阈值触发相应的操作
- 高水位(High Watermark):当系统的内存空闲页数超过高水位,则代表目前内存资源较为充足
- 低水位(Low Watermark):当系统的内存空闲页数接近低水位时,系统可能会开始采取一些预防措施,例如启动内存回收或释放不必要的内存资源(kswapd进程被唤醒,将内存回收至高水位)
- 最低水位(Min Watermark):当系统的内存空闲页数低于最低水位,代表内存严重不足,此时可能会触发OOM(Out of Memory)机制,选择性地终止一些进程来释放内存
查看节点每个zone的水位线(单位均为页的数量,不是具体的kb或bit):
[root@test ~]# cat /proc/zoneinfo | grep Node -A8
Node 0, zone DMA
pages free 3540 当前可用的页面数
min 253 最低水位
low 316 低水位
high 379 高水位
scanned 0
spanned 4095 总页面数
present 3995 当前存在的页面数
managed 3974 该内存节点当前受内存管理系统管理的页面数
--
Node 0, zone DMA32
pages free 97163
min 27820
low 34775
high 41730
scanned 0
spanned 1044480
present 496317
managed 436917
--
Node 0, zone Normal
pages free 1272099
min 1017723
low 1272153
high 1526584
scanned 0
spanned 16252928
present 16252928
managed 15983061
--
Node 1, zone Normal
pages free 1443899
min 1051355
low 1314193
high 1577032
scanned 0
spanned 16777216
present 16777216
managed 16511240
三个水位的大小受参数min_free_kbytes值的影响,单位为KB(千字节),定义了系统在分配内存时所保留的最小空闲内存量。当可用内存低于该阈值时,系统会尝试回收内存以增加可用内存的数量。这有助于避免内存的过度使用和内存耗尽问题。
[root@test ~]# cat /proc/sys/vm/min_free_kbytes
8388608
这个数值具体如何影响每个zone的水位线,博主暂时还没弄清楚,有知道的可指出。
总的来说,min_free_kbytes设的越大,watermark的线越高,同时三个线之间的buffer量也相应会增加。这意味着会较早的启动kswapd进行回收,且会回收上来较多的内存(直至watermark[high]才会停止),这会使得系统预留过多的空闲内存,从而在一定程度上降低了应用程序可使用的内存量。极端情况下设置min_free_kbytes接近内存大小时,留给应用程序的内存就会太少而可能会频繁地导致OOM的发生。
min_free_kbytes设的过小,则会导致系统预留内存过小。kswapd回收的过程中也会有少量的内存分配行为(会设上PF_MEMALLOC)标志,这个标志会允许kswapd使用预留内存;另外一种情况是被OOM选中杀死的进程在退出过程中,如果需要申请内存也可以使用预留部分。这两种情况下让他们使用预留内存可以避免系统进入deadlock状态。
4.2.2.2 swap
在Linux系统中,Swap是一种虚拟内存技术,用于扩展系统的可用内存空间。当物理内存(RAM)不足以容纳当前运行的进程和数据时,Swap允许将部分内存数据转移到磁盘上的Swap空间中,从而释放物理内存供其他进程使用。
以下是一些关于Linux Swap的重要信息和概念:
- Swap分区:Swap分区是在磁盘上划分的专用空间,用于存储被交换出的内存页面。Swap分区可以是独立的分区,也可以是专门的Swap文件。
- 交换(Swapping):当系统需要更多的物理内存时,操作系统会将不活动的内存页面(也称为页面交换)移至Swap空间,并将需要的页面从Swap空间调入物理内存。这个过程涉及到将页面从磁盘读取到内存或将页面从内存写入到磁盘。
- Swap空间的大小:Swap空间的大小可以根据系统需求进行配置。通常建议将Swap空间设置为物理内存大小的1到2倍,但实际设置可能会根据系统的特定需求和配置而有所变化。
- Swappiness:Swappiness是一个可以在Linux系统上配置的参数,用于控制内核在内存不足时进行交换的倾向程度。它的取值范围是0到100,其中0表示尽量减少交换,100表示尽量增加交换。通过调整Swappiness值,可以影响系统对内存和Swap的使用方式。
- Swap的使用场景:Swap在以下情况下可能会被使用:
- 当物理内存不足以容纳所有运行的进程和数据时。
- 当系统休眠(Hibernate)时,将内存数据保存到Swap中以便恢复。
- 当预留内存以供内核使用(例如内核数据结构)。
现代大多数的操作系统,建议关闭swap
原因如下:
- SWAP对磁盘的占用设定好之后是固定的,无法动态调整,增加了磁盘的读写次数和损耗几率,减少磁盘使用寿命
- 回收性能:交换内存页面到磁盘上会引入额外的磁盘I/O操作,可能会导致响应时间延迟和系统吞吐量下降。在某些情况下,关闭Swap可以提高系统的整体性能和响应能力。
- SSD和闪存存储:Swap通常位于磁盘上,而现代计算机越来越多地采用固态硬盘(SSD)或闪存存储。由于SSD和闪存存储的访问速度相对较快,与传统机械硬盘相比,交换页面到这些存储介质上的影响可能较小。因此,在使用SSD或闪存存储的系统上,可能更倾向于不启用Swap。
- 内存压缩和管理技术:现代操作系统通常采用各种内存压缩和管理技术,例如内存页面的压缩、页面合并等。这些技术可以提供更高效的内存利用,减少对Swap的需求。
查看是否开启swap:使用free -m 命令,如果看到swap分配的总内存为0,则不开启swap分区
可 cat /proc/swaps
活跃页链表与不活跃页链表
文件页和匿名页的回收都是基于 LRU 算法(最近最少使用),此算法实际上维护着 active 和 inactive 两个双向链表,其中:
- active_list 活跃内存页链表,这里存放的是最近被访问过(活跃)的内存页
- inactive_list 不活跃内存页链表,这里存放的是很少被访问(非活跃)的内存页
越接近链表尾部,就表示内存页越不常访问。这样,在回收内存时,系统就可以根据活跃程度,优先回收不活跃的内存。
第二次机会法
第二次机会法(Second Chance Algorithm),也被称为时钟算法(Clock Algorithm),是一种用于页面置换(Page Replacement)的算法。它主要用于操作系统中管理虚拟内存的页表,以决定哪些页面被保留在物理内存中,哪些页面被换出到磁盘上。
第二次机会法基于近似最佳置换算法(Approximate LRU Algorithm),它通过给每个页面分配一个位(也称为访问位或R位),用于标记页面是否被访问过。算法按顺序扫描页面,如果某个页面的位为0(即未被访问过),则将该页面置换出去;如果位为1(即已被访问过),则将位清零,并将页面移到链表的尾部,以给该页面一个新的机会被保留在内存中。
第二次机会法的基本思想是给予刚刚访问过的页面第二次机会,以避免频繁置换刚被访问过的页面。它通过循环地扫描页面链表,维护一个类似时钟的指针,当指针指向页面时,检查该页面的位。如果位为0,则将该页面置换出去;如果位为1,则清零位,并将指针移动到下一个页面。
第二次机会法的优点是相对简单且易于实现,适用于操作系统中的页面置换算法。然而,它可能对某些访问模式的页面表现不佳,例如具有周期性访问模式的页面。在这种情况下,其他更高级的页面置换算法(如LRU、LFU等)可能会提供更好的性能。
swap倾向程度
从文件页和匿名页的回收操作来看,文件页的回收操作对系统的影响相比匿名页的回收操作会少一点,因为文件页对于干净页回收是不会发生磁盘 I/O 的,而匿名页的 Swap 换入换出这两个操作都会发生磁盘 I/O
Linux 提供了一个 /proc/sys/vm/swappiness 选项,用来调整文件页和匿名页的回收倾向,即swap的倾向程度,取值范围是0到100,默认值是60,数值越大,越积极使用 Swap,也就是更倾向于回收匿名页;数值越小,越消极使用 Swap,也就是更倾向于回收文件页。
swappiness=0
:表示禁用Swap,内核尽量避免使用Swap空间。swappiness=100
:表示内核更积极地使用Swap空间,以便释放物理内存。
一般建议 swappiness 设置为 0(默认值是 60),这样在回收内存的时候,会更倾向于文件页的回收,但是并不代表不会回收匿名页。
4.2.2.3 OOM Killer
OOM Killer(Out of Memory Killer)是Linux操作系统中的一个内核机制,用于处理内存不足(OOM)情况。当系统的可用内存不足以满足进程的内存需求时,OOM Killer会被触发,它的作用是终止一个或多个进程,以释放内存资源。
OOM Killer的主要目标是选择最合适的进程进行终止,以最大程度地减少对系统的影响。它会根据一些策略和算法来选择要终止的进程,通常会选择那些被认为是最不重要或最消耗内存的进程。
以下是一些影响OOM Killer选择终止进程的因素:
- OOM分数(OOM Score):每个进程都有一个OOM分数,用于衡量进程的优先级。较低的OOM分数表示进程在终止方面的优先级更高。
- 内存使用情况:OOM Killer会考虑各个进程的内存使用情况,选择那些占用较多内存的进程进行终止。
- 进程的重要性:有些进程对于系统的正常运行至关重要,如init进程或关键系统服务进程。OOM Killer会尽量避免终止这些进程。
- 进程的年龄:较新创建的进程可能更容易被终止,因为它们在整个系统中的重要性可能较低。
修改相关参数:
- /proc/[PID]/oom_score:该文件显示进程的OOM分数。可以通过读取和修改此文件来手动调整进程的OOM分数。取值范围0 ~ 1000,0代表never kill,值越大,进程被kill的概率越大
- /proc/sys/vm/overcommit_memory:此文件用于控制内存超额分配策略。可以将其值设置为0、1或2来分别表示不检查、按需检查和总是检查内存分配。这会影响系统在内存不足时触发OOM Killer的条件。
- /proc/sys/vm/oom_kill_allocating_task:此文件控制当内存不足时,是否可以终止正在分配内存的进程。将其值设置为0表示禁止终止正在分配内存的进程,设置为1表示允许终止。
- /proc/sys/vm/oom_adj:系统级别的文件,用于设置所有进程的默认OOM调整值,修改此文件的值将影响所有新创建的进程的OOM优先级
- /proc/[PID]/oom_adj:针对特定进程的文件,用于设置单个进程的OOM调整值,取值范围为 -17 ~ 15,具有较低OOM调整值的进程更有可能被OOM Killer选择终止
需要注意的是,从Linux内核版本2.6.36开始,/proc/[PID]/oom_adj
已被 /proc/[PID]/oom_score_adj
取代。/proc/[PID]/oom_score_adj
使用更直观的范围 -1000 到 1000,并与OOM分数相关联,提供了更精细的控制。
4.2.3 Node内存回收策略
在 NUMA 架构下,当某个 Node 内存不足时,系统可以从其他 Node 寻找空闲内存,也可以从本地内存中回收内存。
具体选哪种模式,可以通过 /proc/sys/vm/zone_reclaim_mode 来控制。它支持以下几个选项:
- 0 (默认值):在回收本地内存之前,在其他 Node 寻找空闲内存;
- 1:只回收本地内存;
- 2:只回收本地内存,在本地回收内存时,可以将文件页中的脏页写回硬盘,以回收内存。
- 4:只回收本地内存,在本地回收内存时,可以用 swap 方式回收内存。
在使用 NUMA 架构的服务器,如果系统出现还有一半内存的时候,却发现系统频繁触发「直接内存回收」,导致了影响了系统性能,那么大概率是因为 zone_reclaim_mode 没有设置为 0 ,导致当本地内存不足的时候,只选择回收本地内存的方式,而不去使用其他 Node 的空闲内存。
虽然说访问远端 Node 的内存比访问本地内存要耗时很多,但是相比内存回收的危害而言,访问远端 Node 的内存带来的性能影响还是比较小的。因此,zone_reclaim_mode 一般建议设置为 0
4.4.4 物理内存压缩
为了减少物理内存的使用量,除了前面提及的几项内存回收方法外,linux还有一种减少使用量的方法,即物理内存压缩。
物理内存压缩是用于在内存不足的情况下减少系统中的内存使用量。它通过对内存中的数据进行压缩,以节省内存空间并允许更多的数据存储在物理内存中。它可以在不引入较大的延迟的情况下提供额外的内存容量,这可以减少对交换空间(swap space)的依赖,并改善系统的性能和响应速度。但会增加压缩与解压缩的cpu耗时,即以时间换空间。
- 选择目标页:操作系统会选择一些内存页面,这些页面上存储的数据被认为是适合进行压缩的。选择的目标页通常是那些具有重复模式或压缩效果较好的页面。
- 压缩页面:选定的目标页将被传送到压缩算法中,以减小它们在内存中的占用空间。常见的压缩算法包括LZ77、LZRW、LZO等。
- 存储压缩数据:压缩后的数据将替代原始的页面数据,并存储在物理内存中;若物理内存不足,可将压缩数据交换到磁盘的交换空间上(可以是特定的分区,也可以是一个交换文件,即磁盘可不开启swap分区)
- 访问压缩页面:当进程需要访问被压缩的页面时,操作系统会将其解压缩并还原为原始数据。这可以通过硬件支持或软件算法来完成。
压缩后的数据是不可以直接读写的,需要经过解压缩后才能访问,但相对来说,其访问速度依然较快,这样的一种特殊内存被称之为transcendent memory(简称tmem)。作为内存中的一部分区域,一段时间没有被操作的APP可以被压缩后放到tmem中,当用户再次操作这个应用时,再从tmem中恢复,这比从磁盘中恢复要迅速,这也是我们可以快速打开一个APP的关键(保活度)
对于现代操作系统,是否推荐开启物理内存压缩需要根据具体情况而定。
如果系统的物理内存充足,且系统的性能已经足够满足需求,那么开启内存压缩可能会对系统性能产生一定的负面影响,因为内存压缩需要消耗额外的CPU资源。在这种情况下,可以考虑关闭内存压缩以获得更好的系统稳定性。
如果系统的物理内存不足,或者系统的性能不能满足需求,那么开启内存压缩可能会提供一些性能提升。内存压缩可以减少内存占用,提高系统的可用内存空间,从而允许系统运行更多程序或提高程序的响应速度。在这种情况下,可以考虑开启内存压缩。
需要注意的是,内存压缩技术并不是完美的解决方案。它有时会在不该压缩的时候去压缩,造成性能降低。此外,内存压缩需要消耗额外的CPU资源,也增加了耗电量。因此,在决定是否开启内存压缩时,需要综合考虑系统的硬件配置、内存使用情况以及系统的性能需求等因素。
优点:
- 优化内存使用率。通过压缩存储空间减少占用空间,为其他应用程序留出更多的空间。
- 提高系统性能。在释放内存时不需要创建和复制交换分区的数据,减少CPU和内存资源消耗。
- 缩短存储空间。对于内存中存储大量重复数据的应用程序,比如虚拟机、容器等,可以将数据压缩,提高系统的性能。
- 提高应用程序的响应速度。可以提高内存的利用率,使应用程序更快地响应。
缺点:
- 时间复杂度高。在插入节点或删除节点时可能会造成连锁更新,导致比较高的时间复杂度。
- 对CPU资源消耗大。虽然Linux内存压缩技术可以减少CPU和内存资源消耗,但相较于传统内存管理方式,对CPU的消耗依然很大。
本章节介绍两种主流的内存压缩技术:zSwap, zRAM
4.4.4.1 zSwap
zSwap是在内存与磁盘之间的一层“缓存”,当内存不足时,zSwap会将一部分未使用的页面压缩,并将其存储在特定的压缩缓存中;当此缓存空间不足时,会按照LRU算法,选择一个压缩的页面进行解压缩,并将其放置到内存中。同时,被解压的页面会被置换到磁盘上的交换空间中,以便为新的压缩页面腾出空间;当需要恢复被压缩的页面时,ZSwap会从压缩缓存中选择一个页面进行解压缩,并将其放置到内存中,或者是从磁盘上交换页面
优点:
- 内存利用率提高:ZSwap通过压缩未使用的内存页面,可以在内存不足时提供额外的可用内存。这可以提高系统的内存利用率,减少对交换空间的需求。
- 性能改善:相比传统的磁盘交换,ZSwap的压缩和解压缩操作在内存中进行,避免了频繁的磁盘I/O操作,从而提高了系统的响应性能和吞吐量。
- 硬件友好:相对于传统的磁盘交换,ZSwap减少了对磁盘的使用,延长了磁盘寿命,并降低了对磁盘带宽的需求。
缺点:
- CPU开销:ZSwap的压缩和解压缩操作需要消耗一定的CPU资源。在高负载情况下,这可能导致CPU使用率的增加,从而对系统的整体性能产生一定影响。
- 内存碎片化:由于压缩页面的大小可能不一致,使用ZSwap可能导致内存碎片化。这可能会影响到内存分配的效率,尤其是在长时间运行的系统中。
- 配置复杂性:配置ZSwap需要对内核参数进行调整,这对于一些用户来说可能比较复杂。此外,不同的应用场景可能需要不同的ZSwap配置,需要进行适当的测试和调整。
查看/修改缓存空间的大小
sysfs接口:使用sysfs接口可以在运行时动态调整ZSwap的参数,该参数表示用于zSwap缓存的最大物理内存百分比
echo 20 > /sys/module/zswap/parameters/max_pool_percent
内核参数:可以通过编辑系统的启动配置文件(如/etc/default/grub
)来设置zSwap的参数。在GRUB_CMDLINE_LINUX
行中添加或修改zswap.max_pool_percent
参数的值
GRUB_CMDLINE_LINUX="zswap.max_pool_percent=[percentage]"
```
保存文件后,更新GRUB配置并重新启动系统。
开启/关闭zSwap
sysfs接口:zSwap的配置参数可以通过sysfs接口进行动态调整,1(启用)或0(禁用)
echo 1 > /sys/module/zswap/parameters/enabled
内核参数:可以通过编辑系统的启动配置文件(如/etc/default/grub
)来添加或修改内核参数。在GRUB_CMDLINE_LINUX
行中添加或修改zswap.enabled
参数的值
GRUB_CMDLINE_LINUX="zswap.enabled=1"
```
保存文件后,更新GRUB配置并重新启动系统。
4.4.4.2 zRAM
zRAM(也称为压缩内存块设备)是Linux内核中的一个功能,旨在提供一种内存压缩技术,以提高系统性能和效率。它通过将内存中的数据进行压缩存储,从而实现更有效地利用物理内存空间。
以下是一些关于zRam的重要特点和工作原理的介绍:
- 内存压缩:zRAM通过使用压缩算法将内存中的数据进行压缩,从而减少数据占用的物理内存空间。这使得系统可以存储更多的数据在有限的内存空间中,减少对交换空间(swap)的需求。
- 虚拟设备:zRAM以一种特殊的方式创建一个虚拟设备,该设备被当作块设备使用。它可以被视为一种逻辑上的内存磁盘,用于存储压缩后的数据。
- 多个压缩存储区:zRam支持创建多个压缩存储区,每个存储区都可以独立地进行数据压缩和存储。每个存储区都有自己的压缩算法和缓冲区。
- 数据交换:当系统内存不足时,zRAM可以将压缩后的数据交换到硬盘的交换空间。这比传统的交换机制更高效,因为压缩后的数据可以占用更少的磁盘空间,并且压缩和解压缩速度相对较快。
- 减少I/O延迟:由于zRAM减少了对物理磁盘的访问需求,因此可以帮助降低I/O延迟,提高系统的响应速度。
zRAM会在以下情况下对内存进行压缩:
- 内存不足:当系统的物理内存不足时,zRAM会开始压缩内存。它会将内存中的部分数据进行压缩,并释放出更多的可用物理内存空间。这可以帮助系统在内存有限的情况下继续运行,并减少对交换空间的需求。
- 交换需求:当系统需要进行交换操作时,zRAM可以作为一种替代方案。它可以将压缩后的数据存储在内存中的压缩存储区,而不是将数据交换到物理磁盘上的交换分区。这样可以提高交换操作的效率,减少对磁盘的访问,并减少交换操作的延迟。
- 内存压缩策略:zRAM通常会根据内存压缩策略来判断哪些数据应该被压缩。具体策略可能因操作系统和配置而有所不同。一般来说,zRAM倾向于压缩较少访问的数据、较长时间不活跃的数据或者较大的内存页面。
常用压缩策略:
- LRU(Least Recently Used):LRU策略基于最近使用的原则,将较长时间没有被访问的数据压缩到zRAM中。这样可以优先保留活跃数据在物理内存中,提高对活跃数据的访问速度。
- LFU(Least Frequently Used):LFU策略基于最不频繁使用的原则,将较少访问的数据压缩到zRAM中。这样可以优先保留频繁访问的数据在物理内存中,减少对不经常访问的数据的内存占用。
- Size-based(基于大小):Size-based策略将较大的内存页面或对象压缩到zRAM中。这是因为较大的页面通常具有更高的压缩潜力,可以节省更多的内存空间。
- Hybrid(混合):混合策略结合了不同的压缩策略。它可以根据不同的场景和需求自动调整压缩行为。例如,在内存不足时使用LRU策略,而在交换需求较大时使用Size-based策略。
优缺点
优点:
- 内存压缩:zRAM通过使用压缩算法将数据压缩后存储在内存中,有效地节省了内存空间。这可以使系统在有限的物理内存情况下容纳更多的数据,从而减少了对交换分区或交换文件的需求。
- 快速交换:由于zRAM存储区在内存中,交换数据的速度比传统的交换分区或交换文件快得多。这减少了数据交换的延迟,有助于提高系统的响应性能。
- 低磁盘访问:使用zRAM可以减少对物理磁盘的访问次数,因为它将数据压缩后存储在内存中。这可以降低磁盘的负载,并提高整体系统性能。
- 灵活性:zRAM可以与传统的交换分区和交换文件并存。系统可以根据需要配置zRAM的大小和交换空间的大小,以达到最佳的内存管理和性能平衡。
缺点:
- CPU开销:zRAM需要额外的CPU资源来执行数据的压缩和解压缩操作。这可能会增加系统的CPU使用率,尤其是在系统内存压力较大时。
- 内存使用效率:尽管zRAM可以节省内存空间,但压缩和解压缩操作本身也需要一定的内存和CPU资源。因此,在某些情况下,zRAM可能会导致内存使用效率略有下降。
- 压缩效率:zRAM的压缩效率取决于数据的特性和压缩算法。某些类型的数据可能无法有效地进行压缩,从而降低了内存节省的效果。
- 适用范围:zRAM主要适用于内存受限的环境,例如嵌入式设备或低内存的系统。在具有大量物理内存的系统中,使用zRAM可能没有明显的性能优势,并且可能会引入额外的复杂性。
启用zRAM
检查内核支持:首先,确保内核已启用zRAM模块,并且系统支持zRAM
#若支持会返回有关zram模块的信息
modinfo zram
加载zRAM模块:设置zRam设备数量,使用的算法
modprobe zram zram_num_devices=1 algorithm=lz4
创建zRAM设备:一旦zRAM模块加载成功,可以创建一个或多个zRAM设备,每个设备都代表一个压缩存储区
#大小可以使用单位(如M、G)指定,例如`512M`
echo 512M > /sys/block/zram0/disksize
启用zRAM设备
#将zRAM设备格式化为交换分区,并为其创建一个交换标识
mkswap /dev/zram0
#将zRAM设备添加到系统的交换空间中,并设置其优先级为5
swapon -p 5 /dev/zram0
检查zRAM设备
cat /proc/swaps
五、补充
5.1 内存泄漏
内存泄漏是指在程序中动态分配的内存空间在不再被使用时没有被正确释放的情况。这种情况下,分配的内存将一直占用系统资源,无法被重新利用,最终导致系统内存耗尽。
内存泄漏可能会导致以下问题:
- 内存消耗增加:每次发生内存泄漏时,系统可用内存减少,如果泄漏发生频繁或泄漏的内存块很大,系统的可用内存将逐渐减少,可能导致系统性能下降或崩溃。
- 性能下降:内存泄漏会导致内存碎片化,使得有效内存的连续空间变少,这可能导致内存分配和访问效率降低,造成程序性能下降。
- 崩溃和不稳定:如果内存泄漏严重,系统可能会因为内存耗尽而崩溃。此外,内存泄漏也可能导致应用程序崩溃或不稳定的情况。
要解决内存泄漏问题,可以尝试以下方法:
- 使用内存管理工具:使用内存管理工具(如Valgrind、AddressSanitizer等)来检测和诊断内存泄漏问题。这些工具可以帮助您找到泄漏的内存块和泄漏发生的位置。
- 定期进行代码审查:定期审查代码,查找可能存在的内存泄漏点。特别注意在使用动态内存分配函数(如malloc、new等)后,确保在不再需要内存时进行适当的释放。
- 使用智能指针和自动内存管理:使用智能指针和自动内存管理机制(如RAII)来管理动态分配的内存。这些机制可以自动处理内存的分配和释放,减少手动管理内存的出错可能性。
- 编写健壮的代码:编写健壮的代码,处理异常情况和错误条件,确保在任何情况下都能正确释放内存。
- 进行性能测试和内存监控:进行性能测试和内存监控,以便及早发现和解决内存泄漏问题。
5.2 内存竞争
内存竞争(Memory race)是指在多线程或多进程环境中,多个并发的线程或进程同时读写共享内存区域的情况下,由于缺乏适当的同步机制,导致对共享内存的读写操作产生冲突和不一致的结果。
内存竞争可能导致以下问题:
- 不确定性:由于不同线程或进程之间的执行顺序是不确定的,内存竞争可能导致不可预测的结果,使程序的行为变得不确定。
- 数据损坏:当多个线程或进程同时尝试读写相同的内存区域时,可能会导致数据的损坏或破坏。例如,一个线程正在写入数据,而另一个线程同时尝试读取该数据,可能读取到不正确或不完整的数据。
- 崩溃和错误:内存竞争可能导致程序崩溃、死锁或其他错误状态,从而影响程序的可靠性和稳定性。
为了解决内存竞争问题,可以采取以下措施:
- 同步机制:使用适当的同步机制来确保多个线程或进程之间对共享内存的访问是有序和一致的。常见的同步机制包括互斥锁、条件变量、信号量等。
- 原子操作:使用原子操作来保证特定操作的原子性,防止多个线程同时对同一内存位置进行读写操作。原子操作可以确保对共享数据的读写是不可分割的。
- 并发编程模型和工具:使用并发编程模型和工具,如线程池、消息传递机制、并发编程库等,来管理并发访问共享内存的复杂性和确保正确性。
- 代码审查和测试:进行仔细的代码审查和全面的测试,特别关注并发访问共享内存的部分,以发现潜在的内存竞争问题。
- 使用内存模型和并发编程规范:了解所使用编程语言的内存模型和并发编程规范,并遵循最佳实践,以确保正确处理内存竞争问题。