OS_lab7

内存管理

实验概述

  • 学习如何使用位图地址池来管理资源
  • 实现在物理地址空间下的内存管理
  • 学习并开启二级分页机制=>实现在虚拟地址空间下的内存管理

基于分页机制,我们可以将连续的虚拟地址空间映射到不连续的物理地址空间。
对于同一个虚拟地址,在不同的页目录表和页表下,我们会得到不同的物理地址。
开启了分页机制后,程序中使用的地址是虚拟地址。我们需要结合页目录表和页表才能确定虚拟地址对应的物理地址。

实验要求:

  • ddl:6月1日

实验内容

Assignment 1

复现参考代码,实现二级分页机制,并能够在虚拟机地址空间中进行内存管理,包括内存的申请和释放等,截图并给出过程解释。

实现过程

参考example一步步照着写。

Assignment 2

参照理论课上的学习的物理内存分配算法如first-fit, best-fit等实现动态分区算法等,或者自行提出自己的算法。

提示:基于scr/3下的代码进行修改,你需要改动的文件:
src/kernel/setup.cpp

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
32
void first_thread(void *arg)
{
// 第1个线程不可以返回
// stdio.moveCursor(0);

// 定义你自己分配想要分配的页数来模拟你实现的算法正确与否,这里我们提供了一个样例,你也可以自行修改:
char *pages_0 = (char *)memoryManager.allocatePhysicalPages(AddressPoolType::KERNEL, 128);
printf("Allocated 128 pages for pages_0, starting at %d.\n", pages_0);

char *pages_1 = (char *)memoryManager.allocatePhysicalPages(AddressPoolType::KERNEL, 64);
printf("Allocated 64 pages for pages_1, starting at %d.\n", pages_1);

char *pages_2 = (char *)memoryManager.allocatePhysicalPages(AddressPoolType::KERNEL, 16);
printf("Allocated 16 pages for pages_2, starting at %d.\n", pages_2);

char *pages_3 = (char *)memoryManager.allocatePhysicalPages(AddressPoolType::KERNEL, 8);
printf("Allocated 8 pages for pages_3, starting at %d.\n", pages_3);

memoryManager.releasePhysicalPages(AddressPoolType::KERNEL, int(pages_0), 128);
printf("Released 128 pages for pages_0.\n");

memoryManager.releasePhysicalPages(AddressPoolType::KERNEL, int(pages_2), 16);
printf("Released 16 pages for pages_2.\n");

char *pages_4 = (char *)memoryManager.allocatePhysicalPages(AddressPoolType::KERNEL, 16);
printf("Allocated 16 pages for pages_4, starting at %d.\n", pages_4);

char *pages_5 = (char *)memoryManager.allocatePhysicalPages(AddressPoolType::KERNEL, 129);
printf("Allocated 129 pages for pages_5, starting at %d.\n", pages_5);

asm_halt();
}

src/utils/bitmap.cpp

1
2
3
4
int BitMap::allocate(const int count){
// 你实现的算法
...
}

我选择实现best-fit(因为原本例子已经实现了first-fit了)
实现代码:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//best fit
int BitMap::allocate(const int count)
{
if (count == 0)
return -1;

int index = 0, empty = 0, start = 0;
int best = length + 1, best_idx = -1;

while (index < length)
{
// 跳过已分配的资源
while (index < length && get(index))
++index;

if (index == length)
break;

// 记录空闲块的起始地址
start = index;
empty = 0;

// 计算当前空闲块的大小
while (index < length && !get(index))
{
++empty;
++index;
}

// 如果当前空闲块满足需求且更优,更新 best 和 best_idx
if (empty >= count && empty < best)
{
best = empty;
best_idx = start;
}
}

// 分配资源
if (best_idx != -1)
{
for (int i = 0; i < count; i++)
{
set(best_idx + i, true);
}
return best_idx;
}

return -1; // 无法分配
}

实现结果:

分析一下:
我们可以看到,我先为pages0到pages3分配了对应的页大小,然后释放了pages0以及pages2,再次分配16页的空间给pages4,可以看到分配的空间的首地址在2883584,说明分配到了pages2原本的位置,这个位置是最佳的位置。然后再看我们为pages5分配129页的空间,发现前面无法满足,就再往后开辟了。

Assignment 3

