Linux 内核 vm_area_struct与vm_struct

vm_area_struct :管理【进程的用户态虚拟内存】

vm_struct :管理【内核态的虚拟内存】

vm_area_struct进程用户态虚拟内存的分段描述符vm_struct内核全局虚拟内存的描述符 ,二者无任何继承关系,是两套独立的内存管理体系。一个管用户层虚拟内存,一个管内核层虚拟内存;

vm_area_struct与vm_struct对比表

对比维度 vm_area_struct vm_struct
核心归属 进程地址空间(mm_struct 内核全局地址空间(唯一,所有进程共享)
管理对象 进程的用户态虚拟地址区间 内核的内核态虚拟地址区间
映射物理内存 物理内存页(用户进程的堆 / 栈 / 代码段 /mmap 映射) 物理内存页(内核的缓冲区 / 驱动内存 / 模块空间)
是否参与页表映射 参与,最终会生成进程的用户态页表项 参与,最终会生成内核的全局页表项
是否支持 Swap 换出 支持,普通 VMA 的物理页可被 swap out 到交换分区 绝对不支持,内核内存禁止 Swap
是否可加锁(vm_lock) 支持,打VM_LOCKED标记,物理页常驻内存 无锁机制,内核内存本身就常驻物理内存
内存属性 可私有(进程独占)、可共享(多进程共享) 全局共享,所有进程的内核态都能访问同一段地址
核心标志位 VM_READ/WRITE/EXEC/LOCKED/SHARED/ANON VM_IO/VM_ALLOC/VM_MAP 等内核专属标志
核心关联结构体 mm_struct(一对多,一个进程对应多个 VMA) vmap_areavmap_pgtables(内核虚拟内存池)
是否有并发锁保护 Linux5.4 + 有vm_lock自旋锁 + 进程mmap_lock 内核全局vmap_lock自旋锁保护,全局唯一

vm_area_struct 是 Linux 内核描述进程虚拟地址空间中连续区间 的核心数据结构,它也被称为虚拟内存区域(VMA) 。内核通过管理多个 vm_area_struct 实例,构建出进程的完整虚拟地址空间,同时实现虚拟内存的权限控制、映射管理和缺页异常处理。

进程的虚拟地址空间并非整块连续的区域,而是由多个互不重叠、权限不同的 VMA 组成,例如:

  • 代码段(.text):只读、可执行
  • 数据段(.data/.bss):可读可写、不可执行
  • 堆(heap):可读可写、动态扩展
  • 栈(stack):可读可写、自动扩展
  • 内存映射段(mmap):如共享库、文件映射

vm_area_struct 的核心作用:

  1. 描述一个连续虚拟地址区间的起始地址、结束地址、访问权限
  2. 关联该区间对应的内存映射类型(如匿名映射、文件映射)。
  3. 提供缺页异常处理函数,当进程访问该区间未映射的地址时,内核会调用对应的处理逻辑。
  4. 组织成红黑树和双向链表,方便内核快速查找、插入和删除 VMA。

vm_area_struct 的结构体定义

关键字段解析

字段 核心作用
vm_start/vm_end 定义 VMA 的虚拟地址范围,左闭右开 区间 [vm_start, vm_end)
vm_mm 关联到进程的 mm_struct,一个 mm_struct 对应多个 vm_area_struct
vm_page_prot 页表项的保护属性,最终会写入 PTE 表项的权限位,由 MMU 硬件强制执行
vm_flags 软件层面的标志,控制内核对该 VMA 的处理逻辑,如 VM_SHARED(共享映射)、VM_ANON(匿名映射)
vm_ops 缺页处理的核心接口,例如 fault 函数用于处理该 VMA 的缺页异常
vm_file 区分映射类型:非 NULL 为文件映射 (如 mmap 共享库),NULL 为匿名映射(如堆、栈)

vm_area_struct 的组织方式

每个进程的 mm_struct 中,通过两种数据结构组织所有 vm_area_struct

  1. 双向链表 :字段 mm->mmap 指向链表头,vm_next/vm_prev 串联所有 VMA,适合遍历所有区间(如进程退出时销毁所有 VMA)。
  2. 红黑树 :字段 mm->mm_rb 指向树根,vm_rb 为树节点,适合快速查找(如缺页异常时定位 VMA)。

两种结构的关系:同一个 VMA 同时存在于链表和红黑树中,内核根据场景选择不同的结构操作。

关键标志位 vm_flags 与权限控制

vm_flags 是 VMA 的核心属性,控制内核对该区间的管理策略,常见标志位如下:

标志位 含义
VM_READ 该区间允许读访问
VM_WRITE 该区间允许写访问
VM_EXEC 该区间允许执行
VM_SHARED 共享映射(多个进程可共享该区间的物理页)
VM_ANON 匿名映射(无对应的文件,如堆、栈)
VM_GROWSDOWN 该区间可向下扩展(如栈)
VM_GROWSUP 该区间可向上扩展(如堆)

权限检查规则

  • 进程访问 VMA 的权限必须包含在 vm_flags 中,否则触发缺页异常。
  • vm_page_protvm_flags 的硬件映射,内核会将 vm_flags 转换为页表项的权限位(如 _PAGE_READ_PAGE_WRITE),由 MMU 强制执行。

VMA 的创建

当进程通过 mmap()brk() 等系统调用申请虚拟内存时,内核会创建新的 vm_area_struct

  • mmap() 系统调用 :用户态调用 mmap() 后,内核执行 do_mmap() 函数,分配一段空闲虚拟地址区间,初始化 vm_area_struct 的字段(如 vm_startvm_endvm_opsvm_file),并将其插入到 mm_struct 的链表和红黑树中。
  • 堆扩展(brk()brk() 会调整堆对应的 VMA 的 vm_end 地址,若相邻地址无冲突,直接扩展;否则创建新 VMA。
  • 栈扩展:栈的扩展是自动的,当进程访问栈顶之外的地址时,触发缺页异常,内核会扩展栈对应的 VMA。

VMA 的查找

内核需要频繁查找进程虚拟地址对应的 VMA,例如缺页异常处理内存保护检查 时,核心函数是 find_vma()

struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr);

  • 功能:在 mm 指向的地址空间中,查找包含 addr 或第一个大于 addr 的 VMA。
  • 实现:优先通过红黑树快速查找(时间复杂度 O(logn)),链表仅用于遍历所有 VMA。

VMA 的合并与拆分

  • 合并 :当新创建的 VMA 与相邻 VMA 的权限、映射类型、vm_ops 完全相同时,内核会将它们合并为一个大的 VMA,减少 VMA 数量,提升管理效率。
  • 拆分 :当进程通过 munmap() 释放 VMA 中间的一段地址时,内核会将原 VMA 拆分为两个独立的 VMA。

VMA 的销毁

当进程释放虚拟内存时(如 munmap()、进程退出),内核会调用 do_munmap() 函数,从链表和红黑树中移除对应的 vm_area_struct,并释放其占用的内存。

vm_area_struct 与缺页异常

vm_area_struct 是内核处理缺页异常的关键依据,完整流程如下:

  1. 进程访问一个虚拟地址,MMU 发现该地址对应的页表项无效,触发缺页异常。
  2. 内核进入缺页异常处理函数 do_page_fault()
  3. 调用 find_vma(mm, addr) 查找该地址所属的 VMA:
    • 若未找到 VMA → 地址非法,发送 SIGSEGV 信号终止进程。
    • 若找到 VMA → 检查访问权限是否匹配(如写只读 VMA),不匹配则发送 SIGSEGV
  4. 调用 VMA 的 vm_ops->fault 函数,处理具体的缺页逻辑:
    • 匿名映射(堆 / 栈) :分配物理页,建立页表映射(anon_vma 相关逻辑)。
    • 文件映射(共享库) :从磁盘读取文件数据到物理页,建立页表映射(filemap_fault)。
  5. 更新页表项,刷新 TLB,异常返回后进程继续执行。

vm_struct 的设计初衷

专门用来描述内核态中,vmalloc() 系列函数分配的「虚拟地址连续、物理地址离散」的内核虚拟内存区间,是内核管理「vmalloc 内存区」的核心结构体。

vm_struct 结构体完整定义

vm_struct 关键字段

  1. addr :核心字段,内核分配的连续虚拟地址起始地址 ,内核代码直接使用这个地址访问内存,这个地址一定落在内核的 vmalloc 地址区间(x86_64: 0xffffc90000000000 ~ 0xffffe7ffffffffff);
  2. pages + nr_pagespages是物理页指针数组,每个元素指向一个struct page物理页结构体;nr_pages是数组长度,记录该虚拟地址映射了多少个离散的物理页;这两个字段是实现「虚拟连续、物理离散」的核心;
  3. flags :内核虚拟内存的类型标志,核心取值如下,无其他杂项:
    • VM_ALLOC:该内存由vmalloc()分配,物理页离散,虚拟地址连续;
    • VM_MAP:该内存由vmap()分配,把已有的物理页映射成连续虚拟地址;
    • VM_IOREMAP:该内存是内核对硬件 IO 地址的映射(如驱动映射外设寄存器);
  4. next :所有的vm_struct通过单向链表串联,内核维护一个全局的vmlist链表头,管理所有内核态的vm_struct
  5. phys_addr :仅在映射物理连续的内存时有效(如 IO 映射),vmalloc()分配的内存该字段为 0。

vm_struct 核心特性

归属「内核全局共享」,所有进程共用一份

内核只有一个全局的 vm_struct 链表vmlist),所有进程的内核态都访问这同一个链表,所有进程看到的内核虚拟地址空间是完全相同的。

