目录

​​​​​​​

一、虚拟地址和页表的由来

1、无虚拟内存和分页机制时的内存困境

2、虚拟内存和分页机制的诞生

3、页表的作用与工作原理

4、总结

二、物理内存管理

1、struct page结构体:物理页的“身份证”(先描述,再组织!!!)

2、关键参数详解

1. flags字段

2. _mapcount字段

3. virtual字段

3、内存消耗分析

4、页大小的影响

5、总结

三、页表

1、32 位系统下的虚拟内存与页表规模

2、页表与物理内存的映射关系

3、页表的空间占用分析

4、大容量页表带来的问题

5、多级页表思想的提出

6、多级页表的优势体现

7、问题引入和总结

1. 回顾:单级页表的问题(第3、4点)

2. 理解:多级页表的解决方案(第5点)

3. 解答可能遇到的疑惑:为什么“没算”页表项大小?

4. 多级页表的真正优势(解决核心疑惑)

5. 总结

8、单级页表与多级页表在内存占用和运行机制上的根本性区别

1. 单级页表:一次性全部驻留

2. 多级页表:按需创建和调入

3. 一个生动的比喻

公司A:单级页表式管理(扁平化管理)

公司B:多级页表式管理(层级化管理)

总结对比

4. 操作系统的核心工作

操作系统的三项核心工作

1. 查询空闲资源(物理内存)

2. 分配资源

3. 建立映射关系(更新页表)

为什么这个操作不会陷入死循环?

总结

5. 深入探讨:为什么单级页表不能像多级页表那样通过操作系统帮忙重新分配其中无效的页表项?

场景再现:单级页表公司的“死亡循环”

多级页表公司的“万能钥匙”

6. 技术核心:内核页表 vs 进程页表

结论

四、页目录结构深度解析

1、二级页表的基本构成与指向关系

2、二级页表的工作原理示例

3、操作系统加载用户程序时的内存分配任务

4、二级页表的优势与意义

5、回顾与补充

五、两级页表的地址转换深度剖析

1、示例逻辑地址及地址结构划分

2、两级页表地址转换的具体步骤

1. 读取页目录起始地址

2. 一级页表查询

3. 二级页表查询

4. 生成物理地址

3、MMU 的工作流程及性能考量

4、多级页表的利弊及优化方案

5、TLB 的工作原理及作用

六、缺页异常深度解析

1、缺页异常的产生场景

2、缺页异常的类型及处理方式

1. Hard Page Fault(硬缺页错误/主要缺页错误)

2. Soft Page Fault(软缺页错误/次要缺页错误)

3. Invalid Page Fault(无效缺页错误)

3、与缺页异常相关的重要问题探讨

1. 对 new 和 malloc 的理解

2. 对写时拷贝的理解

3. 申请内存的实际操作

4. 区分缺页和越界的方法

5. 越界访问是否一定会报错

4、线程资源划分与虚拟地址空间的关系


一、虚拟地址和页表的由来

        在计算机系统的内存管理领域,虚拟地址和页表的出现有着深刻的背景和重要的意义。让我们先设想一个没有虚拟内存和分页机制时的内存使用场景。

1、无虚拟内存和分页机制时的内存困境

        在没有引入虚拟内存和分页机制的情况下,每个用户程序在物理内存中所对应的存储空间必须是连续的。这是由于早期计算机系统采用简单的内存分配方式,直接将程序的代码和数据映射到连续的物理内存区域。

        然而,不同程序的代码和数据长度差异巨大,有的程序代码简短、数据量少,占用的物理内存空间就小;而有的程序代码复杂、数据庞大,所需物理内存空间则大。按照这种连续映射的方式,物理内存会被无情地分割成各种离散的、大小各不相同的块。

        随着系统的运行,一些程序完成其任务后会正常退出,它们原本占据的物理内存空间就会被回收。但这种回收并不是整齐划一的,而是导致物理内存中出现了大量碎片。

        这些碎片有的过小,无法满足新程序对连续内存空间的需求,即使剩余的碎片总容量足够,也会因为不连续而无法被有效利用。这就好比一个堆满了各种大小不一的杂物的仓库,虽然总体空间还有剩余,但却很难再放入一个较大且完整的物品。长此以往,物理内存的利用率会大幅下降,系统的性能也会受到严重影响。

2、虚拟内存和分页机制的诞生

为了解决上述物理内存碎片问题,同时满足操作系统提供给用户连续内存空间的需求,虚拟内存和分页机制应运而生。

  • 分页机制的核心思想是将物理内存按照一个固定的长度进行分割,这些被分割出来的存储区域被称为页框(page frame),有时也直接叫做物理页。

  • 每个页框都包含一个固定大小的物理页(page),并且一个页的大小等于页框的大小。

  • 在大多数32位体系结构的计算机系统中,通常支持4KB的页大小;而对于64位体系结构,一般会支持8KB的页。

  • 这里需要明确区分页框和页的概念:页框是一个实实在在的物理存储区域,它位于物理内存中;而页则是一个逻辑上的数据块,它可以存放在任何页框中,甚至当物理内存不足时,还可以被暂时存放到磁盘中。

        有了分页机制后,CPU不再直接访问物理内存地址,而是通过虚拟地址空间来间接访问物理内存地址。虚拟地址空间是操作系统为每一个正在执行的进程精心分配的一个逻辑地址范围。在32位机上,虚拟地址空间的范围从0到4G - 1,这为每个进程提供了一个看似连续且巨大的内存空间,让进程可以方便地进行内存操作,而无需关心物理内存的实际布局。

3、页表的作用与工作原理

        为了实现从虚拟地址空间到物理内存地址的映射,操作系统引入了页表这一关键数据结构。页表就像是一本详细的地址对照手册,上面记录了每一对页和页框的映射关系。

  • 当CPU发出一个虚拟地址时,内存管理单元(MMU)会首先根据这个虚拟地址在页表中查找对应的页框号,然后再结合页内偏移量,计算出最终的物理内存地址,从而实现CPU对物理内存的间接访问。

  • 具体来说,虚拟内存下的逻辑地址空间被划分为若干个页,物理内存空间被划分为若干个页框。通过页表,连续的虚拟内存页可以被灵活地映射到若干个不连续的物理内存页框上。

  • 这种映射方式打破了物理内存连续性的限制,使得操作系统可以更加高效地利用物理内存资源。

        即使物理内存中存在碎片,只要这些碎片的总容量能够满足程序的需求,就可以通过页表的映射将程序的虚拟内存页分配到这些不连续的物理内存页框中,从而解决了使用连续物理内存造成的碎片问题。

