【操作系统入门】第七章:内存管理(一)------ 分页、分段与地址空间
本系列共10篇,这是第7/10篇。在第六章,我们探讨了死锁问题。现在,我们将进入操作系统另一个核心领域------内存管理,探索如何为众多进程高效、安全地分配有限的内存资源。
开篇:内存管理的根本挑战
想象一个大型图书馆:
- 成千上万的书籍(进程数据)
- 有限的阅览桌空间(物理内存)
- 读者需要快速找到特定书籍(内存访问)
- 书籍需要合理摆放避免混乱(内存分配)
操作系统作为图书管理员,必须解决:如何让数百个进程共享有限物理内存,同时让每个进程都感觉自己拥有完整的私人图书馆?
第一部分:内存管理基础
1.1 内存层次结构
现代计算机采用金字塔形的存储层次:
寄存器 → L1/L2/L3缓存 → 主内存(DRAM) → 磁盘(SSD/HDD)
↑ ↑ ↑ ↑
最快 快 中等 慢
最小 小 大 巨大
内存管理主要关注主内存的高效利用。
1.2 地址绑定时机
程序中的内存地址在何时被绑定到物理地址?
- 编译时:如果内存位置已知,生成绝对代码
- 加载时:编译生成可重定位代码,加载时确定最终位置
- 执行时:地址在运行时才确定,需要硬件支持(MMU)
1.3 逻辑地址 vs 物理地址
- 逻辑地址:CPU生成的地址,也称为虚拟地址
- 物理地址:内存单元的实际地址
- 内存管理单元(MMU):硬件设备,负责逻辑地址到物理地址的动态映射
第二部分:连续内存分配
2.1 内存分配策略
早期系统使用连续分配,主要有三种策略:
首次适应:
c
// 从内存开始顺序查找第一个足够大的空闲分区
void* first_fit(int size) {
for (partition *p = free_list; p != NULL; p = p->next) {
if (p->size >= size) {
return allocate_from(p, size);
}
}
return NULL; // 没有足够大的分区
}
最佳适应:
c
// 查找最接近需求大小的空闲分区
void* best_fit(int size) {
partition *best = NULL;
for (partition *p = free_list; p != NULL; p = p->next) {
if (p->size >= size && (best == NULL || p->size < best->size)) {
best = p;
}
}
return best ? allocate_from(best, size) : NULL;
}
最差适应:
c
// 总是分配最大的空闲分区
void* worst_fit(int size) {
partition *worst = NULL;
for (partition *p = free_list; p != NULL; p = p->next) {
if (p->size >= size && (worst == NULL || p->size > worst->size)) {
worst = p;
}
}
return worst ? allocate_from(worst, size) : NULL;
}
2.2 碎片化问题
连续分配面临的核心问题:
外部碎片:
内存布局:[进程A:100K][空闲:50K][进程B:200K][空闲:80K][进程C:150K]
问题:虽然总空闲空间=130K,但无法分配120K的进程
内部碎片:
分配策略:按2^n大小分配
请求:5KB → 分配8KB → 内部碎片=3KB
2.3 压缩技术
通过移动进程来合并空闲分区:
c
// 内存压缩算法
void compact_memory() {
uint32_t new_base = 0;
// 移动所有进程到内存低端
for (process *p = process_list; p != NULL; p = p->next) {
if (p->base != new_base) {
move_process(p, new_base); // 移动进程
update_page_tables(p); // 更新页表
}
new_base += p->size;
}
// 合并剩余空间为一个大的空闲分区
free_list->base = new_base;
free_list->size = total_memory - new_base;
}
问题:压缩开销大,且需要所有进程可重定位。
第三部分:分页------非连续分配的突破
3.1 分页的基本概念
分页将物理内存和逻辑地址空间都划分为固定大小的块:
- 页:逻辑地址空间的固定长度块
- 帧:物理内存的固定长度块
- 页表:维护页到帧的映射关系
3.2 地址转换机制
逻辑地址结构:
| 页号(p) | 页内偏移(d) |
地址转换过程:
物理地址 = 页表[页号].帧号 × 页大小 + 页内偏移
3.3 页表结构
基本页表项(PTE):
c
typedef struct {
uint32_t frame_number : 20; // 物理帧号
uint32_t present : 1; // 页是否在内存中
uint32_t writable : 1; // 是否可写
uint32_t user_mode : 1; // 用户模式可访问
uint32_t accessed : 1; // 是否被访问过
uint32_t dirty : 1; // 是否被修改过
uint32_t cache_disabled : 1; // 是否禁用缓存
uint32_t reserved : 6; // 保留位
} page_table_entry;
3.4 页表性能问题
简单线性页表的问题:
- 32位系统,4KB页 → 2²⁰个页表项 → 4MB页表
- 64位系统,4KB页 → 2⁵²个页表项 → 不可行!
第四部分:多级页表与TLB
4.1 多级页表
通过层次结构减少页表内存占用:
二级页表地址转换:
逻辑地址:| 页目录索引 | 页表索引 | 页内偏移 |
转换过程:
1. 用页目录索引在页目录中找到页表地址
2. 用页表索引在页表中找到物理帧号
3. 物理帧号 + 页内偏移 = 物理地址
x86-64四级页表示例:
c
// 64位虚拟地址结构
typedef struct {
uint64_t page_offset : 12; // 4KB页内偏移
uint64_t table_index : 9; // 第四级页表索引
uint64_t directory_index: 9; // 第三级页目录索引
uint64_t directory_ptr_index: 9; // 第二级页目录指针索引
uint64_t pml4_index : 9; // 第一级PML4索引
uint64_t sign_extension : 16; // 符号扩展
} virtual_address_64bit;
4.2 转换检测缓冲区(TLB)
TLB是MMU中的高速缓存,存储最近使用的页表项:
TLB查找过程:
c
phys_addr_t translate_address(virt_addr_t vaddr) {
// 1. 首先检查TLB
tlb_entry *entry = tlb_lookup(vaddr.page_number);
if (entry != NULL && entry->valid) {
tlb_hits++;
return (entry->frame_number << PAGE_SHIFT) | vaddr.offset;
}
// 2. TLB未命中,查找页表
tlb_misses++;
page_table_entry pte = walk_page_table(vaddr);
// 3. 更新TLB
tlb_insert(vaddr.page_number, pte.frame_number, pte.flags);
return (pte.frame_number << PAGE_SHIFT) | vaddr.offset;
}
TLB性能分析 :
假设:
- TLB命中率:99%
- TLB访问时间:1ns
- 内存访问时间:100ns
- 页表遍历:4次内存访问
平均内存访问时间:
TLB命中:1ns + 100ns = 101ns
TLB未命中:1ns + 4×100ns = 401ns
平均:0.99×101 + 0.01×401 = 105ns
4.3 反向页表
解决传统页表空间开销大的另一种方案:
基于哈希的反向页表:
c
typedef struct {
virt_addr_t page_number; // 虚拟页号
pid_t pid; // 进程ID
phys_addr_t frame_number; // 物理帧号
} inverted_pte_t;
// 哈希表查找
inverted_pte_t* inverted_page_lookup(virt_addr_t vaddr, pid_t pid) {
uint32_t hash = hash_function(vaddr.page_number, pid);
for (int i = 0; i < HASH_CHAIN_LENGTH; i++) {
inverted_pte_t *entry = &inverted_table[hash];
if (entry->pid == pid && entry->page_number == vaddr.page_number) {
return entry;
}
hash = (hash + 1) % INVERTED_TABLE_SIZE;
}
return NULL; // 页错误
}
第五部分:分段------逻辑视图的内存管理
5.1 分段的基本概念
分段基于程序的逻辑结构划分:
- 代码段、数据段、堆段、栈段等
- 每个段有独立的逻辑地址空间
逻辑地址结构:
| 段号(s) | 段内偏移(d) |
5.2 段表与地址转换
段表项结构:
c
typedef struct {
uint32_t base_address; // 段在物理内存中的基址
uint32_t segment_limit; // 段长度限制
uint8_t present : 1; // 段是否在内存中
uint8_t readable : 1; // 代码段可读
uint8_t writable : 1; // 数据段可写
uint8_t executable : 1; // 代码段可执行
uint8_t privilege : 2; // 特权级
uint8_t granularity: 1; // 粒度(字节/4KB页)
} segment_descriptor;
地址转换:
c
phys_addr_t segment_translation(logic_addr_t laddr) {
// 1. 检查段选择子
if (laddr.segment_index >= MAX_SEGMENTS) {
raise_protection_fault();
}
// 2. 获取段描述符
segment_descriptor *desc = &segment_table[laddr.segment_index];
// 3. 段界限检查
if (laddr.offset >= desc->segment_limit) {
raise_general_protection_fault();
}
// 4. 权限检查
if (!check_privilege(desc->privilege, current_privilege_level)) {
raise_protection_fault();
}
// 5. 计算物理地址
return desc->base_address + laddr.offset;
}
5.3 分段 vs 分页
| 特性 | 分页 | 分段 |
|---|---|---|
| 划分单位 | 固定大小的页 | 可变长度的段 |
| 视角 | 系统视角 | 用户/程序视角 |
| 碎片 | 内部碎片 | 外部碎片 |
| 共享 | 页级共享 | 段级共享 |
| 保护 | 页级保护 | 段级保护 |
| 实现 | 硬件自动 | 需要软件参与 |
第六部分:段页式内存管理
6.1 结合分段与分页
现代系统(如x86)结合两种技术:
- 分段:提供逻辑隔离和保护
- 分页:提供虚拟内存和物理内存管理
地址转换流程:
逻辑地址 → 分段单元 → 线性地址 → 分页单元 → 物理地址
6.2 x86段页式实例
c
// x86-32地址转换
phys_addr_t x86_address_translation(logic_addr_t laddr) {
// 第一阶段:分段
segment_descriptor *seg_desc =
get_descriptor(laddr.segment_selector);
linear_addr_t linear_addr =
seg_desc->base + laddr.offset;
// 第二阶段:分页
uint32_t dir_index = (linear_addr >> 22) & 0x3FF;
uint32_t table_index = (linear_addr >> 12) & 0x3FF;
uint32_t page_offset = linear_addr & 0xFFF;
page_dir_entry *pde = current_page_dir[dir_index];
page_table_entry *pte = pde->page_table[table_index];
return (pte->frame_number << 12) | page_offset;
}
6.3 现代趋势:平坦地址空间
现代64位系统倾向于简化分段:
- 代码段、数据段基址=0,界限=最大
- 实际使用纯分页模型
- 分段主要用于特权级控制和系统段
第七部分:内存保护机制
7.1 基于页的保护
通过页表项控制访问权限:
c
void handle_page_fault(virt_addr_t vaddr, fault_reason_t reason) {
page_table_entry *pte = find_pte(vaddr);
switch (reason) {
case FAULT_NOT_PRESENT:
if (pte->present) {
// TLB无效,重新加载
reload_tlb(vaddr);
} else {
// 真正的页错误,调页
handle_page_in(vaddr);
}
break;
case FAULT_PROTECTION:
if (current_privilege > pte->user_mode && !pte->writable) {
// 用户模式访问内核页或写只读页
kill_process(current_process, SIGSEGV);
}
break;
case FAULT_RESERVED:
// 保留位被设置,可能是内核漏洞
panic("Reserved bit set in PTE");
}
}
7.2 基于段的保护
通过段描述符实现:
- 特权级检查(Ring 0-3)
- 段类型检查(代码/数据)
- 段界限检查
7.3 内存保护键
现代CPU支持内存保护键:
c
// 设置内存区域保护
void set_memory_protection(virt_addr_t start, size_t len,
uint8_t protection_key, uint16_t permissions) {
// 配置页表项的保护键字段
for (virt_addr_t addr = start; addr < start + len; addr += PAGE_SIZE) {
page_table_entry *pte = find_pte(addr);
pte->protection_key = protection_key;
pte->access_permissions = permissions;
}
// 配置PKRU寄存器
uint32_t pkru = (permissions << (2 * protection_key));
write_pkru_register(pkru);
}
第八部分:实际系统分析
8.1 Linux内存管理
Linux采用三级/四级页表模型:
c
// 四级页表遍历
pgd_t *pgd = pgd_offset(mm, address); // 页全局目录
p4d_t *p4d = p4d_offset(pgd, address); // 第四级目录
pud_t *pud = pud_offset(p4d, address); // 页上层目录
pmd_t *pmd = pmd_offset(pud, address); // 页中间目录
pte_t *pte = pte_offset_map(pmd, address); // 页表项
8.2 Windows内存管理
Windows使用工作集管理器:
- 每个进程有工作集(常驻内存页集合)
- 平衡集管理器定期调整工作集大小
- 使用备用列表和修改列表管理页帧
8.3 性能优化技术
大页支持:
c
// 配置2MB大页
void setup_large_pages() {
// 设置CR4.PSE=1启用大页扩展
write_cr4(read_cr4() | CR4_PSE);
// 配置页目录项为大页
page_dir_entry *pde = &page_dir[directory_index];
pde->page_size = 1; // 大页模式
pde->frame_number = large_frame_base >> 22;
}
预取优化:
c
// 硬件预取提示
void prefetch_pages(virt_addr_t start, int count) {
for (int i = 0; i < count; i++) {
// 预取到TLB和缓存
__builtin_prefetch((void*)(start + i * PAGE_SIZE), 0, 3);
}
}
总结与展望
今天我们深入探讨了:
- 连续内存分配的策略和碎片问题
- 分页系统的地址转换机制和性能优化
- 分段系统的逻辑视图和保护机制
- 段页式结合的现代内存管理实践
- 各种内存保护技术和实际系统实现
内存管理的核心思想是:通过层次化、虚拟化的手段,在有限的物理资源上为进程提供看似无限、安全隔离的地址空间。
系列导航:
- 上一篇:[操作系统入门] 第六章:死锁------当进程陷入僵局
- 下一篇:[操作系统入门] 第八章:内存管理(二)------虚拟内存与页面置换
(你的名字) | (你的博客链接/签名)