OS-内存管理

OWPETER Lv3

需求与存储管理目标

  • 需求:
    • 程序员:希望所有地址空间都是我的

程序的链接和装入

几个概念

我们可以用两个非常简单的C语言程序来辅助理解编译、链接、重定位的概念。

1
2
3
4
5
6
7
8
9
10
11
12
13
// main.c
void writing(char* str);

int main() {
writing("hello world\n");
return 0;
}

// write.c
#include <stdio.h>
void writing(char* str) {
printf("%s", str);
}

编译

编译,指由编译程序将用户源程序编译成若干个目标模块(object modules),也就是.o文件。

使用以下命令将main.cwrite.c编译为目标模块

1
gcc -c main.c write.c

发现目录中多出main.o文件和write.o文件。利用objdumpmain.o反汇编后可以得到如下x86语言的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
main.o:     文件格式 elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # f <main+0xf>
f: 48 89 c7 mov %rax,%rdi
12: e8 00 00 00 00 call 17 <main+0x17>
17: b8 00 00 00 00 mov $0x0,%eax
1c: 5d pop %rbp
1d: c3 ret

call调用函数的指令,e8call指令的机器码,其后应该跟着writing()函数的地址。但现在,它们为0,也就是说,当前的main.o模块并不能正确的调用writing函数,函数的调用地址需要由下一步链接来填充。

链接

链接,指由链接程序将目标模块和相应的库函数链接成可转载的模块(load module),通常是单个可执行文件

现在,我们允许gcc进行编译且链接:

1
gcc -o test main.c writing.c

反汇编test文件后,可以得到这么一些内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
0000000000001149 <writing>:
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 48 89 e5 mov %rsp,%rbp
1151: 48 83 ec 10 sub $0x10,%rsp
1155: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1159: 48 8b 45 f8 mov -0x8(%rbp),%rax
115d: 48 89 c6 mov %rax,%rsi
1160: 48 8d 05 9d 0e 00 00 lea 0xe9d(%rip),%rax # 2004 <_IO_stdin_used+0x4>
1167: 48 89 c7 mov %rax,%rdi
116a: b8 00 00 00 00 mov $0x0,%eax
116f: e8 dc fe ff ff call 1050 <printf@plt>
1174: 90 nop
1175: c9 leave
1176: c3 ret

0000000000001177 <main>:
1177: f3 0f 1e fa endbr64
117b: 55 push %rbp
117c: 48 89 e5 mov %rsp,%rbp
117f: 48 8d 05 81 0e 00 00 lea 0xe81(%rip),%rax # 2007 <_IO_stdin_used+0x7>
1186: 48 89 c7 mov %rax,%rdi
1189: e8 bb ff ff ff call 1149 <writing>
118e: b8 00 00 00 00 mov $0x0,%eax
1193: 5d pop %rbp
1194: c3 ret

我们发现了main函数对于writing函数的调用,以及writing函数对于printf的调用。与之前仅编译不链接进行对比,可以知道链接就是一个“将空缺地址补全”的操作。

需要知道的是,链接有动态链接和静态链接两种方式。静态链接指程序在编译的时候就将所有需要的库文件代码复制到可执行文件中。这样生成的可执行文件较大,但它可以在没有外部依赖的情况下运行。在动态链接中,程序只有在运行时才加载所需要的库文件

装入

装入,指由装载程序将可装载模块装入内存

装入的方式有三种,其中常用于多道程序系统的是可重定位装入。这是因为程序在内存中的位置经常需要发生变化,为保证程序在内存中的位置不影响程序运行的正确性,经过编译、链接后的可装入模块的起始地址通常从0开始,而程序中使用的地址都是相对于始址的。因此程序在装入内存时需要经历一个重定位过程,其定义为“在装入过程对目标程序中的指令和数据地址的修改过程”。对于静态装入,我们是不需要一个额外的重定位寄存器的,因为一旦始址的绝对地址被确定下来,其他部分的相对地址对应的绝对地址也就被确定下来了。

