清华大学MOOC《操作系统》第5讲:“物理内存管理:连续内存分配”总结(转自张慕晖博客)

课程内容概述

本节课的内容比较简单。

  • 计算机体系结构和内存层次
  • 地址空间和地址生成
  • 连续内存分配
    • 三种不同的分类策略
    • 碎片整理
    • 伙伴系统
  • uCore中的连续内存管理实现框架

计算机体系结构和内存层次

讲了一些比较抽象的东西。
计算机体系结构由CPU、内存、I/O设备、总线组成。
CPU中包括:

  • ALU、控制逻辑
  • 寄存器
  • 高速缓存:加快读写速度
  • 存储管理单元(MMU)
    内存的特点:
  • 最小访问单位是字节(8bit)
  • 一次可以读/写4字节(32位),有地址对齐问题

内存可以分为如下层次:

  • CPU中:
    • L1缓存
    • L2缓存
    • 这些缓存都是由硬件(MMU)来控制的,软件看不到
  • 高速缓存未命中:(这之下由操作系统软件来控制)
    • 内存
  • 缺页:
  • 外存(虚拟内存)

操作系统的内存管理

OS内存管理的特点:

  • 每个字节有自己的物理地址
  • 分为内存和外存
  • 每个进程有自己用的内存片,它们自己的地址之间是可以重叠的。
  • MMU:将逻辑(虚拟)地址空间转换为物理地址空间

OS内存管理的目标:

  • 抽象:逻辑地址空间
  • 保护:独立地址空间
  • 共享:访问相同内存(虽然和保护有一定的矛盾)
  • 虚拟化:更大的地址空间

操作系统采用的内存管理方式:

  • 重定位(relocation):段地址+偏移
  • 分段(segmentation):程序的逻辑结构不需要连成一片,而是分成代码、数据、堆栈3块,每一块的空间就减少了;但每段的内容是连续的
  • 分页(paging):把内存分成最基本的单位
  • 虚拟存储(virtual memory):目前多数系统(如Linux)采用的是按需页式虚拟存储

内存管理方式的实现是高度依赖硬件的:

  • 与计算机存储架构紧密耦合
  • MMU(内存管理单元):处理CPU存储访问请求的硬件

(当然,我很好奇为什么重定位也算是一种内存管理方式。)

静态地址重定位:即在程序装入内存的过程中完成,是指在程序开始运行前,程序中的各个地址有关的项均已完成重定位,地址变换通常是在装入时一次完成的,以后不再改变,故成为静态重定位。
优点:无需硬件支持
缺点:1)程序重定位之后就不能在内存中搬动了;2)要求程序的存储空间是连续的,不能把程序放在若干个不连续的区域中。
动态地址重定位:不是在程序执行之前而是在程序执行过程中进行地址重定位。更确切的说,是在每次访问内存单元前才进行地址变换。动态重定位可使装配模块不加任何修改而装入内存,但是它需要硬件一定位寄存器的支持。
优点:1)目标模块装入内存时无需任何修改,因而装入之后再搬迁也不会影响其正确执行,这对于存储器紧缩、解决碎片问题是极其有利的;2)一个程序由若干个相对独立的目标模块组成时,每个目标模块各装入一个存储区域,这些存储区域可以不是顺序相邻的,只要各个模块有自己对应的定位寄存器就行。
缺点:需要硬件支持。
摘自地址重定位:静态重定位和动态重定位

地址空间和地址生成

一般来说,地址空间至少有3种:

  • 物理地址空间:硬件支持的地址空间
    • 起始地址0
    • 到MAXsys
  • 线性地址空间:CPU看到的地址
    • 起始地址0
    • 大小取决于地址线的宽度
  • 逻辑地址空间:在CPU中运行的进程看到的地址
    • 起始地址0
    • 到MAXprog
    • 也就是用户程序可见的地址