复现“虚拟页内存管理”一节的代码,完成如下要求。

  • 结合代码分析虚拟页内存分配的三步过程和虚拟页内存释放。
  • 构造测试例子来分析虚拟页内存管理的实现是否存在bug。如果存在,则尝试修复并再次测试。否则,结合测例简要分析虚拟页内存管理的实现的正确性。
  • 不做要求,对评分没有影响)如果你有想法,可以在自己的理解的基础上,参考ucore,《操作系统真象还原》,《一个操作系统的实现》等资料来实现自己的虚拟页内存管理。在完成之后,你需要指明相比较于本教程,你的实现的虚拟页内存管理的特点所在。

实现过程:

补充初始化:

地址空间包括用户虚拟地址空间、用户物理地址空间、内核虚拟地址空间、内核物理地址空间。其中用户虚拟地址池是局部的,放在PCB中。我们的MemoryManager只需要管理剩下的三个地址池。

1
2
3
4
5
6
// 内核物理地址池
AddressPool kernelPhysical;
// 用户物理地址池
AddressPool userPhysical;
// 内核虚拟地址池
AddressPool kernelVirtual;

在初始化函数中,我们需要对这三个地址池进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kernelPhysical.initialize(
(char *)kernelPhysicalBitMapStart,
kernelPages,
kernelPhysicalStartAddress);

userPhysical.initialize(
(char *)userPhysicalBitMapStart,
userPages,
userPhysicalStartAddress);

kernelVirtual.initialize(
(char *)kernelVirtualBitMapStart,
kernelPages,
KERNEL_VIRTUAL_START);
页内存分配:

虚拟页内存分配的三步过程:

  • 从虚拟地址池中分配连续的多个虚拟页
  • 从物理地址池中为每一个虚拟页分配相应大小的物理页
  • 在页目录表和页表中建立虚拟页和物理页之间的对应关系

页内存分配函数:allocatePages(enum AddressPoolType type, const int count)

分配连续的多个虚拟页:

1
2
3
4
5
6
// 第一步:从虚拟地址池中分配若干虚拟页
int virtualAddress = allocateVirtualPages(type, count);
if (!virtualAddress)
{
return 0;
}

分配虚拟页的函数:只分配内核虚拟页。这里的allocate是first-fit

1
2
3
4
5
6
7
8
9
10
11
int allocateVirtualPages(enum AddressPoolType type, const int count)
{
int start = -1;

if (type == AddressPoolType::KERNEL)
{
start = kernelVrirtual.allocate(count);
}

return (start == -1) ? 0 : start;
}

为每个虚拟页指定对应的物理页:physicalPageAddress = allocatePhysicalPages(type, 1);每次为一个虚拟页分配一个物理页,同时会对内核物理页和用户物理页做不同的区分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int MemoryManager::allocatePhysicalPages(enum AddressPoolType type, const int count)
{
    int start = -1;

    if (type == AddressPoolType::KERNEL)
    {
        start = kernelPhysical.allocate(count);
    }
    else if (type == AddressPoolType::USER)
    {
        start = userPhysical.allocate(count);
    }
    return (start == -1) ? 0 : start;

}

建立虚拟页和物理页之间的对应关系:
flag=connectPhysicalVirtualPage(vaddress, physicalPageAddress);
建立对应的函数:
通过计算对应的地址,如果页目录项中没有对应的页表就会进行分配,如果有就直接将页表项指向物理页。

  • 这里计算对应的地址要记得二级分页机制下的虚拟地址可以分为三部分,页目录项+页表项+页内偏移。需要分别拆解,然后进行转换提取,最后得到物理地址。
1
2
3
4
5
6
7
8
9
int toPDE(const int virtualAddress)
{
return (0xfffff000 + (((virtualAddress & 0xffc00000) >> 22) * 4));
}

int toPTE(const int virtualAddress)
{
return (0xffc00000 + ((virtualAddress & 0xffc00000) >> 10) + (((virtualAddress & 0x003ff000) >> 12) * 4));
}

具体实现:

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
bool MemoryManager::connectPhysicalVirtualPage(const int virtualAddress, const int physicalPageAddress)
{
// 计算虚拟地址对应的页目录项和页表项
int *pde = (int *)toPDE(virtualAddress);
int *pte = (int *)toPTE(virtualAddress);

// 页目录项无对应的页表,先分配一个页表
if(!(*pde & 0x00000001))
{
// 从内核物理地址空间中分配一个页表
int page = allocatePhysicalPages(AddressPoolType::KERNEL, 1);
if (!page)
return false;

// 使页目录项指向页表
*pde = page | 0x7;
// 初始化页表
char *pagePtr = (char *)(((int)pte) & 0xfffff000);
memset(pagePtr, 0, PAGE_SIZE);
}

// 使页表项指向物理页
*pte = physicalPageAddress | 0x7;

return true;
}

