Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
slab
图解 slab 内存分配算法
发布于 2024-09-08 13:03:12 浏览:536
订阅该版
[tocm] # 图解 slab 内存分配算法 ## 1. 前言 学习内存分配算法,往更深层次底层知识学习,这是一条少有人走的路,但成功一定很酷。 关于slab算法,网上也有很多文章介绍,也不乏有很多讲解的很全面的,但是感觉都还是很难以理解消化,此篇文章继续采用之前的风格,以图解的方式对整个算法原型进行讲解,由浅入升,相信这回事一篇成功之作。 在之前我们已介绍过小内存分配算法和静态内存池分配算法,但是对应都有各自的优劣势,本文我们开始继续图解 slab 内存分配算法。 最原始的 `SLAB` 算法是 `Jeff Bonwick` 为 `Solaris` 操作系统而引入的一种高效内核内存分配算法,在 `linux` 内核内也有所使用。在阅读本文之前可以先阅读前两篇内存分配算法文章: + [图解小内存分配算法](https://blog.csdn.net/qq_43332314/article/details/141281432?spm=1001.2014.3001.5502) + [图解静态内存池分配算法](https://blog.csdn.net/qq_43332314/article/details/141284644?spm=1001.2014.3001.5502) 推荐的原因是,对于slab算法的实现,其实是上述两种算法的一种结合,因此先理解上述两种内存分配算法之后再来学习slab分配算法效果会更好。 本文以 `rt-thread` 的 `slab` 算法为基,分析 `slab` 的实现过程,源码见 [RT-Thread/rt-thread/blob/lts-v3.1.x/src/slab.c](https://github.com/RT-Thread/rt-thread/blob/lts-v3.1.x/src/slab.c)。需注意的是 `rt-thread` 内的 `slab` 算法精简掉了对象的构造和析构过程,只保留了纯粹的缓冲型内存池算法的部分,但这更有利于降低复杂度来学习算法的底层原理。接下来就进入正文吧~ ## 2. Slab 算法详解 在学习 slab 算法之前,先讲解几个概念。 ### 2.1 概念 #### 2.1.1 页 slab 算法将整个内存采用页分割(一块大内存均分为很多个页),由页管理器进行管理。slab算法在内部执行的时候会按页进行,但应用层申请的时候对此无感知。 一个页的大小通常在 4k Byte。 ![screenshot_page.png](https://oss-club.rt-thread.org/uploads/20240908/c7e8f6abac1809e7cf7682e6de081ade.png) #### 2.1.2 zone 一个小内存块,每个zone的大小是固定的,在系统初始化的时候会根据整个内存空间大小自动计算合适的zone大小,**通常由几个页组成**。一个zone下面其实就是一个内存池,包含了一种类型大小的数据分配,在此zone下每次分配出来的内存大小都是一样大的。 通常实际上,一个 zone 的大小在 32k Byte ~ 128k Byte 直接。我们这里假定一个 zone 的大小是 4 个page,如下图所示。 ![screenshot_zone.png](https://oss-club.rt-thread.org/uploads/20240908/f358bd173958c9e61315c6c00e0ab8b8.png) #### 2.1.3 chunk chunk 即用户每次申请的内存块。chunk是从一个zone里面分配出去的,每个zone下面包含了若干个同一类大小的chunk。 chunk的大小也不是任意的,chunk的有这么几类:`8 Byte`、`16 Byte`、`32 Byte`、`64 Byte`、`128 Byte`、`256 Byte`、`512 Byte`、`1024 Byte`、`2048 Byte`。chunk的大小满足2的幂次方,因此用户在申请内存的时候,并不是完全按照用户指定的大小去分配chunk的,实际分配的时候,会根据用户申请的大小向上对齐补齐到一个最近的符合规则的 chunk 大小进行分配(也即向上取2的幂次方)。 ![screenshot_chunk.png](https://oss-club.rt-thread.org/uploads/20240908/3574675ef0a7fd384e83e354379bfcee.png) ### 2.2 算法 #### 2.2.1 页分配器算法 slab将整个内存空间按页分割,slab内部进行内存申请释放的时候基本都是基于页操作的。因此理解页分配算法是理解 slab 算法关键的一步。整个页分配算法其实有点类似于小内存分配算法。 我们先来看下单个页的构造数据结构: ![screenshot_page_struct.png](https://oss-club.rt-thread.org/uploads/20240908/dd08426c953d5e6f56cde0c537a19df8.png) + `struct rt_page_head *next`:指针,指向下一个page,用于将各个页链接起来 + `rt_size_t page`:页的数量,记录此页与后方page - 1页会组成一个整体挂载链表内,详见后文申请与释放。 + `char dummy[]`:定义一个数组,占据此页剩余的内存空间,因此整个页的数据结构对应一个页空间大小。 ##### 2.2.1.1 页初始化 ![screenshot_page_initial.png](https://oss-club.rt-thread.org/uploads/20240908/b994199f4b83763710a2cce2d5fc238a.png.webp) ##### 2.2.1.2 页申请 第一次页申请通常如下: ![screenshot_alloc_page.png](https://oss-club.rt-thread.org/uploads/20240908/9c0020c7547720974e32458b7406ec3f.png.webp) 经过若干次页申请与释放之后的页空间可能如下: ![screenshot_page_map2.png](https://oss-club.rt-thread.org/uploads/20240908/f52a614d1f1e97530a5b250a4dc3e9f0.png.webp) 当出现上述情况,在页申请的时候,会遍历 rt_page_list 的成员,找到某个成员下的 page 指示后续的空闲页满足申请的页的要求时,再对页进行分割,之后进行分配。 ##### 2.2.1.3 页释放 页的释放,也即把需要释放的页重新插入上述 `rt_page_list` 链表内,同时需检查回收的页的前后是否页存在空闲的多个页,存在的话需要整合组成一个大的页,以便于后续分配。因此再插入 `rt_page_list` 链表时应注意插入的位置不是直接插入链表头,也不是插入链表尾,而是根据回收页的当前地址,插入对应的链表内的位置,且同时检查对应位置的链表前一项成员记录的多个页是否与此页连续,以及对应位置链表的后一项成员的多个页是否与是否的页连续,连续的话就进行整合,删除掉一个链表成员,修改保留的链表成员的page参数。 如果释放的页前后都没有空闲链表成员指示的页与释放的页均不连续,则将释放的一个or多个页添加到链表内,操作如下: ![screenshot_free_page3.png](https://oss-club.rt-thread.org/uploads/20240908/4496d1191f2e270e74f2065f022cfb52.png.webp) 如果释放的页只有前是空闲的,操作如下: ![screenshot_free_page2.png](https://oss-club.rt-thread.org/uploads/20240908/daba29b51183c09512a4dc76ac561a3e.png.webp) 下面直接上图,下述是释放的页刚好前后页都是空闲的情况: ![screenshot_free_page.png](https://oss-club.rt-thread.org/uploads/20240908/8c91c56c6bbfe14a2fde4159d4cf7b3d.png.webp) 在内存初始化的时候,首先计算整个能存能分割的页的个数 `npages`,之后进行页释放,即完成了内存页的分割。 #### 2.2.2 页管理器 :ok: 上述完成了页分配实现,对于这么多页的使用信息,在 slab 中还有一个页管理器记录了各页的使用个情况,每个页在页管理器中都对应有一个结构体记录相关使用信息,其结构体如下: ```c struct memusage { rt_uint32_t type: 2 ; /* page type */ rt_uint32_t size: 30; /* pages allocated or offset from zone */ }; ``` + `type`:页类型,共支持三种页类型 + `PAGE_TYPE_FREE` 指示此页为空闲页 + `PAGE_TYPE_SMALL` 指示此页为小页,被分配给 zone 使用的页都是此类型。 + `PAGE_TYPE_LARGE` 指示此页为大页,当用户申请内存的空间非常大,超过了一个阈值 `zone_limit` 大小时,不再通过 zone 分配,而是直接按页进行分配,对应的页类型便是 `PAGE_TYPE_LARGE` + `size`:不同的页类型 `type`,`size` 的含义亦不一样。 + 当页类型为 `PAGE_TYPE_SMALL` 时,`size` 存放的内容为当前页在对应 zone 内的偏移页。 + 当页类型为 `PAGE_TYPE_LARGE` 时,`size` 存放的内容为大块内存空间包含的页的个数。 在完成内存页的分割之后,根据页的个数 `npages`,每个页分配一个 `struct memusage` 进行管理,因此页管理器需要占用 `npages * sizeof(struct memusage)` 大小的内存空间,此空间我们从内存中分配,因此需要进行页申请,页管理器实际会占用 `ALIGN(npages * sizeof(struct memusage), RT_MM_PAGE_SIZE) / RT_MM_PAGE_SIZE` 个页。【ALIGN(a, b) 是a按照b的大小向上取值对齐】 由于页管理器是在内存页初始化后第一次申请的页,因此页管理器占用的内存一定是在整个内存的头部的几个页空间内。 #### 2.2.3 zone 管理 当用户申请内存的空间非常大,直接通过页管理器分配,但是当用户申请内存比较小的时候,就会从 zone 内去分配了。前面我们介绍zone的时候已经介绍过了,每个zone的大小是一样的,通常由几个页组成,因此zone的申请也就是先向也分配器申请对应大小的页,之后用于zone下的内存分配。每个zone能提供某一种类型大小的内存分配。那么zone又是如何管理的呢? 我们继续假定一个 zone 由4个页 page 组成(实际不止四个),zone的头部存储了整个zone的管理结构体信息。通过此结构体控制整个zone空间下的chunk分配与释放。 ![screenshot_chunk.png](https://oss-club.rt-thread.org/uploads/20240908/3574675ef0a7fd384e83e354379bfcee.png) zone的管理头数据结构如下: ```c typedef struct slab_zone { rt_int32_t z_magic; /* magic number for sanity check */ rt_int32_t z_nfree; /* total free chunks / ualloc space in zone */ rt_int32_t z_nmax; /* maximum free chunks */ struct slab_zone *z_next; /* zoneary[] link if z_nfree non-zero */ rt_uint8_t *z_baseptr; /* pointer to start of chunk array */ rt_int32_t z_uindex; /* current initial allocation index */ rt_int32_t z_chunksize; /* chunk size for validation */ rt_int32_t z_zoneindex; /* zone index */ slab_chunk *z_freechunk; /* free chunk list */ } slab_zone; ``` + `z_magic`:填充固定值 `0x51ab51ab`,一般用来校验此为一个 `zone` 的管理头 + `z_nfree`:记录总共的空闲 `chunk` 个数 + `z_nmax`:记录此 `zone` 下所有 `chunk` 总数 + `z_next`:指针,用来连接下一个 `zone` + `z_baseptr`:此 `zone` 内的第一个 `chunk` 的起始地址 + `z_uindex`:一个索引号,大有意思,后面细说 + `z_chunksize`:记录此 `zone` 下的 `chunk` 的空间大小 + `z_zoneindex`:用来记录此 `zone` 的在 `zone_array[]` 下的索引,什么是 `zone_array[]` ,后面细说 + `z_freechunk`:一个列表,此 `zone` 下的 `chunk` 回收的时候,均将其挂载到此列表下。 对于每个zone内的内容管理采用上述结构体进行管理记录,此外,slab算法内还有一个`static slab_zone *zone_array[NZONES]` 指针数组,此数组采用静态分配,每个数组成员是一个 `struct slab_zone ` 类型的指针,所有的 zone 都通过 `struct slab_zone *z_next` 挂载在这些数组内的指针列表下面。那么每次申请完一个zone之后,又该挂载在 `zone_array` 哪个数组成员下面呢? `static slab_zone *zone_array[NZONES]` 的每个数组成员分别对应着拥有对应大小 `chunk` 的 `zone`。因此申请完 `zone` 之后,会根据此 `zone` 的 `chunk` 大小挂载在对应的 `zone_array` 下面 ![screenshot_zone_array.png](https://oss-club.rt-thread.org/uploads/20240908/b618c63e7ea1ce248e96f015462ab3ec.png.webp) **重点再强调一遍:** 并不是每一个 `zone_array` 成员下都挂有`zone`,只有有对应的`zone`申请之后,才会被挂上来。其中`zone` 管理数据结构头`struct slab_zone` 内的`z_zoneindex` 指示的便是此 zone 挂载在 `zone_array` 的哪一个成员下面了。 ##### 2.2.3.1 zone 的申请与释放 **zone 的申请:** 也即向页分配器申请分配固定页的个数,同时修改页管理器对应页的类型以及大小,完成一个zone页的申请。之后初始化 `struct slab_zone` 管理结构体,信息存储在此zone的头部,剩余空间则均分为若干个 chunk。 **zone的释放:** 在用户调用 free 操作触发一次释放的时候才会触发 zone 的释放检查,当判断到`struct slab_zone` 内的 `z_nfree == z_nmax` 时,认为整个zone下的所有chunk均已空闲。 此外还需判断此 `zone` 是否为 `zone_array[z_zoneindex]` 下挂载的第一个 zone 是否就是当前zone,如果是,则不对此zone释放,保留着,只有当 `zone_array[z_zoneindex]` 下挂载了多个zone,且此 zone 不是`zone_array[z_zoneindex]` 下挂载的第一个zone时才对此 zone 进行释放。这样做的目的是保证之前有程序内有使用过此大小chunk的数据结构,则至少保留一个 zone 用于后续分配,减少不必要的页分配,这也就是 zone 的缓冲型特性。 如果上述条件均符合释放条件,`z_nfree == z_nmax` 且 `zone_array[z_zoneindex] != this_zone` 且 `z_next != NULL`,则可以对此 zone 进行释放,第一步则是将此 zone 从 `zone_array[z_zoneindex]` 内移除,之后将其挂载到 `static slab_zone *zone_free;` 全局的空闲 zone 链表上去,到这里还没有释放哦!因为每个zone的大小都是一样的,因此下次如果需要申请新的zone即可直接从此 `zone_free` 上直接获取一个空闲的zone,又可以省去一次申请zone时的页分配操作!只有当`zone_free`列表下挂载的空闲zone数量超过阈值 `ZONE_RELEASE_THRESH 2` 时才真正释放此zone,从 `zone_free` 列表下移除一个zone,之后调用页释放接口释放对应的页。 ##### 2.2.3.2 chunk 的申请与释放 讲完了zone的申请与释放,我们再来看看chunk的申请与释放,同时想要知道 `z_uindex` 是什么含义,我们就需要来看下zone下的chunk分配了! **chunk 的申请:** `zone` 初始化之后其值为0,整个`zone` 下所有 `chunk `未曾被分配一次。此时我们可以理解为整个 `zone` 下有一个数组 `chunk_buffer`,其起始地址为 `z_baseptr`,数组总共有 `z_nmax` 个成员,每个成员的大小为 `z_chunksize` 字节。 那么此时如果需要从此 `zone` 下分配一个 `chunk`,也就是提取各数组成员了,即 `chunk_buffer[z_uindex]`,分配完一个之后把 `z_uindex + 1`,指向下一个还未分配出去过的成员。 通过这样的方式,此 `zone` 下的所有 `chunk` 的第一次分配,都可以根据 `z_uindex` 快速找到对应的 `chunk`,时间复杂度为 O1!而当判断到 `z_uindex == z_nmax` 的时候,说明整个 `zone` 下的所有 `chunk` 都分配过一次了,此时还需要 `chunk` 就得从 `z_freechunk` 列表下提取回收回来的 `chunk` 了。 **chunk 的释放:** 而对应分配出去的 `chunk`,当用户调用 `free` 函数进行内存释放的时候,通过其所在地址按页大小对齐,先找到此chunk所在的页,同时通过查询页管理器内关于此页的信息记录,确认此页类型为 `PAGE_TYPE_SMALL` 小页,则对应页地址头部存储的就是 zone 管理结构体的信息,这样便可找到此chunk对应的zone管理结构体,之后将此 chunk 挂载到 `z_freechunk` 列表下即可。同时注意修改 `z_nfree`值加一,更改空闲 chunk 个数。 #### 2.2.4 malloc 和 free :ok: 上面已经把 页、zone、chunk的申请与释放都基本分析完了,那么接下来我们再来完整的梳理下,用户调用 malloc 和 free 时是怎样的一个过程吧! ##### 2.2.4.1 大内存申请与释放 申请内存大小大于等于 `zone_limit` 值时,直接通过页分配器实现内存分配。`zone_limit = zone_size / 4`,最大为 16k(参数可调)。
大内存申请流程如下:
1. malloc 传入需要申请的内存大小 size 2. 将 size 按页大小向上对齐 3. 调用页分配器申请对应页大小 4. 修改页管理器内对应页的信息,所有对应页的页类型均配置为 `PAGE_TYPE_LARGE`,所有对应页的页大小字段填入分配的页个数 5. 返回申请的页中第一页的首地址
大内存释放流程如下:
1. free 传入需要释放的内存的首地址 ptr 2. 根据此地址按页向下对齐,获取到对应页首地址(正常情况下页首地址应该与free传入地址是一致的) 3. 根据页首地址,查询页管理器内对应页的类型,比对确认是否为`PAGE_TYPE_LARGE`,同时通过页大小字段获取此内存占用的页个数 4. 调用页释放接口,释放对应页 ##### 2.2.4.2 小内存申请释放 申请内存大小小于 `zone_limit` 时,通过 zone 分配。`zone_limit = zone_size / 4`,最大为 16k(参数可调)。
小内存申请流程如下:
1. malloc 传入需要申请的内存大小 size。 2. 根据 zone 支持的 chunk 的 size 大小,调整 size 大小,向上对齐到chunk的size。 + 如 malloc 传入 6,由于chunk支持的size不包含6,因此需要向上对齐,对齐到 8。chunk的size类型见 [2.1.3 chunk](#2.1.3 chunk) 3. 根据调整后的 size 查询对应的 `zone_array[]` 成员列表内是否有有效 zone。 1. 若此 size 对应的`zone_array[]`成员列表内**无有效zone** 1. 则按照 [2.2.3.1 zone 的申请与释放](#2.2.3.1 zone 的申请与释放) 走zone分配流程,从页分配器申请一个新zone。 2. 并格式化此zone下的chunk为size大小,将此zone下的第一个chunk分配给此次申请的内存。 3. 将此zone挂载到 `zone_array[]` 对应成员的列表内。 2. 若此 size 对应的`zone_array[]`成员列表内**有有效zone** 1. 则按照 [2.2.3.2 chunk 的申请与释放](#2.2.3.2 chunk 的申请与释放) 走chunk分配流程,从zone内分配出一个空闲的 chunk。 2. 同时通过 `z_nfree` 判断此 zone 分配完此空闲 chunk 之后是否还存在空闲 chunk,若此chunk是此zone的最后一个空闲chunk,则分配完此chunk之后需要将此 zone 从对应的 `zone_array[]` 成员列表下移除。 小内存释放流程如下: 1. free 传入需要释放的内存的地址 ptr。此值应为一个chunk的首地址。 2. 根据此地址按页向下对齐,获取到对应页首地址。 3. 根据页首地址,查询页管理器内对应页的类型,比对确认是否为`PAGE_TYPE_SMALL`,同时通过页大小字段获取此页位于对应zone的页偏移值。 4. 通过页偏移值找到对应zone的起始页的页首地址。zone的管理结构体便存放在起始页的头部。由此可以找到对应的 zone 管理结构体`struct slab_zone`。 5. 将此 chunk 挂载到 zone 的 `z_freechunk` 空闲chunk 列表下完成 chunk 的回收。 6. 同时判断此 zone 的 `z_nfree ` 的值。 1. 如果 `z_nfree ` 为 0,则表面此 zone 在上次分配 chunk 的时候已经被移除对应的 `zone_array[]` 成员列表了,现在回收了一个 chunk ,那么再将其挂回对应的 `zone_array[]` 成员列表,下次 malloc 的时候才能利用到此 zone。 2. 如果 `z_nfree ` 等于 `z_nmax`,且对应的 `zone_array[]` 成员列表下不止一个 zone,且当前 zone 不是对应的 `zone_array[]` 成员列表下第一个zone,则按照 [2.2.3.1 zone 的申请与释放](#2.2.3.1 zone 的申请与释放) 走zone释放流程 7. 此外修改 `z_nfree ` 值加一,变更空闲 chunk 的数量。 ## 3. 总结 以上便是整个 slab 算法的详细讲解了,最后如果你有时间,推荐阅读下相关源码 [RT-Thread/rt-thread/blob/lts-v3.1.x/src/slab.c](https://github.com/RT-Thread/rt-thread/blob/lts-v3.1.x/src/slab.c) ,相信有了此篇文章的指导,阅读源码时你将得心应手!虽然此篇文章是基于 rt-thread 内核中的 slab 算法,与原始的 slab 有所精简的,但是其核心思想、思路基本是一致,因此本文对于其他扩展的 slab 算法的学习亦能发挥出指导性作用。 --- **创作不易,转载请注明出处!** **关注、点赞+收藏,可快速查收博主有关分享!** ---- 如果你觉得我文章写的不错,欢迎点赞、关注+收藏,谢谢~,以下我的一些推荐: --- 相关推荐: + 个人主页:[**`jaffer`**](https://club.rt-thread.org/u/5f9bb32e3c9a8ca5.html) + 博客主页:[**`博主主页(点击跳转)`**](https://blog.csdn.net/qq_43332314?type=blog) + 博文:[图解小内存分配算法(点击跳转)](https://blog.csdn.net/qq_43332314/article/details/141281432?spm=1001.2014.3001.5502) + 博文:[图解静态内存池分配算法(点击跳转)](https://blog.csdn.net/qq_43332314/article/details/141284644?spm=1001.2014.3001.5502)
3
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
jaffer
You can contact me by Email, jaffer.work@foxmail.com, if you want.
文章
4
回答
10
被采纳
0
关注TA
发私信
相关文章
1
slab内存管理malloc fail
2
内存管理slab算法中的疑问
3
slab动态内存管理中关于void*的用法
4
请教一个问题,slab内存管理算法可以完全避免内存碎片吗
5
关于rt_alloc(SLAB)函数的问题
6
关于SLAB动态内存管理的问题
7
SLAB内存分配与释放问题
8
[已解决]使用slab管理heap时,rt_malloc分配失败
9
slab内存分配器实现
10
slab.c BUG in rt_system_heap_init
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
WIZnet_W5500
UART
ota在线升级
PWM
cubemx
freemodbus
flash
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
中断
Debug
编译报错
msh
SFUD
keil_MDK
rt_mq_消息队列_msg_queue
ulog
C++_cpp
at_device
本月问答贡献
踩姑娘的小蘑菇
7
个答案
3
次被采纳
a1012112796
13
个答案
2
次被采纳
张世争
9
个答案
2
次被采纳
rv666
5
个答案
2
次被采纳
用户名由3_15位
11
个答案
1
次被采纳
本月文章贡献
程序员阿伟
8
篇文章
2
次点赞
hhart
3
篇文章
4
次点赞
大龄码农
1
篇文章
5
次点赞
ThinkCode
1
篇文章
1
次点赞
Betrayer
1
篇文章
1
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部