逻辑地址的生成需要经过如下几个过程:

  • 高级语言程序:写出函数
  • 编译:对源代码进行编译,成为汇编源代码,此时仍然用符号来指代函数
  • 汇编:汇编成二进制代码,用具体地址来代替符号了,但是有一些函数还没有找到
  • 链接:加入函数库,找到库函数的地址
  • 重定位:程序加载时进行,视程序实际位置改变符号地址

一般来说,生成地址有几个时机:

  • 编译时(优点:简单)
    • 假设起始地址已知
    • 但如果起始地址改变,就必须重新编译
    • 功能手机一般会有这种情况
  • 加载时
    • 如果加载时起始位置未知,编译器需生成可重定位的代码(relocatable code)
    • 加载时,生成绝对地址
  • 执行时(优点:灵活)
    • 执行时代码可移动
    • 需地址转换(映射)硬件支持(一般是虚拟存储)

连续内存分配

一般分配给一个进程的地址空间是连续的,因此需要进行有效的内存分配。需求是,给进程分配一块不小于指定大小的连续的物理内存区域。定义碎片是过小的不能被利用的空闲内存,分为2类:

  • 外部碎片:分配单元之间的未被使用内存
  • 内部碎片:分配单元内部的未被使用内存(一般是否有内碎片取决于分配单元大小是否要取整)

我们在uCore中进行的是动态分区分配,需要满足以下要求:

  • 当程序被加载执行时,分配一个进程指定大小可变的分区(块)
  • 分区的地址是连续的

一般来说,操作系统需要维护至少2个数据结构,里面存储的内容是:

  • 所有进程的已分配分区
  • 空闲分区(Empty-blocks)

常见的几种连续内存分配策略包括:

  • 最先匹配(First-fit)
  • 最佳匹配(Best-fit)
  • 最差匹配(worst-fit)

总的来说,这些匹配方法各有优劣,至于到底是什么优劣,与使用场景关系很大。

三种不同的分类策略

最先匹配(First Fit Allocation)策略

思路:需要分配n个字节时,使用第一个可用的空间比n大的空闲块

原理和实现:

  • 空闲分区列表按地址顺序排序
  • 分配时搜索第一个合适的分区
  • 释放分区时,检查是否可与邻近的空闲分区合并

优点:

  • 简单
  • 在高地址空间有大块的空闲分区

缺点:

  • 容易产生外部碎片
  • 分配大块时较慢

最佳匹配(Best Fit Allocation)策略

思路:分配n字节内存时,查找并使用不小于n的最小空闲分区

原理和实现:

  • 空闲分区列表按照大小排序
  • 分配时,查找一个合适的分区
  • 释放时,查找并合并邻近的空闲分区(如果找到)

优点:

  • 大部分分配的尺寸较小时,效果很好
    • 可避免大的空闲分区被拆分
    • 可减小外部碎片的大小
    • 相对简单

缺点:

  • 外部碎片较多
  • 释放分区较慢
  • 容易产生很多无用的小碎片

最差匹配(Worst Fit Allocation)策略

思路:分配n字节时,使用尺寸不小于n的最大空闲分区

原理和实现:

  • 空闲分区列表按由大到小排序
  • 分配时,选最大的分区
  • 释放时,检查是否可与邻近的空闲分区合并,进行可能的合并,并调整空闲分区列表顺序

优点:

  • 中等大小的分配较多时,效果最好
  • 避免出现太多的小碎片

缺点:

  • 释放分区较慢
  • 外部碎片较多
  • 容易破坏大的空闲分区,因此难以分配大的分区

碎片整理

上述方法都会产生外碎片。(但是不会产生内碎片,因为是按需分配的)如果碎片太多,就有可能出现,即使空余内存总数足够大,也无法分配出一块连续内存的情况。为此就需要进行碎片整理。碎片整理的定义是通过调整进程占用的分区位置来减少或避免分区碎片。一般有两种碎片整理的方法

  • 紧凑(compaction)通过移动分配给进程的内存分区,以合并外部碎片
    • 进行碎片紧凑的条件:所有的应用程序可动态重定位
    • 需要在应用程序等待时进行移动
    • 需要考虑开销
  • 分区对换(Swapping in/out):通过抢占并回收处于等待状态进程的分区,以增大可用内存空间
    • 这就让更多进程能够在内存里交替运行
    • 需要解决的问题:交换哪个(些)进程?
    • swap分区在linux中是耳熟能详的,在早期很有用,但代价很大,因为外存的速度远远慢于内存
    • 有了虚拟页式存储之后,纯粹的分区对换的意义就不大了

