Skip to content

虚拟内存

按需分页(Demand Paging)

按需分页(Demand Paging)是一种虚拟内存管理技术,核心思想是仅在程序实际需要访问某个页面时,才将其加载到物理内存中,而非一次性加载全部页面。它通过延迟加载(Lazy Loading)优化内存使用,减少程序启动时间和物理内存占用,是操作系统高效管理内存的核心机制之一。

1. 工作原理

核心流程

  1. 初始状态
  2. 程序的可执行文件存储在磁盘(如二进制文件),但仅加载必要部分到内存(如代码段头部)。
  3. 页表中大部分页表项的存在位(Present Bit)标记为0(表示页面不在内存中)。

  4. 访问未加载的页面

  5. 当程序访问一个标记为“不在内存”的页面时,触发缺页异常(Page Fault)
  6. 操作系统接管处理流程:

    • 从磁盘(或交换空间)加载所需页面到物理内存。
    • 更新页表项的存在位和物理页框号。
    • 恢复程序执行,重新执行引发缺页的指令。
  7. 动态管理

  8. 若物理内存不足,操作系统通过页面置换算法(如LRU、FIFO)淘汰旧页面,腾出空间。

2. 具体示例

假设一个程序访问虚拟地址 0x08048000,其映射的物理页面尚未加载到内存。

步骤分解

  1. 触发缺页异常
  2. CPU访问虚拟地址 0x08048000,页表项存在位为0
  3. 硬件触发缺页异常,切换到内核模式。

  4. 操作系统处理缺页

  5. 查询页表项:确定该虚拟地址对应的磁盘位置(如可执行文件中的偏移)。
  6. 分配物理页框:选择一个空闲页框(如物理地址 0x0000A000)。
  7. 加载页面:将磁盘中 0x08048000 对应的数据(如代码或数据段)复制到物理页框 0x0000A000
  8. 更新页表

    • 设置页表项的存在位为1,物理页框号为 0x0000A(高20位)。
    • 更新标志位(如可读写权限)。
  9. 恢复程序执行

  10. CPU重新执行触发缺页的指令,此时页表项已有效,正常完成地址转换。

3. 关键数据结构

页表项(PTE)标志位

  • 存在位(Present Bit):1表示页面在内存,0表示在磁盘。
  • 修改位(Dirty Bit):1表示页面被修改过,淘汰时需写回磁盘。
  • 访问位(Accessed Bit):记录页面是否被访问,用于页面置换算法。

缺页异常处理流程

  • 硬件触发:CPU检测到存在位为0,保存现场并跳转到内核缺页处理程序。
  • 软件处理:操作系统通过虚拟地址和页表定位磁盘数据,完成加载。

4. 优点与缺点

优点 缺点
减少内存占用(仅加载必要页面) 缺页处理引入延迟(需磁盘I/O)
加速程序启动(无需加载全部代码) 频繁缺页导致抖动(Thrashing)
支持运行大于物理内存的程序 需高效置换算法避免性能下降

性能优化

  • 预取(Prefetching):预测即将访问的页面并提前加载(如程序启动时预读代码段)。
  • TLB(快表):缓存常用页表项,减少地址转换开销。
  • 写时复制(Copy-on-Write):延迟页面复制,直到真正修改时触发缺页(用于fork()创建子进程)。

写时复制(copy on write)

写时复制(Copy-on-Write, CoW)是一种内存管理优化技术,核心思想是多个进程共享同一份物理内存页,直到某个进程尝试修改该页时,才真正复制该页并分配新的物理内存。通过延迟复制的策略,CoW显著减少了内存冗余和进程创建的开销,广泛应用于操作系统的进程创建(如fork())和内存共享场景。

核心流程 - 共享初始状态:

父进程与子进程共享所有物理内存页,页表项标记为只读(即使原页可写)。

共享页的引用计数(Reference Count)增加,表示多个进程正在使用。

  • 触发写操作:

当任一进程(父或子)尝试写入共享页时,触发缺页异常(因页表项为只读)。

  • 操作系统接管处理流程:

检查该页是否为CoW页(通过标志位或元数据)。

复制物理页,为写入进程分配新物理页,更新其页表项为可写。

原共享页的引用计数减1,若为0则释放。

  • 后续访问:

修改后的进程使用独立副本,原进程继续共享未修改的页。

内存交换分区(swap space)

内存交换分区(Swap Space)是操作系统用于扩展物理内存(RAM)的一种机制。它通过将部分内存数据临时存储到硬盘上的专用区域(称为交换分区或交换文件),来缓解物理内存不足的问题,确保系统在内存资源紧张时仍能稳定运行。