4、总结

        综上所述,虚拟地址和页表的出现是计算机系统内存管理的一次重大革新。分页机制将物理内存分割成固定大小的页框,虚拟地址空间为进程提供了连续的逻辑内存视图,而页表则建立了虚拟地址与物理地址之间的桥梁。通过这种机制,系统有效地解决了物理内存碎片问题,提高了内存的利用率和系统的整体性能,为现代计算机系统的稳定运行和高效执行提供了坚实的保障。


二、物理内存管理

        在计算机系统的运行过程中,物理内存管理是一项至关重要的任务。假设我们有一个可用物理内存为4GB的系统,为了更高效地管理和利用这些内存资源,通常会按照一个页框大小为4KB来进行划分。通过简单的计算,4GB的内存空间可以划分为4GB / 4KB = 1048576个页框。面对如此众多的物理页,操作系统必须构建一套完善的管理机制,以便清晰地掌握哪些页正在被使用,哪些页处于空闲状态等关键信息。

1、struct page结构体:物理页的“身份证”(先描述,再组织!!!)

        内核采用struct page结构体来表示系统中的每一个物理页。在设计这个结构体时,出于对内存使用效率的考虑,大量运用了联合体(union)。联合体这种数据结构允许在同一个内存位置存储不同的数据类型,从而在满足多种功能需求的同时,最大程度地节省内存空间。以下是对struct page结构体部分关键内容的详细解析(代码示例来自include/linux/mm_types.h):

struct page {
    /* 原子标志,有些情况下会异步更新 */ 
    unsigned long flags;
    union {
        struct {
            /* 换出页列表,例如由zone->lru_lock保护的active_list */ 
            struct list_head lru;
            /* 如果最低为为0,则指向inode 
             * address_space,或为NULL 
             * 如果页映射为匿名内存,最低为置位 
             * 而且该指针指向anon_vma对象 
             */
            struct address_space* mapping;
            /* 在映射内的偏移量 */ 
            pgoff_t index;
            /*
             * 由映射私有,不透明数据 
             * 如果设置了PagePrivate,通常用于buffer_heads 
             * 如果设置了PageSwapCache,则用于swp_entry_t 
             * 如果设置了PG_buddy,则用于表示伙伴系统中的阶 
             */
            unsigned long private;
        };
        struct { /* slab, slob and slub */
            union {
                struct list_head slab_list; /* uses lru */
                struct { /* Partial pages */
                    struct page* next;
#ifdef CONFIG_64BIT
                    int pages; /* Nr of pages left */
                    int pobjects; /* Approximate count */
#else
                    short int pages;
                    short int pobjects;
#endif
                };
            };
            struct kmem_cache* slab_cache; /* not slob */
            /* Double-word boundary */
            void* freelist; /* first free object */
            union {
                void* s_mem; /* slab: first object */
                unsigned long counters; /* SLUB */
                struct { /* SLUB */
                    unsigned inuse : 16; /* 用于SLUB分配器:对象的数目 */ 
                    unsigned objects : 15;
                    unsigned frozen : 1;
                };
            };
        };
        // 其他联合体成员...
    };
    union {
        /* 内存管理子系统中映射的页表项计数,用于表示页是否已经映射,还用于限制逆向映射搜索*/ 
        atomic_t _mapcount;
        unsigned int page_type;
        unsigned int active; /* SLAB */
        int units; /* SLOB */
    };
    // 其他成员...
#if defined(WANT_PAGE_VIRTUAL)
    /* 内核虚拟地址(如果没有映射则为NULL,即高端内存) */ 
    void* virtual;
#endif /* WANT_PAGE_VIRTUAL */
    // 其他成员...
}

2、关键参数详解

1. flags字段

        该字段用于存放页的状态信息。通过每一位单独表示一种状态(例如这些状态包括页面是否为脏页、是否被锁定在内存中等等),它至少可以同时表示出32种不同的状态。这些状态标志定义在<linux/page-flags.h>头文件中。其中一些比特位具有极其重要的作用,例如:

  • PG_locked:用于指定页是否被锁定在内存中,防止其在不需要时被交换到磁盘。

  • PG_uptodate:用于表示页的数据已经从块设备成功读取,并且没有出现任何错误,确保数据的完整性和准确性。

2. _mapcount字段

  • 它表示在页表中有多少项指向该页,也就是这一页被引用的次数。

  • 当计数值变为 -1 时,说明当前内核并没有引用这一页,那么在后续的内存分配中就可以安全地使用它,从而实现内存的高效回收和再利用。

3. virtual字段

  • 代表页的虚拟地址。通常情况下,它就是页在虚拟内存中的地址。

  • 然而,对于一些所谓的“高端内存”,它们并不永久地映射到内核地址空间上。

  • 在这种情况下,这个域的值为NULL,当需要访问这些页时,必须动态地进行映射,以保证对高端内存的有效访问。

3、内存消耗分析

        需要注意的是,struct page结构体是与物理页相关联的,而非虚拟页。系统中的每一个物理页都需要分配一个这样的结构体来进行管理。让我们来计算一下,为所有物理页分配struct page结构体所消耗的内存量。

        假设struct page占用40个字节的内存,系统物理页大小为4KB,且系统拥有4GB物理内存,那么系统中共有1048576个(1兆个)页(页面)。因此,描述这么多页面的page结构体消耗的内存为40MB。相对于系统4GB的内存总量而言,这仅仅是很小的一部分!!!

由此可见,采用这种方式管理系统中众多的物理页面,所付出的内存代价并不算太大。

4、页大小的影响

  • 页的大小对于内存利用和系统开销来说是一个非常关键的因素。

  • 如果页设置得过大,页内必然会剩余较大且无法利用的空间,即页内碎片。这些碎片虽然占用了一定的内存空间,但却无法被有效使用,从而导致内存资源的浪费。

  • 相反,如果页设置得过小,虽然可以减小页内碎片的大小,但是会增加页的数量。过多的页会使页表变得过长,从而占用更多的内存空间。同时,系统需要频繁地进行页转换操作,这无疑会加重系统的开销,影响系统的整体性能。

  • 因此,页的大小应该设置在一个适中的范围内,通常为512B - 8KB。在Windows和Linux系统中,页框大小通常设置为4KB,这是一个经过实践验证的较为合理的值。

5、总结

        操作系统对每一个物理页的管理至关重要,通过struct page结构体,操作系统能够精确地掌握每个物理页的状态和使用情况。同时,合理设置页的大小对于优化内存利用和减少系统开销具有重要意义。只有做好物理内存管理,才能确保计算机系统高效、稳定地运行。


三、页表

        在计算机系统的内存管理中,页表扮演着至关重要的角色。页表中的每一个表项,都精准地指向一个物理页的开始地址,它是实现虚拟内存到物理内存映射的关键桥梁。

