操作系统 3.5-内存换入-请求调页

发布于:2025-04-12 ⋅ 阅读:(45) ⋅ 点赞:(0)

案例分析内存换入

内存换入分析:

内存换入(Swapping)是指操作系统将不常使用的内存页从物理内存(RAM)移动到磁盘上的交换空间(Swap Space),以释放物理内存供其他进程使用。当需要访问这些被换出的内存页时,操作系统会将它们重新加载到物理内存中,这个过程称为换入(Swapping In)。

在图中所示的内存管理模型中,内存换入的过程可以如下分析:

  1. 识别不活跃的内存页

    • 操作系统通过监控内存使用情况,识别哪些内存页不常被访问或可以暂时移除。

  2. 选择换出目标

    • 根据一定的策略(如最近最少使用LRU算法),选择需要换出的内存页。

  3. 写入磁盘

    • 将选定的内存页写入到磁盘上的交换空间。

  4. 更新页表

    • 在页表中标记这些页为不在物理内存中(例如,设置页表项的状态位)。

  5. 释放物理内存

    • 释放这些内存页占用的物理内存,供其他进程使用。

  6. 换入请求

    • 当进程需要访问被换出的内存页时,触发缺页中断。

  7. 从磁盘读取

    • 操作系统从磁盘上的交换空间读取所需的内存页。

  8. 更新页表

    • 在页表中更新这些页的状态,标记为在物理内存中。

  9. 分配物理内存

    • 为这些内存页分配新的物理内存空间。

  10. 继续执行

    • 进程可以继续访问这些内存页,操作系统确保内存访问的透明性。

Swap In的实现原理

在操作系统中,用户眼里的内存是一个连续的、大小为4GB的虚拟内存空间,这个空间被分为不同的段,如用户栈段、用户代码段、用户数据段和操作系统段。

用户可以随意使用这个虚拟内存空间,而不需要关心它如何映射到物理内存。

这种内存管理方式提供了内存使用的灵活性和抽象性,但同时也需要操作系统来管理内存的映射和物理内存的实际使用。

虚拟内存的实现原理---换入与映射机制

这张图解释了操作系统如何通过换入(Swap In)和换出(Swap Out)机制以及映射(Mapping)来实现“大内存”的幻象。即使物理内存有限,操作系统也能让程序感觉它们拥有比实际物理内存更大的内存空间。以下是图中信息的详细解释:

虚拟内存与物理内存:

  • 虚拟内存:用户程序看到的是一个大的、连续的内存空间,图中显示为4GB。

  • 物理内存:实际的硬件内存,图中显示为1GB。

换入换出实现大内存:

  1. 映射机制

    • 当程序访问虚拟内存的某个部分时(例如,访问地址p=0G-1G),操作系统将这部分虚拟内存映射到物理内存。

    • 这个过程是动态的,只有在程序请求访问某个内存区域时才进行映射。

  2. 分部分映射

    • 由于物理内存有限(图中为1GB),操作系统只能将虚拟内存的一部分映射到物理内存。

    • 当程序访问虚拟内存的另一部分(例如,p=3G-4G)时操作系统会将之前映射的部分换出,并将新的部分映射到物理内存。

  3. 按需映射

    • 操作系统只在程序请求访问某个内存区域时才进行映射,这样可以有效地利用有限的物理内存。

虚拟内存的实现原理:

  • 换入换出:当物理内存不足时,操作系统会将一些不常使用的内存页换出到磁盘上的交换空间(Swap Space),并在需要时将它们换入到物理内存。

换入与映射机制的分析:

  • 换入:当程序访问一个不在物理内存中的内存页时,操作系统会从交换空间将该页加载到物理内存。

  • 映射:操作系统更新页表或段表,将虚拟地址映射到新的物理地址。

  • 换出:当物理内存需要为新的内存页腾出空间时,操作系统会选择一些不常使用的内存页换出到交换空间。

实现请求调页的理论原理

请求调页的实现步骤

  1. 地址生成

    • 程序运行时,根据段号和偏移(例如,段寄存器cs和指令指针ip)生成逻辑地址。

  2. 地址转换

    • 逻辑地址通过段表和页表转换为虚拟地址。

  3. 页表查找

    • 操作系统查找页表,确定虚拟地址对应的页是否已在物理内存中。

  4. 缺页中断

    • 如果页不在物理内存中,触发缺页中断。此时,CPU停止当前程序的执行,并跳转到缺页中断处理程序。

  5. 页错误处理

    • 缺页中断处理程序运行,首先保存当前程序的状态(例如,将寄存器值压栈)。

  6. 读取磁盘

    • 从磁盘上的交换空间(Swap Space)中读取所需的页到物理内存。

  7. 页表更新

    • 更新页表,将虚拟页映射到新的物理页。

  8. 恢复执行

    • 恢复程序状态,继续执行导致缺页中断的指令。