核心作用

  1. 扩展可用内存
    当物理内存不足时,操作系统将暂时不活跃的内存页(内存数据单元)移动到交换分区,腾出物理内存供急需的程序使用。
  2. 支持休眠(Hibernate)
    休眠时,系统会将内存的完整内容写入交换分区,以便恢复时快速加载。
  3. 防止内存耗尽(OOM)
    避免因内存不足直接触发内核的“Out-of-Memory Killer”强制终止进程。

工作原理

  1. 页面置换(Paging)
    内核通过算法(如LRU,最近最少使用)选择不活跃的内存页,将其写入交换分区,并标记原物理内存区域为可重用。
  2. 按需换入(Swap-in)
    当程序访问已被换出的页面时,系统将其从交换分区重新加载到物理内存(可能触发磁盘I/O延迟)。

典型使用场景

  1. 物理内存不足
    当程序申请的内存超过物理RAM容量时,系统自动使用交换空间。
  2. 处理内存峰值
    即使物理内存充足,交换空间可应对突发性内存需求高峰。
  3. 长期空闲进程
    后台进程的内存可能被换出,释放物理内存给活跃进程。

优缺点对比

优点 缺点
防止内存耗尽导致系统崩溃 硬盘速度远慢于内存(机械硬盘尤甚)
允许运行更多或更大的程序 频繁交换(Swapping)会显著降低性能
支持休眠功能 固态硬盘(SSD)频繁写入可能缩短寿命

页替换(Page replacement)

页替换(Page Replacement) 是操作系统内存管理中的核心机制,用于在物理内存不足时,选择合适的页面(Page)从内存换出到交换空间(Swap Space),以腾出空间加载新页面。其目标是尽可能减少因缺页中断(Page Fault)导致的性能损失,同时保证系统稳定运行。

  • 为什么需要页替换?
  • 物理内存有限:当进程申请的内存超过物理内存容量时,需通过换出非活跃页面来腾出空间。
  • 虚拟内存支持:虚拟内存机制允许进程使用比物理内存更大的地址空间,依赖页替换实现动态映射。
  • 多任务优化:通过换出后台进程的页面,优先为活跃进程分配内存。

页替换算法

以下是常见的页替换算法及其特点:

算法 描述 优点 缺点
最优置换(OPT) 换出未来最长时间不再被访问的页面(理论最优) 命中率最高(理想情况) 无法实现(需预知未来访问顺序)
先进先出(FIFO) 换出最早进入内存的页面 实现简单 可能淘汰高频访问页面(Belady异常)
最近最少使用(LRU) 换出最长时间未被访问的页面 接近OPT的实际效果 实现复杂(需维护访问时间戳或链表)
时钟算法(Clock) 环形链表模拟LRU,通过“使用位”标记页面是否被访问 低开销,近似LRU 命中率略低于LRU
最不常用(LFU) 换出访问频率最低的页面 适合访问模式稳定的场景 对突发访问敏感,需维护计数器
工作集算法(WS) 基于局部性原理,保留进程当前工作集内的页面 符合程序运行规律 需动态跟踪工作集,开销较大

算法对比示例

假设页面访问序列为:1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5,物理内存容量为3页:

FIFO

访问序列 1 2 3 4 1 2 5 1 2 3 4 5
内存状态 1 1 1 4 4 4 5 5 5 3 4 4
缺页

缺页次数:9次(Belady异常明显:淘汰后面就要访问的页面)

Belady异常可能会导致页数越多时,发生page faults的次数却不会越少。

LRU

访问序列 1 2 3 4 1 2 5 1 2 3 4 5
内存状态 1 1 1 4 4 4 5 5 5 3 4 4
缺页

缺页次数:8次(优于FIFO)

Clock算法

通过环形链表和“使用位”标记,定期清除使用位,优先淘汰未被访问的页面。命中率接近LRU,但实现更高效。


实际系统中的页替换(以Linux为例)

  1. 双链表策略
    Linux使用active_listinactive_list两个链表,基于页面活跃性管理:
  2. 页面首次载入时进入active_list
  3. 定期扫描将不活跃页面移至inactive_list
  4. 缺页时优先从inactive_list淘汰页面。

  5. 交换倾向(Swappiness)
    通过/proc/sys/vm/swappiness(默认值60)控制内核换出页面的积极性:

  6. 值越高,越倾向于换出页面。
  7. 值设为0时,尽量保留物理内存(除非内存耗尽)。

  8. LRU近似实现
    使用“第二次机会”策略(类似Clock算法),结合页面访问标志位(PG_referenced)动态调整活跃性。

