操作系统 3.4-段页结合的实际内存管理

段与页结合的初步思路

  1. 虚拟内存的引入

    • 为了结合段和页的优势,操作系统引入了虚拟内存的概念。虚拟内存是一段地址空间,它映射到物理内存上,但对用户程序是透明的。
  2. 段到虚拟内存的映射

    • 用户程序中的段首先映射到虚拟内存的相应区域。这一步骤通过段表(Segment Table)实现,段表记录了每个段的起始地址和长度。
  3. 虚拟内存到物理内存的映射

    • 虚拟内存中的区域再映射到物理内存的页上。这一步骤通过页表(Page Table)实现,页表记录了虚拟页到物理页框的映射关系。
  4. 地址翻译

    • 用户程序发出的地址首先被翻译成虚拟地址,然后再翻译成物理地址。这一过程通常由硬件中的内存管理单元(MMU)自动完成。
  5. 内存访问

    • 一旦地址被翻译成物理地址,CPU就可以访问物理内存中的数据,执行指令或进行数据操作。

用户从应用程序视角看待虚拟内存中的段和页

从用户程序的视角来看,虚拟内存中的段和页提供了一种抽象的内存模型,使得程序能够以一种简化和统一的方式来访问内存。以下是用户程序如何查看和与虚拟内存中的段和页交互的方式:

  1. 逻辑地址空间

    • 用户程序操作的是逻辑地址空间,这是一个由操作系统管理的虚拟地址空间。在这个空间中,程序可以使用段和偏移量(如cs:ip)来访问内存,而不需要关心物理内存的实际布局。
  2. 地址翻译

    • 用户程序发出的地址(如0x00345008)首先被翻译成虚拟地址,然后再由操作系统和硬件(如内存管理单元MMU)翻译成物理地址(如0x7008)。

    • 这个过程对用户程序是透明的,用户程序不需要知道具体的物理地址。

  3. 内存保护和隔离

    • 段和页的结合允许操作系统实现内存保护和隔离。每个进程都有自己的虚拟地址空间,操作系统确保一个进程不能访问另一个进程的内存空间。
  4. 虚拟内存的优势

    • 虚拟内存提供了内存的抽象,使得程序可以假设它拥有一个连续的、大的内存空间,而实际上物理内存可能是分散的、有限的。

    • 这种抽象简化了程序设计,提高了内存使用的灵活性,并允许操作系统更有效地管理内存资源。

重定位

逻辑地址到物理地址的转换过程

  1. 逻辑地址 :由段号和偏移量组成,例如 段号+偏移(cs:ip)

  2. 段表:包含段号、基址、长度和保护信息。

    • 段号:标识段的编号。

    • 基址:段在内存中的起始地址。

    • 长度:段的大小。

    • 保护:段的访问权限(如只读R、读写R/W)。

  3. 页号和偏移:逻辑地址中的偏移量被进一步分解为页号和页内偏移。

  4. 页表:包含页框号和保护信息。

    • 页框号:页在内存中的物理位置。

    • 保护:页的访问权限(如只读R、读写R/W)。

  5. 物理地址:由物理页号和偏移量组成。

地址翻译过程

  1. 逻辑地址:由段号和偏移量组成。

  2. 段表查找:根据段号查找段表,获取基址和长度。

  3. 偏移分解:将偏移量分解为页号和页内偏移。

  4. 页表查找:根据页号查找页表,获取页框号。

  5. 物理地址计算:将页框号和页内偏移组合成物理地址。

段页同时存在的场景下,重定位过程是怎样的

在断页同时存在的场景下,用户发出的逻辑地址是CS加上段号和偏移量。

操作系统首先通过段表找到段在虚拟内存中的位置,并生成一个虚拟地址。

然而,这个虚拟地址并不是直接对应的物理内存地址,而是需要经过一次映射,根据虚拟地址计算出页号,再结合页内偏移得到物理地址。

最后,操作系统将这个物理地址发送到地址总线上,从而实现从段和页两个层次上的地址翻译,确保代码能够正确执行和获取数据。

一个实际的段页结合的例子

内存分配

内存管理的核心

  • 内存管理核心就是内存分配:强调了内存分配是内存管理的关键部分。

