10. 内存管理

深入理解操作系统的内存管理机制:虚拟内存、页表、MMU、段页式管理与换页算法,掌握 Linux 内存布局与性能调优。

1. 内存管理的发展历程

无内存管理(单道程序) → 静态分区 → 动态分区 → 
分页 → 分段 → 段页式 → 虚拟内存

2. 连续分配方式

2.1 固定分区 vs 动态分区

方式原理问题
固定分区内存分成等大/不等大的固定区域内部碎片,利用率低
动态分区按需求分配连续内存块外部碎片,需紧凑整理

2.2 动态分区的分配算法

首次适应(First Fit):从头找第一个足够大的空闲块
  优点:速度快
  缺点:低地址碎片多

最佳适应(Best Fit):找最小的足够大的空闲块
  优点:减少浪费
  缺点:产生大量小碎片

最坏适应(Worst Fit):找最大的空闲块
  优点:碎片较大可利用
  缺点:大进程无法分配

下次适应(Next Fit):从上次分配位置继续搜索
  优点:分布更均匀
  缺点:尾部碎片可能累积

3. 分页(Paging)

3.1 基本思想

将物理内存和逻辑内存划分为固定大小的页框(Page Frame)页面(Page),大小通常为 4KB。

逻辑地址 → [页号 (p) | 页内偏移 (d)]
                   ↓
              页表查询
                   ↓
物理地址 → [页框号 | 页内偏移]

3.2 页表结构

页表项(PTE):
┌─────────┬─────┬─────┬─────┬─────┬─────┬──────┐
│ 页框号  │ 存在位│ 修改位│引用位│保护位│缓存位│ 脏位  │
└─────────┴─────┴─────┴─────┴─────┴─────┴──────┘

存在位(Present): 页面是否在内存中
修改位(Dirty): 页面是否被写过
引用位(Accessed): 页面是否被访问过(用于 LRU 近似)
保护位(Protection): 读/写/执行权限

3.3 多级页表

32 位系统单级页表的问题:4GB / 4KB = 1M 个页表项 × 4B = 4MB 页表。

二级页表(32位,4KB页):

逻辑地址 = [ 目录索引(10) | 页表索引(10) | 偏移(12) ]
                ↓              ↓
         页目录表(PDE)    页表(PTE)
                │              │
                └────→ 页框号 + 偏移 = 物理地址
// x86 二级页表地址转换示意
uint32_t translate(uint32_t logical_addr, uint32_t *page_directory) {
    uint32_t dir_idx = (logical_addr >> 22) & 0x3FF;
    uint32_t table_idx = (logical_addr >> 12) & 0x3FF;
    uint32_t offset = logical_addr & 0xFFF;
    
    uint32_t pde = page_directory[dir_idx];
    uint32_t *page_table = (uint32_t*)(pde & 0xFFFFF000);
    uint32_t pte = page_table[table_idx];
    uint32_t frame = pte & 0xFFFFF000;
    
    return frame | offset;
}

4. 分段(Segmentation)

4.1 基本思想

按程序逻辑结构划分段:代码段、数据段、栈段、堆段。

逻辑地址 → [段号 (s) | 段内偏移 (d)]
                   ↓
              段表查询(段基址 + 段限长)
                   ↓
              段基址 + 偏移 = 物理地址
                   ↓
              段限长检查(越界保护)

4.2 段页式管理

结合两者优点:

逻辑地址 → [段号 | 页号 | 页内偏移]
              ↓
         段表 → 页表基址
                   ↓
              页表 → 页框号
                   ↓
              页框号 + 偏移 = 物理地址

Linux 实际采用页式管理为主,x86-64 架构使用四级/五级页表。


5. 虚拟内存(Virtual Memory)

5.1 核心思想

每个进程拥有独立的虚拟地址空间(如 64 位 Linux 的用户空间 128TB),物理内存只存放活跃页面,不活跃的页面换出到磁盘(交换空间/Swap)。

进程虚拟地址空间(128TB):