1、32 位系统下的虚拟内存与页表规模

        在 32 位系统中,虚拟内存具有高达 4GB 的最大空间,这是每一个用户程序都独立拥有的虚拟内存区域。为了使这 4GB 的虚拟内存全部可用,页表必须具备表示整个 4GB 空间的能力。由于每个物理页的大小通常为 4KB,因此计算可得页表所需表项数量为 4GB/4KB = 1048576 个。

        从直观上看,虚拟内存被虚线“分割”成一个个单元,但实际上这并非真实的物理分割,虚拟内存依旧保持着连续性。这些虚线所划分的单元,仅仅是为了表明它们与页表中每一个表项的映射关系,并且最终会映射到相同大小的物理内存页上。

2、页表与物理内存的映射关系

  • 页表中的物理地址与物理内存之间,呈现出一种随机的映射关系。

  • 系统会根据物理内存的可用情况,灵活地将页表项指向可用的物理页。

  • 尽管最终使用的物理内存是离散分布的,但与虚拟内存对应的线性地址却是连续的。

  • 在CPU(处理器)访问数据和获取指令时,均使用线性地址。

  • 只要线性地址连续,CPU(处理器)就能够通过页表顺利找到实际的物理地址。

3、页表的空间占用分析

        假设在 32 位系统中,地址的长度为 4 个字节,那么意味着页表中的每一个表项就会占用 4 个字节。由此可计算出页表占据的总空间大小为:1048576(总的页个数) * 4(每个页表项) = 4MB。这意味着映射表(页表)自身就需要占用 4MB / 4KB = 1024 个物理页。

4、大容量页表带来的问题

        这种大容量的页表设计会引发一系列问题。首先,回顾当初引入页表的初衷,是为了将进程划分为一个个页,使其能够不连续地存放在物理内存中,从而提高内存的利用率和灵活性。然而,此时页表却需要 1024 个连续的页框,这与最初的目标似乎背道而驰。

        其次,根据局部性原理,进程在一段时间内通常只需要访问少数几个页就能够正常运行。因此,没有必要一次性让所有的物理页都常驻内存,这无疑会造成内存资源的浪费。

5、多级页表思想的提出

        为了解决大容量页表带来的问题,一个绝佳的方法是将页表视为普通的文件,对其进行离散分配,也就是对页表进行分页处理,由此形成了多级页表的思想。

        具体而言,可以把这个单一的页表拆分成 1024 个体积更小的映射表。这样一来,每个小表中包含 1024 个表项,1024 个小表相互配合,即 1024(每个表中的表项个数)* 1024(表的个数),仍然能够完整地覆盖 4GB 的物理内存空间。

        这里的每一个小表,就是真正意义上的页表,所以总共会有 1024 个页表。一个页表自身占用 4KB,那么 1024 个页表一共会占用 4MB 的物理内存空间。从总数上看,这与之前单一页表占用的空间似乎没有差别。

6、多级页表的优势体现

但实际上,一个应用程序是不可能完全使用全部的 4GB 空间的。很多时候,只需要几十个页表就能够满足需求。

例如,一个用户程序的代码段、数据段、栈段,总共只需要 10MB 的空间。下面进行详细的计算分析:

        每一个页表项指向一个 4KB 的物理页,那么一个页表中 1024 个页表项,一共能够覆盖 4MB 的物理内存(计算方式:1024 * 4KB = 4MB)。

        而对于一个 10MB 的程序,为了方便管理,需要向上对齐取整到 4MB 的倍数,即 12MB。由于每个页表覆盖 4MB 空间,所以 12MB / 4MB = 3,也就意味着使用 3 个页表就足够了。通过这种方式,多级页表有效地减少了实际占用内存的页表数量,提高了内存的使用效率。

7、问题引入和总结

1. 回顾:单级页表的问题(第3、4点)

  • 前提:32位系统,4GB虚拟地址空间,页大小为4KB。

  • 计算页数:4GB / 4KB = 2^20 = 1,048,576 个页。

  • 每个页表项(PTE):如第三点所说,一个页表项(用来记录一个虚拟页映射到哪个物理页帧)需要4字节。

  • 单级页表总大小:1,048,576 个页表项 × 4 字节/项 = 4 MB。

  • 问题:这个4MB的单一大页表需要连续的物理内存,这违背了分页的初衷。

2. 理解:多级页表的解决方案(第5点)

多级页表的核心思想是:“对页表本身进行分页”

现在,我们把那1,048,576个页表项(总共4MB)不是放在一张大表里,而是分装到1024个更小的“页”里面

  • 每个“小页表”有多大?

    • 一个物理页框是4KB。

    • 每个页表项是4字节。

    • 那么,一个4KB的物理页框可以存放多少个页表项?

    • 4KB / 4字节 = 4096 / 4 = 1024 个页表项。

  • 需要多少个这样的“小页表”?

    • 总共有1,048,576个页表项需要被存放。

    • 1,048,576 / 1024 = 1024 个小页表。

所以,结论是:

  • 我们有 1024个 小页表。

  • 每个小页表正好占用一个物理页框(4KB),因为它里面包含了1024个页表项(每个4字节)。

  • 所有小页表占用的总空间是:1024个小页表 × 4KB/个 = 4 MB

3. 解答可能遇到的疑惑:为什么“没算”页表项大小?

实际上已经算了! 当说“一个页表自身占用4KB”时,这个“4KB”正是由 “1024个页表项 × 每个4字节” 计算得来的。

  • 第3点:是在抽象地、直接地计算所有页表项的总容量:总项数 × 项大小 = 总空间

  • 第5点:是在物理地、具体地描述这些页表项是如何被“打包”存储的:每个物理页能打包1024个项 → 需要1024个物理页来打包所有项 → 总空间 = 打包用的物理页数量 × 每个物理页大小

        这两种计算方法最终都得到了 4MB 这个相同的总空间。第五点的描述是把“每个页表项4字节”这个事实作为已知条件,隐含在了“一个页表自身占用4KB”这个结论里。

4. 多级页表的真正优势(解决核心疑惑)

既然总空间还是4MB,那多级页表的优势在哪里?这才是关键!

单级页表的问题是 “需要4MB的连续物理内存”

而多级页表的优势在于:

  • 离散存储:这1024个小页表(每个4KB)不需要在物理内存中连续存放。它们可以像进程数据页一样,被分散到内存的任何地方。这解决了“大块连续内存”的需求。

  • 按需加载:后面我们引入了页目录(Page Directory) 来管理这1024个小页表。页目录本身很小(比如1024个表项,占4KB)。操作系统只需要常驻这个小小的页目录在内存中。而具体的页表(那1024个小页表),只有在进程真正访问到对应区域的地址时,才需要被创建并调入物理内存。如果一个进程只使用了几个MB的内存,可能只需要1-2个小页表在内存中,其余1000多个小页表根本不存在,这就极大地节约了内存。

5. 总结

没有矛盾:第五点计算总空间时,已经包含了“每个页表项4字节”的前提。4KB/页表 = 1024项/页表 × 4字节/项