可重定位装入方式又成为静态装入,因为目标程序的地址转换是在装入过程中一次性完成的,并且一旦其内存被分配,作业进入内存,其整个运行期间就不能再移动位置。

与之相对的,更加灵活的装入方式是动态运行装入,又称动态重定位。其最大的特点是,装入程序不会将装入模块中的相对地址立即转化为绝对地址,而是将这一过程推迟到程序真正要运行时才进行。要达到这个效果,我们需要一个重定位寄存器,动态的将相对地址转换为绝对地址。

pEUDjFf.md.png

由于重定位寄存器的存在,我们可以将程序的不同部分装入到不同的内存区域,同时在程序运行前只需要装入部分代码,后续根据程序运行需要动态的分配内存。

多重分区分配:

首先引入一种重要假设:破除连续性假设。它是指,一个作业往往由多个相对独立的程序段和数据段组成,那么我们可以将这些片段装载在整个存储空间的不同位置,而不一定要连续存放

但我们在写程序的时候一定是连续着写的,那是谁把一个连续的程序,拆分成了多个段呢?答案是编译器。编译器将文件编译成ELF文件,即Linux中的可执行文件。ELF文件依靠的数据结构存储了程序的代码、数据、符号表、重定位信息等,确保程序能够被正确加载和执行。

ELF文件的主要内容

1. ELF文件头

其中记录了各种该文件的基本信息,其中比较重要的有:程序头、节头表相对文件头的偏移,程序头、节头表表项大小、项数。

2. 程序头表(段头表)

程序头表用于描述文件的物理结构,指导操作系统如何将文件加载到内存

3. 节头表

节头表记录了ELF文件中各个节的信息。在链接和调试的过程中需要使用节头表。

4. 节、段

节和段只是程序数据的两种视图。

一个程序主要由3个段组成:bss段,用于存储未初始化的全局变量;data段,用于存储已经初始化的全局变量;text段,也称代码段,用来存储程序执行代码,通常属于只读

假设我们有这么一段程序:

1
2
3
4
5
6
7
8
9
int global_uninit_val;        // 未初始化的全局变量
int global_init_val = 123; // 已经初始化的全局变量
static int static_val = 456; // 静态全局变量,仅限本文件使用

int main() {
int a = 123;
printf("a = %d", a);
return 0;
}

其程序段示意图应为:

需要注意的是,text段和data段都在链接后的可执行文件中,而bss段不在其中,由系统在装载阶段为其初始化(装载后的地址空间中有bss段)。这是因为text段和data段的内容在编译和链接阶段就已经确定,可执行程序中必须有相应的空间存储这些内容,而bss段中的变量在程序运行时才会被初始化,可执行文件中并不需要分配空间来存储他们的值,只需要记录下这些变量的大小和位置信息等。

另外,程序的局部变量会被存储在栈上

存储管理

存储管理的功能

  • 存储分配和回收
  • 地址变换:做了一层解耦,方便程序在拥有不同地址空间的机器上运行。
  • 存储共享和保护
  • 存储器扩充

几个概念

  • 地址空间:“逻辑地址”的集合
  • 存储空间:“物理地址”的集合

所有在程序中的地址都是逻辑地址。PC寄存器中存储的大部分也都是逻辑地址。只有操作系统关心物理地址。

操作系统在内存中的位置:

  • 放在靠近0地址的地方。对于操作系统友好,保证操作系统一定能够装载在内存中
  • 放在靠近最高地址的位置:对用户友好,用户可以获得较低位置的地址
  • 放在两端

单道程序的内存管理

  • 在运行程序前就可以知道程序的位置,因此可以采用静态地址翻译,即在程序运行前计算出所有物理地址

多道程序:分区式内存管理

固定分区分配

将存储空间分成若干个固定大小连续区域,然后将这些区域分配给用户。程序适应分区

优点:实现简单,开销小,用一个分配表即可