页内存释放:

在分配页内存时,如果遇到物理页无法分配的情况,之前成功分配的虚拟页和物理页需要释放掉(打回原形),否则会导致内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void MemoryManager::releasePages(enum AddressPoolType type, const int virtualAddress, const int count)
{
int vaddr = virtualAddress;
int *pte, *pde;
bool flag;
const int ENTRY_NUM = PAGE_SIZE / sizeof(int);

for (int i = 0; i < count; ++i, vaddr += PAGE_SIZE)
{
releasePhysicalPages(type, vaddr2paddr(vaddr), 1);

// 设置页表项为不存在,防止释放后被再次使用
pte = (int *)toPTE(vaddr);
*pte = 0;
}

releaseVirtualPages(type, virtualAddress, count);
}

对于每一个虚拟页,释放为其分配的物理页:vaddr2paddr()函数实现了地址的转换。
然后将页表项设置为不存在,防止释放后被再次使用。
最后再释放虚拟页。

1
2
3
4
5
6
7
void MemoryManager::releaseVirtualPages(enum AddressPoolType type, const int vaddr, const int count)
{
if (type == AddressPoolType::KERNEL)
{
kernelVirtual.release(vaddr, count);
}
}

测试例子:

所做的测试内容是:
先为三个线程分配100,10,100页大小的空间,然后再释放线程2,再为线程2分配100页大小的空间,再分配10页的空间。
可以看到输出结果,由于虚拟页的分配是连续的:
(1)第一次分配:
p1 = allocatePages(KERNEL, 100)

  • 分配 100 页,地址跨度为:
1
100 × 4KB = 409600 字节(0x64000)
  • 如果分配起始地址为 0xC0100000,则分配结束地址为:
1
0xC0100000 + 0x64000 = 0xC0164000
  • 结果p1 = 0xC0100000

(2)第二次分配: p2 = allocatePages(KERNEL, 10)`

  • 分配 10 页,地址跨度为:
    10 × 4KB = 40960 字节(0xA000)
  • 计算出地址:0xC0164000 + 0xA000 = 0xC016E000

(3)第三次分配:
p3 = allocatePages(KERNEL, 100)

  • 分配 100 页,地址跨度为:100 × 4KB = 409600 字节(0x64000)
  • 计算出地址:0xC016E000 + 0x64000 = 0xC01D2000

(4)释放 10 页: releasePages(KERNEL, (int)p2, 10)`

  • 释放 p2 指向的 10 页(0xC0164000 到 0xC016E000)。
  • 内存管理器将这段地址标记为可用。

(5) 重新分配:
p2 = allocatePages(KERNEL, 100)

  • 内存管理器重新分配 100 页。
  • 根据分配策略(假设优先复用刚释放的地址),从 0xC0164000 开始分配:
1
0xC0164000 + 0x64000 = 0xC01D2000
  • 结果p2 = 0xC0164000
    (6) 再次分配:
    p2 = allocatePages(KERNEL, 10)
  • 分配 10 页,地址跨度为:
1
10 × 4KB = 40960 字节(0xA000)
  • 从上一次分配结束地址 0xC01D2000 开始分配:
1
0xC01D2000 + 0xA000 = 0xC01DC000
  • 结果p2 = 0xC01D2000

可以看出是很符合的。

(选做)Assignment 4

参照理论课上虚拟内存管理的页面置换算法如FIFO、LRU等,实现页面置换,也可以提出自己的算法。可以在ubuntu实验环境中实现,也可以在win环境使用cpp模拟。

页面置换算法–进程运行时,若其访问的页面不在内存而需将其调入,但内存已无空闲空间时,就需要从内存中调出一页程序或数据,送入磁盘的对换区,其中选择调出页面的算法就称为页面置换算法。

缺页(所需要的页面不在内存中):

  • 先看TLB里是否有目标页面(但是我们这里没有实现TLB–skip)
  • 再看页表中是否有目标页面(如果有就不会缺页)
  • 页表中也没有就发生了缺页(需要去磁盘中调取)

