内存管理
参考:《openEuler操作系统》
内存管理的目标
- 多进程并发的如何安全高效的共享内存
- 提高内存利用率和内存寻址效率
内存管理的技术
- 引入虚拟内存,使进程对内存地址的访问从直接变为间接,实现了进程地址空间的隔离
- 引入分页机制,实现细粒度的动态内存分配和管理,有效减少了内存碎片,提高了内存利用率
- 通过TLB(地址转换旁路缓存)和多级页表等机制,实现内存快速寻址,提升了内存寻址效率
- 利用外存对物理内存进行扩充,使得实际内存需求量大于剩余物理内存容量的进程依然能在操作系统中顺利运行
1 虚拟内存:程序通过间接的方式访问内存
内存(Memory)是 CPU 能直接寻址的存储器。内存的每一个字节单元都被分配了一个唯一的存储器地址,称为物理地址(Physical Address)。程序只有被加载到内存后,CPU才能从内存中读取指令和数据。
在早期的计算机中,程序访问的内存地址都是实际的物理地址,比如洗衣机、冰箱这样的嵌入式系统。如果只是运行一些确定的程序,这种直接访问物理地址的方式可以满足需求。但是在多进程并发的场景中,这种方式是有问题的。因为物理地址是由硬件提供的,程序直接访问硬件,很容易互相造成干扰,导致程序异常甚至系统崩溃。
Linux 使用虚拟内存机制具有以下优势:
-
隔离性:每个进程都有自己的虚拟地址空间,不同进程之间的地址空间相互隔离,一个进程无法直接访问其他进程的虚拟地址空间。这样可以确保进程之间的数据不会互相干扰。
-
保护性:通过虚拟内存机制,操作系统可以为每个页面设置访问权限,如只读、读写、执行等。这样可以防止进程访问无权限的内存区域,提高系统的安全性。
-
虚拟化:虚拟内存使得进程可以使用一致的虚拟地址空间,无论实际物理内存的大小和分布如何。这样可以使程序在不同的硬件平台上运行,提高了可移植性。
-
内存管理:虚拟内存机制允许操作系统对内存进行动态管理,包括内存分配、释放和页面置换等。这样可以优化内存使用,提高系统的性能。
2 分页机制:进程以页为单位装入内存
如果将地址空间划分成不同长度的分区,每个进程被加载到其中一个分区中运行。那么随着进程不断的被分配、回收、再分配,必然会出现严重的内存碎片问题:所需内存较小的进程会不断被加载到原先的分区中,后面的进程就会出现无法正常加载的情况。
于是有了内存的分页机制,其基本思想是:将进程的虚拟地址空间分割成固定长度的单元(通常取4KB),称为页(Page);将物理地址空间也分割成固定长度的单元,称为页框(Page Frame);页与页框长度相等。 在这种内存管理方式下,进程在被装入内存时,不再以整个进程为单位,而是以页为单位,所以进程不再需要存放在一块连续的物理地址空间。
分页机制减少了内存碎片(外部碎片和内部碎片),而且不同的进程可以将虚拟地址映射到同一个页框,实现页的共享。例如,多个进程可以选择映射同一段可重定位的代码。
3 TLB:缓存最近的虚拟地址映射
进程使用的是内存的虚拟地址,而CPU使用物理地址,CPU 通过 MMU 完成虚拟地址到真实地址的转换。如果 MMU 在地址转换时每次都需要先从内存中查表,然后CPU才可以执行,那么程序运行的速度会成倍的下降。当操作系统想要使某个过程更快、更高效时,往往求诸于硬件的辅助。
TLB(Translation Lookaside Buffer)是 MMU 芯片中的一个硬件模块,用来缓存最近的虚拟地址到物理地址的映射。当 CPU 访问内存时,MMU 会将虚拟地址发送给 TLB 进行查找,如果 TLB 中有对应的物理地址,就直接返回给 CPU;如果 TLB 中没有对应的物理地址,MMU 就会访问页表,并将转换结果存入 TLB 以供以后使用。
TLB 的大小是有限的,当 TLB 不能容纳所有的页表项时,就可能出现 TLB miss(未命中)的情况,此时需要访问页表进行地址转换。当发生 TLB miss 时,会引起额外的访存开销。
至于为什么增加缓存可以加快地址转换过程,可以参考程序执行的局部性原理,即程序执行时,查询映射关系会以很高的概率在TLB中命中。
TLB的存储空间大小有限(约16~512个记录),当空间用尽后需要增加新的记录时,会基于一定的算法替换出一个旧记录。TLB的维护完全由硬件来完成,相应的TLB替换策略也固定在硬件中。
4 多级页表:减小进程页表的开销
进程的虚拟地址空间,在32位系统中一般为4G,在64位的系统中可能是512G。假设在页表中,每个记录的大小是4B,那么即便以4G来计算,也需要4MB的空间来存储页表。多级页表的基本思想是,将页表分成页大小的单元,只将有效的单元保留在内存中;引入页目录来标记当前目录是否包含有效的单元,以及有效单元所在的位置。
附录
空闲内存管理(物理内存)
buddy(伙伴)系统:在分配页框之初就将页框以连续内存的形式组织起来;在页框分配时尽可能按所需连续页框的数目分配对应数量的页框;在页框回收时尽量将页框合并为连续的页框。
在buddy系统中,2的i次方个连续页框块称为i阶页框块。伙伴系统将满足以下条件的两个i阶页框块定义为伙伴关系:
- 两个页框大小相同并且物理地址连续
- 合并后的i+1阶页框块,第一个页框的编号必须要为2的i+1次方的整数倍
优缺点:
- 优点:原理简单,可分配连续的物理内存
- 缺点:内部碎片问题严重。
buddy系统按2的i次方分配内存,且仅考虑与伙伴块合并。
PS... 这里让我回想起之前在华为实验室管理服务器的欢乐时光。实验室几百台服务器和配套的交换机,怎么分配回收使之利用率最大,这一直是个问题。最后形成的按机柜组织划分资源的方案与伙伴系统有点相似的。
基于MMU辅助的地址转换
- CPU核向MMU发送虚拟地址,以22位为例,前10位表示页号,后12位表示页内偏移
- MMU取该地址的前10位表示的页号,查询页表基址寄存器保存的页表基址,获得PTE的物理地址
- 读取PTE,获得页框号
- MMU根据页框号 * 页大小 + 虚拟地址中后12位表示的页内偏移的计算出物理地址
- MMU将该物理地址发送到地址总线上,进行访存操作
PS... 因为页表一般使用的是连续的物理地址,所以页表中不需要保存页号:从页表基址开始,每增加1个 sizeof PTE 的地址 页号增加1。
地址转换中操作系统的职责
- 为进程建立页表,保存页表基址
- 在进程开始运行时,将页表基址存放到页表基址寄存器中
- 处理访存异常
内存访问控制
分页机制通过硬件和操作系统协同控制内存访问。
因为每次地址转换都需要通过页表查询到PTE,所以可以在PTE中添加一些额外的属性位来实现权限的检查。在通过TLB实现地址转换的情况中,TLB的条目中也有一个标识进程的字段。
linux 是怎么对内存进行分页的
在 Linux 中,内存分页是通过页表和分页机制实现的。具体来说,Linux 将虚拟地址空间划分为大小固定的页,每个页的大小通常为4KB或2MB。物理内存也按照相同的页大小进行划分,每个物理页称为页面框(Page Frame)。
Linux 使用多级页表(Multi-Level Page Table)结构来管理页表,以支持大的虚拟地址空间。多级页表将页表划分为多个层级,每个层级都有自己的页表。通常情况下,Linux 使用三级页表结构,称为页全局目录(Page Global Directory)。
以下是 Linux 对内存进行分页的过程:
-
初始化:在系统启动时,Linux 内核会初始化页全局目录,并设置好初始的页表项。
-
分配和映射:当进程需要访问虚拟地址时,Linux 内核会根据进程的页表将虚拟地址映射到物理地址。如果虚拟页面已经在物理内存中,内核会将对应的页表项指向物理页面框。如果虚拟页面不在物理内存中,内核会将其从磁盘中读取到物理内存。
-
页面置换:当物理内存不足时,Linux 内核会使用页面置换算法来选择一个页面进行置换,以腾出空间给新的页面使用。常用的页面置换算法包括最近未使用(LRU)算法和时钟(Clock)算法。
-
页面回写:当页面被置换出物理内存时,如果它被修改过,Linux 内核会将其回写到磁盘上的交换空间,以保持数据的一致性。
-
内存释放:当进程不再需要某个虚拟地址范围时,Linux 内核会将对应的页表项释放,并回收物理内存。
通过分页机制,Linux 实现了虚拟内存管理、内存保护和内存共享等功能。
页表项
在 Linux 中,PTE(Page Table Entry)是页表项,用于描述虚拟地址和物理地址之间的映射关系。每个页表项记录了一段虚拟地址范围映射到的物理地址和相关的控制信息。
每个页表项的大小通常是 8 字节或者 4 字节,一个典型的 Linux PTE 结构包含以下字段:
- 物理页框号(Physical Page Frame Number,PFN):该字段记录了虚拟地址映射到的物理页框的物理页框号。通过该字段可以确定要访问的物理内存位置。
- 访问权限(Access Permissions):该字段指示了对该页的访问权限。包括读权限、写权限和执行权限。
- 页面标志(Page Flags):该字段包含了一系列的标志位,用于描述该页的属性和状态。例如,标志位可以指示该页是否被修改过、是否为只读页、是否为用户态可访问等。
- 其他控制信息(Other Control Information):该字段包含一些其他的控制信息,如页面大小、缓存策略等。
大页内存
多级页表
在 Linux 中,多级页表是一种用于管理虚拟地址和物理地址之间映射关系的页表结构。多级页表将页表分为多个层级,每个层级都有自己的页表,从而实现了更高效的地址转换和内存管理。
Linux 通常使用三级或四级页表结构,具体的页表层级数量取决于系统的架构和配置。以下以三级页表为例进行说明:
-
第一级页表:也称为页全局目录(Page Global Directory)。该级别的页表用于将虚拟地址空间分成多个区域,每个区域都有自己的第二级页表。
-
第二级页表:也称为中间页表(Intermediate Page Table)。每个页全局目录项指向一个中间页表,每个中间页表可以管理一个较大的虚拟地址范围。
-
第三级页表:也称为页表(Page Table)。每个中间页表项指向一个页表,每个页表项对应一个虚拟页面,并记录了虚拟地址和物理地址之间的映射关系。
通过多级页表结构,Linux 实现了灵活的地址转换和内存管理。当进程访问虚拟地址时,Linux 内核首先使用页全局目录将虚拟地址划分到对应的中间页表。然后使用中间页表将虚拟地址划分到对应的页表。最后,使用页表找到对应的物理地址。
多级页表的优势在于节省内存空间。因为虚拟地址空间通常很大,一级页表无法容纳所有的页表项。通过将页表分层级,可以根据需要动态分配和管理页表项,避免了不必要的内存开销。
需要注意的是,多级页表的层级数量会影响地址转换的速度和内存开销。层级数量越多,地址转换需要的访存次数越多,可能会导致性能下降。因此,在设计页表时需要权衡层级数量和性能要求。同时,不同的硬件架构可能有不同的页表层级和结构,具体实现可能会有所差异。
linux怎么处理进程页表占用内存过多的问题
在 Linux 中,每个进程都有自己的页表,用于管理其虚拟地址空间和物理地址的映射关系。这些进程页表通常是存储在物理内存中的。当进程访问虚拟地址时,Linux 内核会根据进程的页表进行地址转换,并将对应的物理地址返回给进程。
当进程的页表占用的内存过多时,会导致系统的内存资源紧张,可能影响到其他进程的运行。为了处理进程页表占用内存过多的问题,Linux 提供了以下几种解决方案:
-
Demand Paging(按需分页):Linux 采用了虚拟内存系统,只有在需要的时候才将虚拟页面加载到物理内存中。因此,不是所有的进程页表都同时存在于物理内存中,而是根据进程需要进行动态加载和释放。这样可以有效地减少页表占用的内存空间。
-
Huge Pages(大页):传统的页表以4KB为单位进行分页,但对于一些需要大内存块的应用程序,这种粒度可能导致页表占用过多的内存。为了解决这个问题,Linux 提供了大页(Huge Pages)机制,可以将一部分内存划分为更大的页面,如2MB或1GB。使用大页可以减少页表的数量,从而减少页表占用的内存空间。
-
内存压缩:Linux 内核提供了一种称为内存压缩(Memory Compression)的技术,用于将不常用的页压缩存储,从而减少页表的内存占用。这样可以在不丢失太多性能的情况下,提高内存的利用率。
-
内存回收:当系统内存不足时,Linux 内核会使用页面置换算法来选择一些不常用或者未被修改的页面进行置换,从而为新的页面腾出空间。这样可以释放一些页表占用的内存空间。
c程序在操作系统中执行过程
- 操作系统设置c语言的运行环境,例如设置栈指针
- 设置程序计数器PC跳转到main()函数的起始地址
- CPU中的控制部件去内存中PC的地址取回指令交给CPU执行部件执行
- CPU依次从内存中读入指令执行,指令执行时可能会向内存写入、读入不同的值
PC(程序计数器)
PC(程序计数器)是一个特殊的寄存器,用于存储下一条要执行的指令的内存地址。每当执行一条指令时,PC的值都会根据指令的跳转或分支条件进行更新。
PC的更新机制取决于特定的计算机体系结构和指令集架构。在大多数体系结构中,执行指令时会自动更新PC的值,以指向下一条将要执行的指令。这是计算机执行程序的基本工作原理之一。
因此,对于大多数计算机体系结构来说,每执行一条指令,PC都会自动更新到下一条指令的地址。这使得计算机能够按照程序的顺序逐条执行指令。
请注意,这只是一般情况下的执行流程。在某些情况下,例如条件分支或跳转指令,PC的更新可能会受到程序逻辑或特定指令的影响,以便跳转到不同的指令地址。
ELF文件
ELF 全称 Executable and Linkable Format,即可执行和可链接格式,是Unix系列操作系统,包括Linux中常用的文件格式。ELF文件是二进制文件,包含了在Linux系统上运行程序所需的可执行代码、库和其他数据。
ELF文件可以分为不同类型:
-
可执行文件(ELF可执行文件): 这些文件包含了程序的可执行代码和数据。当你运行一个程序时,操作系统将可执行文件加载到内存中并开始执行它。
-
共享库(ELF共享对象): 这些文件包含了可重用的代码和数据,可以动态链接到多个可执行文件中。共享库允许不同的程序共享公共函数,减少代码重复并提高效率。
-
目标文件(ELF目标): 这些文件是在编译过程中生成的中间文件。它们包含了编译后的代码和数据,将被链接在一起创建可执行文件或共享库。
-
核心转储文件(ELF核心转储): 当程序崩溃或遇到致命错误时,它可以生成一个核心转储文件。该文件包含了程序在崩溃时的内存快照,可以用于调试和分析错误的原因。
ELF文件是与平台无关的,这意味着它们可以在不同的体系结构上执行,只要所需的系统库可用。ELF格式提供了动态链接、符号解析和其他运行时操作的功能。
在Linux中,有各种工具可用于处理ELF文件,如readelf、objdump和elfdump。这些工具允许你查看和分析ELF文件的内容,包括可执行代码、段、符号和重定位信息。
ELF格式有着详细的文档,你可以在ELF规范和你所使用的Linux发行版的相关文档中找到更多详细信息。
程序执行流程
参考:程序执行流程
参考:程序装载与运行流程分析
寄存器与内存在CPU计算中的作用
- cpu的计算是在内存上做的吗?
计算过程中的数据存储在内存中,但CPU执行计算的过程是在寄存器中进行的,而不是直接在内存上进行。当需要使用内存中的数据时,CPU会将数据加载到寄存器中进行操作,并将结果存回内存。这样可以加快计算的速度,因为寄存器是位于CPU内部,速度比内存更快。然而,寄存器的容量有限,所以只能存放一部分数据,而其他数据需要通过访问内存来获取。
- cpu 怎么在寄存器中进行计算的?
CPU在执行计算时,会将需要计算的数据从内存中加载到寄存器中。CPU内部有多个不同类型和不同大小的寄存器,如通用寄存器、累加寄存器等。通用寄存器用于存储计算过程中的临时数据,而累加寄存器则用于存储累加操作的结果。
在执行计算时,CPU将需要计算的数据从内存中加载到合适的寄存器中,然后利用寄存器中的数据进行相应的操作,如加法、减法、乘法、除法等。计算完成后,CPU将结果存回寄存器,再将结果写回内存中。这样就完成了在寄存器中进行计算的过程。
寄存器具有以下优点:
速度快:寄存器位于CPU内部,速度比内存更快,可以加快计算速度。
容量较小:寄存器容量有限,只能存放少量数据,可以提高计算的效率和速度。
方便访问:在运算过程中,CPU可以直接在寄存器中读取和存储数据,无需频繁访问内存,提高了计算的效率。
但寄存器的容量有限,且每个寄存器的类型和大小都有限制,不能直接存储大量数据,所以寄存器一般只用于存储临时数据和计算结果,而不是用于长期存储。长期存储需要的数据通常存放在内存中。
- 结论
计算是在cpu内部完成的,寄存器用于存储临时数据和计算结果;内存中存取的是长期的(想对于计算过程而言)(非持久化)数据。
进程的页表基址指向进程的页目录吗?
是的,进程的页表基址指向进程的页目录。在Linux操作系统中,每个进程都有自己的页目录表和页表用于管理其虚拟内存空间。页目录表存储在进程的页表基址寄存器(Page Table Base Register,CR3)中。
进程的页表基址寄存器(CR3)保存着指向当前进程页目录表的物理地址。通过将CR3寄存器设置为正确的页目录表基址,CPU能够根据虚拟地址查找正确的物理地址。当进程访问虚拟内存时,CPU会根据CR3寄存器中的页目录表基址来查找相应的页表,并使用页表来进行地址转换。
需要注意的是,每个进程都有自己独立的页目录表和页表,这样可以实现进程间的地址隔离和内存保护。每个进程的页表基址寄存器(CR3)存储了该进程独立的页目录表的物理地址,确保了每个进程访问的是自己的虚拟内存空间。
总之,进程的页表基址指向进程的页目录,这样CPU可以根据页目录和页表来进行虚拟地址到物理地址的转换。这种机制实现了进程的地址隔离和内存管理。

浙公网安备 33010602011771号