图示解释

  1. 逻辑地址生成

    • 用户程序通过段号和偏移生成逻辑地址。

  2. 页表查找

    • 逻辑地址转换为虚拟地址后,操作系统查找页表。

  3. 缺页处理

    • 如果页不在物理内存中,操作系统通过页错误处理程序从磁盘读取页,并更新页表。

  4. 物理内存映射

    • 页加载到物理内存后,页表更新,将虚拟页映射到物理页。

  5. 程序继续执行

    • 页表更新完成后,程序可以继续执行。

请求调页的代码实现

从中断开始

提取的代码

void trap_init(void)
{
    set_trap_gate(14, &page_fault);
}
​
#define set_trap_gate(n, addr) \
    _set_gate(&idt[n], 15, 0, addr);

代码解释

  1. 函数 trap_init

    • 这个函数用于初始化函数(trap),在这里特指页错误中断的处理。

    • 它调用 set_trap_gate 函数,将中断号为14的中断(页错误中断)关联到 page_fault 处理函数。

  2. 宏定义 set_trap_gate

    • 这是一个宏,用于设置中断描述符表(IDT)中的一个条目。

    • n 是中断号,addr 是中断处理函数的地址。

    • _set_gate 函数用于设置IDT中的一个门描述符,这里设置的是陷阱门(trap gate)。

    • 第二个参数15表示中断门,第三个参数0通常表示门描述符的特权级,最后一个参数 addr 是中断处理函数的地址。

页缺失中断处理

图中展示的是Linux操作系统中处理页错误(Page Fault)中断的汇编代码。这段代码位于 linux/mm/page.s 文件中,是内核的一部分,用于处理页错误中断。以下是提取的代码和总结:

提取的代码:

.globl _page_fault
_page_fault:
    // 取出错码到 eax。错误码被压入栈中,这里将其与 eax 交换位置。
    xchgl %eax, (%esp)  // 将栈顶的错误码与 eax 交换,此时 eax 中保存错误码

    // 保存寄存器状态,以便后续恢复
    pushl %ecx          // 保存 ecx 寄存器
    pushl %edx          // 保存 edx 寄存器
    push %ds            // 保存 ds 段寄存器
    push %es            // 保存 es 段寄存器
    push %fs            // 保存 fs 段寄存器

    // 设置内核数据段选择符,确保后续操作在内核空间进行
    movl $0x10, %edx    // 内核数据段选择符为 0x10
    mov %dx, %ds        // 设置 ds 段寄存器为内核数据段选择符
    mov %dx, %es        // 设置 es 段寄存器为内核数据段选择符
    mov %dx, %fs        // 设置 fs 段寄存器为内核数据段选择符

    // 获取引起页面异常的线性地址
    movl %cr2, %edx     // cr2 寄存器保存引起页面异常的线性地址

    // 压入参数,将线性地址和错误码压入栈中,作为将调用函数的参数
    pushl %edx          // 压入线性地址
    pushl %eax          // 压入错误码

    // 测试标志 P(位 0),判断是否为缺页异常
    testl $1, %eax      // 测试错误码的最低位(P 标志)
    jne 1f              // 如果不是缺页异常,跳转到标签 1

    // 调用缺页处理函数 _do_no_page
    call _do_no_page    // 处理缺页异常

    jmp 2f              // 跳转到标签 2,清理堆栈并恢复寄存器

1:  // 如果是写保护异常
    call _do_wp_page    // 调用写保护处理函数

2:  // 清理堆栈并恢复寄存器
    addl $8, %esp       // 弹出压入的两个参数(线性地址和错误码)
    pop %fs             // 恢复 fs 段寄存器
    pop %es             // 恢复 es 段寄存器
    pop %ds             // 恢复 ds 段寄存器
    popl %edx           // 恢复 edx 寄存器
    popl %ecx           // 恢复 ecx 寄存器
    popl %eax           // 恢复 eax 寄存器

    iret                // 返回用户模式

代码解释:

  1. 全局标签 _page_fault

    • 定义了一个全局标签 _page_fault,这是页错误中断的处理函数。

  2. 寄存器保存

    • 使用 xchg 指令将 %eax 寄存器的值与栈顶的值交换,并将 %eax 的原始值压入栈中。

    • 依次将 %ecx%edx%ds%es%fs 寄存器的值压入栈中,以保存当前的寄存器状态。

  3. 设置段寄存器

    • 0x10(内核数据段的选择子)加载到 %edx 寄存器中,并将 %edx 的值写入 %ds%es%fs 寄存器,以设置内核数据段。

  4. 获取页错误线性地址

    • 使用 movl %cr2, %edx 指令将页错误线性地址加载到 %edx 寄存器中。

  5. 压入参数

    • %edx%eax 寄存器的值压入栈中,作为函数调用的参数。

  6. 测试标志

    • 使用 testl %eax, %eax 指令测试 %eax 寄存器的值,并根据测试结果跳转到不同的代码段。

  7. 调用处理函数

    • 如果 %eax 寄存器的值为零,则调用 _do_no_page 函数处理页错误。

    • 否则,调用 _do_wp_page 函数处理写保护错误。

  8. 恢复现场

    • 在函数调用完成后,使用 add $8, %esp 指令调整栈指针,以移除压入栈中的参数。

    • 依次弹出保存的寄存器值,恢复现场。

  9. 中断返回

    • 使用 iret 指令从中断返回,恢复程序的执行。