所以如果发现虚拟页没有成功分配物理页,就说明缺页了,需要进行页面置换。

FIFO算法:

  • 需要一个变量来记录找到最先进来的虚拟页 firstPage,当虚拟页不够时,顺着这个list释放内存。

实验知识学习

Example1:内存的探查

获取操作系统重可管理的内存的容量:

  • 实模式下,可以通过int 15h中断来获取机器的内存大小。

  • 功能号为 0xe80115h 中断获取内存较为简单:
    • ax寄存器中存入功能号0xe801即可,无需其他的输入数据
    • 中断返回的结果是内存的大小,结果保存在寄存器中。返回结果分两部分存储,ax寄存器中存放0-15MB的内存大小(单位1KB),bx寄存器中存放的是16MB-4GB的内存大小(单位是64KB)
    • 内存总容量为$内存总容量=(ax⋅1024+bx⋅64⋅1024) bytes$
  • 由于15h只能在实模式下使用,所以我们在保护模式下如果要获取内存大小,则需要先在实模式下获取之后,保存在一个固定位置,最后在保护模式下建立内存管理时,从固定地址读取。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...

load_bootloader:
push ax
push bx
call asm_read_hard_disk ; 读取硬盘
add sp, 4
inc ax
add bx, 512
loop load_bootloader

; 获取内存大小
mov ax, 0xe801
int 15h
mov [0x7c00], ax
mov [0x7c00+2], bx
//在跳转之前就获取了内存大小
jmp 0x0000:0x7e00 ; 跳转到bootloader

...
  • 在first_thread中读取内存大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...

void first_thread(void *arg)
{
...

int memory = *((uint32 *)MEMORY_SIZE_ADDRESS);
// ax寄存器保存的内容
int low = memory & 0xffff;
// bx寄存器保存的内容
int high = (memory >> 16) & 0xffff;
memory = low * 1024 + high * 64 * 1024;
printf("total memory: %d bytes (%d MB)\n", memory, memory / 1024 / 1024);

asm_halt();
}

...

ps:

1
#define MEMORY_SIZE_ADDRESS 0x7c00

运行得到:

位图

  • 用来标识资源状态的位的集合称为位图。
  • BitMap使用一位来和一个资源单元建立映射关系

    对于4GB的内存,在分页机制下,设置一个物理页大小为4KB。那么BitMap的大小为$\frac{4GB}{8*4KB}=128KB$

BitMap的实现

BitMap的成员包括

  • 一块存放BitMap的内存区域。
  • BitMap管理的资源单元数量
  • 单独存取位和批处理存取位的方法。
    定义:
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
class BitMap
{
public:
// 被管理的资源个数,bitmap的总位数
int length;
// bitmap的起始地址
char *bitmap;
public:
// 初始化
BitMap();
// 设置BitMap,bitmap=起始地址,length=总位数(被管理的资源个数)
void initialize(char *bitmap, const int length);
// 获取第index个资源的状态,true=allocated,false=free
bool get(const int index) const;
// 设置第index个资源的状态,true=allocated,false=free
void set(const int index, const bool status);
// 分配count个连续的资源,若没有则返回-1,否则返回分配的第1个资源单元序号
int allocate(const int count);
// 释放第index个资源开始的count个资源
void release(const int index, const int count);
// 返回Bitmap存储区域
char *getBitmap();
// 返回Bitmap的大小
int size() const;
private:
// 禁止Bitmap之间的赋值
BitMap(const BitMap &) {}
void operator=(const BitMap&) {}
};

我们是从外界向BitMap提供存储空间的,因为无法自己管理自己。所以我们会在内存中手动划分出一块区域来存储BitMap用来标识资源分配情况的位。

[!NOTE]
注意,BitMap的成员是有指针的。一般情况下,成员涉及指针的对象的赋值都需要使用动态内存分配获得一个新的指针,但我们还没有实现动态内存分配。所以,我们将copy constructoroperator=定义为private,以禁止BitMap之间的直接赋值。这也是为什么我们在BitMap的初始化函数initialize中需要提供BitMap的存储区域。

当我们使用指针来访问BitMap的存储区域时,最小的访问单位是字节,而资源单元的状态是使用一个位来表示的。
故给定一个资源单元的序号i,无法通过bitmap[i] 的方式来直接修改资源单元的状态,而是定位到这个位的字节序号pos,再确定这个位在字节中的偏移量,使用位运算来修改。
$i=8·pos+offset,0\leq{offset}<8$