缺点:内碎片造成空间浪费

内碎片:程序需要的空间大小操作系统分配的空间大小(在分配空间内部)

分配算法:单队列、多队列…

动态分区分配

装入进程时,根据进程实际需要的内存大小,动态为其分配内存,使得分配的内存大小刚好等于进程需要的大小

内碎片就不会有了,但会有外碎片

外碎片:分配空间之间的无法利用的地址空间(分配、释放、再分配,就会产生外碎片)

既然是动态分配,那么操作系统就需要知道那块空间被分配了,哪块没有分出去。以下有2种方法解决这个问题:

  • 位图法:

    给每个分配单元一个字位来表示是否被占用

    位图占用空间:Space/every_block_space * 1bit

    时间成本低,操作简单。空间成本固定

  • 链表表示法:用链表将整个地址空间的占用情况记录下来

    时间成本大,因为链表扫描速度慢,还需要插入、删除、修改。空间成本取决于运行程序数量

操作系统只知道哪块内存可以被分配是不够的,还需要知道对于某一个作业,需要为其分配哪一块具体的内存。按分区索引方式,我们有顺序搜索算法和索引搜索算法:

  • 基于顺序搜索的分配算法

    • 首次适应算法(First Fit):从最开头扫,找到第一个可以分配的区域就分配

      会在高地址留下大片空闲区域,为后续的大作业留下空间,但在低地址留下很多碎片,且查找过程中开销较大

    • 下次适应算法(Next Fit):从上次分配的空间开始往下找,找到一个可以分配的空间就分配

      使得存储空间利用更加均匀(如果合适,那么在每一空闲区块都会分出存储区域)。但会导致缺乏大的空闲分区。通常比First Fit效果更差

    • 最佳适应算法(Best Fit):总是找到大小最接近于作业要求的存储区域(保证内碎片最少)

      只划分比作业稍大的空闲分区,因此会保留大的空闲分区。但会剩下许多非常小的外碎片(最多外碎片)

    • 最坏适应算法:总是寻找最大的空白区域

      最大的空闲区域总是首先被划分,因此当较大的作业到来时,往往不会有可供分配的空闲区域

  • 基于索引搜索的分配算法

    索引搜索的基本思想是:根据空闲分区大小对其进行分类,对每一类具有相同容量的空闲分区,单独设立一个空闲分区链表,同时设置一张索引表来管理这些链表。具体来说,每个索引项对应一种空闲分区类型,并记录该类型空闲分区链表的表头指针。

    • 快速适应(分类搜索)

      当需要分配内存时,根据进程所需的内存大小,从索引表中找到最小且能满足需求的空闲分区链表,直接从链表中取出第一块分配给进程。

      优点是查找效率高、在分配过程中不对分区进行分割(就算有空余也分给进程,因此有内碎片),所以能保留大的分区,同时不产生外碎片,但这样会导致归还算法复杂,系统开销大。

      归还算法复杂的主要原因在于需要与前后空闲分区进行合并,合并后还需要将新的分区重新插入到空闲分区链表中,同时更细索引表中的指针、分配表的信息。例如,若前一个分区空闲,则将归还分区与前一个分区合并,同时进行一系列更新操作

    • 伙伴系统

      ppt上讲的挺长,但是感觉还挺好理解的。首先需要知道,这个系统只会划分大小的区块。

      考虑一个作业,需要申请的块的大小为。首先在大小为的所有块中寻找,其中满足,如果有空闲块,则将该块分配给作业。如果没有,从大小的所有块中寻找空闲块。如果有,那么将其一分为二,其中一块分配给作业,另一块加入大小为的块的列表中,以备使用。如果没有,继续寻找大小为的空闲块,如果有,那么现将其一分为二,再将其中一块一分为二,将一个的块分配给作业,将剩下的的块均加入对应大小的块的链表中。

      现在考虑释放,假设释放大小为的块,如果有伙伴块正巧空闲,那么将他们合并为大小为的空闲块,如果还有伙伴块空闲,那么将他们合并为的空闲块… 感觉很像2048,但是只有满足伙伴块这一条件才能合并。

      那么怎样判定判定两个块是否为伙伴块呢?满足三个条件即可:

      1. 大小相同
      2. 物理地址连续
      3. 起始地址对齐:第一个块的地址必须是块大小两倍的整数倍

      这三个条件共同保证了两个伙伴块一定是从一个大块分离出来的。

      既有内碎片,又有外碎片

