Linux 内存管理 | 物理内存、内存碎片、伙伴系统、SLAB分配器

文章目录

  • 物理内存
  • 物理内存分配
    • 外部碎片
    • 内部碎片
    • 伙伴系统(buddy system)
    • slab分配器


物理内存

在Linux中,内核将物理内存划分为三个区域。

在解释DMA内存区域之前解释一下什么是DMA:

DMA(直接存储器访问) 使用物理地址访问内存,将数据从一个地址空间复制到另外一个地址空间,从而加快磁盘和内存之间数据的交换,不经过MMU(内存管理单元),这时CPU可以去干别的事,大大增加了效率。

  • DMA内存区域(ZONE_DMA): 包含 0M~16M 之内的内存页框,该区域的物理页面专门供I/O设备的DMA使用,DMA需要连续的缓冲区,为了能够提供物理上连续的缓冲区,必须从物理地址空间专门划分一段区域用于DMA。
  • 普通内存区域(ZONE_NORMAL): 包含 16MB~896M 以上的内存页框,可以直接映射到内核空间中的直接映射区。
  • 高端内存区域(ZONE_HIGHMEM): 包含 896M 以上的内存页框,不可以进行直接映射,可以通过 高端内存映射区中的永久内存映射区 以及 临时内存映射区(固定内存映射区中的一部分) 来对这块物理内存进行访问。

内存分布如下图:
在这里插入图片描述


物理内存分配

在Linux中,通过分段和分页的机制,将物理内存划分为4k大小的内存页(page),并且将作为物理内存分配与回收的基本单位。通过分页机制我们可以灵活的对内存进行管理。

  • 如果用户申请了小块内存,我们可以直接分配一页给它,就可以避免因为频繁的申请、释放小块内存而发起的系统调用带来的消耗。
  • 如果用户申请了大块内存,我们可以将多个页框组合成一大块内存后再进行分配,非常的灵活。

但是,这种直接的内存分配非常容易导致内存碎片的出现,下面就分别介绍内部碎片外部碎片这两种内存碎片。

为了方便接下来的阅读,这里科普一下 页框

  • 分页单元认为所有的RAM被分成了固定长度的 页框 ,页框是主存的一部分,是一个实际的存储区域。
  • 是指一系列的线性地址和包含于其中的数据,每页被视为一个数据块。而存放数据块的物理内存就是 页框 ,也就是说一个 页框 的长度和一个 的长度是一样的, 可以存放在任何页框或磁盘中。

外部碎片

当我们需要分配大块内存时,操作系统会将连续的页框组合起来,形成大块内存,来将其分配给用户。但是,频繁的申请和释放内存页,就会带来 内存外碎片 的问题,如下图。
在这里插入图片描述
假设我们这块内存块中有10个页框,我们一开始先是分配了3个页框给 进程A ,而后又分配了5个页框给 进程B 。当进程A结束后,其释放了申请的3个页框,此时我们剩余空间就是内存块起始位置的3个页框,以及末尾位置的2个页框。

假如此时我们运行了 进程C ,其需要5个页框的内存,此时虽然这块内存中还剩下5个页框,但是由于我们频繁的申请和释放小块空间导致内存碎片化,因此如果我们想申请5个页框的空间,只能到其他的内存块中申请,这块内存的空闲页框就被浪费了。


要想解决 外部碎片 的问题,无非就两种方法:

  • 外碎片问题的本质就是 空闲页框不连续 ,所以可以将 非连续的空闲页框 映射到 连续的虚拟地址空间 ,如果 现存的空闲页框总大小 满足进程的需求,则允许将一个进程分散地分配到许多不相邻的分区中,从而避免直接申请新的内存块;
  • 记录现存的 连续空闲页框块 的情况,如果有 能满足的小块内存需求 直接从记录中分配 相等或大于 内存需求的 连续空闲页框块 ,从而避免直接申请新的内存块。

第一种方法就是将上面举例中的 C进程 一部分分配到前面的 3个页框 , 另一部分分配到后面的 2个页框 ,如此一来不用申请新的内存块即可满足C进程的需求,详细内容将在分页知识中讲述。