对比:vm_area_struct 是进程私有,每个进程都有自己的 VMA 链表;vm_struct 是内核全局,所有进程共享。

管理「内核态虚拟内存」,绝对禁止用户态访问

vm_struct 描述的虚拟地址,全部落在内核地址空间 (高于PAGE_OFFSET),用户态进程无法直接访问,用户态访问会触发段错误(SIGSEGV),只有内核态代码(驱动、内核模块、内核系统调用)能访问。

对比:vm_area_struct 描述的是用户态虚拟地址,用户态进程可直接访问,内核态通过copy_to_user()/copy_from_user()间接访问。

虚拟地址连续,物理地址离散

这是vm_struct最核心的特点,也是和「线性映射」的核心区别:

  • 内核通过vmalloc()申请内存时,返回的addr连续的内核虚拟地址,内核代码可以像访问数组一样连续读写;
  • 但这个虚拟地址对应的物理内存页是离散的、不连续的 ,内核通过pages数组管理这些离散的物理页;
  • 内核会为这些离散的物理页,单独建立内核页表映射,MMU 通过页表完成地址翻译,对内核代码透明。

永不换出、永不回收,无 Swap、无锁定机制

vm_struct管理的内核内存,有两个「绝对禁止」:

  1. 绝对禁止被 Swap 换出到交换分区,内核内存永远不会进入 Swap;
  2. 绝对不会被内核内存回收机制(kswapd)回收 ,除非内核主动调用vfree()释放;