地址池

目前只需要实现页内存管理(每次分配的内存大小是一个页,每次释放的内存大小也是一个页)

也就是只需要使用一种结构(即地址池)来标识地址空间中的哪些页是已经被分配的,哪些是未被分配的。

  • 当需要页内存分配时,我们可以从地址池中取出一个空闲页。然后地址池便会标识(可以用BitMap)该空闲页已被分配,最后计算并返回该空闲页对应的地址。空闲页只要没有被释放,就不会被再次分配。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class AddressPool
{
public:
BitMap resources;
int startAddress;
public:
AddressPool();
// 初始化地址池
void initialize(char *bitmap, const int length,const int startAddress);
// 从地址池中分配count个连续页,成功则返回第一个页的地址,失败则返回-1
int allocate(const int count);
// 释放若干页的空间
void release(const int address, const int amount);
};

$address=startAddress+i×PAGE_SIZE$

物理页内存管理

物理内存划分为两部分:内核空间和用户空间

  • 内核需要的物理页只会从内核空间中分配,用户程序需要的物理页也只会从用户空间中分配。
  • 因此我们会使用两个地址池来对这两部分物理地址进行管理
1
2
3
4
5
enum AddressPoolType
{
USER,
KERNEL
};

内存管理:

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
class MemoryManager
{
public:
// 可管理的内存容量
int totalMemory;
// 内核物理地址池
AddressPool kernelPhysical;
// 用户物理地址池
AddressPool userPhysical;

public:
MemoryManager();

// 初始化地址池
void initialize();

// 从type类型的物理地址池中分配count个连续的页
// 成功,返回起始地址;失败,返回0
int allocatePhysicalPages(enum AddressPoolType type, const int count);

// 释放从paddr开始的count个物理页
void releasePhysicalPages(enum AddressPoolType type, const int paddr, const int count);

// 获取内存总容量
int getTotalMemory();

};

初始化地址池:
获取全部内存大小;预留部分内存用于存放内核;计算剩余空间内存=>同时算出空闲页等。

实现物理内存管理:
分配物理页和释放物理页

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
int MemoryManager::allocatePhysicalPages(enum AddressPoolType type, const int count)
{
int start = -1;

if (type == AddressPoolType::KERNEL)
{
start = kernelPhysical.allocate(count);
}
else if (type == AddressPoolType::USER)
{
start = userPhysical.allocate(count);
}

return (start == -1) ? 0 : start;
}

void MemoryManager::releasePhysicalPages(enum AddressPoolType type, const int paddr, const int count)
{
if (type == AddressPoolType::KERNEL)
{
kernelPhysical.release(paddr, count);
}
else if (type == AddressPoolType::USER)
{

userPhysical.release(paddr, count);
}
}

二级分页机制

  • 程序如何装入内存
  • 内存的保护是如何实现的

程序的装入

  • 绝对装入:在链接时,知道程序将存放在内存的具体位置,则连接程序根据实际运行的地址来修改程序的标号的地址。
    • 例如ld中的-Ttext 0x00020000,此时直接将程序加载到预先确定的位置便可运行。但是,当加载位置变化后,链接时的地址也要发生变化,否则必定发生错误。
  • 静态重定位:在装入时,我们根据程序被加载的位置来修改程序的指令和数据地址。区别在于修改地址的时间
  • 动态重定位:被装入内存后的程序起始地址依旧是从0开始。地址的转换被推迟到寻址的时候。使用MMU来进行变换(可以改变MMU的内容来实现不同的变换方式)。
    • 即使对于相同的线性地址,在不同的变换方式下,得到的物理地址就会不同

[!NOTE]
对于绝对装入,程序的地址就是实际的使用的地址,而对于静态重定位,程序的地址依旧是从0开始,只有在被加载到内存时才会被修改。

分页机制

一级页表:在分页机制下,内存被划分为大小相等的内存块,称为页(Page)

  • 未开启分页机制:$物理地址=段地址+偏移地址$
  • 开启分页机制后:$虚拟地址=段地址+偏移地址$ ,虚拟地址再经过MMU转换才能变成物理地址。

一级页表的虚拟地址到物理地址的转换关系如下。

  • 先取虚拟地址的高20位,高20位的数值表示的是页号。而每一个页表项占4字节,所以高20位的数值乘4后才是对应的页表项的地址。
  • 从页表项中读出页地址后,由于低12位是页内偏移,使用物理页地址+低12位即可得出需要访问的物理地址。
