操作系统 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(内存管理单元)自动访问相同的物理内存页。

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

相关推荐
挣扎的蓝藻4 小时前
使用 Python 扫描 Windows 下的 Wi-Fi 网络实例演示
网络·windows·python
cdg==吃蛋糕4 小时前
windows sc 创建删除服务
windows
再玩一会儿看代码5 小时前
[特殊字符] 深入理解 WSL2:在 Windows 上运行 Linux 的极致方案
linux·运维·windows·经验分享·笔记·学习方法
cyz1410016 小时前
树莓派4B配置wifi热点,可访问http协议
linux·网络·windows·后端·网络协议·http·树莓派
PfCoder6 小时前
WinForm真入门(13)——ListBox控件详解
windows·c#·visual studio·winform
神州永泰7 小时前
Java logback框架日志输出中文乱码的解决方案(windows)
java·windows·logback
Java&Develop12 小时前
redis 免安装版本 启动方法 windows 安装包
数据库·windows·redis
搏博14 小时前
在WPS中通过JavaScript宏(JSA)调用DeepSeek官网API优化文档教程
javascript·人工智能·windows·深度学习·机器学习·wps
hvinsion18 小时前
【Python 开源】你的 Windows 关机助手——PyQt5 版定时关机工具
windows·python·开源·定时关机