对比:vm_area_struct管理的用户内存,默认可被 Swap 换出,需要手动加VM_LOCKED才常驻内存;vm_struct无需任何锁定,天生常驻物理内存。

内存分配是「立即映射」,无懒加载

vmalloc()分配内存时,内核会立即分配物理页 + 建立内核页表映射,返回的虚拟地址可以直接使用,不会触发缺页异常;

对比:vm_area_struct是「懒加载」,创建时只分配虚拟地址,访问时才触发缺页异常分配物理页。

vm_area_structvm_struct 核心区别

【管理的内存层级】------ 最核心区别

  • vm_area_struct:管理进程的用户态虚拟内存(3G 以下,x86_32)/(0~0x7fffffffffff,x86_64);
  • vm_struct:管理内核的内核态虚拟内存(3G 以上,x86_32)/(0xffff888000000000 以上,x86_64)。

【归属与作用域】

  • vm_area_struct进程私有 ,每个进程都有独立的mm_struct和 VMA 链表,进程 A 的 VMA 对进程 B 不可见;
  • vm_struct内核全局共享 ,只有一个全局链表vmlist,所有进程的内核态都访问同一份vm_struct,内核内存对所有进程可见。

【物理地址与虚拟地址的关系】

  • vm_area_struct:无固定关系,可映射「物理连续」(如共享库)或「物理离散」(如堆)的内存,虚拟地址连续即可;
  • vm_struct固定为「虚拟连续、物理离散」 ,这是其核心设计目标,也是vmalloc()的核心价值。

【Swap 交换支持】

  • vm_area_struct:支持,普通 VMA 的物理页可被内核 kswapd 换出到 Swap 分区,需要时再换入;
  • vm_struct:完全不支持,内核内存永不 Swap,这是内核的硬性规定,防止内核崩溃。

【内存锁定机制】

  • vm_area_struct:支持,通过VM_LOCKED标志位 +vm_lock自旋锁,实现物理页常驻内存,禁止换出;
  • vm_struct:无需锁定,内核内存本身就常驻物理内存,无任何锁定相关的字段和机制。

