一个地址指向一个字节

重定位:修改程序(静态代码)中的地址(相对地址)

程序每次运行时,在内存中的地址都是不一样的,所以我们不可能直接使用固定的地址。

在程序中出现的地址叫做偏移地址(相对地址),它是相对于程序第一条指令的偏移地址。所以只要确定了程序第一条指令在内存中的具体地址,就能确定程序中的所有地址。而程序第一条指令的地址就叫做基地址(base),CPU在处理程序的地址时只需要将基地址偏移地址相加就可以得到在内存中的物理地址,得到物理地址的过程叫做地址翻译

由于程序的代码会在内存和硬盘中来回交换,所以每次程序的基地址都会发生变化,而这种变化都会记录在PCB中,就是说PCB会记录程序的基地址。每次翻译时都会从PCB找到这个基址

内存的使用与代码分段

为了提高程序和内存的效率,代码会进行分段:代码段数据段栈段等等

写汇编时也是这样使用地址的,比如 mov ax, ds:[100]

程序载入内存时,各个段会被分割开存入,离散的放入内存中,所以就需要记录每个段的起始地址,或者说基址。

每一个段都会有一个编号(0、1、2….),比如说CS=0,DS=1….

这样就形成了一个表,用来描述各个段。即进程段表或者说 LDT ,存储在PCB

段号 基址 长度 保护
0 180K 150K R
1 360K 60K R/W
2 70K 110K R/W
3 460K 40K R

内存分区和分页

操作系统管理内存,可以将内存分为多个大小不等的区域,用一个数据结构表去记录空闲分区的信息,比如(基址和长度)。然后如果有一个进程需要申请内存空间,操作系统只需要遍历这个表找到对应的空间然后返回地址即可。

如此,因为不同的程序大小不等,且在内存中都是离散分布的,这就造成了内存碎片,使得内存浪费

内存碎片虽然可以将各个段移动让它们更加密集,但是这相当的浪费时间

这就引出了分页

  • 将面包切成片,将内存分成页
  • 针对每个段内存请求,系统一页一页的分配这个段

所谓,其实就是将一个打散,分为若干页,向上取整,也就是说页是段的组成单位。而一个页大小为4K,这样一个段最多浪费4K的内存,还是很有性价比的

每个段都会有若干页,每个页又是离散的存储在内存中的,这样就需要个表来记录这些页的信息。内存就这样被分成了好多好多的页

这个记录页的信息的表就叫做页表CPUcr3寄存器会指向它

页号 页框号 保护
0 5 R
1 1 R/W
2 3 R/W
3 6 R

当我们执行某条指令比如:mov ax, [0x2240]时,CPU是这样找地址为[0x2240]这个内存单元的

首先,页的大小是4K,那么用0x2240 / 4*2^10,右移3位得出这个内存单元所在页号2,然后通过页表查找该页号对应的页框号3,页号是由代码段按顺序分的,页框号是内存从小到大排的。然后3 * 4K得出这个页框的基址,左移3位得0x3000加上偏移地址0x0240得最终物理地址0x3240

多级页表与快表

但是接下来又引出了一个问题,假如在一个32位处理机上,一个程序所能表示的地址最多就是4G,假如一个页的大小为4K那么一个程序就需要4G / 4K = 1M个页表项,一个页表项一般是4 byte,这样一个程序的页表就需要4M。一个处理机上不可能只运行一个程序,这样的话内存中光是页表的开销就很大了。

于是有了第一个想法,能不能将程序中用不到的页对应的页表项去掉,只保留程序中会用到的页的页表项。这样就会导致页表项的页号是不连续的,虽然是有顺序的。但这样还是需要用查找算法去查找页表项,这个操作是涉及内存操作的,这样的话一条指令除了本身需要的一次内存操作还需要额外多好几次内存操作,性能大打折扣。显然只存放用到的页这个想法是不合适的。所以页表项必须是连续的。

减少不必要的页表项是减少内存占用的唯一办法。但是某种程度上,又要求页表项必须是连续的。

将二者结合起来,便有了多级页表

多级页表就是将几个页项分为一组,组成几个单独的页表,又称页目录,由页目录项指向这些页表。这些页目录项组成页目录表

1
2
3
4
   10bits     10bits      12bits
+----------+----------+------------+
| 页目录号 | 页号 | Offset |
+----------+----------+------------+

页目录项就好比 ,页表项就好比 ,一个页目录项指向一个页表,就好比一个章指向一片连续的节;一个页表项指向一页的具体地址,就好比一个节指向一页的具体页数。

用多级页表这种方式,虽然页目录因为要满足连续性,必须是4KB大小不可避免。但是有些连续的页表项是用不到的,将这些页表项组成的页表去掉,就省下了大批空间,然后令页目录项置为NULL就行了。但是,如果某块页表中尽管只有一个页表项有用到,为了满足连续性,仍然需要4KB的大小去组成页表,即使有99%的页表项是NULL

需要注意的是,每多加一级页表,能够减少一次内存浪费,但同时会增加一次内存访问。

所以设置几级页表,这也是需要考虑的。

对于一条需要访问内存的指令,访问一次内存这是不可避免的。但是由于内存碎片的存在,需要将段打散分成页离散存储在内存中,于是需要一个页表去维护逻辑页号与页框的关系,这就额外产生了一次内存访问。因为页表过于占内存,需要将页表打散分成页目录,于是需要一个页目录表去维护页表号与页表的关系,这样又额外产生了一次内存访问。

因为只访问一次就能得出地址这个条件太诱人了,难以舍弃。于是又有了快表(TLB)。

快表是一级页表,并不存在内存中,它作为寄存器嵌在CPU中,快表中的页号并不是连续的,但是可以设计一种电路,用硬件的方式只用一次比对就可以直接得出想要的页表项。虽然这个想法很好,但是硬件的造假是很昂贵的,所以快表中页表项的数量不能太多。这就需要一个交换算法,以提高快表的命中率。即使未命中也无非是回到多级页表中重找一次,这并不太亏。

1
2
3
4
        20bits           12bits
+--------------------+------------+
| 页号 | Offset |
+--------------------+------------+
有效 页号 修改 保护 页框号
1 140 0 R 56
1 20 1 R/W 23
0 19 0 R/X 29
1 21 0 R 43

将多级页表和快表两种方式结合起来,既提高了空间利用率,又加快了地址翻译的时间。(与极端的方法对比

段页结合的实际内存管理

对于用户来说,将程序以段的形式载入内存是更具逻辑的方式;但是对于内存来说,将程序以页的方式载入内存是最能提高内存利用率的方式。显然这两种方式是有冲突的。

###内存的换入