《操作系统真相还原》实验记录2.9——完善内存管理系统

一、malloc 底层原理

  1. 之前我们虽然已经实现了内存管理,但显得过于粗糙,分配的内存都是以 4KB 大小的页框为单位的,当我们仅需要几十字节、几百字节这样的小内存块时,显然无法满足这样的需求了,为此必须实现一种小内存块的管理,可以满足任意内存大小的分配,这就是我们为实现 malloc 要做的基础工作。
  2. arena介绍
    1. arena 是很多开源项目中都会用到的内存管理概念,将一大块内存划分成多个小内存块,每个小内存块之间互不干涉,可以分别管理,这样众多的小内存块就称为arena
    2. 原有系统只能分配 4KB 粒度的内存页框,因此 arena 的这“一大块内存”也是通过 malloc_page 获得的以 4KB 为粒度的内存,根据请求的内存量的大小,arena 的大小也许是 1 个页框,也许是多个页框,随后再将它们平均拆分成多个小内存块。
    3. 按内存块的大小,可以划分出多种不同规格的 arena,比如一种 arena 中全是 16 字节大小的内存块,故它只响应 16 字节以内的内存分配,另一种 arena 中全是 32 字节的内存块,故它只响应 32 字节以内的内存分配。
      1. 内存块规格以 16 字节为起始,向上依次是 32 字节、64 字节、128 字节、256字节、512 字节、1024 字节,总共 7 种规格
    4. 我们平时调用 malloc 申请内存时,操作系统返回的地址其实就是某个内存块的起始地址,操作系统会根据 malloc 申请的内存大小来选择不同规格的内存块。因此,为支持多种容量内存块的分配,我们要提前建立好多种不同容量内存块的arena。
    5. 不同内存块规格下:一个Page中划分的内存块数量 = arena 内存池区域的大小(如One-Page) / 内存块规格容量(如16字节)
  3. arena 是个提供内存分配的数据结构,它分为两部分
    1. 第一部分是元信息
      1. 元信息用来描述自己内存池中空闲的内存块数量,这其中包括内存块描述符指针(后面介绍),通过它可以间接获知本 arena 所包含内存块的规格大小,此部分占用的空间是固定的,约为 12 字节。
    2. 第二部分就是内存池区域
      1. 内存池区域里面有无数的内存块,此部分占用 arena 大量的空间。我们把每个内存块命名为mem_block,它们是内存分配粒度更细的资源,最终为用户分配的就是这其中的一个内存块
  4. 在我们的实现中,针对小内存块的 arena 占用 1 页框内存,除了元信息外的剩下的内存被平均分成多个小内存块。整个 arena 就像个仓库一样,元信息部分相当于库房管理员,内存块相当于库中物品。
    arena简图
  5. arena 容纳的内存块是有限的,总会有内存块供不应求的时候。当某一规格 arena 中的内存块全部分配出去时,必须再增加新的同一规格的arena,由多个同一规格的 arena 组合为一个“大的仓库”,为同一规格的内存块提供货源。
    1. 初始时,为某一类型内存块供货的 arena 只有 1 个,当此 arena 中的全部内存块都被分配完时,系统再创建一个同规格的 arena 继续提供该规格的内存块,当此 arena 又被分配完时,再继续创建出同规格的arena,arena 规模逐渐增大,逐步形成 arena 集群。
  6. 既然同一类内存块可以由多个 arena 提供,为了跟踪每个 arena 中的空闲内存块,分别为每一种规格的内存块建立一个内存块描述符,即 mem_block_desc,在其中记录内存块规格大小,以及位于所有同类 arena 中的空闲内存块链表
    内存块规格为 64Bbyte 的内存块描述符简图
  7. 内存块描述符将所有同类 arena 中空闲内存块汇总,因此它相当于内存块超级大仓库,分配小块内存时必须先经过此入口,系统从它的空闲内存块链表 free_list 中挑选一块内存,也就是说,最终所分配的内存属于此类 arena 集群中某个 arena 的某个内存块
  8. 内存块规格有多少种,内存块描述符就有多少种,因此各种内存块描述符的区别就是 block_size 不同,free_list 中指向的内存块规格不同
  9. 由于有了内存块描述符,arena 中就没有必要再冗余记录本 arena 中内存块规格信息,而是用位于 arena 的元信息当中的内存块描述符指针指向本 arena 所属的内存块描述符,间接获得本 arena 中内存块的规格大小。
  10. 注意:处理大内存请求(申请的内存大于1024字节)时也会创建个 arena,但不会再将它拆分成小内存块,而是直接将整块大内存分配出去。故此类 arena 没有对应的内存块描述符,元信息中的内存块描述符指针为空。
    大内存arena简图
  11. 【总结】
  12. 在内存管理系统中,arena 为任意大小内存的分配提供了统一的接口,它既支持 1024 字节以下的小块内存的分配,又支持大于 1024 字节以上的大块内存,malloc 函数实际上就是通过 arena 申请这些内存块。
  13. arena 是个内存仓库,并不直接对外提供内存分配,只有内存块描述符才对外提供内存块,内存块描述符将同类 arena 中的空闲内存块汇聚到一起,作为某一规格内存块的分配入口。
  14. 因此,内存块描述符与 arena 是一对多的关系,每个 arena 都要与唯一的内存块描述符关联起来,多个同一规格的 arena 为同一规格的内存块描述符供应内存块,它们各自的元信息中用内存块描述符指针指向同一个内存块描述符。
    arena 与 mem_block_desc 的逻辑关系

