太原营销型网站,智汇隆网站建设,精美ppt模板免费下载完整版,wordpress时间轴插件文章目录物理内存物理内存分配外部碎片内部碎片伙伴系统(buddy system)slab分配器物理内存
在Linux中#xff0c;内核将物理内存划分为三个区域。
在解释DMA内存区域之前解释一下什么是DMA#xff1a;
DMA#xff08;直接存储器访问#xff09; 使用物理地址访问内存内核将物理内存划分为三个区域。
在解释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块链表每个块链表分别包含大小为1248163264128256512和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_cachep 的 kmem_cache 节点它存放了一些 inode 对象。当内核请求分配一个新的 inode 对象时slab分配器 就开始工作了
首先要查看 inode_cachep 的 slabs_partial 链表如果 slabs_partial 非空就从中选中一个 slab 返回一个指向已分配但未使用的inode结构的指针。 完事之后如果这个 slab 满了就把它从 slabs_partial 中删除插入到 slabs_full 中去结束如果 slabs_partial 为空也就是没有半满的 slab 就会到 slabs_empty 中寻找。如果 slabs_empty 非空就选中一个 slab 返回一个指向已分配但未使用的inode结构的指针 然后将这个 slab 从 slabs_empty 中删除插入到 slabs_partial或者 slab_full 中去结束如果 slabs_empty 也为空那么没办法cache_chain 内存已经不足只能新创建一个 slab 了。
内核中slab分配对象的全过程
根据对象的类型找到 cache_chain 中对应的高速缓存 kmem_cache如果 slabs_partial 链表非空则选择其中一个 slab 将 slab 中一个未分配的对象分配给需求来源。如果分配之后这个 slab 已满则移动这个 slab 到 slabs_full 链表如果 slabs_partial 链表没有未分配的空间则去查看 slabs_empty 链表如果 slabs_empty 非空则选择其中一个 slab 将 slab 中一个未分配的对象分配给需求来源同时移动 slab 进入 slabs_partial 链表中如果 slabs_empty 也没有未分配的空间则说明此时空间不足就会请求伙伴系统分页并创建新的空闲 slab 节点放入 slabs_empty 链表中回到步骤3
从上面可以看出slab分配器的本质其实就是 将内存按使用对象不同再划分成不同大小的空间即对内核对象的缓存操作 。