实现内存检测,理解Linux内存管理,实现101012分页
参考:
检测内存容量
趣谈 Linux 操作系统
内存管理
《操作系统真相还原》
1.内存检测
BIOS 中断 0x15 的子功能够获取0xE820 能够获取系统的内存布局,由于系统内存各部分的类型属性不同,
BIOS 就按照类型属性来划分这片系统内存,所以这种查询呈迭代式,每次 IOS 只返回一种类型的内存
信息,直到将所有内存类型返回完毕。子功能 OxE820 的强大之处是返回的内存信息较丰富,包括多个属
性字段,所以需要一种格式结构来组织这些数据。内存信息的内容是用地址范围描述符来描述的,用于存
储这种描述符的结构称之为地址范围描述符( Address Range Descriptor Structure, ARDS )
typedef struct {
unsigned int base_addr_low;
unsigned int base_addr_high;
unsigned int length_low;
unsigned int length_high;
unsigned int type;
}ARDS;
其中的 Type 宇段用来描述这段内存的类型,这里所谓的类型是说明这段内存的用途,即其是可以被操作系统使用,还是保留起来不能用。
BIOS 中断 0x15 的子功能 0xE820 是一个用于获取系统物理内存布局的功能。它可以检测系统中可用或不可用的内存地址范围以及其属性,包括内存大小、类型(例如 RAM、ROM、ACPI 等)、保留位等。该子功能通常用于可引导操作系统、虚拟机管理软件和其他需要在系统启动时了解内存布局信息的应用程序。
该子功能由以下输入参数和输出参数组成:
- 输入参数:
- eax:功能号,应设置为 0xE820(十进制数为 0x0000E820)
- ebx:签名标识符(signature),应设置为字符串"SMAP"
- ecx:缓冲区大小,表明可以返回的可用内存段结构体所占的最小字节数
- edx:传入此次调用之前使用该功能获得的下一个可用内存段信息的标识符,初始值为 0
- 输出参数:
- eax:操作完成后返回给系统的值,代表可用内存段信息的数量
- edx:传回一个下一可用段信息的标识符,该值在下一次调用时作为输入参数 edx 的值。如果没有其他可用段,则返回值为 0
- 可用内存段信息结构体:代表物理内存段的信息,共24个字节,结构如下: BaseAddr:8字节,代表内存段的基地址
- Len:8字节,代表内存段的长度(单位:字节)、
- Type:4字节,代表内存段类型(1 为可用,其余值不可用)
- Reserved:保留字段,大小为4字节
使用该子功能的步骤通常为:
- 1.使用 INT 0x15 中断的子功能 0x88 获取 BIOS 版本信息,检查是否支持子功能 0xE820。
- 2.如果支持,则通过循环调用子功能 0xE820 来获取所有可用的内存段信息。
- 3.当返回参数 eax 的值为 0 或者已经获取到了所有可用的内存段时,停止调用。
memory_check:
xor ebx, ebx ; 第一次调用前 需要初始化为0
mov di, BIOS_ARDS_CACHE ; es:di 将BIOS获取到的内存信息写到这里
.smap_check:
mov eax, 0xe820
mov ecx, 20
mov edx, 0x534d4150 ; smap
int 0x15
jc check_memory_error ; cf = 1
add di, cx ; 移动写入的字节数
inc dword[MEM_CEHCK_TIMES] ; 检测次数 + 1
cmp ebx, 0 ; cf = 0, ebx会被bios修改,ebx不为0就要继续检测
jne .smap_check
mov ax, [MEM_CEHCK_TIMES]
mov [BIOS_ARDS_TIMES], ax
.check_memory_success:
mov si, check_memory_success_msg
call print
2.内存管理
内存管理模块
内存都被分成一块一块儿的,都编好了号。当程序要访问虚拟地址的时候,由内核的数据结构进行转换,转换成不同的物理地址,这样不同的进程运行的时候,写入的是不同的物理地址,这样就不会冲突了。
内存管理系统精细化为下面三件事情:
- 第一,虚拟内存空间的管理,将虚拟内存分成大小相等的页;
- 第二,物理内存的管理,将物理内存分成大小相等的页;
- 第三,内存映射,将虚拟内存也和物理内存也映射起来,并且在内存紧张的时候可以换出到硬盘中。
分段
02.加载GDT表,进入保护模式中讨论过了分段机制。
存在不足:
- 内存碎片的问题。
- 每个段的长度不固定,所以多个段未必能恰好使用所有的内存空间,会产生了多个不连续的小物理内存,导致新的程序无法被装载,所以会出现外部内存碎片的问题。
- 内存交换的效率低的问题。用分段的方式,外部内存碎片是很容易产生的。
- 因为硬盘的访问速度要比内存慢太多,每一次内存交换,我们都需要把一大段连续的内存数据写到硬盘上。
- 如果内存交换的时候,交换的是一个占内存空间很大的程序,这样整个机器都会显得卡顿
分页
从虚拟地址到物理地址的转换方式,称为分页(Paging)。 对于物理内存,操作系统把它分成一块一块大小相同的页,这样更方便管理,例如有的内存页面长时间不用了,可以暂时写到硬盘上,称为换出。一旦需要的时候,再加载进来,叫作换入。这样可以扩大可用物理内存的大小,提高物理内存的利用率。 这个换入和换出都是以页为单位的。页面的大小一般为 4KB。为了能够定位和访问每个页,需要有个页表,保存每个页的起始地址,再加上在页内的偏移量,组成线性地址,就能对于内存中的每个位置进行访问了。
动分页机制后,应用程序就看不到机器的物理内存了,它看到的是一块虚拟的,不存在的内存空间。这块虚拟的内存空间也是从0开始的,到2^32-1。
这块虚拟内存空间也是划分成了一个个4KB的内存页。操作系统会通过页表将这些在虚拟内存空间中连续的内存页,从物理内存空间中找空闲的物理页,建立一个映射关系。这些物理页之间可以是不连续的。
如此以来,就可以将多个不连续的物理页,变成了进程使用的连续内存页。这样进程就感觉自己在使用连续的内存页,而实际最终在物理内存的表现上却是实现不连续的内存页
虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址。这个基地址与页内偏移的组合就形成了物理内存地址。虚拟内存中的页通过页表映射为了物理内存中的页。
10 10 12多级分页
寻址方式
在操作系统中,PDT、PTT、PDE和PTE都是页表相关的数据结构,用于实现虚拟内存管理。它们的具体含义如下:
- PDT(Page Directory Table,页目录表):是一个 1024 项的表格,每项占用 4 字节。PDT 中每一项记录了一个指向 PTE 的物理地址的指针,用于实现虚拟地址的转换。
- PTT(Page Table ,页树):是一个 1024 项的表格,每项占用 4 字节。PTT 中每一项记录了一个指向物理内存中的一页的基地址的指针,用于实现虚拟地址到物理地址的映射。
- PDE(Page Directory Entry,页目录项):是 PDT 中的每个表项。PDE 共有 32 位,其中高 20 位表示 PTT 的物理地址,低 12 位用于页表的属性设置。
- PTE(Page Table Entry,页表项):是 PTT 中的每个表项。PTE 共有 32 位,其中高 20 位表示对应物理页的基地址,低 12 位用于页的属性设置。
在进行地址转换时,操作系统会首先根据虚拟地址的高 10 位找到对应的 PDT 表项,然后根据中间 10 位找到对应的 PTT 表项,最后根据低 12 位找到物理页的基地址。通过这种方式将虚拟地址映射到了物理地址,从而实现了内存管理的功能。
CR3中含有页目录表物理内存基地址,因此该寄存器也被称为页目录基地址寄存器PDBR(Page-Directory Base address Register)
10-10-12 多级分页机制是一种用于管理虚拟地址和物理地址映射关系的数据结构,它将虚拟地址空间划分为多个层次,每一层都使用单独的页表进行维护。具体来说,它将一个 32 位的虚拟地址分为三个部分:
前 10 位为页目录项索引,中间 10 位为页表项索引,后 12 位为页内偏移量。
PDT、PTE的属性
PDT 和 PTE 是分页机制中的两个重要数据结构。它们分别代表着页目录表和页表中的一个条目,用于进行虚拟地址到物理地址的映射。在实现分页机制时,PDT 和 PTE 中会设置一些属性位,用于控制页面的访问权限、页面的映射关系、页面的使用状态等。下面是 PDT 和 PTE 中常见的一些属性及其含义:
属性 | 位数 | 含义 |
---|---|---|
Present/有效位 | 1 | 当PDE或PTE中有一个的属性P=0时,物理页就是无效的。 |
W/R 位 | 0 表示只读。1表示可读可写 | |
User/Supervisor | 1 | 用户/特权位,用于控制该页或页表是否仅对特权级(管理员)访问。0 表示只有特权用户才能访问(0到2环),1表示普通用户也可以访问(3环) |
Accessed/访问位 | 1 | 表示该页或页表是否被访问过,如果为 1,则表示该页或页表已经被访问过,0 表示该页未被访问 |
Dirty/脏位 | 1 | 表示该页是否被修改过,0表示该页未写过,1表示该页被写过 |
PS(Page Size)位 | 1 | 标志为 0,则表示页面大小为 4KB,如果 PS 为 1,则表示页面大小为 4MB |
Global/全局位 | 1 | 全局位用于控制页面的全局性,如果该位为 1,则表示该页面是全局可见的,可以被进程之间共享 |
PDE 属性表格:
31 - 12 | 11 - 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|
页表基址 | 有效 | G | PS | 0 | A | PCD | PWT | U / S | R / W | P |
PTE 属性表格:
31 - 12 | 11 - 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|
页表基址 | 有效 | G | PAT | D | A | PCD | PWT | U / S | R / W | P |
页表基址是地址的高20位,最终加上偏移量12位,20+12 = 32位,完成32位寻址。
开启分页
当操作系统启动时,分页机制并没有被启用,因此所有的内存地址都是物理地址。为了启用分页机制,下面是一般的详细步骤:
- 创建页目录表和页表:操作系统需要为页表分配一些连续的物理内存空间作为存放页目录表和页表的位置。然后初始化这些表格,将虚拟地址与物理地址进行映射,设置相应的权限信息等。
- 举例来说,如果某个进程使用了虚拟地址0x1234,对应的物理地址是0x5678,那么需要在页表中将虚拟地址0x1234指向物理地址0x5678,同时设置该页面的读写和执行权限。这些步骤都需要在操作系统的内核代码中进行实现。
- 将虚拟内存0-4MB与物理内存0-4MB(内核)进行恒等映射。程序就可以继续在这些地址内找到正确的代码和数据去运行。
// 分配一个物理页框作为PDT表
uint * PDT = (uint*)PDT_START_ADDR;
// 清零
memset(PDT, 0, 4 * 1024);
// 外层循环遍历PDE表
for (int i = 0; i < 16; i++) { // 4M * 16 = 64M
// 分配一页内存作为PTT
int PTT = (int)PDT_START_ADDR + ( (i + 1) * 0x1000); // addr + 4k
// 将PDT的地址填充到PDE表项中,同时设置相关标志位
pt_entry PDE = PTT | PDT_PRESENT | PDT_READWRITE | PDT_USER; // U/S = 1, R/W = 1, P = 1
PDT[i] = PDE;
pt_entry* ptt_arr = (pt_entry*)PTT;
if (i == 0) {
// 第一块映射区,给内核用
// 0~1024 *4k =4m
for (int j = 0; j < 1024; ++j) {
int* item = &ptt_arr[j];
int virtual_addr = j * 0x1000;
*item = virtual_addr | PDT_PRESENT | PDT_READWRITE | PDT_USER;
}
}
}
- 设置页表寄存器(CR3):将硬件中的 CR3 寄存器设置为页目录表的起始地址,这样 CPU 便可以访问到页表。
inline void set_cr3(uint v) {
asm volatile("mov cr3, eax;" ::"a"(v));
}
- 开启分页机制:将控制寄存器 CR0 中的分页标志位(PG)置为1,开启分页机制。
- PE(Protection Enable):启用保护模式
- PG(Paging):启用分页机制
- CD(Cache Disable):禁用CPU 缓存
- NW(Not Write-through):将写入设置为非写通方式
- AM(Alignment Mask):对齐掩码。当开启此标志位时,CPU会对内存访问地址进行对齐
- NE(Numeric Error):启用 x87 FPU 浮点错误处理
- ET(Extension Type):CPU 类型扩展标志位
inline void enable_page() {
asm volatile("mov eax, cr0;"
"or eax, 0x80000000;"
"mov cr0, eax;");
}
- 配置其他相关寄存器:按需配置其他的与分页机制相关的寄存器,比如设置控制寄存器 CR4 的“页全局位”(PGE)标志位等。
当CPU要访问一个虚拟地址时,首先会查找该地址所在的页表,然后根据页表中的物理地址进行内存访问。如果该虚拟地址还没有被映射到物理地址上,访问就会失败,导致进程出现错误或崩溃。
除此之外,操作系统还需要定期地更新页表,根据程序的运行情况来调整页表的映射关系,以优化内存使用效率和系统性能。