【内存分配的加载方式】

  • vm_area_struct懒加载(按需映射),创建 VMA 时只分配虚拟地址区间,物理页和页表映射在进程访问时通过缺页异常分配;
  • vm_struct立即映射 ,调用vmalloc()时,内核立即分配物理页、建立页表映射,返回的虚拟地址可直接使用,无缺页异常。

【内存释放的主动程度】

  • vm_area_struct:可主动释放(munmap()),也可进程退出时由内核自动释放所有 VMA;
  • vm_struct必须主动释放 ,内核不会自动回收,调用vmalloc()后必须调用vfree()释放,否则会造成内核内存泄漏,最终导致系统崩溃。

【数据结构的组织方式】

  • vm_area_struct:双结构组织 → 双向链表 + 红黑树,链表用于遍历所有 VMA,红黑树用于快速查找指定地址的 VMA;
  • vm_struct:单结构组织 → 单向链表 ,内核中vm_struct的数量远少于进程的 VMA 数量,单向链表足够高效,无需红黑树。

【并发保护的锁机制】

  • vm_area_struct:双层锁保护 → 进程的mmap_lock(读写信号量,粗粒度) + 每个 VMA 的vm_lock(自旋锁,细粒度);
  • vm_struct:单层锁保护 → 内核全局的vmap_lock(自旋锁),保护vmlist链表的并发修改,全局唯一。

【核心创建 / 释放接口】

  • vm_area_struct:创建→do_mmap(),释放→do_munmap();用户态对应mmap()/munmap()系统调用;
  • vm_struct:创建→vmalloc()/vmap(),释放→vfree()/vunmap()只有内核态能调用,用户态无对应的系统调用。

vm_area_structvm_struct 的唯一关联

二者虽然是两套独立的内存管理体系,无继承、无嵌套、无包含关系 ,但在内核源码中,有且仅有一处间接关联: 二者都依赖内核的页表机制完成虚拟地址到物理地址的映射,最终都会把映射关系写入页表,由 CPU 的 MMU 硬件完成地址翻译。

  • vm_area_struct的映射关系 → 写入进程的用户态页表(每个进程独立的页表);
  • vm_struct的映射关系 → 写入内核的全局页表(所有进程共享的页表);

内核中内存分配的选型原则

原则 1:用户态进程申请内存 → 必用 mmap() → 对应 vm_area_struct

用户态进程所有的内存申请(堆、栈、共享内存、文件映射),最终都会调用mmap()系统调用,内核通过vm_area_struct管理这些内存,这是唯一的方式。

原则 2:内核态申请「小内存 + 物理连续」→ 用 kmalloc() → 走「线性映射」,无vm_struct

内核中绝大多数内存申请都是这种场景,比如分配内核结构体、缓冲区等,kmalloc()分配的内存是物理连续的,直接通过线性映射访问,效率极高,无需vm_struct参与。

原则 3:内核态申请「大内存 + 物理离散」→ 用 vmalloc() → 对应 vm_struct

当内核需要申请大块内存(如几 MB / 几十 MB),且不需要物理连续时,用vmalloc(),内核通过vm_struct管理这段内存,虚拟地址连续,物理地址离散,适合驱动的缓冲区、内核模块的大内存申请。

原则 4:内核态映射「硬件 IO 地址」→ 用 ioremap() → 对应 vm_struct(flags=VM_IOREMAP)

驱动开发中,映射外设的物理寄存器地址到内核虚拟地址,用ioremap(),底层会创建vm_struct结构体,标记VM_IOREMAP标志位,这是驱动开发的标配。

相关推荐
txinyu的博客2 小时前
sprintf & snprintf
linux·运维·算法
EverydayJoy^v^2 小时前
RH124简单知识点——第2章——调度未来任务
linux·运维
顶点多余2 小时前
静态链接 vs 动态链接,静态库 vs 动态库
linux·c++·算法
有梦想有行动3 小时前
ClickHouse的Partition和Part概念
linux·数据库·clickhouse
物理与数学3 小时前
linux内核 Multi-Gen LRU 算法
linux·linux内核
强风7943 小时前
Linux-线程的同步与互斥
linux·服务器
提伯斯6463 小时前
Orangepi R1内置了哪些网卡驱动?(全志H3的板子)
linux·网络·wifi·全志h3
技术摆渡人3 小时前
专题二:【驱动进阶】打破 Linux 驱动开发的黑盒:从 GPIO 模拟到 DMA 陷阱全书
android·linux·驱动开发
wishchin4 小时前
Jetson Orin Trt: No CMAKE_CUDA_COMPILER could be found
linux·运维·深度学习