分页式存储管理

概念区分

  • 程序:静止的
  • 进程:动态的,有生命周期,是静止计算机系统有限资源的基本单位
  • 作业:一个作业可以由多个进程组成

基本思想

上面提到的固定分区和动态分区分配方式都会带来碎片问题。分页思想的提出是为了解决碎片问题,同时打破连续性假设,使进程在物理上零散但逻辑上连续。

基本概念

页:把每个作业的地址空间分成大小相等的片,称之为页

页框:把主存的存储空间分成与页面大小相同的的片

地址结构

逻辑地址:逻辑页号p + 页内偏移w → 以为单位将地址空间分为个大小相同的块

物理地址:保证偏移量相同(即页大小相同),页号可以比逻辑地址的页号位数少

页的大小

在设计页面大小时,我们的目标是使页内碎片页表引起的额外内存开销最小。页内碎片造成的平均开销仅与页面大小有关,为 ,存储页表导致的额外内存开销与平均进程大小和页面大小有个,为 ,那么总开销为

要求上式的最小值,只需导数为0即可

如何分配?

页是内存分配的基本单位,按照进程需要的内存大小分(页数)进行分配

从分配形式来看,分页很像固定分区式分配,但一页比一个分区小的多,二者最根本的区别也在于此,分页不需要把一整个进程装入一个页中,而固定分区分配需要将进程统一塞到一个分区里。从这里就能够看出,分页比固定分区要灵活的多,而这样的灵活也顺带着解决了碎片问题。由于页是紧密划分的,因此是一定没有外碎片的,而对于一个进程来说,可能出现内碎片的位置也只有最后一页,因此平均来说,每个进程的内碎片大小应为半页。

页表

用途

分页式分配将一个进程存储在若干个页当中,因此每个进程需要有一个页表,用于记录每个进程中的每个页存储的物理位置

页表项

页表项是页表中的基本元素,其最主要的内容为页框号,即物理页号。同时可能包含有效位、脏位等信息。因此我们说,页表记录了虚拟页号到物理页号的映射关系

映射方式

当进程希望访问某个逻辑地址时,分页地址变换机构会将该逻辑地址自动分为逻辑页号页内偏移。根据逻辑页号,通过得到页表项地址,然后查询页表项内容得到页框号,最终通过得到物理地址。

注意事项

  • 页表需要存储在内存中,并且占用内存的一块连续空间
  • 通过一级页表访问一次数据,需要访问内存2次,包括访问页表一次,访问内存一次

物理页面表:整个系统有一个物理页面表

请求表

如何提高分页效率

空间开销:多级页表

假设我们的逻辑地址空间有32位,页面大小为4KB,那么就需要分出个页面,假设页表项占用4Bit,那么每个进程的页表就需要占据4MB空间。

问题的本质是页表占用了过大的连续存储空间,那么我们可以用分页式存储的思想对页表进行拆分,即将页表按页离散存储,破除页表的连续性,这就是所谓多级页表。

另外,多级页表可以不加载没有用的页表,来节省存储空间。

pEw0gsJ.png

在这个例子中,32位的地址被划分为10-10-12的形式。其中PT1为一级页表页号,说明一级页表中有1024个页表项,每个页表项都对应着一个二级页表。PT2为二级页表号,说明每个二级页表中都有1024个页表项,每个页表项对应着一个物理页框。