伙伴系统

伙伴系统(Buddy System)是一种连续存储分配的办法,它解决了分配位置和碎片的问题。

假定整个可分配的分区大小为2u,伙伴系统的分配和释放过程如下:

  • 分配过程:
    • 需要的分区大小为2u−1<s≤2u时,把整个块分配给该进程
    • 若s≤2i−1−1,则将大小为2i的当前空闲分区划分成两个大小为2i−1−1的空闲分区
    • 重复划分过程,直到2i−1<s≤2i,并把一个空闲分区分配给该进程
  • 释放过程:
    • 将进程占用的块释放
    • 查看它能否与相邻的空闲块合并(注意边界条件)
    • 如果能合并,则不断合并到不能再合并为止

由分析可知,内碎片的大小最多是2i−1−1,没有外碎片。

伙伴系统的具体实现

数据结构:

  • 空闲块按大小和起始地址组织成二维数组(或者说一维数组+一维链表)
  • 第一维:大小;第二维:地址
  • 初始状态:只有一个大小为2u的空闲块

分配过程:

  • 由小到大 在空闲块数组中找最小的可用空闲块(只要有合适的空闲块,就不切分大块,这是隐含的一个原则吧)
  • 如果块太大,则对可用空闲块进行二等分,直到得到合适大小的块

释放过程:

  • 把释放的块放入空闲块数组
  • 合并满足合并条件的空闲块,合并条件是:
    • 大小相同,均为2i
    • 地址相邻
    • 相邻两块的低地址必须是2^(i+1)的倍数

uCore中的连续内存管理实现框架

这部分就比较简略了。简单来说,uCore定义了一个struct pmm_manager的数据结构,其中保存了各种操作(如分配、释放等)对应的函数指针。因此,可以定义各种不同的管理方法函数,并把函数指针放到该结构体的实例中。这很面向对象了。下面的代码摘自lab2/kern/mm/pmm.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// pmm_manager is a physical memory management class. A special pmm manager - XXX_pmm_manager
// only needs to implement the methods in pmm_manager class, then XXX_pmm_manager can be used
// by ucore to manage the total physical memory space.
struct pmm_manager {
    const char *name;                                 // XXX_pmm_manager's name
    void (*init)(void);                               // initialize internal description&management data structure
                                                      // (free block list, number of free block) of XXX_pmm_manager
    void (*init_memmap)(struct Page *base, size_t n); // setup description&management data structcure according to
                                                      // the initial free physical memory space
    struct Page *(*alloc_pages)(size_t n);            // allocate >=n pages, depend on the allocation algorithm
    void (*free_pages)(struct Page *base, size_t n);  // free >=n pages with "base" addr of Page descriptor structures(memlayout.h)
    size_t (*nr_free_pages)(void);                    // return the number of free pages
    void (*check)(void);                              // check the correctness of XXX_pmm_manager
};

然后就可以定义一个default_pmm_manager用来对内存进行管理了(lab2/kern/mm/default_pmm.c):

1
2
3
4
5
6
7
8
9
const struct pmm_manager default_pmm_manager = {
    .name = "default_pmm_manager",
    .init = default_init,
    .init_memmap = default_init_memmap,
    .alloc_pages = default_alloc_pages,
    .free_pages = default_free_pages,
    .nr_free_pages = default_nr_free_pages,
    .check = default_check,
};

练习

选择填空题