高地址 → ┌──────────────────┐
         │  栈(向下增长)     │
         │   (映射到物理页)  │
         ├──────────────────┤
         │   内存映射区       │  ← mmap 区域
         │   (按需分配)      │
         ├──────────────────┤
         │   堆(向上增长)    │
         ├──────────────────┤
         │  BSS / 数据段      │
         │  代码段            │
低地址 → └──────────────────┘
              ↑
         未分配区域 → 不占物理内存
              ↑
         已分配但未被访问 → 分配页表但无物理页
              ↑
         已访问 → 页表指向物理页框

5.2 页错误(Page Fault)

1. 合法访问,页面不在内存 → 请求调页(Demand Paging)
   → 从磁盘加载页面 → 继续执行

2. 合法访问,页面在 Swap
   → 从交换区读入 → 更新页表 → 继续执行

3. 非法访问(越界/权限不足)
   → 触发段错误(Segmentation Fault)→ 进程终止

6. 页面置换算法

6.1 最优置换(OPT)

置换未来最久不被访问的页面。

理论最优但不可实现(需要预知未来),用于评估其他算法。

6.2 FIFO(先进先出)

from collections import deque

def fifo(pages, capacity):
    """Belady 异常:增加页框数可能缺页更多"""
    memory = deque(maxlen=capacity)
    faults = 0
    for page in pages:
        if page not in memory:
            faults += 1
            memory.append(page)
    return faults

6.3 LRU(最近最少使用)

def lru(pages, capacity):
    """最近最少使用,性能接近 OPT"""
    from collections import OrderedDict
    memory = OrderedDict()
    faults = 0
    
    for page in pages:
        if page in memory:
            memory.move_to_end(page)
        else:
            faults += 1
            if len(memory) >= capacity:
                memory.popitem(last=False)
            memory[page] = True
    return faults

LRU 实现方式:哈希表 + 双向链表(O(1))或计数器法。

6.4 Clock(时钟/第二次机会)算法

def clock_algorithm(pages, capacity):
    """
    环形链表 + 引用位
    每个页面有一个引用位 R
    指针遍历:R=1 则清 0 并跳过;R=0 则置换
    """
    clock = [None] * capacity  # [(page, R), ...]
    hand = 0
    faults = 0
    
    for page in pages:
        # 检查是否已在内存中
        found = False
        for i, entry in enumerate(clock):
            if entry and entry[0] == page:
                clock[i] = (page, 1)
                found = True
                break
        
        if found:
            continue
        
        # 需要置换
        faults += 1
        while True:
            if clock[hand] is None or clock[hand][1] == 0:
                clock[hand] = (page, 1)
                hand = (hand + 1) % capacity
                break
            else:
                clock[hand] = (clock[hand][0], 0)
                hand = (hand + 1) % capacity
    
    return faults

6.5 算法对比

算法缺页率实现复杂度特点
OPT最优理论基准
FIFO较差O(1)可能 Belady 异常
LRU接近最优O(1)需要维护访问顺序
Clock接近 LRUO(1)近似 LRU,开销更小
LFU视访问模式O(log n)保留频繁访问页面

7. Linux 内存查看与调优

# 查看内存使用情况
free -h

# 查看进程内存
cat /proc/[pid]/status | grep VmRSS
cat /proc/[pid]/maps   # 完整地址空间映射

# Swap 使用情况
swapon -s
cat /proc/swaps

# 调整 Swappiness(0-100,越大越倾向用 Swap)
cat /proc/sys/vm/swappiness
sudo sysctl vm.swappiness=10

# 查看缺页统计
ps -o min_flt,maj_flt -p [pid]
# min_flt: 轻微缺页(无需磁盘 I/O)
# maj_flt: 严重缺页(需要磁盘 I/O)

# 清除页缓存(谨慎使用)
sync; echo 3 | sudo tee /proc/sys/vm/drop_caches

参考文章

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「计算机基础」更多文章

  1. 16. 数据链路层
  2. 15. 网络层与路由
  3. 14. 网络模型与协议