那么如何进行地址变换呢?要回答这个问题,我们必须理解每一级页表的映射关系

  • 一级:index -> 页表所在物理块号
  • 二级:index -> 页框号

如此,地址变换过程便清晰了。还是以上图的地址格式为例,先去逻辑地址最高10位作为一级页表索引,得到二级页表基地址,用该基地址与逻辑地址之后10位相加,得到页表项索引,访问该页表项得到页框号,页框号与逻辑地址最后12为位相加,得到物理地址

这样,对于一个一级页表来说,一个占据4KB大小的页表就可以管理,即4MB空间内存

然而通过上面的地址变换过程我们可以看出,要想访问到数据,我们需要访问3次内存,这会导致内存访问效率严重下降,我们必须给出对于时间开销的解决方案。

时间开销:TLB

设置一个映射结构存放在cache中,将其称为TLB,由它存储一定数量的页号->页框号的映射关系。当得到一个逻辑地址时,转换为页号,然后去TLB中查找是否存在该页号与页框号的映射关系,找到了则直接去访问物理地址(这样只需要访问一次内存),如果找不到,再去访问页表,并且要将该页号与页框号的映射关系存入TLB。

由于不同进程的页表映射关系不同,因此切换进程时要flush TLB,但这会导致TLB直接变成空的TLB,造成较大性能开销。那么我们可以为每一个进程设置一个唯一的进程编号ASID

更多TLB实现细节,可以参看MIPS下的TLB实现手册3.3 3.4节

页面共享:

不同进程可能要用到同样的数据,那么可以把这些数据放在一个物理块中

反置页表 Inverted Page Table

正常页表的将逻辑页号作为索引,页表项为物理页框,而反置页表将物理页框号作为索引,页表项中存储虚拟页号和进程信息。这样做的最大好处是页表大小不再与逻辑地址大小相关,而是取决于物理地址大小,而物理地址空间通常要比虚拟地址空间小的多,例如一个逻辑地址空间为4GB的系统,其内存空间可能仅为64MB,因此能够减小页表占用的内存空间。

缺点也是显而易见的,因为我们查找页表的动因是将虚拟地址转换为物理地址,那么每次我们手持虚拟地址访问反置页表时,都需要通过额外的哈希函数寻找物理页框号,导致查找效率堪忧。

分段存储管理

不同段的内存管理需求并不相同

由于“段”是信息存储的逻辑单位,因此按段存储更符合“信息共享”的需求。在信息共享时,以页为单位是盲目的,以段为单位是精巧的

分段地址空间

每个段内部一定是连续的,段间可以是离散的

逻辑地址结构:段号,段内偏移量

同样的,我们可以用段表存储段的逻辑地址与物理地址的映射关系。需要注意的是,段表项除了存储段的始址(物理地址)外,还存储段长

地址变换过程与页式存储类似,但这里需要检查段内偏移量是否大于段长,若大于,则发出中断信号。

与分页的区别?

  • 页式存储管理的地址空间是一维的,段式存储管理的地址空间是二维的。我的理解是,在页式存储系统中,用户只需要一个单一的逻辑地址就可以获得其对应的物理地址。即使有分页机制的存在,地址变化过程中系统会将逻辑地址划分为页号和偏移两部分,用户无需知道”页“的存在;而对于段式存储来说,用户必须指明要访问“哪个段”中的“多少偏移量”,此之谓“二维”。

  • 页式有效解决了碎片问题,没有外碎片且内碎片不超过每一页大小。段式能够更好的实现数据共享,可适应程序内存的动态增长。

    为什么说段比页更好进行数据共享:假设有一段只被读不会被修改的代码,我们称之为可重入代码,我们可以将其共享给各个进程来使用。如果使用分页存储,我们必须将这段代码分为若干页,每个进程的页表中页需要存储同样数量的页表项,指向这些代码所在的页框。而对于分段存储,每个进程的段表只需要一个段表项指向这段代码所在的物理段。从以上差别可以看出,段的划分是更讲究逻辑的,一段代码不会被段拆分开来,因此能够被更方便的共享。