具体示例

  • 指令_sum: .int 0 定义了一个名为 _sum 的变量,初始值为0。

  • 指令_main: mov [300], 0 将0移动到偏移量为300的地址。

  • 地址计算base + 300(offset) 表示将基地址和偏移量相加得到物理地址。

内存管理过程

  • 分配段、建段表:为进程分配内存段,并建立段表来管理这些段。

  • 分配页、建页表:为进程分配内存页,并建立页表来管理这些页。

  • 进程带动内存使用的图谱:展示了进程如何使用内存的图谱。

  • 从进程fork中的内存分配开始:说明了进程创建(fork)时的内存分配过程。

载入内存

程序加载过程

  1. 分配段:为程序的不同部分(如代码、数据、栈)分配内存段。

  2. 建立段表:记录每个段的基址、长度和保护属性。

  3. 分配页:将每个段进一步分为页,并为每页分配物理内存。

  4. 建立页表:记录每个页的页框号和保护属性。

  5. 地址转换:将程序中的逻辑地址转换为物理地址,以便访问实际的内存。

示例

  • 逻辑地址 0x300:表示用户数据段中的一个地址。

  • 指令 _main: mov [300], 0:将0移动到偏移量为300的地址。

  • 段表和页表:展示了如何通过段号和页号查找对应的基址和页框号,从而完成地址转换。

分配虚拟内存,建立段表

从幻灯片中提取的代码如下:

cpp 复制代码
int copy_process(int nr, long ebp, ...)
{
    ...
    copy_mem(nr, p); ...
}
​
int copy_mem(int nr, task_struct *p)
{
    unsigned long new_data_base;
    new_data_base = nr * 0x40000000;  // 64M * nr
    set_base(p->ldt[1], new_data_base);
    set_base(p->ldt[2], new_data_base);
}
  1. 计算新的数据基址

    • copy_mem函数中,首先计算新的数据基址**new_data_base** 。这个基址是通过将进程编号nr乘以0x40000000(即64MB)来得到的。这意味着每个进程将获得64MB的虚拟内存空间。
  2. 设置段表

    • 使用**set_base** 函数将计算出的新数据基址设置到进程的局部描述符表(LDT)中的相应段。在这个例子中,p->ldt[1]p->ldt[2]分别代表了两个不同的段(例如,代码段和数据段)。

    • set_base函数的作用是更新段描述符中的基址字段,使其指向新的虚拟内存区域。

  3. 进程控制块(PCB)

    • **task_struct *p是一个指向进程控制块(PCB)的指针,**它包含了进程的所有信息,包括段表。

    • 在进程创建时,操作系统会为新进程分配一个新的PCB,并复制父进程的相关信息,同时为新进程分配独立的虚拟内存空间。

  4. 内存分配

    • 在Linux中,每个进程都有独立的虚拟内存空间。通过fork()系统调用创建的新进程会继承父进程的虚拟内存布局,但是操作系统会确保新进程的虚拟内存空间是独立的,以防止进程间的相互干扰。
  5. 进程切换

    • 在进程切换时,操作系统需要更新CPU的段寄存器,以指向新进程的段表。这样,当新进程开始执行时,它将在自己的虚拟内存空间中运行。

分配内存,建立页表

这张幻灯片展示了在Linux操作系统中,如何为新创建的进程分配内存并建立页表。以下是提取的代码和对分配内存、建立页表过程的总结:

提取的代码

cpp 复制代码
int copy_mem(int nr, task_struct *p)
{
    unsigned long old_data_base;
    old_data_base = get_base(current->ldt[2]);
    copy_page_tables(old_data_base, new_data_base, data_limit);
}
​
int copy_page_tables(unsigned long from, unsigned long to, long size)
{
    from_dir = (unsigned long *)(((from >> 20) & 0xffc));
    to_dir = (unsigned long *)(((to >> 20) & 0xffc));
    size = (unsigned long)((size + 0x3fffff) >> 22);
    for (; size-- > 0; from_dir++, to_dir++) {
        from_page_table = (0xfffff000 & *from_dir);
        to_page_table = get_free_page();
        // 这里应该还有代码来复制页表项和设置新的页表
    }
}