二级页表

通过页目录表来访问页表,然后通过页表访问物理页的方式被称为二级分页机制

一个32位的虚拟地址被划分为3部分。

  • 31-22,共10位,是页目录项的序号,可以表示$2^{10}=1024$个页目录项。
  • 21-12,共10位,是页表项的序号,可以表示$2^{10}=1024$个页表项。
  • 11-0,共12位,是页内偏移,可以表示$2^{12}=4KB$的物理页内的偏移地址。

二级页表的虚拟地址到物理地址的转换关系:

  • 给定一个虚拟地址,先看页目录表项,其数值乘4之后得到页目录表现在页目录表的偏移地址。这个偏移地址加上页目录表的物理地址后得到页目录项的物理地址
  • 取页目录项中的内容,得到页表的物理地址。页表的物理地址加上21-12位乘4的结果后,得到页表项的物理地址。
  • 取页表项的内容,即物理页的物理地址,加上11-0位的内容后便得到实际的物理地址。

开启二级页表分页机制

具体请看:
https://gitee.com/kpyang5/sysu-2025-spring-operating-system/blob/main/lab7/README.md#%E5%BC%80%E5%90%AF%E4%BA%8C%E7%BA%A7%E9%A1%B5%E8%A1%A8%E5%88%86%E9%A1%B5%E6%9C%BA%E5%88%B6

启动分页机制的流程:

  • 规划好页目录表和页表在内存中的位置(在内存中特意划分出位置),然后初始化。
  • 将页目录表的地址写入cr3。
  • 将cr0的PG位置1。

第一步,规划好页目录表和页表在内存中的位置并写入内容

  • 页目录表和页表的物理地址必须是4KB的整数倍,也就是低12位为0。
  • 根据线性地址空间的大小来确定需要分配的页表的数量和位置。
  • 页目录项:
  • 31-12位是页表的物理地址位的高20位,这也是为什么规定了页目录表的地址必须是4KB的整数倍。页目录表和页表实际上也是内存中的一个页,而内存被划分成了大小为4KB的页。自然地,这些物理页的起始就是4KB的整数倍。
  • P位是存在位,1表示存在,0表示不存在。
  • RW位,read/write。1表示可读写,0表示可读不可写。
  • US位,user/supervisor。若为1时,表示处于User级,任意级别( 0、 1、 2、3)特权的程序都可以访问该页。若为0,表示处于 Supervisor 级,特权级别为3的程序不允许访问该页,该页只允许特权级别为0、1、2的程序可以访问。
  • PWT位,这里置0。PWT, Page-level Write-Through,意为页级通写位,也称页级写透位。若为 1 表示此项采用通写方式,表示该页不仅是普通内存,还是高速缓存。
  • PCD位,这里置0。PCD, Page-level Cache Disable,意为页级高速缓存禁止位。若为 1 表示该页启用高速缓存,为 0 表示禁止将该页缓存。
  • A位,访问位。1表示被访问过,0表示未被访问,由CPU自动置位。
  • D位,Dirty,意为脏页位。当CPU对一个页面执行写操作时,就会设置对应页表项的D位为1。此项 仅针对页表项有效,并不会修改页目录项中的D位。
  • G位,这里置0,和TLB相关。
  • PAT, 这里置0。Page Attribute Table,意为页属性表位,能够在页面一级的粒度上设置内存属性。

页表项:(结构和页表项完全类似)

第二步,将页目录表的地址写入cr3
cr3寄存器保存的是页目录表的地址。使得CPU的MMU能够找到页目录表的地址,然后自动地将线性地址转换成物理地址。
建立好页目录表和页表之后,要把页目录表地址放到CR3寄存器(页目录基址寄存器PDBR)–可以使用mov赋值

第三步,将cr0的PG位置1
启动分页机制的开关是将控制寄存器 cr0 的 PG 位置 1,PG 位是cr0寄存器的第31位,PG位为1后便进入了内存分页运行机制。

运行结果:

虚拟页内存管理

  • 从虚拟地址池中分配连续的多个虚拟页。注意,虚拟页之间的虚拟地址是连续的。
  • 从物理地址池中为每一个虚拟页分配相应大小的物理页
  • 在页目录表和页表中建立虚拟页和物理页之间的对应关系。此时,由于分页机制的存在,物理页的地址可以不连续。CPU的MMU会在程序执行过程中将虚拟地址翻译成物理地址。