最后提一嘴段页式存储。希望取两家之所长,先分段,在段内分页,以此来应对程序的动态增长

虚拟存储管理

覆盖和交换

  • 覆盖段

程序的不同部分(模块或代码段)在执行时并不是同时都需要的。因此,可以将程序划分为若干个功能上相对独立的模块,那些不会同时执行的模块可以共享同一块内存区域,这些模块被称为覆盖段

程序员需要手动划分覆盖段,是较为繁琐且容易出错的一个过程。但覆盖技术体现了“按需加载”的思想,即只把当前需要的程序部分放入内存,这与虚拟存储管理中“按需调页”或“按需分段”的核心思想是类似的。

  • 交换

把暂时不用的进程或进程的一部分从内存移动到外存,当进程需要再次执行时,再将其从外存中调入。用户不需要在编程时理解交换。

优点:对程序员透明;增加并发程序数量

缺点:要换入换出,就要计算谁要被交换出去,谁要被交换进来,不可避免的会有处理机开销;另外交换的粒度过大(以进程为单位),没有考虑空间局部性

虚拟存储管理

局部性原理:时间局部性,空间局部性

目标:将覆盖和交换结合起来。一次将程序的部分内容装载到内存,使处理机能够运行比空闲内存空间更大的程序,但无需程序员干涉;同时能够实现更细粒度的、进程的部分内容的内存外存之间的交换。

基本原理:

  • 按需装载:正如上面所说,装入程序时不需要将程序的所有内容读入到内存,仅装载当前需要的页或段
  • 缺页调入:由于按需装载,我们几乎必然的会面临“需要的东西还在辅存”的情况,这时会触发缺页异常,操作系统需要处理该异常,并将缺失的页或段调入内存,然后恢复程序的执行。
  • 不用调出:并非不用调出,而是不用调出,由于内存空间有限,操作系统必须判断当前内存中哪些页可能是不需要的,然后将这些它认为不需要的页和缺页时需要调入的页进行交换

那么有了虚拟存储管理机制,我们就可以访问无限大的地址空间了吗?显然不是的,访问限制从内存的物理大小变为了CPU地址机构决定的逻辑地址上限大小。

逻辑地址空间

比如说我们的CPU是32位的,那我们就可以访问的所有地址了吗?也不是的,因为操作系统需要占用一部分地址空间来做它该做的事,这部分地址空间用户是无法访问的。

地址映射

虚拟存储管理的地址映射是通过页表来实现的。具体的实现细节请参考页表一节。

调入问题

缺页异常

对于虚拟内存管理,调入问题是核心问题之一。

当进程开始时,所有页都在磁盘上,每个页都需要通过缺页错误调入到内存里。操作系统按照下图的方式处理缺页错误。

缺页错误处理

更为具体的流程为:

  1. 现场保护:陷入内核态,保存现场必要信息
  2. 页面定位:定位发生页面中断的虚拟页面
  3. 检查权限:检查虚拟地址的有效性和安全保护位
  4. 页面调入:查找一个空闲的页框,如果没有则要通过置换算法找到一个需要换出的页框
  5. 写回:如果找到的页框中的内容被修改过,那么需要将修改的内容写会到磁盘上
  6. 页面调入2:将磁盘上页面的内容赋值到该页框中
  7. 更新页表:页面调入后操作系统要更新内存中的页表项,将虚拟页面对应的页框号更新为刚刚写入的页框号
  8. 恢复现场:恢复缺页中断前的状态,PC重新指向引发缺页中断的指令(异常指令再执行一遍)

通过上面的流程我们可以看出,用户程序是不知道且不需要知道缺页问题的,整个缺页异常全权由操作系统负责处理,对于用户程序就好像什么也没发生过一样。

在操作系统处理缺页时会面临一个常见问题:如果页框都满了怎么办,应该把谁换出去。这就是页面置换策略需要考虑的问题了。

