ZeroOne AI
← 返回文章列表

Linux系统篇(十五)——进程(四):一文吃透 Linux 虚拟地址空间:从页表映射到内核结构体全链路拆解

👁 3
分类:工业互联网

从全局变量入手,串联进程地址隔离、页表翻译、写时拷贝、mm_struct/vm_area_struct 结构体与缺页中断,打通应用代码到内核的内存链路。

项目背景

初学 Linux 操作系统时,虚拟地址空间、页表、进程内存分段、fork 写时拷贝(COW)这些概念总是晦涩难懂,很多人停留在"背概念"阶段,看不懂底层数据结构之间的关联。我们平时写 C 语言定义全局变量、用 malloc 申请堆内存、调用 fork 创建子进程,所有内存操作看似运行在连续内存上,实则全部依托操作系统的虚拟内存机制做中转。

本文从一个全局变量 int g_val = 100 入手,由浅入深地串联起进程地址隔离、页表地址翻译、父子进程 COW、内核 mm_struct/vm_area_struct 结构体、缺页中断、mmap 映射这一整套逻辑,打通从应用代码到操作系统内核的完整内存链路。弄懂虚拟地址空间,是理解进程隔离、内存管理、段错误、缺页异常的核心钥匙。

技术方案

1. Linux 32 位进程虚拟地址空间分布

32 位系统总虚拟地址为 4GB:高地址 1GB 是内核空间(操作系统内核独占,用户进程无法直接访问),低地址 3GB 是用户空间。用户空间按从低地址到高地址可分为:

用一段代码验证各段的地址分布:

#include <stdio.h>
#include <stdlib.h>

int g_unval;          // .bss 段
int g_val = 100;      // .data 段

int main(int argc, char *argv[], char *env[])
{
    const char *str = "helloworld";     // 只读字符串常量
    char *heap_mem = (char*)malloc(10); // 堆内存

    printf("code addr: %p\n", main);        // 代码段地址
    printf("init global addr: %p\n", &g_val);     // .data
    printf("uninit global addr: %p\n", &g_unval); // .bss
    printf("heap addr: %p\n", heap_mem);          // 堆
    printf("stack addr: %p\n", &heap_mem);        // 栈
    printf("read only string addr: %p\n", str);   // 只读区

    for (int i = 0; i < argc; i++)
        printf("argv[%d]: %s\n", i, argv[i]);
    return 0;
}

打印出的地址会呈现出"代码段 < 数据段 < 堆 < 栈 < 命令行参数区"的递增规律,直观验证分区结构。

系统架构

1. 为什么需要虚拟地址空间

从三个基础概念切入:

  1. 一个进程,一个虚拟地址空间:Linux 为每个进程分配独立的虚拟地址空间,这是进程隔离的基石;
  2. 一套页表,绑定一个进程:页表是虚拟地址与物理地址之间的"翻译手册",每个进程有专属页表;
  3. 页表 = 虚实地址翻译中介:CPU 访问变量时,通过 MMU(硬件地址翻译器)查页表,完成虚拟地址到真实物理内存的转换。

int g_val = 100 为例:代码里操作的是虚拟地址,CPU 借助页表查询找到对应的物理内存单元完成读写。

页表存在的三大意义:

2. 写时拷贝(COW)

子进程通过 fork 创建后,初期并不会立刻复制物理内存,而是采用写时拷贝策略:父子进程先共享同一份物理页;只有任意一方修改变量时,操作系统才分配新物理内存并更新页表映射。这正是 fork 高效的核心原因。

#include <stdio.h>
#include <unistd.h>

int gval = 100;

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        while (1)
        {
            printf("子: gval: %d, &gval: %p, pid: %d, ppid: %d\n",
                   gval, &gval, getpid(), getppid());
            sleep(1);
            gval++;   // 子进程写入触发 COW,与父进程分离
        }
    }
    else
    {
        while (1)
        {
            printf("父: gval: %d, &gval: %p, pid: %d, ppid: %d\n",
                   gval, &gval, getpid(), getppid());
            sleep(1);
        }
    }
    return 0;
}

运行后可以发现:父子进程打印的 &gval 虚拟地址相同,但 gval 值互不影响——虚拟地址一样,物理页却已经通过 COW 分开了。

3. 缺页中断

当程序访问了一个尚未映射到物理内存的虚拟地址时,MMU 硬件会立刻暂停程序并通知操作系统"快去把数据加载到内存",这个"通知 + 加载"的过程就是缺页中断。之所以会有缺页中断,是因为虚拟地址不等于物理内存——操作系统为了省内存、提高效率,不会把程序的所有代码和数据一次性全部加载进内存,而是按需加载。

实施过程

1. 分段管理:区域边界可调

进程的虚拟地址被划分为多个固定区域:代码段(Text)、数据段(Data/BSS)、堆(Heap)、栈(Stack)、共享库区。想要新增或修改内存区域,只需要修改对应区域的 start/end 边界值,内核即可识别新的地址区间。

2. 内核管控结构体:mm_struct

mm_struct 是整个进程虚拟内存的总描述符,一个进程只有一个 mm_struct,记录代码段、数据段、堆、栈等全段边界和页表指针:

struct mm_struct {
    // 1. .text 代码段
    unsigned long start_code;
    unsigned long end_code;

    // 2. .data 初始化全局数据段
    unsigned long start_data;
    unsigned long end_data;

    // 3. .bss 未初始化全局区位于 end_data ~ start_brk 之间

    // 4. 堆边界:start_brk 是堆起始,brk 是堆当前末尾
    //    (malloc/brk 扩展通过修改 brk 实现)
    unsigned long start_brk;
    unsigned long brk;

    // 5. mmap 共享库/文件映射区起始基准
    unsigned long mmap_base;

    // 6. 用户栈起始(栈从高地址往下增长)
    unsigned long start_stack;

    // 7. 命令行参数 argv 区域
    unsigned long arg_start;
    unsigned long arg_end;

    // 8. 环境变量 env 区域
    unsigned long env_start;
    unsigned long env_end;
};

task_struct(进程 PCB)中内嵌 struct mm_struct *mm 指针,形成 task_struct → mm_struct → vm_area_struct 三级内存管理链。

3. 内核管控结构体:vm_area_struct

vm_area_struct(VMA)描述单个内存区域,一个进程有多个 VMA(代码区一个、堆一个、栈一个、每个动态库一个),串联成链表:

struct vm_area_struct {
    struct mm_struct *vm_mm;  // 归属哪个进程的地址空间
    unsigned long vm_start;   // 该 VMA 区域起始虚拟地址
    unsigned long vm_end;     // 区域结束后第一个地址(左闭右开:[vm_start, vm_end))
    // ... 链表、权限、文件映射等成员
};

整体层级关系:task_struct(进程)→ mm_struct(内存总管)→ 多个 vm_area_struct(各内存分区)。

应用价值

Linux 虚拟地址空间的整套机制可以概括为一条完整链路:内核靠 task_struct → mm_struct → vm_area_struct 三层结构体管理进程虚拟地址,MMU 配合页表完成虚实地址转换,缺页中断实现按需分配物理内存,写时拷贝保证 fork 的高效与进程的独立。

理解这条链路,才能回答"为什么段错误会发生""为什么 fork 那么快""malloc 到底怎么工作"这类核心问题,也是深入内核内存管理、性能调优和容器隔离机制(命名空间 + 独立地址空间)的必要前提,是从"会用 Linux"迈向"懂 Linux"的关键一步。

SEO关键词

Linux, 虚拟地址空间, 页表, mm_struct, vm_area_struct, 写时拷贝, 缺页中断, 内存管理

评论(0