核心区别:多级页表的革命性不在于减少理论上的总映射空间(它还是4MB),而在于:

  • 将大块连续需求转化为小块离散需求

  • 通过“页目录”实现了页表的“按需加载”,从而在绝大多数情况下,极大地减少了实际驻留在内存中的页表大小。

8、单级页表与多级页表在内存占用和运行机制上的根本性区别

1. 单级页表:一次性全部驻留

  • 工作机制:在进程被创建并准备执行时,操作系统就必须为其分配并建立完整的单级页表。这个页表(在我们32位4KB页的例子中,大小为4MB)需要作为一个完整的、连续的数据结构一次性调入物理内存,并始终保持驻留,直到进程结束。

  • 为什么必须全部驻留? 因为单级页表是一个“扁平”的结构。当CPU的MMU(内存管理单元)需要将虚拟地址转换为物理地址时,它必须能够直接、立即地访问到页表中的任何一个表项。如果页表本身有一部分不在内存中,转换就无法完成,会导致“页错误”(Page Fault)。但处理这个页错误又需要访问页表,这就形成了一个死循环。

  • 核心问题:这就导致了巨大的内存浪费。正如局部性原理所指出的,一个进程在绝大多数时间内,只会访问其全部地址空间中的一小部分页面(代码、堆栈、部分数据)。然而,为了映射那可能永远也用不到的地址范围,进程却不得不背负着一个完整的、庞大的页表。

2. 多级页表:按需创建和调入

工作机制:多级页表(以二级为例)引入了一个页目录

  • 页目录很小且常驻内存:页目录通常很小(例如4KB),操作系统可以轻松地让每个进程的页目录常驻在物理内存中。

  • 下级页表按需创建:当进程启动时,操作系统只为其创建页目录。下级的具体页表(即我们之前讨论的1024个小页表)在初始时并不存在

  • 访问时触发创建/调入:当进程第一次访问一个尚未映射的虚拟地址区域时,会发生页错误。操作系统处理这个错误时,会发现其对应的页目录项是“空的”或“无效的”。这时,操作系统才会按需地分配一个物理页框,将其作为新的下级页表,并初始化该页表项,然后将其地址填入页目录项中。最后,再重新执行那条引发错误的指令,此时地址转换就能成功了。

核心优势

  • 节约内存:一个只使用了几MB代码和数据的进程,可能只需要1-2个下级页表(占用4-8KB)加上一个页目录(4KB)。这与单级页表必须的4MB相比,内存节约是数量级的差异。

  • 自然支持稀疏地址空间:对于地址空间中巨大的“空洞”(如未分配的堆、未使用的库),多级页表根本不为它们创建下级页表,实现了“零开销”。

3. 一个生动的比喻

我们用两个公司的管理方式来比喻,这个抽象概念就会变得非常具体。

        想象一下,你有两家超大型的跨国公司,每家都有 100万名员工。你的任务是:随时能联系到任何一位员工,但又不想浪费钱给所有员工都配一部常驻办公室的电话。

公司A:单级页表式管理(扁平化管理)

这家公司只有一个巨大的总通讯录

  • 这个通讯录有多大? 它是一本有100万条记录的厚书,每条记录对应一个员工。

  • 初始状态: 这本书里大部分员工的电话位置都是“空白的”(页表项无效),只有极少数核心员工的电话被填上了。

  • 发生一件事: 你现在需要联系一个在巴西分部的、名叫“张三”的普通员工。

  • 你的操作(MMU查表): 你拿起这本巨大的总通讯录,按照员工编号(虚拟页号)找到了“张三”的那一条记录。

  • 发现问题: 记录上写着“电话号码:未分配”(页表项无效)。

  • 你求助行政部(操作系统处理页错误): 你打电话给行政部说:“快给张三配个电话,并把号码填到通讯录里!”

  • 行政部的致命操作:
    行政部接到你的请求,他们也需要在这本总通讯录里,找到‘行政部-更新科-李四’这条记录,才能通知他去干活
    结果一查,“李四”这条记录也是“电话号码:未分配”
    于是,行政部又需要先找人给“李四”配电话……而给“李四”配电话,又需要找“王五”……就此陷入了“先有鸡还是先有蛋”的无限死循环

结论: 为了避免这个死循环,公司A只能在一开始就给所有100万员工都配好电话,并把号码全部填进那本巨大的通讯录里。这本通讯录本身就成了一个极其昂贵且大部分空间都被浪费的庞然大物。(这就是单级页表必须常驻内存的原因)

公司B:多级页表式管理(层级化管理)

这家公司采用了一种聪明的层级管理结构。

它有一本非常薄的“部门总监通讯录”(页目录)

  • 这本总监通讯录: 只有1000条记录,每条记录对应一个部门(如“亚洲区”、“欧洲区”、“巴西分部”...)。最关键的是,公司强制规定,这1000位总监的电话必须7x24小时畅通,永远不能关机(常驻内存)

  • 每个总监手下: 都有一本自己部门的“员工通讯录”(下级页表),这本通讯录里有1000个员工的名字。

  • 初始状态: 大部分部门的“员工通讯录”都还是空白的,还没有创建。

  • 发生同样的事: 你需要联系在巴西分部的“张三”。

  • 你的第一步操作(MMU查页目录):

    1. 你先翻开那本薄薄的、永远在线的“部门总监通讯录”。

    2. 你找到“巴西分部总监-赵总”的记录,上面有他的电话。

  • 你的第二步操作(MMU查下级页表):

    1. 你给赵总打电话:“把你们部门的员工通讯录给我。”

    2. 赵总一查,回复你:“抱歉,我们部门的员工通讯录还没打印呢(下级页表无效)。”

  • 你求助行政部(操作系统处理页错误):

    1. 你打电话给行政部(注意:你通过那本永远在线的总监通讯录,一定能找到行政部总监,所以这个求助请求本身永远不会失败)。

    2. 行政部接到请求,立刻行动:他们打印出一本新的“巴西分部员工通讯录”(创建下级页表),然后把“张三”的名字写进去,并给他配了电话(分配物理页框)。

    3. 最后,行政部把这份新通讯录的副本交给了“赵总”(更新页目录项)。

  • 你再次尝试:

    1. 你再次翻开“总监通讯录”找到赵总。

    2. 赵总现在有了通讯录,他帮你查到了张三的电话。

    3. 成功联系上张三!

结论: 公司B通过建立一个小而稳定的核心管理层(页目录常驻内存),成功地将庞大的员工管理任务下放。只有当真正需要和某个部门打交道时,才去建立该部门的具体联络名单(按需创建下级页表)。对于那些永远没有业务往来的部门(如“南极洲办事处”),它的员工通讯录就永远不需要创建,为公司节省了大量的资源和空间。