操作系统中可采用的内存管理方式包括()

  • 重定位(relocation)
  • 分段(segmentation
  • 分页(paging)
  • 段页式(segmentation+paging)

都有。虽然我还是很难想象重定位是内存管理方式,这难道不是进程的管理方式么,虽然能够把进程在内存中搬移大概是上述几种分配策略的前提……


在启动页机制的情况下,在CPU运行的用户进程访问的地址空间是()

  • 物理地址空间
  • 逻辑地址空间
  • 外设地址空间
  • 都不是

用户进程访问的内存地址是虚拟地址。虚拟地址加上对应的段选择子构成逻辑地址。逻辑地址经过分段翻译成线性地址。线性地址经过分页翻译成物理地址。(但是,即使没有启动页机制,用户进程访问的地址空间也应该是逻辑地址空间吧)


连续内存分配的算法中,会产生外碎片的是()

  • 最先匹配算法
  • 最差匹配算法
  • 最佳匹配算法
  • 都不会

三种算法都会有外碎片,而没有内碎片。相比之下,分页不会有外碎片,只会有内碎片。伙伴系统是可能会产生外碎片的,当然也有内碎片。


在使能分页机制的情况下,更合适的外碎片整理方法是()

  • 紧凑(compaction)
  • 分区对换(Swapping in/out)
  • 都不是

分页方式不会有外碎片。虽然很对,但这道题完全毫无意义。


描述伙伴系统(Buddy System)特征正确的是()

  • 多个小空闲空间可合并为大的空闲空间
  • 会产生外碎片
  • 会产生内碎片
  • 都不对

小空闲空间在满足一定条件时可以合并。因为是一个不断二分的过程,所以外碎片是可能会产生的。因为是分配2的幂大小的内存,所以内碎片也是有的。

简答题

操作系统中存储管理的目标是什么?

  • 抽象
  • 保护
  • 共享
  • 虚拟化

描述编译、汇编、链接和加载的过程是什么?

  • 编译:将程序源代码转换为汇编代码
  • 汇编:将汇编代码转为二进制的机器码
  • 链接:将多个二进制的机器码结合成一个可执行环境
  • 加载:将程序从外存中加载到内存中

这几个过程并不是完全分开的,例如动态加载的库通常在加载过程中进行链接。 详细过程可参考:http://blog.csdn.net/ajianyingxiaoqinghan/article/details/70889362


什么是内碎片、外碎片?

内碎片是指分配给任务的内存大小比任务所要求的大小所多出来的内存。外碎片指分配给任务的内存之间无法利用的内存。当然,一块内存是否为外碎片取决于需要分配的内存的大小。


最先匹配会越用越慢吗?请说明理由(可来源于猜想或具体的实验)?

最先匹配总是先找低地址空间的内存,到后期低地址空间都是大量小的不连续的内存空间,每次都要扫描低地址空间后到达高地质空间才能得到可用的内存。所以大概是会越用越慢的。


最差匹配的外碎片会比最优适配算法少吗?请说明理由(可来源于猜想或具体的实验)

应该会的。因为每次都找到最大的内存块进行分割,因此分割剩下的内存块也很大,往往还可以再装下一个程序。


理解0:最优匹配,1:最差匹配,2:最先匹配,3:buddy systemm算法中分区释放后的合并处理过程? (optional)

它们的处理方式都是查看边上是否也有空闲块,如果有,则合并空闲块,然后将空闲块管理数据插入链表中。如果能进行合并,都需要连续合并。当然,伙伴系统的合并过程需要判断是否满足条件。


对换和紧凑都是碎片整理技术,它们的主要区别是什么?为什么在早期的操作系统中采用对换技术?

区别是,紧凑是在内存中搬动进程占用的内存位置,以合并出大块的空闲块;对换是把内存中的进程搬到外存中,以空出更多的内存空闲块。采用对换的原因是,处理简单。不过代价也比较高,因为外存比较慢。


一个处于等待状态的进程被对换到外存(对换等待状态)后,等待事件出现了。操作系统需要如何响应?

将进程从硬盘中读取到内存中,在这个过程中,操作系统将该进程标为等待状态并且调度其他进程。

这道题似乎大跃进到第11讲的等待进程模型了。总之,就是进程从等待挂起状态转换到就绪挂起状态,然后在优先级足够高的时候从硬盘读入到内存,进入就绪状态,然后运行。当然,这个做法在现代操作系统里已经凉了。

进程状态转换过程进程状态转换过程


伙伴系统的空闲块如何组织?

按照内存的大小由一系列链表组织。类似于哈希表,将相同大小的内存区域首地址连接起来。(因为一般来说,内存要按首地址大小排列,链表的插入删除比较简单啊)


伙伴系统的内存分配流程?

当向内核请求分配(2i−1,2i]数目的页块时,按照2i大小的块来请求处理。如果对应的块链表中没有空闲页块,则在更大的页块链表中找空闲块,并将大块进行切分,直到得到满足要求的块。如果切出了多余的块,伙伴系统会将这些块插入到对应的空闲页块链表中。


伙伴系统的内存回收流程?

当释放多页的块时,内核首先计算出该内存块的伙伴的地址。内核将满足以下条件的三个块称为伙伴:

  1. 两个块具有相同的大小,记作b。
  2. 它们的物理地址是连续的。
  3. 第一块的第一个页的物理地址是2∗(2b)的倍数。

如果找到了该内存块的伙伴,确保该伙伴的所有页都是空闲的,以便进行合并。内存继续检查合并后页块的“伙伴”并检查是否可以合并,依次类推。

(所以才叫伙伴系统,了解了)

实践题

动态链接如何使用?尝试在Linux平台上使用LD_DEBUG查看一个C语言Hello world的启动流程。

编译链接和加载

LD_DEBUG 是Linux平台查看程序运行加载库的重要工具,其可以详细的列出程序执行时如何加载相应符号的。 通过LD_DEBUG =help查看如何使用, LD_DEBUG=all ./hello 完成题目

显然这道题我没有做。


观察最先匹配、最佳匹配和最差匹配这几种动态分区分配算法的工作过程,并选择一个例子进行分析分析整个工作过程中的分配和释放操作对维护数据结构的影响和原因。


请参考xv6(umalloc.c),ucore lab2代码,选择四种(0:最优匹配,1:最差匹配,2:最先匹配,3:buddy systemm)分配算法中的一种或多种,在Linux应用程序/库层面,用C、C++或python来实现malloc/free,给出你的设计思路,并给出可以在Linux上运行的malloc/free实现和测试用例。 可参考:https://github.com/shellphish/how2heap


阅读slab分配算法,尝试在应用程序中实现slab分配算法,给出设计方案和测试用例。 可参考: https://github.com/bbu/userland-slab-allocator

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
哈尔滨工业大学(哈工大)开设的操作系统MOOC课程是一个非常受欢迎的课程,该课程的习题对于学习操作系统是非常有帮助的。 首先,该课程的习题设计非常合理,能够帮助学习者理解操作系统的概念和基本原理。习题包括了对操作系统的各个方面的测试,如进程管理、内存管理、文件系统、输入输出等。 其次,该课程的习题涵盖了不同难度的内容,从基础的概念题到更复杂的应用题,对于不同层次的学习者来说都具有挑战性。学习者可以根据自己的实际情况选择适合自己的习题进行练习和巩固。 再次,该课程的习题配有详细的解答和解,学习者在做习题的过程中会遇到困惑或疑问,可以通过查看解答和解来理解和掌握相关知识。这对于自学者来说非常有帮助,能够快速解决问题,提高学习效率。 最后,该课程的习题还提供了一些实践任务,让学习者通过实际操作来加深对操作系统的理解和掌握。这些实践任务对于学习者来说是非常宝贵的学习机会,通过亲自动手实践可以更好地掌握操作系统的原理和技术。 综上所述,哈尔滨工业大学MOOC操作系统课程的习题是非常有价值的学习资源,通过做习题可以加深对操作系统的理解和掌握,提高学习效果。对于想要学习操作系统的人来说,这些习题是不可或缺的学习工具。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值