二、底层初始化

  1. 本节我们要完成上一节中介绍的基础,构建 7 种规格的内存块描述符,以后在实际 malloc 时,若发现缺失内存块时再创建相应的arena。
  2. 将来我们会从堆中创建arena,我们会给 arena 结构体指针赋予 1 个页框及以上的内存,那时候的 arena 就是个名符其实的内存仓库了,页框中除了arena元信息结构体外的部分都将作为 arena 的内存池区域,该区域会被平均拆分成多个规格大小相等的内存块,即 mem_block,这些 mem_block 会被添加到内存块描述符的 free_list。

三、实现 sys_malloc

  1. 对计算机来说,内存资源再大也不嫌多,因此不能浪费,必须本着按需分配的原则合理使用,内存块并不是提前“盲目”准备好的,它在需要时由程序动态创建,创建它的函数就是sys_malloc,即 malloc 对应的子功能处理函数 sys_malloc,sys_malloc 的功能是分配并维护内存块资源,动态创建 arena 以满足内存块的分配
  2. 【总结】
    1. 在各种 list 中的结点是 list_elem 的地址,并不是 list_elem 所在的“宿主数据结构”(这个词是作者自己杜撰的,仅供说清楚问题),比如在就绪队列 thread_ready_list 中的是 pcb 的 general_tag 的地址,pcb 便是 general_tag 的宿主数据结构。宿主数据结构中 list_elem 的地址才是链表中的结点,而 list_elem 中存储的是前躯和后继结点的地址,也就是其他宿主数据结构的 list_elem 的地址。当结点从链表中脱离时,要将其还原成宿主数据结构才能使用,还原工作是通过宏 elem2entry 完成的,本节的内存块分配便是通过该宏得到内存块的起始地址。内存块地址被返回给用户后,用户可以自由使用此内存块,自然也会把此内存块中的 “list_elem型的变量free_elem” 覆盖,不过没关系,它并不影响该内存块的回收和分配,因为 free_list 中的元素是list_elem 的地址,地址是不变的,将来回收或再次分配时依然可以正常使用。
  3. 下图框出来的是两个线程的输出,我们注意各线程获得的内存地址,分别是 0xc010200c 和 0xc010204c,下面分析下这两个地址背后的“故事”。
    1. 这两个线程申请的内存字节一个是 33,一个是 63,它们与规格为 64 字节的内存块最接近,因此 sys_malloc 会创建规格为 64 字节的 arena,然后把它拆分成 64 字节的内存块,由于是第 1 次申请内存且只申请了一种内存块,故系统中只存在这一个 arena。把线程 thread_a 获得的内存地址 0xc010200c 拆分成 0xc0102000+0xc 来看,其中 0xc0102000 是 arena 的首地址,0xc 是arena 元信息大小,故返回的 0xc010200c 是 arena 中第 1 个 64 字节内存块的地址。线程 thread_b 获得的内存地址是0xc010204c,它与 0xc010200c 相差为0x40,即十进制 64 字节,这证明 thread_a 申请的 33 字节也占用了 64 字节的内存块,thread_b 申请的 63 字节占用的是 arena 中第 2 个 64 字节内存块。
      内存分配结果

