Skip to content

分段机制:让程序说「人话」

你有没有想过,为什么程序里的代码和数据是分开放的? 为什么函数调用要用栈,局部变量存在栈里,全局变量存在数据段里?

这就是分段机制要解决的问题——让内存管理符合程序的逻辑结构

为什么需要分段?

分页解决了外部碎片问题,但页是固定大小的,没有「意义」。 分段则是按程序逻辑划分的。

分页的视角(物理化):
┌─────────────────────────────────────────────────────────┐
│ 虚拟地址空间(程序员的困惑):                             │
│                                                          │
│ 0x0000 ┌────────────────┐                               │
│        │  代码 | 数据 |  │  ← 都混在一起,程序员不理解     │
│        │  堆   | 栈    │                                │
│ 0xFFFF └────────────────┘                               │
│                                                          │
│ 物理内存(硬件):                                        │
│ 帧0 ┌────┐  帧1 ┌────┐  帧2 ┌────┐  帧3 ┌────┐        │
│     │ 页1 │     │ 页5 │     │ 页3 │     │ 页2 │        │
│     └────┘      └────┘      └────┘      └────┘        │
│     完全分散,物理上连续或不连续对程序员没有意义           │
└─────────────────────────────────────────────────────────┘

分段的视角(逻辑化):
┌─────────────────────────────────────────────────────────┐
│ 程序的逻辑结构:                                          │
│                                                          │
│ 代码段  ┌─────────────────────┐  ← 只读、可执行          │
│        │ main() {...}        │                          │
│        │ function() {...}    │                          │
│        └─────────────────────┘                          │
│                                                          │
│ 数据段  ┌─────────────────────┐  ← 可读写、初始化        │
│        │ global_var = 100    │                          │
│        └─────────────────────┘                          │
│                                                          │
│ BSS段   ┌─────────────────────┐  ← 可读写、未初始化       │
│        │ uninitialized_data  │                          │
│        └─────────────────────┘                          │
│                                                          │
│ 堆      ┌─────────────────────┐  ← 可读写、动态分配      │
│        │ malloc()返回的内存   │                          │
│        └─────────────────────┘                          │
│              ↑                                          │
│             malloc                                       │
│                                                          │
│ 栈      ┌─────────────────────┐  ← 可读写、自动管理      │
│        │ function()的局部变量 │                          │
│        │ 返回地址             │                          │
│        └─────────────────────┘                          │
└─────────────────────────────────────────────────────────┘

分段 vs 分页

特性分段分页
大小变量大小固定大小
视角程序员视角(逻辑)硬件视角(物理)
碎片外部碎片内部碎片
目的逻辑分离、共享、安全高效利用内存、简化分配
权限可设不同权限(代码只读、数据可读写)通常统一管理

段表

每个进程有一个段表,记录每个段的基址和界限。

┌──────────────────────────────────────────────────────────┐
│                      段表结构                              │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  段号 │   基址(Base)   │  界限(Limit) │  权限  │ 说明    │
│  ───┼──────────────┼────────────┼──────┼─────────   │
│   0  │   0x0000    │   4KB      │  R/X │ 代码段     │
│   1  │   0x1000    │   8KB      │  R/W │ 数据段     │
│   2  │   0x3000    │   4KB      │  R/W │ BSS段      │
│   3  │   0x7000    │   动态    │  R/W │ 堆段       │
│   4  │   0xFF000   │   动态    │  R/W │ 栈段       │
│                                                          │
└──────────────────────────────────────────────────────────┘

段表基址寄存器(STBR):指向段表在内存中的起始地址
段表长度寄存器(STLR):记录段表有多少个段

地址转换

逻辑地址 = 段号 + 段内偏移

     ┌────────────────────────┐
     │     段号    │  段内偏移   │
     └─────────┬──┴────────────┘


     ┌────────────────────────┐
     │    检查段号 < STLR     │──否──→ 越界错误
     └─────────┬──────────────┘
               │是

     ┌────────────────────────┐
     │  查段表:基址+界限检查  │──否──→ 越界错误
     └─────────┬──────────────┘
               │是

     ┌────────────────────────┐
     │    物理地址 = 基址+偏移 │
     └────────────────────────┘
java
public class Segmentation {
    private static final int SEGMENT_COUNT = 5;