总结对比
特性 单级页表 (公司A) 多级页表 (公司B)
管理结构 一本巨大的总通讯录 一本薄的总监通讯录 + 许多本部门通讯录
核心层 总监通讯录(强制常驻)
按需创建 不可能,会导致死循环 完全可以
资源占用 一开始就为所有100万员工买单,浪费惊人 只为活跃的部门创建通讯录,极度节省

        现在你应该能直观地感受到,为什么单级页表不能像多级页表那样“按需创建”了吧?因为它缺少那个稳定、可靠、且永远在线的“核心管理层” 来安全地执行“创建”这个操作本身。

4. 操作系统的核心工作

        整个过程中最核心的一环——操作系统作为最高管理者(超级行政部)的角色。让我们继续用这个公司比喻,来详细拆解“行政部”(操作系统)在接到请求后所做的具体工作:

操作系统的三项核心工作

当“页错误”发生,CPU把控制权交给操作系统后,操作系统这个“超级行政部”会进行以下一系列关键操作:

1. 查询空闲资源(物理内存)
  • 比喻:行政部需要知道公司还有没有空闲的办公室和电话线路(物理页框)可以分配。

  • 实际行为:操作系统维护着一个或多个叫做 “空闲页框列表” 的数据结构。这就像一个“公司空置资产登记表”,上面记录了当前所有可用的物理内存页。

  • 动作:操作系统查看这个列表,看看有没有空闲的物理页。

2. 分配资源
  • 比喻:行政部从空置资产中,划拨出一间办公室和一条电话线路给张三。

  • 实际行为:操作系统从“空闲页框列表”中取出一个空闲的物理页框,将其标记为“已使用”。这个物理页框现在正式分配给了发出访问请求的那个虚拟页(张三)。

3. 建立映射关系(更新页表)

这是最关键的一步,它又分为两个动作:

a) 建立“员工-办公室”的映射(更新下级页表)

  • 比喻:行政部在刚刚打印好的 “巴西分部员工通讯录” (下级页表)中,找到“张三”那一栏,填上他新分配到的办公室号码(物理页框号)和电话权限(读写执行等标志位)。

  • 实际行为:操作系统找到发生错误的那个虚拟地址所对应的下级页表项,将其从“无效”改为“有效”,并写入分配到的物理页框号

