寻址
地址的数量表明了有多少个存储单元,可以由地址总线的条数表示。地址的表示受限于寄存器的位数,如果寄存器的位数大于等于地址总线的位数,那么就可以直接表示存储单元的地址,否则就需要寻找其他的方法。
早期 CPU 有 20 位地址线,但寄存器只有 16 位,因此采用了 [段基址:段偏移量]
的方式来表示。第一个寄存器高四位和第二个寄存器一起组成 20 位的内存地址。
由于两个寄存器表示的内存地址是真实的物理地址,因此这种寻址方式也叫做实模式。
随着计算机的发展, CPU 的地址线的个数和寄存器的位数相同了,但因为兼容性等原因,仍然采用 [段基址:段偏移量]
的表示方式。与此同时,为了更安全、更灵活访问内存空间,引入了保护模式。
在保护模式下,每一个进程都会有段基址、界限地址等描述进程分配的内存大小与内存位置的信息,这一信息存储在全局描述表(GDT,Global Description Table)中(表示为一块内核内存空间),可以有不同的硬件实现。全局描述表只可以由操作系统(在内核态)修改。
存储全局描述表的硬件叫做基地址寄存器、重定位寄存器,或统称为内存管理单元(Memory-management unit,MMU)
用户视角下得到的内存地址实际上是段偏移量,是逻辑地址。在不涉及 IO 操作时,用户对逻辑地址的任何操作都不涉及到物理地址。在涉及 IO 操作时,逻辑地址会通过 MMU 转换为物理地址,并执行实际的操作,但用户程序在保护模式下绝对无法得到真实的物理地址。
内存的分配方式
连续内存分配
采用连续内存分配方式时,如果用户申请一块 2M 的内存空间,那么物理内存上就会开辟一块 2M 的内存空间,这 2M 的内存空间地址将是连续的(如从 0-2048)。
当新的进程需要一块连续内存空间,物理内存中存在空闲的足够的空间,但是因为不连续导致无法分配时候,就会产生外部碎片问题(external fragmentation)。解决这一问题的方法是允许物理地址空间非连续,如分页/分段/段页式分配。
采用连续内存分配时,物理地址仍然可以沿用 段基址, 段偏移
的方式来表示,而逻辑地址则可以直接用 段偏移
表示
分页
分页将整个内存空间划分分成细粒度的块,以块为单位分配给进程。分页的过程需要保证物理内存和逻辑内存的块大小一致(物理内存的块叫帧,逻辑内存的叫页),用于建立逻辑块和物理块的映射关系。
分页系统中逻辑地址和物理地址之间的映射通过页表来实现,页表存储了每个页号到帧号的映射关系,此时单个逻辑内存地址用 页号, 页偏移
来唯一表示,在转换时通过页表转换为物理内存地址 帧号, 帧偏移
。
分页避免了外部碎片问题,保证所有的块都能被分配,但会产生内部碎片问题,但这可以通过调整块大小来缓解。
分页功能可以由不同的硬件支持,如转换表缓冲区(translation look-aside buffer, TLB),可以看成是一种存储页号到帧号映射关系的缓存。
页表不仅仅是一个线性表,为了支撑更多的页分配,会有更复杂的页表数据结构实现,如多层页表。
分段
分段是一种用户视角的内存管理方案,反映了程序申请内存的逻辑结构。分段系统将进程的逻辑内存空间区分为代码段、数据段、堆栈等,可以帮助用户更好的操作、共享和保护每一段的内存空间。
分段系统中逻辑地址和物理地址的映射通过段表实现,段表保存了每个段的基地址和界限地址,并用 段号, 段偏移
的方式来表示和获取物理内存地址。
段页式
段页式将程序分为多个逻辑段,在每个段里面又进行分页,即将分段和分页组合起来使用。段页式系统中,内存地址由 段号, 页号, 页偏移量
组成。
在进行地址转换时,首先通过段表查到页表起始地址,然后通过页表找到页帧号,最后形成物理地址。
交换
逻辑地址的设定,不仅提供了安全性,也允许逻辑空间大于实际的物理空间。当物理空间不够用又需要分配时,可以将一块不经常使用的内存备份到硬盘的空间上,从而空出这一块空间用于分配,这一操作叫做交换(swap)。
理论上所有的硬盘空间都可以用于交换,但实际中需要定义用于交换的内存空间大小。此时,逻辑内存空间=物理内存空间+交换空间
页置换
分页系统也允许交换操作,称为页置换(page replacement),此时交换操作以页为粒度进行。页置换的步骤为:
- 用户需要使用一个不在内存上的页,产生一个页错误陷阱(page-fault trap)
- 检查该进程的内部页表来确认该引用是否合法,如果非法则终止进程,否则继续执行页置换过程
- 在磁盘上查找待置换到内存中的页
- 在内存上查找一个空闲帧(没有被分配的帧),如果没有则通过页置换算法选择一个牺牲帧(victim frame)写到磁盘上(并更新页表、帧表)并将该帧的位置作为空闲帧。
- 将页置换到空闲帧位置上(并更新页表、帧表)
- 中断返回,此时该页存在,用户进程无感使用该页(外部仅表现为少许延迟)
页置换过程中,最重要的是页置换算法,该算法的目的是寻找一个最优解(一个被占用帧),该帧被置换在之后一段时间内造成页错误的数量最少,因为未来无法预测,所以所有算法只能通过过去一段时间的页使用信息来预测并选择。在这种情况下,常用的算法有:
- FIFO(先进先出)
- LRU(least-recently-used,最近最少使用)算法
- …
虚拟内存
虚拟内存是对内存交换特性的利用。虚拟内存的虚拟强调进程在执行过程中可能需要的内存不一定真实存在在物理内存中。比如一个程序中存在一个很少被调用的逻辑,该部份逻辑对应的内存可能在程序启动时不会被分配具体的内存空间,而是等到进程真正调用该部份逻辑时,通过缺页中断来动态的加载该部分内存,该操作被称为按需调页。按需调页在缺少物理内存时,可能会反复的置换需要的页,使进程在换页上用的时间多于执行时间,导致 CPU 利用率下降,这一过程称为颠簸(thrashing)。
实际上这种特性在编程语言中也有体现,即懒加载(lazy load)
因此,虚拟内存使得程序的初始启动速度加快,也减少了单个进程的物理内存使用量。同时程序不再受物理内存空间的限制。
写时复制
写时复制可以看成是另一种懒操作,当进程 fork 自身时,操作系统可以直接共享原进程的所有内存页,仅在这些页发生了写操作时,创建写操作对应页的副本。
Reference
- 实模式和保护模式 https://zhuanlan.zhihu.com/p/42309472
- 操作系统概念第七版