总结:

页缺失中断处理是操作系统虚拟内存管理中的一个关键环节,它最终实现了以下几个主要功能:

  1. 内存访问合法性检查

    • 确认触发页缺失中断的内存访问是否合法,例如检查访问的地址是否在程序的地址空间内。

  2. 页面置换

    • 如果当前进程的物理内存不足以加载所需的页面,则可能需要从物理内存中移除(置换)某个页面,以便为新页面腾出空间。

  3. 页面分配

    • 为缺失的页面分配物理内存(页框)。

  4. 页面调入

    • 从磁盘上的交换区或文件系统中将缺失的页面内容读入到分配的物理内存中。

  5. 页表更新

    • 更新页表以反映新的虚拟地址到物理地址的映射关系。

  6. 内存保护

    • 确保进程只能访问其地址空间内的合法内存区域,防止进程间内存非法访问。

  7. 错误处理

    • 如果页缺失是由于错误(如访问了无效的内存地址)引起的,操作系统需要进行相应的错误处理。

  8. 提高内存利用率

    • 通过将不常使用的页面换出到磁盘,操作系统可以提高物理内存的利用率,允许更多的进程同时运行。

没有页面映射的情况

图中展示的是Linux操作系统中处理页错误(Page Fault)时调用的 do_no_page 函数的C代码。这段代码位于 linux/mm/memory.c 文件中,用于处理没有页面映射的情况。以下是提取的代码和总结:

提取的代码:

// 在linux/mm/memory.c中
void do_no_page(unsigned long error_code, unsigned long address)
{
    address &= 0xfffff000;  // 页面地址
    tmp = address - current->start_code;  // 页面对应的偏移
​
    if (!current->executable || tmp >= current->end_data) {
        get_empty_page(address);
        return;
    }
​
    page = get_free_page();
    bread_page(page, current->executable->i_dev, nr);
    put_page(page, address);
}
​
void get_empty_page(unsigned long address)
{
    unsigned long tmp = get_free_page();
    put_page(tmp, address);
}

代码解释:

  1. 函数 do_no_page

    • 这个函数处理没有页面映射的情况,即页错误中断。

    • address 参数是发生页错误的虚拟地址。

  2. 页面地址计算

    • 使用 address &= 0xfffff000 将地址转换为页面地址(清除低12位,因为页面大小为4KB)。

  3. 页面偏移计算

    • 计算页面相对于程序代码段的偏移量。

  4. 检查页面范围

    • 检查偏移量是否在程序的可执行代码范围内。如果不在范围内,调用 get_empty_page 获取一个空页面并返回。

  5. 获取空闲页面

    • 调用 get_free_page 获取一个空闲页面。

  6. 读取页面内容

    • 调用 bread_page 从文件系统中读取页面内容到空闲页面。

  7. 映射页面

    • 调用 put_page 将页面映射到指定的虚拟地址。

  8. 函数 get_empty_page

    • 这个函数用于获取一个空页面并将其映射到指定的虚拟地址。

有没有页面映射的情况

图中展示的是Linux操作系统中 put_page 函数的C代码,该函数用于将一个物理页面映射到指定的虚拟地址。以下是提取的代码和总结:

提取的代码:

// 在linux/mm/memory.c中
unsigned long put_page(unsigned long page, // 物理地址
                       unsigned long address)
{
    unsigned long tmp, *page_table;
    page_table = (unsigned long *)(((address >> 20) & 0xffc));
​
    if ((*page_table) & 1)
        page_table = (unsigned long *)(0xfffff000 & *page_table);
    else {
        tmp = get_free_page();
        *page_table = tmp | 7;
        page_table = (unsigned long *)tmp;
    }
    page_table[(address >> 12) & 0x3ff] = page | 7;
    return page;
}

代码解释:

  1. 函数定义

    • put_page 函数接受两个参数:page(物理地址)和address(虚拟地址)。

  2. 计算页目录项

    • 使用 address 的高20位(通过右移20位并和 0xffc 进行位与操作)来计算页目录项的地址。

  3. 检查页目录项

    • 检查页目录项的最低位是否为1,这表示该项是否已经指向一个页表。

  4. 页表地址计算

    • 如果页目录项已经指向一个页表,则直接计算页表的地址。

    • 如果页目录项未指向页表,则需要分配一个新页表,并更新页目录项。

  5. 分配页表

    • 如果页表不存在,则调用 get_free_page 函数分配一个新的页表,并更新页目录项。

  6. 设置页表项

    • 计算页表项的索引(通过 address 的中间12位)。

    • 将物理页面地址和一些属性(如存在位、读写位等)设置到页表项中。

  7. 返回物理页面

    • 函数返回物理页面地址。


网站公告

今日签到

点亮在社区的每一天
去签到