第二种方法就是,虽然 C进程 要申请新的内存块,但是如果接下来 A进程 又开始运行,那我们就将 B进程 所在的内存块中 3块连续空闲页框块 分配给 A进程 而不是直接申请新的 10块连续页框 分配给 A进程

Linux选择了第二种方法,引入 伙伴系统算法 ,来解决 外部碎片 的问题。


内部碎片

内部碎片 是 的未被利用的空闲区域。一开始的时候也说了,由于页是物理内存分配的基本单位,因此即使我们需求的内存很小,Linux也会至少给我们分配 4k 的内存页,此时会造成内存浪费。

举个例子:当一个进程需要 7K 大小的内存时,我们必须给他分配 2个页框 以满足需求,但是第 2 个页框我们只使用了其中 3K 的内存,因此有 1K 的内存被浪费掉了。

在这里插入图片描述

如上图,倘若我们需求的只有几个字节,那该内存页中又有大量的空间未被使用,就造成了内存浪费的问题,而如果我们频繁的进行小块内存的申请,这种浪费现象就会愈发严重。

内碎片问题的本质就是 页内空闲内存 无法被其他进程再次利用。而 SLAB分配器 就可以 对内部碎片进行再利用 ,从而解决内部碎片问题。


伙伴系统(buddy system)

什么是伙伴系统算法呢?其实就是 把相同大小的连续页框块用链表串起来 ,这使页框之间看起来就像是手拉手的伙伴,这也就是其名字的由来。

伙伴系统将所有的空闲页框分组为11块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页框块,即2的0~10次方,最大可以申请 1024 个连续页框,对应 4MB(最大连续页框数 * 每个页的大小 = 1024 * 4k) 大小的连续内存。每个页框块的第一个页框的物理地址是该块大小的整数倍。

在这里插入图片描述

因为任何正整数都可以由 2^n 的和组成,所以我们总能通过拆分与合并,来找到合适大小的内存块分配出去,减少了外部碎片产生 。

倘若我们需要分配1MB的空间,即256个页框的块,我们就会去查找在256个页框的链表中是否存在一个空闲块,如果没有,则继续往下查找更大的链表,如查找512个页框的链表。如果存在空闲块,则将其拆分为两个256个页框的块,一个用来进行分配,另一个则放入256个页框的链表中。

释放时也同理,它会将多个连续且空闲的页框块进行合并为一个更大的页框块,放入更大的链表中。


slab分配器

虽然伙伴系统很好的解决了外部碎片的问题,但是它还是以页作为内存分配和释放的单位,而我们在实际的应用中则是以字节为单位,例如我们要申请2个字节的空间,其还是会向我们分配一页,也就是 4096字节(4K) 的内存,因此其还是会存在内部碎片的问题。

为了解决这个问题,slab分配器就应运而生了。其以 字节 为基本单位,专门用于对 小块内存 进行分配。slab分配器并未脱离伙伴系统,而是对伙伴系统的补充,它将伙伴系统分配的大内存进一步细化为小内存分配(对内部碎片的再利用)。

那么它的原理是什么呢?

对于内核对象,生命周期通常是这样的: 分配内存->初始化->释放内存 。而内核中如文件描述符、pcb等小对象又非常多,如果按照伙伴系统按页分配和释放内存,不仅存在大量的空间浪费,还会因为频繁对小对象进行 分配-初始化-释放 这些操作而导致性能的消耗。

所以为了解决这个问题,对于内核中这些需要重复使用的小型数据对象,slab通过一个缓存池来缓存这些常用的已初始化的对象

  • 当我们需要申请这些小对象时,就会直接从缓存池中的slab列表中分配一个出去。
  • 而当我们需要释放时,我们不会将其返回给伙伴系统进行释放,而是将其重新保存在缓存池的slab列表中。

通过这种方法,不仅避免了内部碎片的问题,还大大的提高了内存分配的性能。