页面置换策略

  • OPT最优置换:始终向后看,基于之后要输入的内容来决策当前的置换策略。其淘汰的是以后用不使用以后最少使用的页面。但由于我们不可能事先知道以后使用页面的情况,因此该算法实际上上不可能被实现,仅用于理论分析

  • FIFO 先进先出:把最早进来的页面替换掉,在实现时维护一个队列即可

    Belady现象:按理来说,随着物理块数量增加,缺页的几率应该更低。但是对于FIFO来说,物理块数量增加反而会导致缺页几率增加。只有FIFO会出现Belady现象,OPT和LRU都不会出现

  • 改进的FIFO算法:

    • Second Chance算法,每个页面增加一个标志位,记录此数据放入缓存队列后是否被再次访问过。当要换出一个页面时,如果该页面没有被访问过,则立即换出,如果被访问过,将该页面移动至队列头,并清除访问标识

    • Clock算法,是Second Chance的改进版,通过环形队列避免数据在FIFO队列中移动。产生缺页错误时,当前指针指向C,如果C被访问过,则清除C的访问标志,并将指针指向D;如果C没有被访问过,则将新页面放入到C的位置, 置访问标志,并将指针指向D。这里需要注意,替换后替换指针指向下一个内存块

  • LRU 最近最少使用

    设置一个特殊的栈,保存当前使用的各个页面的页面号每当进程访问某页面时,便将该页面的页面号,从栈中移出,将它压入栈顶。栈底始终是最近最久未使用页面的页面号。

    LRU算法的性能是较好的,甚至接近于OPT,但其实现的开销较大

  • 工作集策略

    工作集指某段时间内进程访问的进程的集合,它可以由当前时间 和工作集窗口尺寸 确定;驻留集指每个进程驻留在内存的页面集合。工作集反映了进程在接下来一段时间内可能要频繁访问的页面,因此驻留集大小不应该小于工作集。那么所谓工作集策略,就是指根据工作集动态调整驻留集的大小。工作集策略是解决内存抖动的一个重要方法。

写时拷贝

两个进程同时访存一块只读页面,如果有一个线程需要改变页面(写操作),需要在内存中复制一个新的页面,之后对这个新的页面中的内容进行写操作。

这样做的好处是当一个进程复制为另一个进程时,不需要复制原进程所使用的所有内存,只有当新进程需要进行写操作时,才复制写操作的目的页面,以达到推迟甚至避免复制数据的目的。

fork操作会复制父进程的虚拟地址空间到子进程中,而这一复制过程采用的就是写时拷贝技术。

自映射

页目录自映射是一种特殊的页表组织方式,它允许页目录本身被映射到虚拟地址空间中。这意味着页目录可以通过虚拟地址访问,而不需要直接使用物理地址。

一个4MB的页表需要分成1024个页表页存储(每个页表页都管理4MB空间),每个页表页有1024个页表项,每个页表项管理了4KB存储空间(一页)。这1024个页表页中一定有一个页表页,它管理的这4MB空间恰好就是页表的这4MB空间,其中的每个页表项记录的正好是这1024个页表页的映射关系,这个页表页就是页目录。要知道这个特殊的页表页相对于页表基址的偏移,只需要知道页表所占的这4MB是4GB空间中的第几个4MB,再乘上4KB即可。即

页目录中的每一个页目录项管理着一个页表页,那么一定有一个页目录项管理的恰好是页目录所在的这一页,我们称之为自映射页目录表项。如果要知道自映射页目录表项,即页目录中管理该页目录的表项的物理地址,还是需要得到页表占的这4MB是是4GB中的第几个,然后乘上页表项的大小即可,加上页目录基地址即可。公式是:

  • Title: OS-内存管理
  • Author: OWPETER
  • Created at : 2025-03-11 15:57:03
  • Updated at : 2025-05-21 20:39:38
  • Link: https://owpeter.github.io/2025/03/11/OS-内存管理/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments