抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

为何需要逻辑地址

由于系统中的物理内存是随分配不断在变化的,有时候这个程序使用,有时候那个程序在使用。如果不使用逻辑地址直接使用物理地址,那当前进程操作的地址被占用,则不能使用内存。通过将连续的逻辑地址映射成不连续的物理地址,程序将只用关系的连续的逻辑地址,而物理地址再通过一些方法找到并映射过去就行了。

考虑一种简单的映射方法:

如果简单的使用<物理地址起始地址,大小>实现:程序A申请前0-199的内存,程序B申请200-299的内存。但程序A的200内存并没有完成使用,大部份是空闲的,将会造成内存浪费。这种其实地址加大小的寻址方式就是所谓的 可变分区管理。容易产生外碎片。

如果此时程序A释放,程序C想要申请300的内存,但是程序A释放的0-199不足以满足C的要求,C需要寻找更大块的内存,而程序A释放导致的空闲内存一直没被用上,导致浪费。

  • 内存碎片
    • 内碎片:申请一段空间,但是没有完全使用,一部分内存的空闲的,造成浪费
      • 用不上
    • 外碎片:申请一段空间,但是前边剩余的一段不满足,需要找后边的大块内存。前边剩余的内存一直用不上操作从浪费
      • 塞不下

因此引入分页机制来优化内存碎片问题

分页

将程序的逻辑地址空间分为若干等大的页,称为 页/页面/虚页 。同样将物理内存分成若干项大小同虚页大小的页,称为 块/页帧/实页。虚页是连续的,实页是不连续的,页表就维护了虚页到实页的映射关系,页表中每一项称为页表项(PTE),表示一个虚页到实页是映射关系。每个进程都有自己的页表,称为进程页表

程序中使用的虚地址大致如下:

1
|页号|页内偏移|

页表中的内容大致如下(当然还可以有一些标志信息,如是否有读写权限、是否可用等):

1
2
3
4
5
6
页号1: |标记|物理页号|
页号2: |标记|物理页号|
页号3: |标记|物理页号|
页号4: |标记|物理页号|

标注页号仅表示页表项之间的位置关系(连续),不占用实际内存

每个进程为了找到自己的进程页表还需要 页表基地址寄存器(PTBR) 的帮忙,其指向进程页表的实页号/页表起始地址(PPN)。进程切换时会切换PTBR的值。

注意 ,页号表示页表项之间的位置关系(连续),实际不占物理内存,通过PTBR+页号x页表项大小算出目标地址

由虚地址到实际物理地址的变化叫做内存映射,过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌────┐    ┌────────┬─────────┐
│PTBR│ │虚拟页号│ 页内偏移│ 主存
└─┬──┘ └──┬─────┴────┬────┘ ┌─────────────┐
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
│ │ │ ├─────────────┤
└──────────+──────────┼────────────►│ │
│ 物理页号 │page table │
x─◄───────────┤ │
│ ├─────────────┤
│ │ │
│ │ │
│ │ │
│ 物理地址 │ │
└───────────► │ │
│ │
│ │
└─────────────┘

如果对应地址缺页,则会触发缺页中断。进入内核态(异常/中断型),从磁盘换入或加载内存并填写到内存中,并填写帧号,然后重新进行寻址过程。

0x000011a3的地址映射过程

  • 逻辑地址32位=20位页号+12位页内偏移
  • 页表项32位=20位块号(与20位页号对应)+12位标记位
  • 物理地址=20位块号+12为页内偏移

页表如下:

1
2
3
4
5

起址:PTBR
+页号 |标记| 帧号 |
|----|--------------|
+0x00001 -> | | 0x000f3 |

第一次访问内存,通过PTBR+0x00001找到对应的页表项的地址,读取物理页号/页帧/实页号。再通过帧号0x000f3拼接页内偏移0x1a3得到物理地址(不需要加法,一部分高位,一部分低位,直接拼接更快),第二次访问内存读取数据。至少需要两次内存访问

分页机制的优化

要真正获取一个内存中的内容实际需要加载两次内存,第一次是查页表找到物理页号,第一次是根据物理页号加页内偏移到对应内存,因此可以进行时间上的优化。而每个进程都需要维护一个页表,页表本身也是占内存空间的,因此还可以进行空间优化。

空间优化与多级页表

引入多级页表有两个原因

  • 如果页表大小超过了一个页面的大小/容量,则可能将数据存放到两个不同的页面,将无法通过PTBR+页号的方式找到
    • 如通常一个页4KB,32位系统中一PTE有32位4B,故PTE数不应超过1k
  • 进程页表本身也是在内存中的,需要占用内存空间

如果不考虑页面容量问题,假设一个页能容纳所有页表项。一个32位的系统和程序,如果页内偏移12位,要访问全部4G内存则需要2^20个物理块/物理页面/物理页号,对应就有2^20个页号/虚页号,即2^20个页表项:

1
|12位标记位|20位表示物理页号|

这么一个页表项32bit=4B,则维护一个进程的页表需要占用2^20x4B=4MB的内存空间。4MB看起来很小,但是操作系统是要运行很多程序的。

如果使用多级页表,应当页表大小==页面容量 ,如将32位分为10位,10位,12位的分级方式。为何10位?10位能索引2^10个PTE,而每个PTE占4B,一个页表刚好就4KB,与页容量相同。如果小于页容量会产生内碎片,浪费;如果大于页容量则无法通过页表起址+offset寻址。

64位系统中则常用9位索引页表,64bit=8B,8Bx2^9=4KB,也刚好是一个页的大小。

1
2
3
4
5
虚地址:
| VPN[1] | VPN[0] | 页内偏移 |

PTE:
| PPN[1] | PPN[0] | 页内偏移 |

PTBR指向一级页表起始地址,加上VPN[1]可以索引到对应页表项,一级页表中的PPN[1]PPN[0]就是二级页表的起始地址,再通过VPN[0]索引二级也被即可得到物理页号,加上页内偏移得到物理地址。

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
27
28
29
30
31
┌────┐   ┌──────┬─────┬───────┐
│PTBR│ │ VPN1 │VPN0 │offset │
└─┬──┘ └──┬───┴──┬──┴───┬───┘ ┌─────────────┐
│ │ │ │ │ │
│ │ │ │ ├─────────────┤
└─────────x──────┼──────┼──────────────────►│xxxxxxxxxxxxx│
│ │ │x L0 xx│
│ │ PPN1,PPN0 │x xx│
x──────┼─────◄─────────────┤xxxxxxxxxxxxx│
│ │ ├─────────────┤
│ │ │ │
│ │ │ │
│ │ │ │
│ │ ├─────────────┤
└──────┼──────────────────►│xxxxxxxxxxxxx│
│ │x L1 xxx│
│ PPN1,PPN0 │x xxx│
x─────◄─────────────┤xxxxxxxxxxxxx│
│ ├─────────────┤
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
└──────────────────►│ │
│ │
│ │
│ │
│ │
│ │
└─────────────┘

如果发现第一级只有3项指向第二级,而第二级页表有2^10项,所以一共3个页表需要(2^10+3x2^10)x4B = 16KB的内存空间。

当然上限还是2^10x2^10个表项x4B占4M空间。

问题就是内存的访问次数增加了

时间优化

前面使用多级页表的方式虽然,减少了空间占用,但是内存的访问次数增加了。

TLB

将最常访问的几个(一般8-128个左右)页表项储存到访问速度更快的硬件中,如关联存储器(相当于哈希表),这个小表的名称为TLB (Translation Lookaside Buffer) *或 *快表 。寻址会同时寻找TLB和页表,如果TLB命中则查页表寻址的操作作废。

使用大页

依旧上述多级页表的例子,但是使用某种机制让大页表地址连续,那么就可以使用起址+offset的方式寻址,而不需要多级页表多次访问内存。

1
2
3
4
5
虚地址:
| VPN[1] | VPN[0] | 页内偏移 |

PTE:
| PPN[1] | PPN[0] | 页内偏移 |

只是访问一级页表(L0)时其返回PPN[1]作为大页的索引(高10位),然后使用虚拟地址的<VPN[0],页内偏移>所谓大页的页内偏移。如此一来访问次数就降低了。

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
┌────┐ ┌──────┬─────┬───────┐
│PTBR│ │ VPN1 │VPN0 │offset │
└─┬──┘ └──┬───┴──┬──┴──┬────┘
│ │ │ │
│ │ └──┬──┘ ┌─────────────┐
│ │ │ │ │
│ │ │ ├─────────────┤
└───────x─────────┼───────────►│xxxxxxxxxxxxx│
│ │x L0 xx│
│ PPN1, │x xx│
x───────◄────┤xxxxxxxxxxxxx│
│ ├─────────────┤
│ │ │
│ │ │
│ │ │
│ ├─────────────┤
└───────────►│ │
│ huge page │
│ │
│ │
│ │
│ │
│ │
│ │
├─────────────┤
└─────────────┘

分段

一个程序可以分成不同的部分,如:程序段、数据段、共享段等,分别对应段号0、1、2…。每段占用一段连续的内存空间。

寻址通过段表起始地址寄存器+段号找到对应段起始地址,再用段起始地址+段内偏移找到数据:

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
27
28
29
 ┌──────────────────┐    ┌────────┬─────────┐
│段表起始地址寄存器│ │段号 │ 段内偏移│ 主存
└─┬────────────────┘ └──┬─────┴────┬────┘ ┌─────────────┐
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
│ │ │ ├─────────────┤
└────────────────────────+──────────┼────────────►│ │
│ 段起始地址 │ 段表 │
+─◄───────────┤ │
│ ├─────────────┤
│ │ │
│ │ │
│ │ │
│ 物理地址 │ │
└───────────► │ │
│ │
│ │
└─────────────┘
段表内容:

段号做索引/偏移

0 | 标记 | 段起始地址 |
1 | 标记 | 段起始地址 |
2 | 标记 | 段起始地址 |
3 | 标记 | 段起始地址 |
4 | 标记 | 段起始地址 |
....

注意 由于段起始地址和段大小自由度很高,不一定是2进制位对齐的,所有需要进行两次加法。

常见分段如下:

1
2
3
4
5
6
7
8
9
Kernel Space // 程序间共享

Stack

Libraries

Heap
Data
Text

评论