虚拟内存
按需分页(Demand Paging)
按需分页(Demand Paging)是一种虚拟内存管理技术,核心思想是仅在程序实际需要访问某个页面时,才将其加载到物理内存中,而非一次性加载全部页面。它通过延迟加载(Lazy Loading)优化内存使用,减少程序启动时间和物理内存占用,是操作系统高效管理内存的核心机制之一。
1. 工作原理
核心流程
- 初始状态:
- 程序的可执行文件存储在磁盘(如二进制文件),但仅加载必要部分到内存(如代码段头部)。
-
页表中大部分页表项的存在位(Present Bit)标记为
0
(表示页面不在内存中)。 -
访问未加载的页面:
- 当程序访问一个标记为“不在内存”的页面时,触发缺页异常(Page Fault)。
-
操作系统接管处理流程:
- 从磁盘(或交换空间)加载所需页面到物理内存。
- 更新页表项的存在位和物理页框号。
- 恢复程序执行,重新执行引发缺页的指令。
-
动态管理:
- 若物理内存不足,操作系统通过页面置换算法(如LRU、FIFO)淘汰旧页面,腾出空间。
2. 具体示例
假设一个程序访问虚拟地址 0x08048000
,其映射的物理页面尚未加载到内存。
步骤分解
- 触发缺页异常:
- CPU访问虚拟地址
0x08048000
,页表项存在位为0
。 -
硬件触发缺页异常,切换到内核模式。
-
操作系统处理缺页:
- 查询页表项:确定该虚拟地址对应的磁盘位置(如可执行文件中的偏移)。
- 分配物理页框:选择一个空闲页框(如物理地址
0x0000A000
)。 - 加载页面:将磁盘中
0x08048000
对应的数据(如代码或数据段)复制到物理页框0x0000A000
。 -
更新页表:
- 设置页表项的存在位为
1
,物理页框号为0x0000A
(高20位)。 - 更新标志位(如可读写权限)。
- 设置页表项的存在位为
-
恢复程序执行:
- 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)的一种机制。它通过将部分内存数据临时存储到硬盘上的专用区域(称为交换分区或交换文件),来缓解物理内存不足的问题,确保系统在内存资源紧张时仍能稳定运行。
核心作用
- 扩展可用内存
当物理内存不足时,操作系统将暂时不活跃的内存页(内存数据单元)移动到交换分区,腾出物理内存供急需的程序使用。 - 支持休眠(Hibernate)
休眠时,系统会将内存的完整内容写入交换分区,以便恢复时快速加载。 - 防止内存耗尽(OOM)
避免因内存不足直接触发内核的“Out-of-Memory Killer”强制终止进程。
工作原理
- 页面置换(Paging)
内核通过算法(如LRU,最近最少使用)选择不活跃的内存页,将其写入交换分区,并标记原物理内存区域为可重用。 - 按需换入(Swap-in)
当程序访问已被换出的页面时,系统将其从交换分区重新加载到物理内存(可能触发磁盘I/O延迟)。
典型使用场景
- 物理内存不足
当程序申请的内存超过物理RAM容量时,系统自动使用交换空间。 - 处理内存峰值
即使物理内存充足,交换空间可应对突发性内存需求高峰。 - 长期空闲进程
后台进程的内存可能被换出,释放物理内存给活跃进程。
优缺点对比
优点 | 缺点 |
---|---|
防止内存耗尽导致系统崩溃 | 硬盘速度远慢于内存(机械硬盘尤甚) |
允许运行更多或更大的程序 | 频繁交换(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为例)
- 双链表策略:
Linux使用active_list
和inactive_list
两个链表,基于页面活跃性管理: - 页面首次载入时进入
active_list
。 - 定期扫描将不活跃页面移至
inactive_list
。 -
缺页时优先从
inactive_list
淘汰页面。 -
交换倾向(Swappiness):
通过/proc/sys/vm/swappiness
(默认值60)控制内核换出页面的积极性: - 值越高,越倾向于换出页面。
-
值设为0时,尽量保留物理内存(除非内存耗尽)。
-
LRU近似实现:
使用“第二次机会”策略(类似Clock算法),结合页面访问标志位(PG_referenced
)动态调整活跃性。
页替换的性能影响
- 缺页中断开销:
- 硬缺页(Hard Fault):需从磁盘加载页面,延迟高(ms级)。
-
软缺页(Soft Fault):页面仍在内存中(如共享库),延迟低(μs级)。
-
交换抖动(Thrashing):
当频繁换入/换出页面时,系统时间主要消耗在I/O操作上,导致性能急剧下降。
解决方案: - 增加物理内存。
- 调整进程优先级或限制内存使用(如
cgroups
)。 - 优化程序的内存访问模式(减少随机访问)。
帧分配(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):
- 释放缓存:
(2) 长期优化
- 扩展物理内存:最直接的硬件升级方案。
- 优化程序内存使用:减少内存泄漏,改进数据局部性(如分块处理大数据)。
- 使用高效置换算法:如LRU-K或工作集时钟算法(Clock with Working Set)。
- 调整进程优先级:通过
nice
或cgroups
限制内存密集型进程的资源使用。
(3) 系统级策略
- 工作集模型动态分配:跟踪进程的工作集大小,动态调整分配的帧数。
- 负载均衡:分布式系统中将任务分散到多台服务器,避免单机过载。
- 使用SSD交换分区:若必须使用交换空间,SSD可减少I/O延迟(但需注意写入寿命)。
7. 与Belady异常的区别
Thrashing | Belady异常 |
---|---|
系统级内存过载导致性能下降 | FIFO算法缺陷,增加帧数反而缺页率升高 |
与并发量和内存需求相关 | 特定置换算法下的理论问题 |
需全局资源调整 | 需更换置换算法(如改用LRU或Clock) |
内核内存分配
内核内存分配 是操作系统的核心功能之一,负责管理内核空间的内存资源,确保内核自身及内核模块的高效运行。与用户态内存分配(如malloc
)不同,内核内存分配需要更高的安全性和实时性,且直接操作物理内存和虚拟内存的底层机制。以下是内核内存分配的详细解析:
1. 内核内存分配与用户态分配的区别
特性 | 内核内存分配 | 用户态内存分配 |
---|---|---|
权限 | 直接操作物理内存和内核地址空间 | 通过系统调用(如brk 、mmap )申请虚拟内存 |
分配器 | 使用伙伴系统(Buddy System)、Slab/Slub分配器 | 使用malloc 、free (基于glibc 的ptmalloc等) |
内存类型 | 物理连续内存(kmalloc )或虚拟连续内存(vmalloc ) |
通常是虚拟连续内存 |
错误处理 | 分配失败可能导致内核崩溃(需严格检查返回值) | 返回NULL ,进程可捕获错误 |
实时性 | 支持原子上下文分配(如中断处理) | 只能在进程上下文中运行 |
2. 内核内存分配的核心机制
(1) 伙伴系统(Buddy System)
- 作用:管理物理内存页框(通常以4KB为单位),解决外部碎片问题。
- 原理:
- 将空闲内存划分为不同大小的块(如2^0, 2^1, ..., 2^10页),每块称为一个“伙伴”。
- 分配时,按请求大小找到最接近的块,若无合适块,则分裂更大的块。
- 释放时,合并相邻空闲块以形成更大的块。
- 应用场景:分配大块物理连续内存(如DMA缓冲区.DMA:内存外设与内存之间的直接数据传输而专门分配的内存区域)。
- 问题:可能会引入内部碎片
(2) Slab/Slub/Slob分配器
- 作用:高效分配小对象(如
task_struct
、inode
),解决内部碎片问题。 - 原理: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或硬件访问的场景。
- 示例:
(2) vmalloc
/ vfree
- 特点:分配虚拟连续但物理不一定连续的内存,适用于大块内存(如模块加载)。
- 示例:
(3) get_free_pages
- 特点:直接分配连续的物理页框(以页为单位)。
- 示例:
4. 内核内存分配策略与优化
(1) 内存碎片处理
- 外部碎片:通过伙伴系统的合并机制缓解。
- 内部碎片:Slab分配器按对象大小精确分配。
(2) 内存回收(kswapd)
- 后台守护进程:定期扫描并回收空闲页框。
- LRU链表:将页面分为活跃(Active)和非活跃(Inactive)链表,优先回收非活跃页。
(3) OOM(Out-Of-Memory)处理
- 触发条件:物理内存和交换空间均耗尽。
- OOM Killer:选择占用内存多且优先级低的进程终止(通过
oom_score
计算)。
5. 调试与监控工具
(1) /proc/meminfo
(2) /proc/slabinfo
(3) vmstat
(4) 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
文件系统)帮助监控与调试。