2023-02-12 16:25阅读: 123评论: 0推荐: 1

MIT6.828_JOS内存管理

MIT6.828_JOS内存管理

内存管理有两个部分

  • 物理内存分配器, 以 4KB为一个页面进行管理
  • 虚拟内存的管理

JOS机器实际物理内存的大小是128MB,JOS以4KB一页将其分为 32768个物理页。

image-20221020200629806

物理内存的管理

JOS的物理内存管理

原始物理内存布局如下:

image-20221021152804757

注意这里的IO hole,这块内存是计算机用来与外部设备进行通信所预留的。按理说32为地址总线能够访问的内存总大小为4GB,但是可以打开你计算机的设备管理器看看它总共识别到的内存,一定小于4GB。少掉的那部分就是用于和外部设备通信的那块内存。通常是显卡的显存和ROM被映射到了这部分。上个lab我们已经体会到了它们的存在:BIOS用于OS启动,而显存用于计算机屏幕的字符显示。

JOS的每一个物理页面,都用一个struct PageInfo 来与之对应

copy
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
struct PageInfo { // Next page on the free list. struct PageInfo *pp_link; // pp_ref is the count of pointers (usually in page table entries) // to this page, for pages allocated using page_alloc. // Pages allocated at boot time using pmap.c's // boot_alloc do not have valid reference count fields uint16_t pp_ref; };

JOS分配一个PageInfo数组,管理所有的物理页,使用单向链表将空闲的PageInfo 结构串接起来,别忘了在低1MB处还有一块地方是可用的。

image-20221021155213054

因此JOS在物理内存的管理方法上,使用的是较为简单的空闲链表管理法。

linux的伙伴系统与slab分配器

JOS的空闲链表法在分配连续内存上会存在外部碎片的问题:频繁地请求和释放不同大小的一组连续页框,必然导致在已分配页框的块内分散了许多小块的空闲页面,由此带来的问题是,即使有足够的空闲页框可以满足请求,但要分配一个大块的连续页框可能无法满足请求。

当然了,JOS这个简单的操作系统只会一页一页地分配物理内存(无论是系统调用sys_page_alloc,还是内核的page_alloc函数都是单独分配一页),使用的数据结构也很少超过4KB的大小,因此在JOS中,外部碎片的问题并不明显。

但是linux这个复杂的系统需要着力决外部碎片的问题,解决方式为伙伴系统。已经有很多很好的文章详细解释了linux的伙伴系统:

极其详尽关于伙伴系统的文章

伙伴系统优缺点(优点是很好地解决了外部碎片的问题,缺点是内部碎片的问题,浪费比较严重)

https://www.cnblogs.com/cherishui/p/4246133.html

有什么方法可以缓解内部碎片的问题呢?如果我们拿出一部分内存专门分配给一些常常会用的数据结构,那么这样的“量身定做”的内存块显然大大缓解内存碎片的问题。

linux系统的slab分配器就体现了这样的思想,linux内核为一些常用的数据结构,比如task_struct、inode、file_struct等,使用slab分配器管理它们,slab为各种数据类型分别建立缓存。该方法中,每种数据结构都有相应的“构造”和“拆除”函数,每个slab都由连续的物理页面组成,每个slab中存储各相同的“对象”,且存放相同对象的slab连成一个双向链表由kmem_cache统一管理,并且slab链表从逻辑上被分为三截,第一截的slab上所有对象都已分配,第二截slab上的某些对象还没有分配,最后一截的slab上的所有对象都处于空闲状态。同时各种kemem_cache_t也都由一个kmem_cache管理,即cache_cache,它是slab分配系统的总根,是一个静态数据结构。

image-20220928165903639

比之伙伴系统,slab分配器的优点如下:

