基于页的管理方式
大部分操作系统,内存都是以4kB为单位的块。文件系统也是4KB的代码块。一个可执行程序是以平坦模式进行整体编制的,可执行程序内部也划分为4KB的数据块。因此磁盘和内存交互的时候,就以4KB为基本单位。4KB的物理数据空间称为页框。
首先,Linux(以及大多数现代操作系统)使用虚拟内存来管理进程的地址空间,但实际的物理内存分配并不总是严格地以4KB为单位。然而,在硬件层面,CPU的内存管理单元(MMU)和页表确实以页为单位进行操作,对于许多系统(包括x86架构)来说,这通常是4KB。这意味着CPU的内存操作,例如页错误(page fault)或页面置换(page swapping),是以4KB的页为单位的。
在内存管理方面,页框(page frame)是物理内存中的一个固定大小的块,通常为4KB。操作系统的内存管理单元(MMU)使用页表来将进程的虚拟地址空间中的页映射到物理内存中的页框。当进程需要访问某个虚拟地址时,MMU会查看页表来确定该地址对应的物理内存位置,并将CPU的指令或数据重定向到正确的物理地址。
在文件系统方面,尤其是Unix和类Unix系统(如Linux)中常见的那些(如ext4、XFS、Btrfs等),通常也使用块(block)或簇(cluster)作为存储数据的基本单位。这些块或簇的大小可能因文件系统和配置而异,但4KB是一个常见的选择。
关于可执行程序,它们是以平坦模式进行编制的,这意味着程序的所有部分(代码、数据、堆、栈等)都位于一个连续的虚拟地址空间中。当可执行程序被加载到内存中执行时,它的代码和数据会被映射到进程的虚拟地址空间中的页。这些页的大小与操作系统的页大小相同,通常为4KB。程序的各个部分(如代码段、数据段等)可能会跨越多个内存页。但是,当这些页面被加载到物理内存或从物理内存中交换出去时,它们是以4KB(或其他页面大小)为单位进行的。
当操作系统需要从磁盘加载数据到内存或从内存写回数据时,它通常以页为单位进行操作。这是因为磁盘I/O操作(特别是旋转式硬盘)的性能通常是按块进行的,而内存页的大小与这些块的大小相匹配,可以提高数据传输的效率。
这种基于页的管理策略有助于操作系统更有效地管理和使用这些资源,提高系统的整体性能。
Linux操作系统如何管理内存?
Linux操作系统通过复杂的内存管理系统来管理物理内存和虚拟内存。在这个系统中,物理内存被抽象为struct page
结构体,这是Linux内核中表示物理内存页的基本数据结构。
内存管理的一个关键组成部分是内存页(memory page)。物理内存被划分为固定大小的页(通常是4KB),每个页都由一个struct page
来表示。这个结构体包含了关于内存页的各种信息,如页是否在使用、页的内容是否在磁盘上(即交换出去)、页被哪个进程或哪些进程使用等。
以下是struct page
可能包含的一些字段(注意:这只是一个简化的例子,实际的struct page
可能包含更多的字段):
struct page {
// 标志位,表示页的状态(如:是否脏、是否锁定、是否在LRU列表中等)
unsigned long flags;
//引用计数,允许内存块被多个进程使用
atomic_t _count;
// 页框号(PFN, Page Frame Number),物理内存页的标识
unsigned long pfn;
// 指向该页所属的内存区域的指针
struct zone *zone;
// 指向该页的引用计数(多少个进程或数据结构引用了这个页)
atomic_t _count;
// 页的引用者列表(如反向映射表,用于追踪哪些PTE指向了这个页)
struct list_head lru;
// 指向该页在交换空间(swap space)中的位置的指针(如果页被交换出去了)
swp_entry_t swap_entry;
// ... 其他字段,如页表项(PTE)的引用、私有数据等
};
操作系统管理内存的主要任务包括:
-
内存分配和回收:当进程请求内存时,操作系统会为其分配一个或多个内存页。当内存不再需要时,操作系统会回收这些页并放回空闲页列表中以供重用。
-
内存映射:操作系统维护一个从虚拟地址到物理地址的映射表(页表)。当进程访问某个虚拟地址时,操作系统会查找页表以确定对应的物理地址。
-
内存保护:操作系统确保进程只能访问其被授权访问的内存区域。如果进程尝试访问一个它没有权限访问的内存地址,操作系统会触发一个页错误(page fault),并可能终止该进程。
-
交换空间(Swap Space)管理:当物理内存不足时,操作系统可以将一些不常用的内存页交换到磁盘上的交换空间中,以释放物理内存供其他进程使用。当这些页再次被需要时,它们会被从磁盘上读回内存。
-
内存压缩和碎片整理:为了减少内存碎片(即小块的空闲内存),操作系统可能会使用内存压缩技术将多个小块内存合并成一个更大的内存块。此外,操作系统还可能会进行碎片整理以优化内存布局。
-
性能优化:操作系统会采取各种策略来优化内存使用性能,如使用缓存(如TLB、页缓存等)来加速内存访问、使用写回策略来减少磁盘I/O等。
通过将这些任务抽象为数据结构和算法(如struct page
和相关的内存管理算法),操作系统能够高效地管理内存资源,确保系统的稳定性和性能。
内存管理的主要组件
- 页表(Page Tables):页表用于将进程的虚拟地址映射到物理地址。每个进程都有自己的页表,它告诉操作系统如何将进程的虚拟内存地址转换为物理内存地址。
- 内存区域(Memory Regions):Linux使用
mm_struct
结构体中的struct vm_area_struct *mmap
来表示进程的虚拟内存区域。这些区域定义了进程地址空间中的不同部分,包括代码、数据、栈、堆和动态加载的库等。mmap
指向进程虚拟内存区域列表的指针。这个列表包含了进程地址空间中的所有内存区域,每个区域都由一个vm_area_struct
结构体表示。 - 内存区域树(Memory Region Trees):为了高效地管理进程的虚拟内存区域,Linux使用红黑树(Red-Black Trees)来组织这些区域。这种数据结构允许内核快速查找、插入和删除内存区域。
- 伙伴系统(Buddy System):伙伴系统是一种内存分配算法,用于管理物理内存页框。它将空闲的页框组织成不同大小的块,以满足不同大小的内存分配请求。当进程请求内存时,伙伴系统会找到一个合适大小的块并分配给进程;当进程释放内存时,伙伴系统会尝试将释放的页框合并回更大的块中。
- 交换空间(Swap Space):当物理内存不足时,Linux会将一些不常用的内存页交换到磁盘上的交换空间中,以释放物理内存供其他进程使用。这个过程称为“页面交换”(paging out)或“页面调出”(swapping out)。当这些页需要再次被访问时,它们会被从磁盘上读回内存,这个过程称为“页面换入”(paging in)或“页面调入”(swapping in)。
- 内存保护(Memory Protection):Linux的内存管理系统还提供了内存保护功能,以防止进程访问其他进程的内存或访问不允许访问的内存区域。这通过页表中的一些特殊位来实现,如只读位、用户/内核模式位等。
内存管理的主要流程
- 进程创建:当一个新的进程被创建时,Linux会为该进程分配一个虚拟地址空间,并初始化其页表和内存区域树。
- 内存分配:当进程需要分配内存时(例如,通过
malloc()
或new
等函数),它会向内核发出请求。内核会查找伙伴系统中的空闲块来满足这个请求,并在进程的虚拟地址空间中分配一个新的内存区域。然后,内核会更新页表以反映这个新的映射关系。 - 内存访问:当进程试图访问一个虚拟地址时,CPU会查看页表来确定该地址对应的物理地址。如果页表中有对应的项,则CPU可以直接访问物理内存;否则,CPU会触发一个页面错误异常,并调用内核中的页面错误处理程序来处理这个异常。
- 页面错误处理:页面错误处理程序会检查引发页面错误的原因,并尝试修复它。例如,如果页面错误是由于进程试图访问一个尚未映射的虚拟地址引起的,则处理程序会尝试从磁盘上加载该页并更新页表;如果页面错误是由于物理内存不足引起的,则处理程序可能会选择将某个不常用的页交换到磁盘上,并释放其占用的物理内存。
- 进程终止:当一个进程终止时,内核会释放其占用的所有物理内存页,并将这些页标记为空闲状态,以便其他进程可以使用它们。
二级页表的内存管理方式
32位下,虚拟地址空间通常被划分为4GB(232字节)。
在Linux内核中,物理内存被划分为多个固定大小的页框(通常是4KB),每个页框由一个struct Page
实例来表示。这些struct Page
实例存储在内存中,但并不是与虚拟地址空间中的每个4KB块一一对应。相反,它们用于跟踪物理内存的状态和属性。
在32位系统中,虽然虚拟地址空间是4GB,但物理内存的大小通常远小于这个数值。因此,struct Page
实例的数量将取决于物理内存的大小,而不是虚拟地址空间的大小。
假设物理内存也是4GB,并且页大小为4KB,那么将会有1M(4GB / 4KB = 220)个页框,也就需要1M个struct Page
实例来管理这些页框。然而,在实际系统中,物理内存的大小可能远小于4GB,因此struct Page
实例的数量也会相应减少。
在Linux中,内存管理的基本单位通常是4KB的页(page)。物理内存被划分为固定大小的页,每个页都由一个struct page
结构体来表示,这个结构体包含了关于内存页的各种信息。
当进程请求内存时,操作系统会为其分配一个或多个内存页。同样,当内存不再需要时,操作系统会回收这些页并放回空闲页列表中以供重用。
此外,虚拟内存管理也依赖于页的概念。每个进程都有自己的虚拟地址空间,这个空间被划分为固定大小的页。进程的页表记录了虚拟地址到物理地址的映射关系,使得进程可以访问其虚拟地址空间中的任意页。
页和页框的区别:
物理内存被划分为固定大小的块,称为页框(Page Frame)。对于每个物理页框,内核中都有一个
struct page
实例来跟踪其状态。而struct page
实例存储在内核的内存中,并且与物理页框之间存在一对一的映射关系。虚拟内存也被划分为相同大小的块,称为页(Page)。每个进程都有它自己的虚拟地址空间,这个空间被划分为多个页。
在32位系统中,如果没有使用多级页表(也称为分页结构或页目录/页表结构),那么整个虚拟地址空间将需要一个巨大的单级页表来映射。
- 页大小:页大小为4KB(212字节)。
- 页表项(PTE, Page Table Entry):一个页表项对应一个页,因此它存储了一个页的物理地址以及该页的访问权限和其他属性。对于大多数系统,页表项的大小是固定的,通常是4字节(32位)。
- 虚拟地址空间:对于32位系统,虚拟地址空间是4GB(232字节)。
- 所需页表项数量:没有多级页表的情况下,整个虚拟地址空间需要 232 / 212 = 220 个页表项来映射。
- 页表大小:如果每个页表项是4字节,那么整个页表的大小将是 220 * 4字节 = 222字节 = 4MB。
那么为什么不使用一级页表而要使用多级页表?
我们从上文可以发现,一个一级页表是4MB,每个进程的页表都是独立的,也就是每个进程都要占用4MB的物理内存。也就是一下原因:
- 节省页表内存:当虚拟地址空间非常大时,一级页表需要占用大量的连续内存空间来存放页表项。然而,在大多数情况下,进程可能只使用了虚拟地址空间中的一部分。多级页表通过只为进程实际使用的那些虚拟地址内存区请求页表,从而显著减少了内存使用量。
- 离散存储:多级页表允许页表在内存中离散存储,这意味着页目录项和页表项不需要连续存放。这在操作系统内存紧张或内存碎片较多时特别有用,因为它可以避免由于寻找连续内存空间而导致的额外开销。
- 支持大虚拟地址空间:对于具有大虚拟地址空间的系统,一级页表在支持大虚拟地址空间时会导致页表过大。而多级页表通过将页表进行分级,可以更好地支持大虚拟地址空间。通过增加索引的层数,系统可以更加灵活地管理虚拟地址空间。
因此,在实际操作中,大多数现代操作系统(包括Linux)都使用了多级页表来减少内存的使用和提高地址转换的效率。一个二级页表结构,包括一个页目录(Page Directory)和多个页表(Page Tables)。页目录中的每个条目指向一个页表,而页表中的每个条目则指向一个物理页框。这种结构使得只有实际使用的内存区域才需要在页表中有对应的条目,从而大大减少了内存的使用。
下面我们回到正题,来介绍二级页表:
在32位系统中,虚拟地址空间经常被划分为三个部分:页目录索引(高位)、页表索引(中间位)和页内偏移(低位)。
- 虚拟地址空间是32位,即4GB。
- 虚拟地址被划分为三部分:
- 页目录索引(高位10位)
- 页表索引(中间10位)
- 页内偏移(低位12位)
我们再来阐述几个概念:
页目录
- 页目录一共有
2^10 = 1024
项(因为页目录索引是10位)。在典型的二级页表结构中,一个进程通常只有一个页目录。这个页目录包含了指向一个或多个页表的指针,这些页表进一步映射了虚拟地址空间到物理内存。 - 每项(称为页目录项)存储一个页表的物理地址。每个页目录项通常包含指向页某个表的物理地址和其他一些标志位(如存在位、写保护位等)。
- 页目录项的大小通常与处理器架构和操作系统实现有关,但在32位系统中,它通常是4字节(32位)。
页表
- 每个页表也有
2^10 = 1024
个项(因为页表索引是10位)。 - 页表项中存储了物理页帧的号码(也称为页框号或物理页面号),该号码用于标识物理内存中的特定页面。(或者在某些情况下,可能是指向另一个页表的地址,形成三级或更多级的页表结构)。
- 页表项的大小也取决于具体的处理器架构和操作系统,但在32位系统中,它通常是4字节(32位)。(页表的数量取决于进程的虚拟地址空间大小和页表的大小。在二级页表结构中,每个页目录项都指向一个页表。因此,页表的数量至少与页目录中的项数相同。)
下图描述的是一个常见的二级页表(也称为分页结构)的设计,
下面我们来根据上图,对32位系统中基于二级页表的虚拟内存到物理内存地址转换的过程做出的解释:
-
从32位虚拟地址中取出最高的10位作为页目录索引。
-
使用这个索引在页目录中找到对应的页目录项。
-
从页目录项中取出页表的物理地址。一旦找到了对应的页目录项,就可以从中读取页表的物理地址。
-
从虚拟地址中取出中间的10位作为页表索引。
-
使用这个索引在页表中找到对应的页表项。使用从虚拟地址中提取的页表索引,可以在之前从页目录项中获取的页表中查找到对应的页表项。
-
从页表项中取出物理页帧的号码。
-
最后,从虚拟地址中取出最低的12位作为页内偏移,表示在物理页面内的相对位置。
-
将物理页帧的号码和页内偏移组合起来形成物理地址。物理页帧的号码通常是一个大数值,而页内偏移是一个相对较小的数值。将物理页帧的号码乘以页面大小(例如,对于4KB页面,页面大小为4096字节),然后加上页内偏移,就可以得到最终的物理地址。
这个过程是虚拟内存管理的一个关键部分,它允许操作系统为每个进程提供独立的虚拟地址空间,同时有效地管理和使用物理内存。
最后我们来计算存储页表所需的字节数:
首先,页面大小是4KB(即4096字节或2^12字节),这意味着页内偏移需要12位来表示。
下面,我们计算页表的大小。由于页表索引是10位,所以一个页表可以有210个PTE。每个PTE是4字节,所以一个页表的大小是 2^10 * 4 = 4096 字节(即4KB)。
由于我们使用的是二级页表结构,我们还需要考虑页目录的大小。页目录索引是10位,所以页目录可以有210个页目录项(PDE, Page Directory Entry)。每个PDE通常包含页表的物理地址和一些标志位,但在这里我们主要关心的是它指向页表的物理地址。通常,PDE的大小也是4字节(在32位系统中)。因此,页目录的大小是 210* 4 = 4096 字节(即4KB)。
但是,请注意,这并不意味着整个页表结构只占用8KB(4KB页表 + 4KB页目录)。这是因为每个PDE指向一个完整的页表,所以整个页表结构的大小取决于系统中有多少物理内存页面以及每个进程需要多少虚拟内存页面。然而,对于给定的进程,其页表结构(包括所有相关的页表和页目录)的大小将是多个4KB的页表和一个4KB的页目录的总和。
那么在最坏的情况下,我们发现一级页表和二级页表的占用的内存大小是相同的,为什么说多级页表可以节省空间呢?
在最坏的情况下,即所有虚拟地址空间都被使用时,一级页表和二级页表(或其他多级结构)的总内存占用可能会相似,因为都需要足够的条目来映射整个虚拟地址空间。但是,这种“最坏情况”在现实中并不常见,而且多级页表结构通常能够在许多场景下节省内存空间,原因有以下几点:
- 内存稀疏分配:在多级页表结构中,页目录中的条目可以指向一个空页表(或标记为未使用的页表),这意味着只有在物理内存被实际分配并映射到虚拟地址空间时,相应的页表条目才会被创建和占用内存。而在一级页表结构中,即使某些虚拟地址空间没有被使用,也需要为它们保留页表条目。
- 局部性原理:应用程序的内存使用往往具有空间局部性,即一段时间内经常访问的虚拟地址通常集中在较小的地址范围内。多级页表结构可以利用这种局部性,通过只加载和保留当前需要的页表来节省内存。相比之下,一级页表需要一次性加载整个页表,即使其中大部分条目在当前阶段都是不必要的。
- 支持大虚拟地址空间:随着计算机硬件的发展,虚拟地址空间的大小不断增加。对于具有非常大虚拟地址空间的系统(如64位系统),使用多级页表结构可以更有效地管理内存,因为它可以避免一次性加载和存储整个巨大的页表。
- 可扩展性:多级页表结构允许在将来根据需要添加更多的层级。这种灵活性使得系统能够轻松地适应不同的虚拟地址空间大小和内存配置。
因此,虽然在最坏的情况下一级页表和二级页表的内存占用可能相似,但在实际使用中,多级页表结构通常能够通过上述机制来节省内存空间并提高系统的性能和可扩展性。