L9 内存的分段与分页

将磁盘中的程序放到内存中,然后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那个属于实模式)的寻址方式如下:

分段机制

图1.1 分段机制

若对保护模式下的寻址方式不太了解,可以参考这篇博客 对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 段页结合与虚拟内存

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

分段机制与分页机制的关系

图2.1 分段机制与分页机制的关系

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

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

——摘取自《操作系统》

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

单级页表结构

图2.2 单级页表结构

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

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

2.2.2 两级页表结构

两级页表结构是一种折中的方案,相比于单级页表结构来说,它减少了页表占用的内存,但也减低了寻址的速度。

两级页表结构

图2.3 两级页表结构

线性地址被分为了3部分:页目录,页面,页内偏移。CR3 中存放了当前进程的页目录基地址,目录项中存放了页表的基地址。两级页表结构中的页表和单级页表结构中的页表结构是类似的,只不过两级页表结构中的页表只有4KB大小,只可以映射4M空间的内存。每个进程都有自己的页目录和页表,页目录必须存在于内存中,但页表可以在需要时再进行分配。两级页表结构并不需要为不存在的或者线性地址未使用的部分分配页表,这使得两级页表结构的大小远小于4MB。当访问的页面不在物理内存中时,处理器就会产生一个缺页异常,操作系统就会通过异常处理过程,将缺失的页面从磁盘调入到物理内存中,并将相应的物理内存地址存放到表项中。

快表(TLB)是为了提高内存寻址的速度。快表存放在寄存器中,利用程序的局部性,快表中记录着最近常访问的一些地址,在内存寻址时,CPU会优先从TLB中查找,若在TLB中找到则可以直接找到线性地址所对应的物理地址,若没有找到再通过两级页表结构找出线性地址所对应的物理地址。

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

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

参考

图2.1和图2.3 截取自《Linux内核完全剖析——基于0.12内核》。

[1] 操作系统-哈尔滨工业大学-中国大学MOOC
[2] Linux内核完全剖析——基于0.12内核

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值