分配内存、建立页表的总结

  1. 获取旧数据基址

    • copy_mem函数中,首先获取当前进程的数据段基址(old_data_base,这通常是通过读取当前进程的段表(LDT)来实现的。
  2. 调用复制页表函数

    • 然后调用copy_page_tables函数,传入旧数据基址、新数据基址(new_data_base)和数据段的大小(data_limit),以复制页表
  3. 计算目录基址

    • copy_page_tables函数中,计算源目录(from_dir)和目标目录(to_dir)的基址。这是通过将虚拟地址右移20位(即页目录的索引)并取低12位(页目录项的偏移)来实现的。
  4. 计算页表项数量

    • 计算需要复制的页表项数量(size),这是通过将数据段大小加上0x3FFFFF(即1MB-1,因为页表项是按页大小为单位的),然后右移22位(即页大小为4KB)来实现的。
  5. 复制页表项

    • 在循环中,对于每个页表项**,从源目录读取页表项(from_page_table),然后从页框(page frame)中获取一个空闲页(to_page_table = get_free_page())来存储目标页表项。**

    • 这里应该还有代码来复制页表项的内容,并设置新的页表项,包括页框号、保护位等。

  6. 建立页表

    • 通过上述步骤,为新进程建立了页表,将虚拟地址映射到物理内存。
  7. 更新进程控制块(PCB)

    • 最后,需要更新新进程的PCB,包括新的页表基址等信息,以便新进程可以正确地访问其虚拟内存。

from_dir,to_dir

cpp 复制代码
from_dir = (unsigned long *)(((from >> 20) & 0xffc));
to_dir = (unsigned long *)(((to >> 20) & 0xffc));
size = (unsigned long)((size + 0x3fffff) >> 22);
​
for (; size-- > 0; from_dir++, to_dir++) {
    from_page_table = (0xfffff000 & *from_dir);
    // 这里应该还有代码来复制页表项和设置新的页表
}
  1. 计算页目录地址

    • from_dirto_dir 是指向页目录的指针。它们是通过将虚拟地址右移20位(即页目录的索引)并取低12位(页目录项的偏移)来计算得到的。这是因为在x86架构中,页目录项是按4KB对齐的,所以需要取低12位来得到页目录项的偏移。
  2. 计算页表项数量

    • size 是需要复制的页表项数量。这是通过将数据段大小加上0x3FFFFF(即1MB-1,因为页表项是按页大小为单位的),然后右移22位(即页大小为4KB)来计算得到的。
  3. 复制页表项

    • 在循环中,对于每个页表项,从源目录读取页表项(from_page_table),然后从页框(page frame)中获取一个空闲页(get_free_page())来存储目标页表项。

    • 这里应该还有代码来复制页表项的内容,并设置新的页表项,包括页框号、保护位等。

  4. 页目录指针

    • 页目录指针(CR3)用于指向当前进程的页目录。在进程切换时,需要更新CR3寄存器以指向新进程的页目录。

from_page_table,to_page_table

从幻灯片中提取的代码和相关信息如下:

提取的代码:

cpp 复制代码
for (; size-- > 0; from_dir++, to_dir++) {
    to_page_table = get_free_page();
    *to_dir = ((unsigned long)to_page_table) | 7;
}
​
unsigned long get_free_page(void) {
    register unsigned long _res asm("ax");
    __asm__("std; repne; scasb\n\t"
            "movl %%edx, %%eax\n\t"
            "D"(mem_map+PAGING_END-1));
    return _res;
}

总结:

  1. 页表复制过程

    • 在循环中,对于每个页表项,首先通过调用get_free_page()函数为新进程分配一个空闲的页框(物理内存页)。

    • 然后,将新分配的页框地址设置到目标页目录项中(to_dir),并设置页表项的权限(这里通过或操作| 7来设置)。

  2. 页表项权限设置

    • 在x86架构中,页表项通常包含页框号和一些权限位。这里的| 7操作可能是设置页表项的权限位,例如可读写(RW)和存在位(P)。
  3. 页表项结构

    • 幻灯片中展示了页表项的结构,包括页目录号(10 bits)、页号(10 bits)和偏移(12 bits)。这是x86架构中分页机制的基本概念。
  4. 页目录和页表

    • from_dirto_dir 分别指向源进程和目标进程的页目录项。通过复制页目录项,可以实现页表的复制和更新。
  5. 获取空闲页框

    • get_free_page()函数通过汇编语言实现,用于扫描内存映射(mem_map)来找到一个空闲的页框。这个过程涉及到检查内存映射中的每个条目,直到找到一个空闲的页框。
  6. 内存映射

    • mem_map是一个内存映射数组,用于跟踪哪些页框是空闲的,哪些已经被使用。PAGING_END可能是定义了内存映射数组的结束位置。

复制和更新页表

这张幻灯片展示了在操作系统中,如何复制和更新页表项以实现内存管理。以下是提取的代码和对过程的总结:

提取的代码:

cpp 复制代码
for (; nr-- > 0; from_page_table++, to_page_table++) {
    this_page = *from_page_table;
    this_page &= ~2; // 只读
    *to_page_table = this_page;
    *from_page_table = this_page;
    this_page -= LOW_MEM;
    this_page >>= 12;
    mem_map[this_page]++;
}

总结:

  1. 页表项复制

    • 代码中的循环遍历所有的页表项,从源页表(from_page_table)复制到目标页表(to_page_table)。
  2. 权限设置

    • this_page &= ~2; 这行代码通过位操作清除页表项中的某个权限位(通常是只读位),确保复制的页表项具有适当的权限设置。
  3. 页表项更新

    • *to_page_table = this_page; 将修改后的页表项写入目标页表。

    • *from_page_table = this_page; 也将修改后的页表项写回源页表,这可能是为了确保源进程的页表项也反映了权限的更改。

  4. 页框号计算

    • this_page -= LOW_MEM; 这行代码从页框号中减去一个基址(LOW_MEM),可能是为了将页框号转换为相对于某个特定内存区域的偏移量。
  5. 页框号转换

    • this_page >>= 12; 这行代码将页框号右移12位,这通常是为了将页框号转换为页框数组中的索引。
  6. 内存映射更新

    • mem_map[this_page]++; 这行代码更新内存映射数组,增加对应页框的使用计数。这是为了跟踪每个页框的使用情况,特别是在使用写时复制(copy-on-write)技术时。

使用内存

写时复制(COW)技术

写时复制是一种优化技术,用于减少复制内存的开销,特别是在创建新进程时。在写时复制中,父子进程最初共享相同的物理内存页,只有当进程实际修改内存页时,才会创建该内存页的副本。

幻灯片内容分析

  1. 内存共享与写时复制

    • 只要段表和页表设置正确,父子进程就可以通过MMU(内存管理单元)自动访问相同的物理内存页。

    • 当父子进程中的任何一个尝试修改共享的内存页时,操作系统会触发写时复制机制,为修改内存页的进程创建一个新的物理内存页副本

相关推荐
一禅(OneZen)1 小时前
「Windows/Mac OS」AIGC图片生成视频 ,webui + stable-diffusion环境部署教程
windows·stable diffusion
AirDroid_cn1 小时前
OPPO手机怎样被其他手机远程控制?两台OPPO手机如何相互远程控制?
android·windows·ios·智能手机·iphone·远程工作·远程控制
小龙在山东3 小时前
Python 包管理工具 uv
windows·python·uv
昏睡红猹4 小时前
我在厂里搞wine的日子
windows·wine
love530love7 小时前
Docker 稳定运行与存储优化全攻略(含可视化指南)
运维·人工智能·windows·docker·容器
1024小神12 小时前
tauri项目在windows上的c盘没有权限写入文件
c语言·开发语言·windows
程序视点20 小时前
Window 10文件拷贝总是卡很久?快来试试这款小工具,榨干硬盘速度!
windows
wuk99820 小时前
基于MATLAB编制的锂离子电池伪二维模型
linux·windows·github
lzb_kkk21 小时前
【C++】C++四种类型转换操作符详解
开发语言·c++·windows·1024程序员节
Paper_Love1 天前
x86-64_windows交叉编译arm_linux程序
arm开发·windows