PS:这里说的 缓存池 是对真正的缓存—— 硬件缓存(cache) 原理的一种模仿:

  • 硬件缓存是为了解决快速的CPU和速度较慢的内存之间速度不匹配的问题,CPU访问cache的速度要快于内存,如果将常用的数据放到硬件缓存中,使用时CPU 直接访问cache而不再访问内存 ,从而提升系统速度。
  • 而这里的 缓存池 实际上使在内存中预先开辟一块空间,使用时直接从这一块空间中去取所需对象(访问的是内存而不是cache),是SLAB分配器为了便于对小块内存的管理而建立的。

下面就由大到小,来画出底层的数据结构:

在这里插入图片描述

slab 分配器把每一个 请求的内存 称之为 对象 ,每种 对象 分配一个 高速缓存(kmem_cache) ,所有的 高速缓存 通过双链表组织在一起,形成 高速缓存链表(cache_chain) ,每个 高速缓存 所占内存区被划分为多个 slab ,这些 slab 都属于一个 slab列表 ,每个 slab列表 是一段连续的内存块,并包含3种类型的 slabs链表

  • slabs_full(完全分配的slab)
  • slabs_partial(部分分配的slab)
  • slabs_empty(空slab,或者没有对象被分配)。

slab 是 slab分配器的 最小单位 ,在具体实现上一个 slab 由一个或者多个连续的物理页组成(通常只有一页)。单个 slab 可以在 slab链表 中进行移动,例如一个 未满的slab节点 ,其原本在 slabs_partial 链表中,如果它由于分配对象而变满,就需要从原先的 slabs_partial 中删除,插入到完全分配的链表 slabs_full 中。

举个具象的例子:

slab分配器 将进程描述符和索引节点对象放在一个 cache_chain ,该 cache_chain 下辖两个 kmem_cache :一个 kmem_cache 用于存放进程描述符,而另一个 kmem_cache 存放索引节点对象,然后这些 kmem_cache 又被划分为多个 slab ,每个 slab 都管辖着若干个对象(进程描述符/索引节点对象),而这些 slab 又根据状态(已满、半满、全空)分布在3个 slabs链表 中,3个 slabs链表 共同构成一个 slab列表

举个例子以说明slab的分配过程:

如果在 cache_chain 里有一个名叫 inode_cachepkmem_cache 节点,它存放了一些 inode 对象。当内核请求分配一个新的 inode 对象时,slab分配器 就开始工作了:

  1. 首先要查看 inode_cachepslabs_partial 链表,如果 slabs_partial 非空,就从中选中一个 slab返回一个指向已分配但未使用的inode结构的指针。 完事之后,如果这个 slab 满了,就把它从 slabs_partial 中删除,插入到 slabs_full 中去,结束;
  2. 如果 slabs_partial 为空,也就是没有半满的 slab ,就会到 slabs_empty 中寻找。如果 slabs_empty 非空,就选中一个 slab返回一个指向已分配但未使用的inode结构的指针 ,然后将这个 slabslabs_empty 中删除,插入到 slabs_partial(或者 slab_full )中去,结束;
  3. 如果 slabs_empty 也为空,那么没办法,cache_chain 内存已经不足,只能新创建一个 slab 了。

内核中slab分配对象的全过程:

  1. 根据对象的类型找到 cache_chain 中对应的高速缓存 kmem_cache
  2. 如果 slabs_partial 链表非空,则选择其中一个 slab ,将 slab 中一个未分配的对象分配给需求来源。如果分配之后这个 slab 已满,则移动这个 slabslabs_full 链表
  3. 如果 slabs_partial 链表没有未分配的空间,则去查看 slabs_empty 链表
  4. 如果 slabs_empty 非空,则选择其中一个 slab ,将 slab 中一个未分配的对象分配给需求来源,同时移动 slab 进入 slabs_partial 链表中
  5. 如果 slabs_empty 也没有未分配的空间,则说明此时空间不足,就会请求伙伴系统分页,并创建新的空闲 slab 节点放入 slabs_empty 链表中,回到步骤3

从上面可以看出,slab分配器的本质其实就是 将内存按使用对象不同再划分成不同大小的空间,即对内核对象的缓存操作

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

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

相关文章

Linux 内存管理 | 虚拟内存管理:虚拟内存空间、虚拟内存分配