  • 伙伴系统虽然缓解了外碎片,但是会有内碎片的缺点,而slab作为小数据量的内存管理器,能够缓解内部碎片
  • 内核中相当大的一部分数据结构的初始化并不是简单地设置为0,而需要一些特殊的初始化(比如队列头的设置)。如果不使用slab,那么每次分配到内存时就重新做一遍初始化,而使用slab分配器时,如果分配到的内存是已分配的对象(怎么说呢,应该是那些之前分配过,但是又被释放的对象,但这里的释放显然并不是回收内存),那就不需要每次都初始化操作了。

分段与分页

由于历史包袱,x86保护模式下的内存管理始终得经过分段机制处理,因此从虚拟地址到物理地址的转换涉及到两方面: segmentation(分段)translation + page(分页)stranslation。

虚拟地址、线性地址、物理地址这三个令人困惑的名词就出现了。

分段(虚拟地址=>线性地址)

lab指导页面的一张图很好地解释了虚拟地址、线性地址、物理地址这三个称呼的关系,其中虚拟地址又时常叫做逻辑地址:

image-20221020232200530

虚拟地址可以理解为C语言中一个指针的值,虚拟地址经过分段后就变成了线性地址,在经过分页机制的加工最后得到物理地址。

分段,这是在20位地址空间的CPU上(如8086)诞生的产物:8086的寄存器只有16位,如何寻址20位地址空间?--采用分段:段基址 : 段内偏移的方式,实际地址位 段基址 * 16 + 段内偏移,这样就可以寻址1MB的空间了。

实模式下,段基址存放在段寄存器中,可以直接使用段基址:段内偏移的方式进行内存寻址;

而在保护模式下,段寄存器被称为选择子,只相当于一个对段描述符表的索引(和一些控制信息),通过选择子的索引在段描述符表中找到对应的段描述符,而段描述符中存放段基址,这样就又可以通过段基址 :段内偏移的方式寻址了。

虽然x86必须支持分段机制,但是linux通过将段描述符的段基址设置为0,而段界限拉满为4GB(又称为平坦模式绕过了分段机制,JOS、xv6页都是这样做的,这样相当于分段基址并没有起作用且仅靠段内偏值就能寻址全部4GB的内存空间,也就是说上图的线性地址等于段内偏值(在数值上,也等于虚拟地址)。

解决了分段,接下来看看分页机制。

分页机制(线性地址 => 物理地址)

分页负责把线性地址转换成物理地址:

首先明确,JOS内存管理是以页为单位的,无论是虚拟的还是物理的,一页4KB,页边界统统与4KB对齐,所以任意一页的起始地址的低12位都是0。如果要存取任意内存,你只需要知道页面在整个页面堆中的页面号(PPN),再加上12位的偏移即可。

image-20221021121214047

下图表示了JOS的分页机制(和分段),图源

image-20221021122120441

把线性地址分成三部分,前两部分各占10位,它们分别做为页目录表和页表的索引,第三部分是偏移量,用来对具体某个页的内存进行寻址。

  1. CR3寄存器中存放页目录表的物理地址,这样得到了页目录表
  2. 根据线性地址的前十位索引到页目录表的对应PDE,PDE的前20位存放页表的物理地址,剩余12位存放一些控制信息(比如:P,表示页表有效;U,表示用户态能够访问;W,表示可写,这个标志位同时对用户态和内核态生效)。
  3. 找到页表后,根据线性地址的中间10位索引对应的页表项PTE,PTE的前20位存放目标页的物理地址,后剩余12位存放控制信息
  4. 找到目标页后,根据线性地址的后12位偏移在这个目标页中寻址内存地址即可

补充一下 TLB(Translation Lookaside Buffer)。可以看到,一次线性地址转换的过程中总共有3次内存访问,相比于分段的方式,二级页表的转换多了两次内存访问操作,相比于硬件执行速度,内存访问非常慢。为了缓解决这个问题,老办法,加一层缓存,也就是常说的TLB。如下图所示,每个TLB表项主要由两部分组成(当然还会有一些控制字段),一部分是虚拟地址的前20位称为VPN,后一部分存放物理页面号,称为PPN。每次要进行一次线性地址转换时,先根据其前20位查找TLB表,如果命中则直接获取到了PPN,省去了两次内存读取。

image-20221021152324676

如下图所示,是分页机制结合TLB缓存的工作流程,图源

image-20221021152456861

为什么需要分段和分页

换句话说,分段和分页解决了哪些问题?

分段主要解决了两个问题:

  1. 进程地址空间不隔离
  2. 程序运行地址不确定

分段会将整个程序划分为几个片段:数据段、代码段、栈段。每个段拥有一个寄存器存放基地址,还有一个寄存器存放段界限。进程切换会保存并恢复这几个寄存器的值,且每个进程的段基地址是不同的,那么即时在两个不同进程间使用相同的偏移地址,得到的物理地址也是不同的。这就实现了进程地址空间的隔离。

那么为什么说分段使得程序的运行地址确定了?---因为使用分段机制存在之后,程序员写代码时,对地址的引用可以仅仅使用相对偏移地址的方式,而不是绝对的物理地址,绝对地址可能会发生变化,但是相对地址是不会变的,也就是说在程序员眼里程序的运行地址是相同的。

但是分段有个很大的问题,就是它的内存使用效率不高。"分段会将整个程序划分为几个片段:数据段、代码段、栈段", 这个划分太简单粗暴了,专业一点就是分段机制对程序内存的划分粒度太大了,很容易造成外内存碎片导致内存使用效率不高

分页就恰好解决了这个问题,虽然虚拟地址上的地址是连续的,但是物理地址可不一定连续,它以4KB一页管理内存,粒度小了很多,可以随意将一个页从磁盘换入内存或者从内存交换出磁盘,因此内存使用效率也上去了。而且分页也同样具有分段的前两个优势,因此现代操作系统基本上都加入了页式内存管理机制。

因此做如下总结,分页解决了一下三个问题:

  1. 进程地址空间不隔离
  2. 程序运行地址不确定
  3. 分段机制的内存使用效率低下

参考文章: 操作系统 虚拟内存 、分段、分页的理解 - myseries - 博客园 (cnblogs.com)

JOS的虚拟地址与物理地址映射

要建立它们之间的映射,就是建立页目录表、页表,以及正确填写PDE和PTE的内容,课程的LAB2帮助你对这个过程建立起代码层面的理解,可能不直观,但是更深刻。

下图是完成lab2后建立的映射,虚拟内存的布局可以看 memlayout.h头文件,因为lab2只涉及UPAGES(0xef000000)以上的映射建立,因此只画部分。

image-20221021172022989

上图的某些细节确实让人感到疑惑,我前后确认了几次,也使用qemu的info pg命令验证(下图),确实是这样的没错。

image-20221021154458555

接下来分析三个使用boot_map_region函数进行映射的三个区域

  1. 虚拟地址空出4MB映射kernel stack,但是此时kernelstack只有1个,大小为32KB,其余部分作为“guard page”防止栈溢出
  2. PageInfo数组的映射:虚拟地址空出4MB来存放这些PageInfo,但是在128MB内存的机子上,一共有32768页,每个PageInfo 8 字节,所以PageInfo数组一共才256KB。但是实验要求却是然我们直接映射4MB,多映射了不不属于PageInfo的物理内存。正因如此,在映射整个内核后,一定有物理地址对应多个虚拟地址,但是没关系,只要虚拟地址映射到唯一物理地址就可以了。
    • 实验指导里问了个问题:JOS一共可以支持多少内存?起初,我直接看了napages的个数算出了实际物理内存为128MB,因此我认为答案是128MB。但是不对,这个问题是,JOS可以支持多少内存。这要看虚拟地址会为PageInfo准备多少空间,没错4MB。4MB可以存放512KB个pageinfo结构,每个pageinfo对应一个页,那么实际可支持的物理内存为2GB
  3. 整个kernel的映射: 从虚拟地址看,内核空间一共有256MB,可是总共物理内存才128MB,直觉上这是一个错误,但仔细阅读源码后,确实可以这样做。有两点原因:
    1. 本lab建立的映射不会对PageInfo的pp_ref产生影响,所以之后用户态的内存映射建立不会受到影响
    2. boot_map_region 建立映射时,只会对虚拟地址检查,而不会对物理内存检查。

可以再看看上图 qemu info pg的结果,整个kernel映射的最后一个物理页面号是 0xfffff,那么最大的物理地址是 0xffffffff = 256MB,而不是128MB。物理内存总共才128MB,怎么内核就占了256MB呢?类似的反直觉现象有:物理内存才128MB,虚拟内存达到了4GB!这就是分页机制带来的灵活性,在linux或其他大型操作系统中,我们是可以通过swap分区来换出物理内存页面到磁盘上,以此获得更大的虚拟内存空间。在这们课程里,JOS所能使用的物理内存很小,因此不用考虑swap机制。

本文作者:别杀那头猪

本文链接:https://www.cnblogs.com/HeyLUMouMou/p/17114026.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   别杀那头猪  阅读(123)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起