b) 确保“总监”能找到新通讯录(更新页目录)

  • 比喻:如果“巴西分部员工通讯录”是刚刚才打印(创建) 的,那么行政部还必须通知“赵总”,告诉他:“你们部门的通讯录已经做好了,放在XX位置,你以后就查这本。”(这对应着创建新的下级页表

  • 实际行为:操作系统在页目录中,找到对应的那个页目录项,填入这个新创建的下级页表所在的物理地址,并将其标记为“有效”。

为什么这个操作不会陷入死循环?

还记得单级页表的死循环问题吗?多级页表之所以能避免,就是因为:

  • 当操作系统在执行上述1、2、3a、3b步骤时,它是在内核模式下运行的。

  • 内核有自己的内存空间,并且用于管理内存的核心数据结构(如空闲页框列表、内核的页目录等)是常驻内存、永远有效的

  • 所以,操作系统在“打印新通讯录”(分配内存、更新页表)时,它不需要通过当前进程那个还不完整的页表来转换地址,它使用的是自己绝对可靠的、常驻的内核页表。这就好比行政部有自己的、独立于所有业务部门的、绝对完善的内部通讯系统,他们用自己的系统来完成调配资源的工作,不会受业务部门通讯录不完整的影响。

总结

“行政部”(操作系统)的行为就是:

  1. 查库存:查询“空闲页框列表”,看有没有空闲物理内存。

  2. 批条子:从中分配一个物理页框。

  3. 更新档案

    • 下级页表中建立 虚拟页 -> 物理页 的映射。

    • 如果需要,在页目录中建立 页目录项 -> 下级页表 的映射。

        完成这一切后,操作系统“挥手示意”,CPU重新执行那条之前失败的指令。这时,MMU再查页表,一切映射都已就绪,访问便顺利完成了。这个过程完美地体现了操作系统作为资源管理者的核心职能。

5. 深入探讨:为什么单级页表不能像多级页表那样通过操作系统帮忙重新分配其中无效的页表项?

        这是一个绝佳的问题,它直指架构设计的核心。单级页表也可以让操作系统帮忙分配,但问题在于,当操作系统“帮忙”时,它自己会掉进一个无法爬出的“陷阱”。让我们用之前公司的比喻,来揭示这个致命的陷阱。

场景再现:单级页表公司的“死亡循环”

公司A(单级页表公司) 只有一本巨大的总通讯录

  1. 需要联系“张三”,于是去查这本总通讯录。

  2. 你发现“张三”的记录是“电话号码:未分配”。

  3. 求助行政部(操作系统)

现在,关键来了!行政部要帮你,它需要做一件事:在总通讯录上,把张三的号码填上去。

但是,行政部的员工“李四”要执行“填写”这个动作,他自身也需要遵守公司规定:

  • 规定: 任何员工要进行任何操作(包括填写通讯录),都必须先在这本总通讯录里查到对方的联系方式。

  • 所以,李四必须先查总通讯录,找到“张三”这一条,才能开始填写。

  • 然而,“张三”这一条目前正是“未分配”状态! 李四的这次查询操作本身就会失败,又会触发一个“求助”请求。

看明白了吗?这就形成了一个无法打破的循环:

  • 任务: 填写通讯录中“张三”的条目。

  • 前提: 必须能成功查询到“张三”的条目。

  • 矛盾: 查询“张三”条目的操作,正是我们最初失败的那个操作。

这就好比你想用一把钥匙打开一个保险箱,而这把钥匙却锁在这个保险箱里面。你永远无法开始。

多级页表公司的“万能钥匙”

现在看公司B(多级页表公司),它是如何解决这个问题的。

公司B有一本薄薄的、永远在线的“总监通讯录”(页目录)

  1. 需要联系“张三”。

  2. 你先查“总监通讯录”,找到“巴西总监-赵总”。

  3. 你向赵总要“巴西分部通讯录”,赵总说:“我们没有。”

  4. 求助行政部(操作系统)

现在,行政部的“李四”要帮忙了,他需要做两件事:

  1. 打印一本新的“巴西分部通讯录”(创建下级页表)。

  2. 把这份新通讯录交给赵总(更新页目录)。

注意这里最精妙的设计:

  • 李四在执行这些操作时,他只需要用到那本薄薄的、永远有效的“总监通讯录”

  • 他找“赵总”通过总监通讯录。

  • 他找“打印部”通过总监通讯录。

  • 他完全不需要、也绝不会去触碰那本还不存在的“巴西分部通讯录”。

        他用来解决问题的工具(总监通讯录),和他正在处理的问题(巴西分部通讯录),是两个完全不同的、独立的东西。 这就好比行政部自己有一把万能钥匙(内核页表),可以打开公司所有的门,他们用这把万能钥匙去为业务部门配置新的门锁,而不会把自己锁在外面。

6. 技术核心:内核页表 vs 进程页表

在计算机中,这个“万能钥匙”就是内核页表

  • 每个进程都有自己的页表(用户页表),用来映射它自己的地址空间。单级页表就是这个。

  • 操作系统内核运行在特权模式,它有自己独立且常驻内存内核页表。内核页表映射了整个物理内存以及它自己的代码和数据。

当发生页错误时,CPU会切换到内核模式。在这个模式下,MMU使用的是内核页表,而不是那个出问题的进程的用户页表。

  • 在多级页表情况下:操作系统使用内核页表安全地访问页目录和下级页表,从而为用户进程建立新的映射。整个过程畅通无阻。

  • 在单级页表情况下:操作系统需要修改的页表项本身,就位于那个出问题的、不完整的用户页表中。即使使用内核页表,要修改的那个目标地址(用户页表项的物理地址)对应的用户页表项是无效的这个事实并没有改变。操作系统无法绕过这个无效项去让它变得有效。

结论

        所以,不是操作系统不想帮单级页表,而是单级页表的结构设计,使得操作系统在帮忙时,其操作对象(无效的页表项)正好是导致它被呼叫的原因。这创造了一个先有鸡还是先有蛋的死结。

        而多级页表通过层级化,将“操作所需的工具”(页目录)和“被操作的对象”(下级页表)分离开来,并通过内核页表这个“万能钥匙”来安全地执行操作,从而完美地解决了这个问题。

这就是为什么多级页表是解决大地址空间管理问题的可行方案,而单级页表不是。


四、页目录结构深度解析

        在计算机内存管理机制中,随着对页表相关知识的深入探究,我们来到了一个关键环节——页目录结构。到目前为止,我们已经了解到每一个页框都由一个页表中的特定表项来指向,这意味着之前拆分得到的 1024 个页表需要被妥善管理起来,以确保整个内存映射系统能够高效、有序地运行。而承担这一管理重任的表,便是页目录表,由此也形成了二级页表这一重要的内存管理架构。下面将对其进行详细阐述。

1、二级页表的基本构成与指向关系

        在二级页表体系中,存在着清晰明确的指向关系。所有页表的物理地址都被页目录表项所指向,这就好比一个大型图书馆中,不同类别的书籍(页表)被整齐地摆放在不同的书架区域(由页目录表项指定位置),方便后续的查找和访问。

        而页目录的物理地址则由 CR3 寄存器来指向。CR3 寄存器在内存管理中扮演着至关重要的角色,它就像是一个精准的导航仪,其中保存了当前正在执行任务的页目录地址。通过这个寄存器,处理器能够快速定位到所需的页目录,进而找到对应的页表,最终实现虚拟地址到物理地址的准确转换。这种设计使得内存管理更加灵活和高效,能够适应不同程序的运行需求。

2、二级页表的工作原理示例

为了更好地理解二级页表的工作原理,我们可以举一个简单的例子:

  1. 假设当前系统正在运行一个用户程序,当处理器需要访问该程序中的某个数据时,它首先会根据程序当前的运行状态,从 CR3 寄存器中获取页目录的物理地址。

  2. 然后,处理器利用虚拟地址中的高位部分(用于索引页目录表项)在页目录中找到对应的表项,该表项指向了相应的页表。

  3. 接着,处理器再使用虚拟地址中的中间位部分(用于索引页表项)在页表中找到具体的页表项,这个页表项最终指向了包含所需数据的物理页框。

  4. 通过这样层层索引和指向,处理器能够准确地找到数据在物理内存中的位置,完成数据的访问操作。

3、操作系统加载用户程序时的内存分配任务

        在操作系统加载用户程序的过程中,内存分配是一项至关重要的工作。操作系统不仅仅需要为程序内容分配物理内存,以确保程序能够正常运行并存储其代码、数据等信息,还需要为用来保存程序的页目录和页表分配物理内存。

        这是因为页目录和页表是内存管理的重要数据结构,它们记录了虚拟地址到物理地址的映射关系。如果没有为页目录和页表分配足够的物理内存,那么整个内存映射系统将无法正常工作,处理器将无法准确地找到程序所需的数据和指令,从而导致程序运行出错甚至崩溃。

        具体来说,操作系统会根据程序的大小和内存需求,计算出所需的页目录和页表数量,并在物理内存中为其分配相应的空间。这些分配的物理内存需要满足一定的连续性和对齐要求,以确保页目录和页表能够正确地被访问和使用。同时,操作系统还会初始化页目录和页表中的内容,建立正确的映射关系,为程序的运行做好充分的准备。

4、二级页表的优势与意义

二级页表的设计带来了诸多优势:

  • 首先,它有效地解决了单级页表占用空间过大且需要连续物理内存的问题。通过将页表进行分级管理,只有在需要访问特定虚拟地址范围时,才加载相应的页表,减少了不必要的内存占用。

  • 其次,二级页表提高了内存的灵活性和可扩展性。不同的程序可以根据自身的需求使用不同数量和大小的页表,操作系统能够更加精细地管理内存资源,提高了内存的利用率。

  • 最后,二级页表为后续更高级的内存管理技术(如多级页表、页表缓存等)奠定了基础,使得计算机系统能够更好地应对日益复杂的内存管理需求,提升系统的整体性能和稳定性。

5、回顾与补充

在32位平台上,地址空间共有2^32个地址,这意味着需要建立2^32个地址的映射关系。

如果采用单级页表结构,那么该页表需要维护2^32个虚拟地址到物理地址的映射项,即页表中将包含2^32个表项。

每个表项不仅包含虚拟地址和对应的物理地址映射,还需要记录权限信息。例如,区分用户级页表和内核级页表的关键就在于这些权限设置。

每个页表项需要存储一个物理地址和一个虚拟地址,共占用8个字节。考虑到还需包含权限信息,每个表项按10字节计算。 在32位系统中,共有2^32个表项,这意味着存储整张页表需要2^32×10字节,即40GB空间。而32位平台的内存通常仅有4GB,显然无法完整存储这样的页表。

实际上,页表并非单一表格结构。

以32位平台为例,其页表映射流程如下:

  1. 使用虚拟地址的前10位在页目录中进行查找,定位到对应的页表

  2. 利用接下来的10位在对应页表中查找,获取物理内存页框的起始地址

  3. 将剩余的12位作为偏移量,从页框起始地址计算最终访问的物理内存字节

补充说明:

  • 物理内存被划分为4KB大小的页框

  • 磁盘程序同样按4KB为单位组织为页帧

  • 内存与磁盘的数据交换以4KB为单位进行

  • 4KB等于2^12字节,正好对应12位偏移量

  • 通过12位偏移量可以精确定位页框内的任意字节

这种结构就是我们所说的二级页表体系,其中页目录项构成一级页表,而页表项则形成二级页表。

        每个表项仍按10字节计算。由于页目录和页表都包含2^10个表项,因此单个表的大小为2^10 × 10字节,即10KB。页目录的2^10个表项意味着存在2^10个页表,也就是说整个系统包含1个一级页表和2^10个二级页表,总内存占用约10MB,这个消耗量在可接受范围内。Linux系统正是采用这种映射机制。

        整个地址映射过程由集成在CPU中的MMU(内存管理单元)硬件完成。页表提供软件层面的映射,而MMU则负责硬件层面的映射,因此虚拟地址到物理地址的转换实际上是通过软硬件协同实现的。

注:在Linux系统中,32位平台使用二级页表,而64位平台则采用多级页表结构。

思考:为何修改字符串常量会导致段错误?

        当尝试修改字符串常量时,虚拟地址需要通过页表映射查找对应的物理内存。在查表过程中,系统会检测到该内存区域的权限为只读。此时若尝试执行写操作,MMU内部就会触发硬件异常。操作系统识别到引发异常的进程后,会向该进程发送终止信号进行处理。

        综上所述,页目录结构作为二级页表的核心组成部分,在计算机内存管理中发挥着不可或缺的作用。它通过清晰的指向关系和合理的内存分配机制,实现了高效的虚拟地址到物理地址的转换,为程序的稳定运行提供了有力保障。


五、两级页表的地址转换深度剖析

        在计算机系统的内存管理机制中,地址转换是一项核心且关键的操作,它直接关系到处理器能否准确、高效地访问到所需的物理内存数据。下面,我们将以一个具体的逻辑地址为例,详细阐述在两级页表架构下,逻辑地址转换为物理地址的完整过程。

1、示例逻辑地址及地址结构划分

        假设我们有一个逻辑地址(0000000000,0000000001,11111111111),在 32 位处理器且采用 4KB 页大小的系统中,虚拟地址有着特定的结构划分:

  • 由于页大小为 4KB,即 2^12 字节,所以虚拟地址中的低 12 位被用作页偏移(Page Offset),用于定位在物理页内的具体字节位置。

  • 而剩下的高 20 位则分配给页表部分,并且为了实现两级页表管理,这 20 位被均匀地分成两级,每级各占 10 个比特(10 + 10)。

  • 这种巧妙的划分方式,使得系统能够以分级的方式管理页表,提高内存管理的灵活性和效率。

2、两级页表地址转换的具体步骤

1. 读取页目录起始地址

  • 处理器首先会从 CR3 寄存器中读取页目录(Page Directory)的起始地址。

  • CR3 寄存器是一个专门用于存储当前任务页目录物理地址的寄存器,它就像是内存管理的一个“导航起点”,为后续的页表查询提供了基础信息。

2. 一级页表查询

  • 根据逻辑地址中的一级页号(即高 20 位中的前 10 位),在页目录表中进行查询。

  • 通过这个查询操作,系统能够找到下一级页表(Page Table)在物理内存中的存放位置。

  • 这一步就像是在一本大型目录中,根据章节编号找到对应的子目录所在的位置。

3. 二级页表查询

  • 接着,利用逻辑地址中的二级页号(高 20 位中的后 10 位),在刚刚找到的二级页表中进行查询。

  • 通过这次查询,系统最终确定了想要访问的内存块号(Page Frame Number)。

  • 这一步类似于在子目录中,根据具体的条目编号找到对应的页面信息。

4. 生成物理地址

  • 在得到了内存块号之后,将其与逻辑地址中的页内偏移量相结合,就可以生成最终的物理地址。

  • 由于一个物理页的地址一定是 4KB 对齐的(即最后的 12 位全部为 0),所以在实际记录物理页地址时,只需要记录物理页地址的高 20 位即可,再加上页内偏移量,就完整地构成了物理地址。

3、MMU 的工作流程及性能考量

以上整个地址转换过程,实际上就是内存管理单元(MMU,Memory Management Unit)的工作流程。

  • MMU 是一种专门设计用于内存管理的硬件电路,它具有极高的运行速度,能够快速地完成地址转换等内存管理任务。

  • 然而,在两级页表的架构下,MMU 需要进行两次页表查询才能确定物理地址。

  • 在确认了权限等必要信息之后,MMU 才会将这个物理地址发送到总线,内存收到地址后开始读取对应地址的数据并返回给处理器。

        当页表变为 N 级时,这个过程就变成了 N 次检索加上 1 次读写操作。显然,页表的级数越多,查询的步骤就越多,对于 CPU 来说,等待时间也会相应延长,从而导致系统的整体效率降低。这就好比我们要查找一个文件,如果文件分类层级过多,我们需要逐层查找,花费的时间就会更多。

4、多级页表的利弊及优化方案

        单级页表在内存管理上存在一个明显的缺点,即对连续内存的要求较高。为了解决这个问题,系统引入了多级页表。多级页表确实在一定程度上减少了连续存储的要求,并且节省了存储空间。然而,它也带来了一些负面影响,其中最主要的就是查询效率的降低。

那么,有没有办法在享受多级页表优势的同时,提升地址转换的效率呢?

  • 在计算机科学中,有一个经典的解决思路,那就是通过添加一个中间层来解决问题。

  • MMU 引入了一个强大的“武器”——转译后备缓冲器(TLB,Translation Lookaside Buffer),在江湖上也被亲切地称为快表。

  • 从本质上来说,TLB 就是一种缓存,它专门用于存储最近使用过的虚拟地址到物理地址的映射关系。

5、TLB 的工作原理及作用

  1. 当 CPU 向 MMU 传递一个新的虚拟地址时,MMU 首先会去询问 TLB 是否存在该虚拟地址对应的物理地址映射。

  2. 如果 TLB 中存在(即命中,Cache Hit),MMU 可以直接从 TLB 中获取物理地址,并将其发送到总线,通知内存进行数据读取操作,整个过程非常迅速。

  3. 然而,由于 TLB 的容量相对较小,难免会出现缓存未命中(Cache Miss)的情况。

  4. 当发生这种情况时,MMU 会回退到传统的页表查询方式,在页表中找到对应的映射关系。

  5. 除了将地址发送到总线通知内存读取数据外,MMU 还会将这条新找到的映射关系记录到 TLB 中,以便下次查询时能够快速命中,从而刷新缓存,提高后续查询的效率。

        通过引入 TLB,系统在多级页表的基础上,有效地提升了地址转换的速度,在一定程度上缓解了多级页表查询效率低的问题。这种硬件与软件相结合的内存管理方式,使得计算机系统能够在复杂的内存环境中高效、稳定地运行。


六、缺页异常深度解析

        在计算机系统的运行过程中,内存管理是一个至关重要的环节,而缺页异常则是内存管理中一个常见且关键的现象。下面我们将详细探讨缺页异常的产生原因、处理机制以及与之相关的一些重要概念。

1、缺页异常的产生场景

设想这样一个场景:

  1. CPU 向 MMU(内存管理单元)传递一个虚拟地址,期望通过地址转换获取对应的物理地址以访问内存中的数据。

  2. 然而,MMU 在 TLB(转译后备缓冲器,用于缓存虚拟地址到物理地址的映射关系)和页表中都没有找到与该虚拟地址对应的物理页。

  3. 在这种情况下,系统就会触发缺页异常(Page Fault)。

缺页异常本质上是一个由硬件中断触发的错误,但这个错误可以通过软件逻辑来进行纠正。

  • 当目标内存页在物理内存中不存在对应的物理页,或者虽然存在但当前进程没有相应的访问权限时,CPU 就无法获取到所需的数据。

  • 由于 CPU 的运行依赖于数据,没有数据就无法进行计算,这就好比工人没有原材料就无法开展工作。

  • 此时,CPU 会“罢工”,导致用户进程出现缺页中断。

  • 进程会从用户态切换到内核态,并将缺页中断交给内核的 Page Fault Handler(缺页异常处理程序)进行处理。

2、缺页异常的类型及处理方式

缺页中断会交由 PageFaultHandler 进行处理,根据缺页中断的不同类型,处理程序会采取不同的处理措施:

1. Hard Page Fault(硬缺页错误/主要缺页错误)

  • 当物理内存中没有对应的物理页时,就会发生硬缺页错误。

  • 这种情况通常是因为所需的数据不在物理内存中,而是存储在磁盘上。

  • 此时,CPU 需要打开磁盘设备,将所需的数据从磁盘读取到物理内存中。

  • 读取完成后,MMU 再建立虚拟地址和物理地址的映射关系,使得 CPU 能够通过虚拟地址访问到这些数据。

  • 例如,当一个程序首次运行时,其代码和数据可能还存储在磁盘的交换(Swap)分区中,此时就会触发硬缺页错误,系统将数据从磁盘加载到内存。

2. Soft Page Fault(软缺页错误/次要缺页错误)

  • 与硬缺页错误不同,软缺页错误发生时,物理内存中其实已经存在对应的物理页,只是发出缺页异常的进程不知道而已。这种情况一般出现在多进程共享内存区域。

  • 例如,多个进程共享同一段代码或数据,当一个进程访问该共享区域时,如果其他进程已经将其调入内存,而当前进程的页表中没有相应的映射,就会触发软缺页错误。此时,MMU 只需要建立虚拟地址和物理地址的映射关系即可,无需从磁盘读取数据写入内存,因此处理速度相对较快。

3. Invalid Page Fault(无效缺页错误)

  • 无效缺页错误通常是由于进程访问了非法的内存地址导致的。

  • 比如,进程访问的内存地址越界,或者对空指针进行解引用等操作。当发生这种情况时,内核会报告 segment fault 错误,并直接中断进程的执行,因为这种错误是无法通过简单的内存操作来纠正的,可能会导致系统的稳定性和安全性受到威胁。

3、与缺页异常相关的重要问题探讨

1. 对 new 和 malloc 的理解

  • 在编程中,我们经常使用 new(C++)和 malloc(C)来申请内存。

  • 从内存管理的角度来看,这些操作实际上是在向操作系统请求分配一定大小的虚拟内存空间。

  • 操作系统会根据当前的内存情况,为用户进程分配相应的虚拟地址范围,但此时并不一定会立即分配物理内存。

  • 只有当进程实际访问这些虚拟内存时,如果发生缺页异常,操作系统才会根据具体情况分配物理内存或从磁盘加载数据。

2. 对写时拷贝的理解

  • 写时拷贝(Copy - On - Write,COW)是一种优化内存使用的技术。

  • 在多进程环境中,当多个进程需要共享同一块内存数据时,操作系统并不会立即为每个进程创建一份独立的物理内存副本。

  • 而是让这些进程共享同一块物理内存,只有在某个进程尝试修改这块共享内存时,才会为该进程创建一份独立的副本。

  • 这样可以减少不必要的内存复制,提高内存的使用效率。当发生写操作触发缺页异常时,操作系统会判断是否需要进行写时拷贝操作。

3. 申请内存的实际操作

  • 申请内存并不仅仅是简单地分配一块连续的物理内存空间。

  • 实际上,它主要是为用户进程分配一定范围的虚拟地址空间,并建立相应的页表项。

  • 初始时,这些虚拟地址可能并没有对应的物理页,只有在进程实际访问这些地址时,才会根据缺页异常的处理机制来分配物理内存或加载数据。

4. 区分缺页和越界的方法

  • 页号合法性检查:操作系统在处理中断或异常时,首先会检查触发事件的虚拟地址的页号是否合法。如果页号在合法的范围内,但对应的页面不在内存中,那么就是缺页中断;如果页号超出了合法的范围,那么就是越界访问。

  • 内存映射检查:操作系统还可以检查触发事件的虚拟地址是否在当前进程的内存映射范围内。如果地址在映射范围内,但对应的页面不在内存中,那么就是缺页中断;如果地址不在映射范围内,那么就是越界访问。

5. 越界访问是否一定会报错

  • 一般情况下,越界访问会导致无效缺页错误,内核会报告 segment fault 错误并中断进程。

  • 但在某些特殊的编程场景或操作系统实现中,可能会有一些特殊的处理机制。

  • 例如,某些嵌入式系统或特定的内存管理策略可能会对越界访问进行一定的容错处理,但这并不是普遍情况。

  • 在大多数现代操作系统中,越界访问是严格禁止的,会导致进程被终止。

4、线程资源划分与虚拟地址空间的关系

  • 只要将虚拟地址空间进行划分,进程资源就天然被划分好了。

  • 这是因为每个进程都有自己独立的虚拟地址空间,操作系统通过管理虚拟地址空间来实现对进程资源的隔离和保护。

  • 线程作为进程内的执行单元,共享进程的虚拟地址空间,但操作系统可以通过对虚拟地址空间的细分和权限管理,为不同的线程分配不同的资源访问权限,从而实现线程资源的有效划分。

  • 例如,操作系统可以为不同的线程分配不同的栈空间,通过虚拟地址的范围来限制线程对栈的访问,确保线程之间的数据安全和独立性。

        综上所述,缺页异常是内存管理中的一个重要概念,理解其产生原因、处理机制以及与之相关的各种问题,对于深入掌握计算机系统的内存管理和编程实现具有重要的意义。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