文章目录虚拟地址空间用户空间内核空间用户空间内存分配malloc内核空间内存分配kmallocvmalloc虚拟地址空间 在早期的计算机中,程序是直接运行在物理内存上的,而直接使用物理内存,通常都会面临以下几种问题: 内存缺乏访问控制&a…

Linux | 编译原理、gcc的命令参数、自动化构建工具 make/Makefile

文章目录编译原理预处理编译汇编链接gcc的常用命令参数make 和 Makefile 的概念make的运行通配符自动化变量伪目标.PHONE:【命令】编译原理 在解释 makefile 前,首先解释一下 .c 文件变成 .exe 文件要经过的四个步骤——预处理、编译、汇编和链接(参考来…

Linux | 进程概念、进程状态(僵尸进程、孤儿进程、守护进程)、进程地址空间

文章目录进程和程序操作系统如何控制和调度程序进程控制块–PCB子进程进程状态僵尸进程孤儿进程守护进程(精灵进程)进程地址空间引言页表进程和程序 程序: 一系列有序的指令集合(就是我们写的代码)。进程:…

Linux 进程控制 :进程创建,进程终止,进程等待,程序替换

文章目录进程创建进程等待程序替换进程终止进程创建 fork函数: 操作系统提供的创建新进程的方法,父进程通过调用 fork函数 创建一个子进程,父子进程代码共享,数据独有。 当调用 fork函数 时,通过 写时拷贝技术 来拷贝…

Linux 内存管理 | 连续分配方式 和 离散分配方式

文章目录前言连续分配单一连续分配分区式分配固定分区分配动态分区分配可重定位分区分配离散分配分段分页多级页表快表(TLB)段页式Linux前言 Linux 内存管理 | 虚拟内存管理:虚拟内存空间、虚拟内存分配 Linux 内存管理 | 物理内存、内存碎片、伙伴系统、SLAB分配器…

操作系统 | 用户态和内核态的切换(中断、系统调用与过程(库函数)调用)

文章目录中断过程调用系统调用过程调用和系统调用的区别中断 用户态、内核态之间的切换是怎么实现的? 用户态→内核态 是通过中断实现的。并且 中断是唯一途径 。核心态→用户态 的切换是通过执行一个特权指令,将程序状态字 (PSW) 的标志位设置为 用户态 。 中断…

管道实现父子进程的信息传递(二)【标准流和其文件描述符、fwrite函数、perror函数】

文章目录代码实现标准流 和 标准流文件描述符代码中用到的函数fwrite()perror()在复习进程间的通信方式时又写了一遍,和 管道实现父子进程的信息传递(一)【fork函数、pipe函数、write/read操作、wait函数】 的区别不是特别大,只是…

命名管道实现进程的信息传递【mkfifo函数、open函数】

文章目录代码实现mkfifo函数open函数代码实现 #include<fcntl.h> // open() #include<sys/wait.h> // wait() #include<sys/types.h> // mkfifo() #include<sys/stat.h> // mkfifo() #include<iostream> #include<unistd.h> // fork()usi…

Linux 进程 | 进程间的通信方式

文章目录管道匿名管道 pipe命名管道 FIFO共享内存共享内存的使用流程&#xff1a;消息队列信号量套接字在之前的博客中讲过&#xff0c;虚拟空间出现的其中一个目的就是解决 进程没有独立性&#xff0c;可能访问同一块物理内存 的问题。因为这种独立性&#xff0c;进程之间无法…

Linux网络编程 | socket介绍、网络字节序与主机字节序概念与两者的转换、TCP/UDP 连接中常用的 socket 接口

文章目录套接字socket 地址通用 socket 地址专用 socket 地址网络字节序与主机字节序地址转换TCP/UDP 连接中常用的 socket 接口套接字 什么是套接字&#xff1f; 所谓 套接字 (Socket) &#xff0c;就是对网络中 不同主机 上的应用进程之间进行双向通信的端点的抽象。 UNIX/L…

网络协议分析 | 传输层 :史上最全UDP、TCP协议详解,一篇通~

文章目录UDP概念格式UDP如何实现可靠传输基于UDP的应用层知名协议TCP概念格式保证TCP可靠性的八种机制确认应答、延时应答与捎带应答超时重传滑动窗口滑动窗口协议后退n协议选择重传协议流量控制拥塞控制发送窗口、接收窗口、拥塞窗口快速重传和快速恢复连接管理机制三次握手连…

JDom,jdom解析xml文件

1.要解析的文件模板如下&#xff1a; <?xml version"1.0" encoding"GBK"?> <crsc> <data><举报信息反馈><R index"1"><举报编号>1</举报编号><状态>1</状态><答复意见>填写…

网络协议分析 | 应用层:HTTP协议详解、HTTP代理服务器

文章目录概念URLHTTP协议的特点HTTP协议版本格式请求报文首行头部空行正文响应报文首行头部空行正文Cookie与SessionHTTP代理服务器正向代理服务器反向代理服务器透明代理服务器概念 先了解一下 因特网&#xff08;Internet&#xff09; 与 万维网&#xff08;World Wide Web&…

MySQL命令(一)| 数据类型、常用命令一览、库的操作、表的操作

文章目录数据类型数值类型字符串类型日期/时间类型常用命令一览库的操作显示当前数据库创建数据库使用数据库删除数据库表的操作创建表显示当前库中所有表查看表结构删除表数据类型 mysql 的数据类型主要分为 数值类型、日期/时间类型、字符串类型 三种。 数值类型 数值类型可…

C++ 继承 | 对象切割、菱形继承、虚继承、对象组合

文章目录继承继承的概念继承方式及权限using改变成员的访问权限基类与派生类的赋值转换回避虚函数机制派生类的默认成员函数友元与静态成员多继承菱形继承虚继承组合继承 继承的概念 继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法等。 当创建一个类时&…

博弈论 | 博弈论简谈、常见的博弈定律、巴什博弈

文章目录博弈论什么是博弈论&#xff1f;博弈的前提博弈的要素博弈的分类非合作博弈——有限两人博弈囚徒困境合作博弈——无限多人博弈囚徒困境常见的博弈定律零和博弈重复博弈智猪博弈斗鸡博弈猎鹿博弈蜈蚣博弈酒吧博弈枪手博弈警匪博弈海盗分金巴什博弈博弈论 什么是博弈论…

MySQL命令(二)| 表的增删查改、聚合函数(复合函数)、联合查询

文章目录新增 (Create)全列插入指定列插入查询 (Retrieve)全列查询指定列查询条件查询关系元素运算符模糊查询分页查询去重&#xff1a;DISTINCT别名&#xff1a;AS升序 or 降序更新 (Update)删除 (Delete)分组&#xff08;GROUP BY&#xff09;联合查询内连接&#xff08;inne…

MySQL | 数据库的六种约束、表的关系、三大范式

文章目录数据库约束NOT NULL&#xff08;非空约束&#xff09;UNIQUE&#xff08;唯一约束&#xff09;DEFAULT&#xff08;缺省约束&#xff09;PRIMARY KEY&#xff08;主键约束&#xff09;AUTO_INCREMENT 自增FOREIGN KEY&#xff08;外键约束&#xff09;CHECK&#xff08…

哈希 :哈希冲突、负载因子、哈希函数、哈希表、哈希桶

文章目录哈希哈希&#xff08;散列&#xff09;函数常见的哈希函数字符串哈希函数哈希冲突闭散列&#xff08;开放地址法&#xff09;开散列&#xff08;链地址法/拉链法&#xff09;负载因子以及增容对于闭散列对于开散列结构具体实现哈希表&#xff08;闭散列&#xff09;创建…

C++ 泛型编程(一):模板基础:函数模板、类模板、模板推演成函数的机制、模板实例化、模板匹配规则

文章目录泛型编程函数模板函数模板实例化隐式实例化显式实例化函数模板的匹配规则类模板类模板的实例化泛型编程 泛型编程旨在削减重复工作&#xff0c;如&#xff1a; 将一个函数多次重载不如将他写成泛型。 void Swap(int& left, int& right) {int temp left;lef…