页替换的性能影响

  1. 缺页中断开销
  2. 硬缺页(Hard Fault):需从磁盘加载页面,延迟高(ms级)。
  3. 软缺页(Soft Fault):页面仍在内存中(如共享库),延迟低(μs级)。

  4. 交换抖动(Thrashing)
    当频繁换入/换出页面时,系统时间主要消耗在I/O操作上,导致性能急剧下降。
    解决方案

  5. 增加物理内存。
  6. 调整进程优先级或限制内存使用(如cgroups)。
  7. 优化程序的内存访问模式(减少随机访问)。

帧分配(Allocations of frames)

指将物理内存的页帧(Frame)分配给不同进程的策略。它的核心目标是平衡内存资源的使用效率与进程的公平性,确保系统在多任务环境下高效运行。

抖动(Thrashing)

1. Thrashing的定义

当物理内存(RAM)不足以容纳所有活跃进程的工作集(Working Set,即进程最近频繁访问的页面集合)时,操作系统会频繁将页面换入(Swap-in)和换出(Swap-out)。此时,系统的大部分时间用于处理缺页中断(Page Fault)和磁盘I/O操作,而非执行实际任务,导致整体性能急剧下降。 goi

2. 发生原因

原因 说明
物理内存不足 进程总需求内存超过RAM容量,迫使系统依赖交换空间(Swap Space)。
多道程序过度并发 同时运行过多进程,每个进程分配到的帧数不足,无法维持其工作集。
低效的置换算法 如FIFO可能误换出高频页面,导致频繁换回(如Belady异常)。
工作集突变 进程突然访问大量新页面(如数据库查询高峰),超出当前分配的帧数。

3. 影响

  • CPU利用率骤降:CPU因等待I/O常处于空闲状态(%wa值高)。
  • 响应延迟增加:用户请求处理时间显著延长。
  • 磁盘I/O过载:频繁的页面交换导致磁盘带宽饱和(机械硬盘尤其明显)。
  • 系统不稳定:可能触发OOM Killer(Linux)强制终止进程。

5. 解决方案

(1) 短期缓解

  • 减少并发进程数:暂停非关键任务(如kill低优先级进程)。
  • 调整交换倾向(Linux):
sysctl vm.swappiness=10  # 降低换出倾向(默认60)
  • 释放缓存
sync; echo 3 > /proc/sys/vm/drop_caches  # 清除PageCache、dentries和inodes

(2) 长期优化

  • 扩展物理内存:最直接的硬件升级方案。
  • 优化程序内存使用:减少内存泄漏,改进数据局部性(如分块处理大数据)。
  • 使用高效置换算法:如LRU-K或工作集时钟算法(Clock with Working Set)。
  • 调整进程优先级:通过nicecgroups限制内存密集型进程的资源使用。

(3) 系统级策略

  • 工作集模型动态分配:跟踪进程的工作集大小,动态调整分配的帧数。
  • 负载均衡:分布式系统中将任务分散到多台服务器,避免单机过载。
  • 使用SSD交换分区:若必须使用交换空间,SSD可减少I/O延迟(但需注意写入寿命)。

7. 与Belady异常的区别

Thrashing Belady异常
系统级内存过载导致性能下降 FIFO算法缺陷,增加帧数反而缺页率升高
与并发量和内存需求相关 特定置换算法下的理论问题
需全局资源调整 需更换置换算法(如改用LRU或Clock)

内核内存分配

内核内存分配 是操作系统的核心功能之一,负责管理内核空间的内存资源,确保内核自身及内核模块的高效运行。与用户态内存分配(如malloc)不同,内核内存分配需要更高的安全性和实时性,且直接操作物理内存和虚拟内存的底层机制。以下是内核内存分配的详细解析:

1. 内核内存分配与用户态分配的区别