初始化

对于虚拟地址空间中的地址,要建立一个虚拟地址池来管理(虚拟地址池可以有多个)

  • 每个进程有自己的用户虚拟地址池,放在PCB中,并不是全局。
  • 全局的:用户物理地址池,内核物理地址池和内核虚拟地址池
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
void MemoryManager::initialize()
{
this->totalMemory = 0;
this->totalMemory = getTotalMemory();

// 预留的内存
int usedMemory = 256 * PAGE_SIZE + 0x100000;
if (this->totalMemory < usedMemory)
{
printf("memory is too small, halt.\n");
asm_halt();
}
// 剩余的空闲的内存
int freeMemory = this->totalMemory - usedMemory;

int freePages = freeMemory / PAGE_SIZE;
int kernelPages = freePages / 2;
int userPages = freePages - kernelPages;

int kernelPhysicalStartAddress = usedMemory;
int userPhysicalStartAddress = usedMemory + kernelPages * PAGE_SIZE;

int kernelPhysicalBitMapStart = BITMAP_START_ADDRESS;
int userPhysicalBitMapStart = kernelPhysicalBitMapStart + ceil(kernelPages, 8);
int kernelVirtualBitMapStart = userPhysicalBitMapStart + ceil(userPages, 8);

kernelPhysical.initialize(
(char *)kernelPhysicalBitMapStart,
kernelPages,
kernelPhysicalStartAddress);

userPhysical.initialize(
(char *)userPhysicalBitMapStart,
userPages,
userPhysicalStartAddress);

kernelVirtual.initialize(
(char *)kernelVirtualBitMapStart,
kernelPages,
KERNEL_VIRTUAL_START);

printf("total memory: %d bytes ( %d MB )\n",
this->totalMemory,
this->totalMemory / 1024 / 1024);

printf("kernel pool\n"
" start address: 0x%x\n"
" total pages: %d ( %d MB )\n"
" bitmap start address: 0x%x\n",
kernelPhysicalStartAddress,
kernelPages, kernelPages * PAGE_SIZE / 1024 / 1024,
kernelPhysicalBitMapStart);

printf("user pool\n"
" start address: 0x%x\n"
" total pages: %d ( %d MB )\n"
" bit map start address: 0x%x\n",
userPhysicalStartAddress,
userPages, userPages * PAGE_SIZE / 1024 / 1024,
userPhysicalBitMapStart);

printf("kernel virtual pool\n"
" start address: 0x%x\n"
" total pages: %d ( %d MB ) \n"
" bit map start address: 0x%x\n",
KERNEL_VIRTUAL_START,
userPages, kernelPages * PAGE_SIZE / 1024 / 1024,
kernelVirtualBitMapStart);
}

页内存分配

页内存分配步骤:

  • 从虚拟地址池中分配若干连续的虚拟页。
  • 对每一个虚拟页,从物理地址池中分配1页。
  • 为虚拟页建立页目录项和页表项,使虚拟页内的地址经过分页机制变换到物理页内。

分配虚拟页:

1
2
3
4
5
6
7
8
9
10
11
int allocateVirtualPages(enum AddressPoolType type, const int count)
{
int start = -1;

if (type == AddressPoolType::KERNEL)
{
start = kernelVrirtual.allocate(count);
}

return (start == -1) ? 0 : start;
}
  • 由于没有实现用户进程,此时能够分配页内存的地址池只有内核虚拟地址池,

为每个虚拟页分配一个物理页:使用 allocatePhysicalPages()来实现

为虚拟页建立页目录项和页表项,使虚拟页内的地址经过分页机制能变换到物理页内:

  • 建立虚拟页到物理页的映射关系通过函数connectPhysicalVirtualPage来实现

[!NOTE]
只有程序才会使用虚拟地址,cr3寄存器,页目录项,页表项和CPU寻址中的地址都是物理地址。

页内存释放

如果遇到物理页无法分配的情况,之前成功分配的虚拟页和物理页都要释放。否则就会造成内存泄漏,这部分内存无法再被分配

页内存的释放是页内存分配的过程,分两个步骤完成。

  • 对每一个虚拟页,释放为其分配的物理页。
  • 释放虚拟页。

OS_lab7
https://pqcu77.github.io/2025/05/11/OS-lab7/
作者
linqt
发布于
2025年5月11日
许可协议