L8-内存的分段与分页

将磁盘中的程序放到内存中,然后CPU从内存中取出指令,并执行,内存就这样被使用起来了。内存管理的目的就是为了更高效的使用内存。本文主要介绍内存的分段机制和分页机制。

1、分段机制

1.1 重定位

程序在被载入内存后需要进行重定位,这是一个很自然而然的过程,下面是一段载入内存中的程序:

.text
_entry: 	// 入口地址
call _main	// _main是一个偏移地址,其值为40
call _exit
_main:		// main函数入口
…
ret

因为程序中的地址大多都是偏移地址(如程序中的 40),想找到真正的内存地址就需要先确定段基址,这和8086的寻址方式差不多,所谓的重定位也就是找到段基址,而段基址可以通过CS,DS这些段寄存器找到,所以重定位可以在程序运行时进行。当又多个进程同时运行时,又该如何重定位呢?毕竟段寄存器只有一个。这个在之前的多进程图像中有提到过,将PCB中记录了CPU中几乎所有寄存器内容,当进程切换时,会同时切换PCB,更新段寄存器的值,进而切换了段基地址。

1.2 内存分段及其分段后的内存寻址

为什么对程序进行分段呢? 了解过8086汇编程序的朋友应该很容易理解分段。内存不同的区域有不同的性质,如有的区域用于存放指令,这种区域只允许读,有的区域用于存放数据,这种区域可读可写。为了方便管理不同性质的区域,需要将内存进行分段处理,让不同的段之间不会相互干扰。此外由于对程序进行了分段,因此在将程序载入到内存时,可以先将程序的其中几个段载入内存,而不用将整个程序载入到内存中。

 

如何进行分段? 首先在内存中找出一个空闲区域,然后将这片区域分成数据段,代码段,堆栈段等几个区域(段),并把这几个区域的基地址及其属性(如读写属性)保存放到该进程的LDT和PCB中。最后把程序中的指令部分放到代码段中,把全局变量放到数据段中等等。当CPU执行这个进程时,先将PCB中的寄存器内容(包括段寄存器)“扣到”寄存器中,若遇到偏移地址则根据指令找到对应的段基地址,用段基地址 + 偏移地址就可以找到内存的物理地址了。

 

在保护模式下(8086那个属于实模式)的寻址方式如下:

img

若对保护模式下的寻址方式不太了解,可以参考这篇博客 对GDT与LDT的理解

最后还有一个问题:要是用户的程序中改变了段寄存器,从而改变了程序的段基地址怎么办?个人认为:

  1. 段寄存器和段基址这些是在程序载入内存时,由操作系统设置的。 用户不用管这些,一般在程序中也就不会去修改段寄存器这些。想想我们在写c程序时有考虑过段寄存器这些吗?应用程序员仅需要和偏移地址打交道。
  2. 即使用户使用汇编语言编程,强行修改段寄存器。这个也没关系,保护模式肯定是提供了保护机制的,用户随意修改段寄存器会造成程序运行时错误,从而使之无法轻易修改段寄存器。这个保护机制就是特权级,在博客 对GDT与LDT的理解 中有提到过。

2、分页机制与多级页表

2.1 内存碎片

本节主要介绍内存碎片的形成过程,进而引出分页机制。在将程序载入内存之前需要先找出一段空闲的区域,这样看似没什么问题,但当系统长时间运行,不断有程序载入退出时,内存中就会形成很多空闲但是不能使用的区域,也就是内存碎片,这些内存碎片造成了内存资源的浪费。可以通过一个例子来看看内存碎片的危害:

假设内存为500M,首先将0~400M的空间分配给操作系统。

(1)载入程序1,从内存中割出12M给程序1,内存剩余88M.

(2)载入程序2,从内存中割出42M给程序2,内存剩余46M.

(3)载入程序3,从内存中割出16M给程序3,内存剩余30M

(4)程序1退出,内存剩余30 + 12M

(5)载入程序4,从内存中割出20M给程序4,内存剩余10 + 12M

(6)载入程序5,程序5需要占用15M,虽然内存有22M空闲空间,但内存中没有连续的15M空闲空间,因此程序5无法被载入。10M和12M这两片区域成为了无法使用的内存碎片。

从上一节的介绍中可以看出,在分段机制下,程序的每一个段必须占用一段连续的内存空间,否则内存寻址就会出错。但仅仅依靠分段来使用内存,随着系统的运行,内存中会形成许多无法使用的碎片。

2.2 内存分页

将面包分成片,将内存分成页。内存分页是一种在不对性能造成较大影响的情况下,减少了内存碎片的好方法。内存分页的思想是将内存分为一页一页的,每页大小为4KB,这样一个进程最多也就浪费4K的内存,相比与之前的直接分段,内存碎片小了很多。

2.2.1 段页结合与虚拟内存

分页机制是在分段机制的基础上实现的:

img

段页结合的方式既满足了用户需要内存分段的体验,也达到了底层需要减少内存碎片的要求。更重要的是段页结合的方式为虚拟内存的实现提供的条件。

虚拟存储技术的基本思想时利用大容量外存来扩充内存,产生一个比有限的实际内存空间大得多的、逻辑的虚拟空间,简称虚存,以便能够有效地支持多道程序系统的实现和大型程序运行的需要,从而增强系统的处理能力。

——摘取自《操作系统》

因此物理内存用分页切割,虚拟内存用分段切割

一个简单的单级页表结构如下:

img

从图中可以看出,分段机制产生的线性地址被分为两个部分:页号(bit12-31)和偏移地址(bit0-11)。页号是页表的索引,页表中的页框号是内存的索引。页号和页框号是任意映射的。页框号可以在运行过程中填入:当需要访问的某个线性地址的页号对应的页框号为空时,内核就需要在内存中找出一个空闲页,让这个线性地址能对应到真正的物理内存中。每个进程都有一个自己页表,每个进程的页表都有着相同的页号。分页机制让每个进程都有了0-4G的线性地址空间,也就是说每个进程都有0-4G的虚拟内存空间。将虚拟内存空间与内存的换入换出技术相结合,一个简单的虚拟内存技术就形成了。

但单级页表结构有个缺陷:需要单独拿出一大片内存来存放页表。若一个页表项为4B(页号和页框号各2B),那么一个进程的页表大小就为4MB,假如有100个进程呢?那就是400MB空间。

2.2.2 多级页表

img

虽然多级页表在空间上做到了精简,但是增加了访问内存的次数

2.2.3 快表

由于多级页表需要多次访问内存,造成了时间上的损耗,因此引入快表 TLB,是一组相联快速存储,是寄存器

img

用硬件电路可以实现一次命中,先在快表中查找,没找到在多级页表中查找 有点类似于cache

最后还存在一个问题:由于在开启分页机制之后,内存地址的映射方式就发生了改变,因此在开启分页机制之前建立的GDT表,在开启分页机制之后还能找到吗?

  1. 是能够找到的。
  2. 需要对内核空间对应的页目录项和页表做特殊处理。在开启分页机制之前线性地址就等于物理地址,因此只要保证在开启分页机制后内核空间的线性地址依旧等于物理地址,那么在开启分页机制后,同样的只要GDT的线性地址不变就能找到。在head.s中填写了内核空间的页目录项和页表,并且填写的内容让内核空间的线性地址与物理地址一一对应。

3、调页

3.1 FIFO置换

img

3.2 MIN置换

长远的眼光看问题

img

3.3 LRU置换

根据程序局部性原理

img

img

问题:每执行一条地址的时候,MMU就要查一次页表,维护这个数字

imgimg

用过打上1

3.4 clock算法

img

快慢指针 循环队列

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