特性 内核内存分配 用户态内存分配
权限 直接操作物理内存和内核地址空间 通过系统调用(如brkmmap)申请虚拟内存
分配器 使用伙伴系统(Buddy System)、Slab/Slub分配器 使用mallocfree(基于glibc的ptmalloc等)
内存类型 物理连续内存(kmalloc)或虚拟连续内存(vmalloc 通常是虚拟连续内存
错误处理 分配失败可能导致内核崩溃(需严格检查返回值) 返回NULL,进程可捕获错误
实时性 支持原子上下文分配(如中断处理) 只能在进程上下文中运行

2. 内核内存分配的核心机制

(1) 伙伴系统(Buddy System)

  • 作用:管理物理内存页框(通常以4KB为单位),解决外部碎片问题。
  • 原理
  • 将空闲内存划分为不同大小的块(如2^0, 2^1, ..., 2^10页),每块称为一个“伙伴”。
  • 分配时,按请求大小找到最接近的块,若无合适块,则分裂更大的块。
  • 释放时,合并相邻空闲块以形成更大的块。
  • 应用场景:分配大块物理连续内存(如DMA缓冲区.DMA:内存外设与内存之间的直接数据传输而专门分配的内存区域)。
  • 问题:可能会引入内部碎片

(2) Slab/Slub/Slob分配器

  • 作用:高效分配小对象(如task_structinode),解决内部碎片问题。
  • 原理:Slab 分配器通过预先分配一定数量的内存块(称为 slab)来提高内存分配和释放的效率。每个 slab 中包含多个对象(例如内核中的数据结构),当需要分配内存时,Slab 分配器从这些对象中获取一个空闲对象,并返回给请求的组件。
  • Slab:预分配内存缓存(Cache),每个缓存管理特定大小的对象。
  • Slub(Linux默认):优化Slab,减少元数据开销,支持动态调试。
  • Slob:极简实现,适用于嵌入式系统。
  • 示例
// 创建Slab缓存
struct kmem_cache *my_cache = kmem_cache_create("my_cache", sizeof(struct my_object), 0, 0, NULL);
// 分配对象
struct my_object *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
// 释放对象
kmem_cache_free(my_cache, obj);

3. 内核内存分配API

(1) kmalloc / kfree

  • 特点:分配物理连续的内存块,适用于需要DMA或硬件访问的场景。
  • 示例
char *buf = kmalloc(1024, GFP_KERNEL);  // 分配1KB内存
kfree(buf);                             // 释放内存

(2) vmalloc / vfree

  • 特点:分配虚拟连续但物理不一定连续的内存,适用于大块内存(如模块加载)。
  • 示例
void *addr = vmalloc(8192);  // 分配8KB虚拟连续内存
vfree(addr);                 // 释放内存

(3) get_free_pages

  • 特点:直接分配连续的物理页框(以页为单位)。
  • 示例
unsigned long pages = get_free_pages(GFP_KERNEL, 3);  // 分配2^3=8页(32KB)
free_pages(pages, 3);

4. 内核内存分配策略与优化

(1) 内存碎片处理

  • 外部碎片:通过伙伴系统的合并机制缓解。
  • 内部碎片:Slab分配器按对象大小精确分配。

(2) 内存回收(kswapd)

  • 后台守护进程:定期扫描并回收空闲页框。
  • LRU链表:将页面分为活跃(Active)和非活跃(Inactive)链表,优先回收非活跃页。

(3) OOM(Out-Of-Memory)处理

  • 触发条件:物理内存和交换空间均耗尽。
  • OOM Killer:选择占用内存多且优先级低的进程终止(通过oom_score计算)。

5. 调试与监控工具

(1) /proc/meminfo

cat /proc/meminfo  # 查看内核内存使用详情(Slab、PageTables等)

(2) /proc/slabinfo

cat /proc/slabinfo | grep "kmalloc"  # 查看Slab缓存分配情况

(3) vmstat

vmstat 1  # 监控内存、交换、缺页等实时数据

(4) kmemleak(内核内存泄漏检测)

echo scan > /sys/kernel/debug/kmemleak  # 扫描内存泄漏

6. 实际案例:内核模块中的内存分配

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>

static char *buffer;

static int __init my_module_init(void) {
    buffer = kmalloc(1024, GFP_KERNEL);
    if (!buffer) {
        printk(KERN_ERR "Failed to allocate memory\n");
        return -ENOMEM;
    }
    printk(KERN_INFO "Allocated 1KB buffer at %p\n", buffer);
    return 0;
}

static void __exit my_module_exit(void) {
    kfree(buffer);
    printk(KERN_INFO "Memory freed\n");
}

module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");

7. 总结

内核内存分配是操作系统资源管理的基石,其核心在于: - 伙伴系统管理物理页框,解决外部碎片。 - Slab分配器优化小对象分配,减少内部碎片。 - GFP标志控制分配行为(如原子性与内存区域)。 - 工具链(如vmstat/proc文件系统)帮助监控与调试。

Comments