    // 段表项
    private static class SegmentTableEntry {
        int base;       // 基址
        int limit;      // 界限
        int rights;     // 权限 (R/W/X)
        boolean valid;  // 是否有效
    }

    private SegmentTableEntry[] segmentTable;
    private int stbr;   // 段表基址寄存器
    private int stlr;   // 段表长度寄存器

    public Segmentation() {
        segmentTable = new SegmentTableEntry[SEGMENT_COUNT];
        for (int i = 0; i < SEGMENT_COUNT; i++) {
            segmentTable[i] = new SegmentTableEntry();
        }
    }

    // 初始化典型段
    public void initializeProcess(int codeSize, int dataSize, int bssSize) {
        int currentBase = 0;

        // 代码段
        segmentTable[0].valid = true;
        segmentTable[0].base = currentBase;
        segmentTable[0].limit = codeSize;
        segmentTable[0].rights = 0b101;  // 读+执行
        currentBase += codeSize;

        // 数据段
        segmentTable[1].valid = true;
        segmentTable[1].base = currentBase;
        segmentTable[1].limit = dataSize;
        segmentTable[1].rights = 0b011;  // 读+写
        currentBase += dataSize;

        // BSS段
        segmentTable[2].valid = true;
        segmentTable[2].base = currentBase;
        segmentTable[2].limit = bssSize;
        segmentTable[2].rights = 0b011;  // 读+写

        stlr = 3;  // 只有3个段
    }

    // 地址转换
    public int translate(int segmentNumber, int offset) throws SegFaultException {
        // 1. 检查段号是否有效
        if (segmentNumber >= stlr || !segmentTable[segmentNumber].valid) {
            throw new SegFaultException("Invalid segment: " + segmentNumber);
        }

        SegmentTableEntry seg = segmentTable[segmentNumber];

        // 2. 检查偏移是否越界
        if (offset < 0 || offset >= seg.limit) {
            throw new SegFaultException("Offset out of bounds: " + offset);
        }

        // 3. 物理地址 = 基址 + 偏移
        return seg.base + offset;
    }

    // 检查权限
    public boolean hasPermission(int segmentNumber, int requiredRights) {
        return (segmentTable[segmentNumber].rights & requiredRights)
               == requiredRights;
    }
}

分段的保护与共享

段级保护

java
public class SegmentProtection {
    public void accessMemory(int segmentNumber, int offset, String operation) {
        int requiredRights = 0;

        switch (operation) {
            case "read":
                requiredRights = 0b001;  // 读权限
                break;
            case "write":
                requiredRights = 0b010;  // 写权限
                break;
            case "execute":
                requiredRights = 0b100;  // 执行权限
                break;
        }

        if (!hasPermission(segmentNumber, requiredRights)) {
            throw new SecurityException("Permission denied: " + operation);
        }
    }
}

段级共享

代码段可以被多个进程共享,节省内存。

进程A:                              进程B:
┌────────────────────┐               ┌────────────────────┐
│ 代码段A (共享)     │◄─────────────►│ 代码段B (共享)     │
│                    │   同一物理帧   │                    │
└────────────────────┘               └────────────────────┘
         ↑                                   ↑
    段表A指向帧3                         段表B指向帧3

各自的数据段独立:
┌────────────────────┐               ┌────────────────────┐
│ 数据段A            │               │ 数据段B            │
│ (独立副本)         │               │ (独立副本)         │
└────────────────────┘               └────────────────────┘

段页式管理

分段和分页各有优缺点,于是有了段页式——先分段,再分页。

为什么需要段页式?

纯分段纯分页
✅ 逻辑清晰✅ 无外部碎片
✅ 便于共享和保护✅ 分配简单
❌ 外部碎片❌ 权限管理不灵活
❌ 内存分配复杂❌ 不符合逻辑视图

段页式的工作原理

1. 逻辑地址:段号 + 段内偏移

2. 查段表:
   段号 → 段描述符(包含段基址和段限长)
   
3. 段内偏移分成:页号 + 页内偏移

4. 查页表:
   页号 → 帧号
   
5. 物理地址:帧号 + 页内偏移