四、内存的释放

  1. 内存管理系统不仅能分配内存,还应该能回收内存,这是最基本的内存管理机制。
  2. 内存的使用情况都是通过位图来管理的,因此,无论内存的分配或释放,本质上都是在设置相关位图中的相应位,都是在读写位图。回收物理地址就是将物理内存池位图中的相应位清0,无需将该 4KB 物理页框逐字节清0。回收虚拟地址就是将虚拟内存池位图中的相应位清0。分配则是相反的,也就是将位图中相应位置为 1 即可。
  3. 【重点知识】分配内存时的一般步骤如下【以下三个步骤将封装在函数 malloc_page 中】
    1. 在虚拟地址池中分配虚拟地址,相关的函数是 vaddr_get,此函数操作的是内核虚拟内存池位图“kernel_vaddr.vaddr_bitmap”或用户虚拟内存池位图“pcb->userprog_vaddr.vaddr_bitmap”。
    2. 在物理内存池中分配物理地址,相关的函数是 palloc,此函数操作的是内核物理内存池位图“kernel_pool->pool_bitmap”或用户物理内存池位图“user_pool->pool_bitmap”。
    3. 在页表中完成虚拟地址到物理地址的映射,相关的函数是page_table_add。
  4. 【重点知识】将要设计的释放内存的步骤如下【功能封装在 mfree_page 中】
    1. 在物理地址池中释放物理页地址,相关的函数是 pfree,操作的位图同 palloc。
    2. 在页表中去掉虚拟地址的映射,原理是将虚拟地址对应 pte 的 P 位置0,相关的函数是 page_table_pte_remove(仅对PTE修改,不对PDE修改)。
      1. 只要 pte 的 P 位为0,CPU 就认为该虚拟地址未做映射,从而达到删除虚拟地址的目的。
      2. P 位的实际意义是当可用物理内存较少时,可以将 pte 指向的物理页框中的数据转储到外存上,这样就省出了 4KB 的物理内存空间。将物理页中的数据存储到外存的同时,需要将 pte 的 P 位置为0。这样在下次访问该 pte 对应的虚拟地址时,由于 pte 的 P 位为0,CPU 会抛出 pagefault 缺页异常,我们可以在处理 pagefault 异常的中断处理程序中将之前保存到外存的页框数据再次载入到物理内存中【依据什么定位外存中的数据呢?后续再补充】,该物理内存可以是原来的物理页,也可以是新的物理页,这取决于实际物理内存的使用情况,然后把目标物理页地址更新到 pte 中,并将 P 位置为 1 。pagefault 中断处理程序退出后,CPU 自动会再次访问引起此 pagefault 的虚拟地址,这次发现 pte 的 P 位为 1,从而访问正常,这就是 CPU 原生支持的页式虚拟地址管理策略,话说 Linux 虚拟地址管理也是利用 P 位和 pagefault 异常实现的。
      3. 按理说,当页表中所有的 pte 都无效时,就可以将页表所在的 4KB 页框回收了,但不值得只为节省一两个页框的内存而在程序运行中频繁操作页表,因此,我们对于删除虚拟地址的处理方法仅仅是将 pte 中的 P 位清 0。
    3. 在虚拟地址池中释放虚拟地址,相关的函数是 vaddr_remove,操作的位图同 vaddr_get。

五、实现 sys_free

  1. 我们之前实现的 mfree_page 只能释放页框级别的内存块,这当然不能满足我们的需求,必须支持释放任意字节大小的内存,而这就是 sys_free 的使命。
  2. sys_free 是内存释放的统一接口,无论是页框级别的内存和小的内存块,都统一用 sys_free 处理。因此,sys_free 针对这两种内存的处理有各自的方法
    1. 对于大内存的处理称之为“释放”,就是把页框在虚拟内存池和物理内存池的位图中将相应位置 0 ,再加对 PTE 的相关操作。
    2. 对于小内存的处理称之为“回收”,是将 arena 中的内存块重新放回到内存块描述符中的空闲块链表 free_list。

六、实现系统调用 malloc 和 free

  1. malloc 和 free 的原型
    1. malloc: void *malloc(size_t size);:分配size字节大小的内存,并返回所分配的地址。
    2. free: void free(void *ptr);:释放ptr所指向的内存。

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

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

相关文章

智能化客户画像构建管理:AI视频监控在大型商场的技术方案介绍(part5)

前言:某商家为了优化卖场服务与营销策略,希望通过非侵入式手段获取客户画像,不仅可以帮助卖场提升服务质量、优化营销策略,还能通过数据驱动的方式提升销售业绩和顾客满意度,为卖场的长期发展奠定坚实的基础。具体需求 1、性别分析:识别顾客的性别比例,为产品布局和服务…

169 使用词嵌入