┌─────────────────────────────────────────────────────────┐
│                    段页式地址转换                         │
├─────────────────────────────────────────────────────────┤
│                                                          │
│  逻辑地址:  ┌────────┬────────────────────────┐          │
│            │ 段号    │      段内偏移           │          │
│            │ (8位)   │      (24位)            │          │
│            └────────┴────────────┬─────────────┘          │
│                                  │                        │
│                                  ▼                        │
│                         ┌───────────────┐                 │
│                         │  段表查找     │                 │
│                         │ 基址+界限检查 │                 │
│                         └───────┬───────┘                 │
│                                 │                        │
│                                 ▼                        │
│  段内偏移:  ┌────────┬────────────┐                      │
│            │ 页号    │  页内偏移   │                      │
│            │ (12位)  │   (12位)   │                      │
│            └────────┴─────┬──────┘                       │
│                            │                             │
│                            ▼                             │
│                   ┌───────────────┐                      │
│                   │  页表查找     │                      │
│                   │  帧号         │                      │
│                   └───────┬───────┘                      │
│                           │                             │
│                           ▼                             │
│  物理地址:  ┌────────┬────────────┐                      │
│            │ 帧号    │  页内偏移   │                      │
│            │ (20位) │   (12位)   │                      │
│            └────────┴────────────┘                      │
│                                                          │
└─────────────────────────────────────────────────────────┘
java
public class SegmentedPaging {
    private static final int PAGE_SIZE = 4096;

    // 段表项(包含指向页表的基址)
    private static class SegmentDescriptor {
        int pageTableBase;   // 页表基址
        int segmentLimit;    // 段界限(页数)
        int rights;          // 权限
    }

    // 页表项
    private static class PageTableEntry {
        int frameNumber;
        boolean valid;
        boolean dirty;
        boolean referenced;
    }

    private SegmentDescriptor[] segmentTable;
    private PageTableEntry[][] pageTables;  // 每个段有自己的页表

    public int translate(int segmentNumber, int offset) throws Exception {
        // 1. 检查段号
        if (segmentNumber >= segmentTable.length) {
            throw new Exception("Invalid segment");
        }

        SegmentDescriptor seg = segmentTable[segmentNumber];

        // 2. 分解页号和页内偏移
        int pageNumber = offset / PAGE_SIZE;
        int pageOffset = offset % PAGE_SIZE;

        // 3. 检查页号是否越界
        if (pageNumber >= seg.segmentLimit) {
            throw new Exception("Offset out of segment");
        }

        // 4. 查页表
        int pageTableIndex = seg.pageTableBase + pageNumber;
        PageTableEntry pte = pageTables[segmentNumber][pageNumber];

        if (!pte.valid) {
            throw new PageFaultException("Page not in memory");
        }

        // 5. 组合物理地址
        return (pte.frameNumber << 12) | pageOffset;
    }
}

实际案例:Linux的段页式

Linux对x86架构做了特殊的段页式处理:

c
// Linux的段表(简化)
// 代码段和数据段分开
// 用户态和内核态分开

// GDT (Global Descriptor Table)
// 4个重要的段描述符:

// 1. 内核代码段
//    Base: 0x00000000
//    Limit: 0xFFFFFFFF
//    Type: Code, Read/Execute

// 2. 内核数据段
//    Base: 0x00000000
//    Limit: 0xFFFFFFFF
//    Type: Data, Read/Write

// 3. 用户代码段
//    Base: 0x00000000
//    Limit: 0xFFFFFFFF
//    Type: Code, Read/Execute
//    DPL: 3 (用户态)

// 4. 用户数据段
//    Base: 0x00000000
//    Limit: 0xFFFFFFFF
//    Type: Data, Read/Write
//    DPL: 3 (用户态)

Linux的秘密:Linux虽然使用了段机制,但所有段的基址都是0,界限都是4GB。 这意味着段不起实际作用,真正管理内存的是分页机制。 这是因为x86架构必须配置段寄存器,但Linux选择「绕过」它。

面试追问方向

  • 段页式管理和纯分段相比,有什么优势? 提示:结合两者优点,消除外部碎片。
  • Linux为什么选择让所有段的基址都是0? 提示:简化设计、统一内存管理。
  • 段的界限检查和页的界限检查有什么不同? 提示:段界限是字节级别,页界限是页对齐的。
  • 如何实现段的动态增长? 提示:类似堆的扩展机制。

基于 VitePress 构建