以判断人名为例。如果我们只使用独热编码,那么我们的训练集不能太大(否则维度爆炸),所以遇到了下面这种情况我们没有在训练集中坚果durian和cultivator,导致我们没有判断出来Robert Lin是人名 但是如果我们使用词嵌入,我们的训练集就可以很大(从网上下载即可),然后通过…

简单的Java程序 HelloWorld

HelloWorld谁便新建一个文件,存放代码 新建一个Java文件文件后缀名为.java Hello.java 系统可能没有显示文件后缀名,需要手动打开编写代码 public class Hello{public static void main(String[] args){System.out.print("Hello,World");} }1. 编译 javac java文…

『ubuntu使用」在桌面建立一个文件夹的快捷方式

ubuntu 24.04 可能是我笨,这个原本觉得非常简单的操作,竟然还有陷阱。我想把主文件夹中的一个目录,放到桌面的快捷方式,希望可以快速访问。试了很多操作都不行,只能百度了…… 问题复述: 我想把“工作目录”这个文件夹在桌面建立一个快捷方式,在查到使用ln -s 命令以后,…

【保姆级教程】DeepSeek R1+RAG,基于开源三件套10分钟构建本地AI知识库

一、总体方案 目前在使用 DeepSeek 在线环境时,页面经常显示“服务器繁忙,请稍后再试”,以 DeepSeek R1 现在的火爆程度,这个状况可能还会持续一段时间,所以这里给大家提供了 DeepSeek R1 +RAG 的本地部署方案。最后实现的效果是,结合本地部署的三个开源工具,包括 1Pane…

【保姆级教程】DeepSeek R1+RAG,基于开源三件套10分钟构建本地AI知识库(文末附笔记及材料)

一、总体方案 目前在使用 DeepSeek 在线环境时,页面经常显示“服务器繁忙,请稍后再试”,以 DeepSeek R1 现在的火爆程度,这个状况可能还会持续一段时间,所以这里给大家提供了 DeepSeek R1 +RAG 的本地部署方案。最后实现的效果是,结合本地部署的三个开源工具,包括 1Pane…

[2025.2.10~16 鲜花] 仆は可怜な少女にはなれない

人間になりたい[2025.2.10~16 鲜花] 仆は可怜な少女にはなれない 要省选了,和以前相比,确实可以确定的是,我的\(oi\)水平还是很有进步的,但是我依旧不会考试,考试的心态依旧不好 平时的模拟赛根本做不到像正式考试一样打,正式考试似乎只要卡壳一下,脑子就钝住了,很奇怪…

【ABP】项目示例(3)——仓储

仓储 在上一章节中,已经完成了领域层的聚合根和实体设计,在这一章节中,实现仓储层的部分功能 仓储作为领域模型和数据模型的桥梁,领域层不关注仓储是怎么实现持久化数据的。对于领域层,仓储层隐藏了持久化数据的细节,所以只需要将仓储接口定义在领域层,而具体的仓储实现…

ZUC算法

1. 算法概述 ZUC(祖冲之密码算法)是我国发布的商用密码算法中的序列密码算法。可用于数据保密性保护、完整性保护等。 ZUC算法密钥长度为128比特,由128比特种子密钥和128比特初始向量共同作用产生32比特位宽的密钥流。 ZUC算法标准包括三个部分,相对应的国家和密码行业标准…

【API】终极 API 学习路线图

API 是 Internet 通信的主干。每个开发人员都需要了解 API。以下是涵盖最重要主题的路线图:API 简介API 是一组用于构建应用程序的协议和工具。存在不同类型的 API,例如 public、private 和 partner。 2. API 术语 需要了解各种 API 术语,例如 HTTP 版本、Cookie 和缓存。 3…

cs144-lab0

CS144: 计算机网络导论 2025年冬季 check0: 本次实验中大概四项任务:在电脑上安装GNU/Linux系统; 手动执行网络任务(如获取网页、发送邮件); 用C++编写一个从互联网获取网页的小程序; 实现网络的核心抽象之一:内存中的可靠字节流(在写入端和读取端之间)。1 配置GNU/L…

[ubuntu使用]安装微信

ubuntu 24.04 一段时间没用ubuntu系统了,刚换的笔记本,二手Thinkpad X1 Carborn 2018,结果装ubuntu24就是装不了,安装完成启动不了。无奈安装的ubuntu20,正常使用后,在更新的时候提示更新到ubuntu22和ubuntu24,最终也升级到最新了,不知道什么问题。 言归正传,使用的时…