本文主要以32位ARM架构为例,说明下Linux内核的内存管理机制。
内存管理的基础知识可参考:
内存管理机制概述
在 Linux 内核中,内存管理机制负责对系统内存进行分配、回收、保护和共享等操作,以高效利用有限的内存资源。ARM 架构作为广泛使用的嵌入式架构,其 Linux 内核的内存管理机制在遵循通用 Linux 内存管理框架的同时,也因 ARM 的硬件特性(如 MMU(内存管理单元)设计、地址空间划分等)存在一些特殊之处。以下从多个核心方面详细说明:
一、内存地址空间划分
ARM 架构的 Linux 内核将内存地址空间划分为物理地址和虚拟地址,通过 MMU 实现两者的映射,这是内存管理的基础。
物理地址(PA)
- 指内存硬件实际的地址,由物理内存芯片决定,范围由硬件配置(如内存容量)确定。例如,32 位 ARM 处理器的物理地址空间最大为 4GB(地址范围 0x00000000~0xFFFFFFFF)。
- 物理内存被划分为多个物理页框(Page Frame),页框大小通常为 4KB(也可配置为 8KB、16KB 等,取决于内核编译选项和硬件支持),是内存分配的最小物理单位。
虚拟地址(VA)
- 是 CPU 执行指令时使用的地址,通过 MMU 的页表映射到物理地址。ARM 的虚拟地址空间划分与处理器位数相关:
- 32 位 ARM:虚拟地址空间共 4GB,分为用户空间(User Space) 和内核空间(Kernel Space),常见划分方式为 “3:1”(用户空间 3GB,内核空间 1GB)或 “2:2”(适用于需要更大内核空间的场景)。
- 64 位 ARM(AArch64):虚拟地址空间通常为 48 位(支持 256TB),用户空间和内核空间的划分更灵活(如低 48 位中,用户空间占低 39 位,内核空间占高 9 位)。
二、页表与地址转换(MMU 的作用)
ARM 的 MMU 通过页表(Page Table) 实现虚拟地址到物理地址的转换,页表结构因 ARM 架构版本(如 ARMv7、ARMv8)略有差异,但核心逻辑一致:
多级页表
- 32 位 ARM(ARMv7):通常采用二级页表:
- 一级页表(Page Directory):每个表项对应一个二级页表,或直接映射一个大页(如 1MB)。
- 二级页表(Page Table):每个表项映射一个小页(如 4KB)。
- 64 位 ARM(AArch64):采用四级页表(Level 0~3),支持更小的页(如 4KB)和更大的块(如 2MB、1GB),提升地址转换效率。
页表项(PTE)的内容
- 除了物理地址的高 bits 外,页表项还包含访问权限(如只读 / 读写、用户 / 内核可访问)、缓存策略(如是否启用 Cache、Write-Back/Write-Through)等标志,MMU 根据这些标志进行内存保护和优化。
地址转换流程
- CPU 访问虚拟地址时,MMU 从页表基地址寄存器(如 ARMv7 的 TTBR0/TTBR1,分别对应用户 / 内核页表)加载页表,逐级解析虚拟地址的不同 bits,最终得到物理地址,并检查访问权限是否合法(不合法则触发缺页异常)。
三、内核空间的内存管理
内核空间的内存管理主要面向内核自身(如驱动、进程管理等),核心机制包括:
物理内存管理(页框分配)
- 内核通过伙伴系统(Buddy System) 管理物理页框,解决内存碎片问题:
- 将物理页框按 “2^n 个连续页框” 的块(如 1 页、2 页、4 页…)进行分组,每组称为一个 “阶”(order,0 阶为 1 页,1 阶为 2 页,最高阶通常为 11 阶,对应 2048 页)。
- 分配内存时,从满足大小的最小阶块中分配;释放时,若相邻块空闲则合并为更高阶块,减少碎片。
- 对于小于 1 页的内存(如几十字节),内核通过slab 分配器管理:
- 基于伙伴系统分配的页框,按对象类型(如进程描述符、inode 等)创建 “slab 缓存”,实现小内存的高效分配 / 释放,避免伙伴系统对小内存的低效处理。
内核空间的映射方式
- 直接映射区(Direct Mapping):内核空间中,大部分物理内存被直接映射到虚拟地址(虚拟地址 = 物理地址 + 偏移量,如 32 位 ARM 中偏移量通常为 0xC0000000),内核访问物理内存时可直接通过虚拟地址操作,无需动态页表映射。
- 高端内存(High Memory):32 位 ARM 中,若物理内存超过内核空间(如 1GB),超出部分称为 “高端内存”,内核通过临时映射(kmap) 或永久映射(kmap_atomic) 动态将其映射到内核虚拟地址的空闲区域,避免虚拟地址空间不足的问题(64 位 ARM 因虚拟地址空间大,通常无需高端内存机制)。
四、用户空间的内存管理
用户空间的内存由内核通过进程地址空间(Process Address Space) 管理,每个进程有独立的虚拟地址空间,通过页表与物理内存隔离:
进程地址空间的结构
- 每个进程的虚拟地址空间包含代码段(.text)、数据段(.data)、堆(Heap,动态分配内存,向上增长)、栈(Stack,函数调用使用,向下增长)、共享库等区域,内核通过
struct mm_struct
结构体维护这些区域的信息。内存分配与缺页异常
- 用户程序通过
malloc()
等函数申请内存时,内核先在进程虚拟地址空间中预留一块区域(不立即分配物理内存),当程序首次访问该区域时,MMU 发现虚拟地址未映射到物理地址,触发缺页异常(Page Fault)。- 内核处理缺页异常时,通过伙伴系统分配物理页框,更新进程页表,建立虚拟地址到物理地址的映射,之后程序可正常访问内存。
内存回收机制
- 当系统内存紧张时,内核通过页回收(Page Reclaim) 释放空闲内存:
- 对于未修改的文件页(如共享库代码),可直接释放,再次访问时从磁盘重新加载。
- 对于修改过的匿名页(如堆 / 栈数据),通过交换分区(Swap) 写入磁盘,释放物理页框,需要时再换入内存。
- 内核通过kswapd进程后台扫描内存页,根据 “最近最少使用(LRU)” 算法选择回收页,平衡内存使用效率和性能。
五、ARM 架构的特殊优化
Cache 管理
- ARM 处理器包含 L1(指令 / 数据分离)、L2(共享)等 Cache,内核通过Cache 操作指令(如
clean
、invalidate
)管理 Cache 与物理内存的一致性,例如:
- 向设备寄存器写入数据时,需确保 Cache 中的数据已刷写到物理内存(避免设备读取旧数据)。
- 读取设备写入的内存时,需先无效化 Cache(避免读取 Cache 中的旧数据)。
内存屏障(Memory Barrier)
- 由于 ARM 处理器可能存在乱序执行和Store Buffer,内核通过内存屏障指令(如
dmb
、dsb
、isb
)保证内存访问的顺序性,在多线程、中断、设备驱动等场景中确保数据一致性。非连续内存分配(CMA)
- 嵌入式场景中,部分设备(如 GPU、摄像头)需要连续物理内存,内核通过连续内存分配器(CMA) 在系统启动时预留一块内存区域,供设备驱动申请,避免因内存碎片无法分配连续页框。
总结
ARM 架构的 Linux 内核内存管理机制以虚拟地址与物理地址的映射为核心,通过 MMU 实现地址转换和内存保护;内核空间利用伙伴系统和 slab 分配器管理物理内存,用户空间通过进程地址空间和缺页异常实现动态内存分配;同时,针对 ARM 的硬件特性(如 Cache、内存屏障、嵌入式场景需求)进行了特殊优化,最终实现高效、安全、稳定的内存管理。
核心数据结构和文件
Linux 内核的内存管理系统依赖于一系列关键数据结构来组织和管理内存资源,同时相关实现代码分布在特定的目录文件中。以下从核心数据结构和关键目录文件两方面进行详细说明:
一、内存管理的关键数据结构
这些数据结构是内存管理的 "骨架",负责描述内存状态、地址空间、页框属性等核心信息:
1. 物理内存管理相关
struct page
(定义于include/linux/mm_types.h
)
最核心的数据结构之一,每个物理页框(Page Frame)对应一个struct page
实例,用于描述物理页的状态和属性。关键字段包括:
flags
:页状态标志(如PG_locked
(锁定)、PG_dirty
(脏页)、PG_reserved
(预留,不可回收)等)。count
:页的引用计数(0 表示空闲,>0 表示被使用)。mapping
:指向映射该页的地址空间(如文件 inode 或匿名映射)。lru
:用于将页链接到 LRU(最近最少使用)链表,供内存回收时选择。
struct zone
(定义于include/linux/mmzone.h
)
表示物理内存的一个区域(因硬件限制划分,如 DMA 区域、常规区域等)。关键字段包括:
free_area
:伙伴系统的核心结构,管理不同阶(order)的空闲页框块。nr_free_pages
:该区域的空闲页数量。zone_pgdat
:指向该区域所属的pg_data_t
(内存节点)。watermark
:内存水位线(min/low/high),用于触发内存回收。
struct pg_data_t
(定义于include/linux/mmzone.h
)
对应一个物理内存节点(适用于 NUMA 架构),管理该节点下的所有内存区域。关键字段:
node_zones
:该节点包含的struct zone
数组(如ZONE_DMA
、ZONE_NORMAL
等)。kswapd
:负责该节点内存回收的 kswapd 内核线程。
struct free_area
(定义于include/linux/mmzone.h
)
伙伴系统中管理空闲页框块的结构,每个zone
包含一个free_area
数组,数组索引对应 "阶"(order):
free_list
:空闲页框块的链表(同阶的块通过链表连接)。nr_free
:该阶的空闲块数量。2. 虚拟内存管理相关
struct mm_struct
(定义于include/linux/mm_types.h
)
描述一个进程的完整地址空间,每个进程(线程组共享)对应一个mm_struct
。关键字段:
pgd
:指向进程的页全局目录(页表的根)。mmap
:指向虚拟内存区域(VMA)的链表头。mmap_sem
:保护 VMA 操作的读写信号量。total_vm
:进程使用的总虚拟页数。start_code
/end_code
:代码段的虚拟地址范围。
struct vm_area_struct
(定义于include/linux/mm_types.h
)
描述进程地址空间中的一个连续虚拟内存区域(如代码段、堆、共享库等)。关键字段:
vm_start
/vm_end
:区域的虚拟地址范围。vm_flags
:区域属性(如VM_READ
、VM_WRITE
、VM_EXEC
权限,VM_ANON
表示匿名映射)。vm_ops
:指向该区域的操作函数集(如open
、close
、fault
缺页处理)。vm_file
:若该区域映射到文件,则指向对应的struct file
(否则为 NULL,即匿名映射)。
struct vm_operations_struct
(定义于include/linux/mm.h
)
虚拟内存区域的操作函数集,核心是fault
回调(处理该区域的缺页异常),例如文件映射的fault
会从磁盘加载数据到内存。3. 内存分配与回收相关
struct kmem_cache
(定义于include/linux/slab.h
)
slab/slub 分配器中的缓存结构,用于管理同类型内核对象(如task_struct
、inode
)的内存池。关键字段:
object_size
:缓存中每个对象的大小。size
:每个 slab 块的总大小。slabs_free
/slabs_used
:空闲 / 使用中的 slab 块数量。
struct scan_control
(定义于include/linux/vmscan.h
)
内存回收(页扫描)的控制参数,用于vmscan.c
中,指定回收目标(如需要回收的页数)、优先级等。
struct swap_info_struct
(定义于include/linux/swap.h
)
描述一个 swap 分区或 swap 文件的信息,包括大小、已使用的 swap 页数量、优先级等。二、内存管理相关的核心目录与文件
Linux 内存管理的代码主要集中在
mm/
目录(通用逻辑)和arch/<架构>/mm/
目录(架构相关适配),关键文件如下:1. 通用内存管理(
mm/
目录)
page_alloc.c
:伙伴系统的核心实现,包括物理页框的分配(__alloc_pages()
)、释放(__free_pages()
)、内存区域(zone)管理等。slab.c
/slub.c
/slob.c
:三种小内存分配器的实现,提供kmalloc()
、kfree()
等接口,用于分配小于 1 页的内核对象。memory.c
:虚拟内存管理的核心,实现缺页异常处理(do_page_fault()
)、页表操作(pgd_alloc()
、pte_alloc()
)等。mmap.c
:进程地址空间管理,处理mmap()
/munmap()
系统调用,负责vm_area_struct
的创建、删除和合并。vmalloc.c
:内核虚拟内存分配器,实现vmalloc()
/vfree()
,用于分配非连续物理内存但连续的虚拟地址(如内核模块)。vmscan.c
:内存回收的核心逻辑,包括 LRU 链表管理、页扫描(shrink_zones()
)、kswapd 内核线程的工作流程。swap.c
/swap_state.c
:swap 机制实现,管理 swap 分区、匿名页的换出(swap_out()
)和换入(swap_in()
)。page_cache.c
:页缓存管理,负责文件数据在内存中的缓存(如add_to_page_cache()
、mark_page_dirty()
)。highmem.c
:32 位系统中高端内存的管理,提供kmap()
(临时映射)、kmap_atomic()
(原子映射)等接口。cma.c
:连续内存分配器(CMA)实现,管理预留的连续物理内存,供设备驱动申请大块连续内存。2. 架构相关实现(
arch/<架构>/mm/
)不同架构(如 x86、ARM)因 MMU、页表结构差异,需单独实现硬件相关逻辑:
arch/arm/mm/
(ARM 架构):
mmu.c
:ARM MMU 初始化、页表项设置(如访问权限、缓存策略)。fault.c
:ARM 缺页异常的底层处理(最终调用mm/memory.c
的通用逻辑)。cache.c
:ARM Cache 操作(清理、无效化),保证 Cache 与内存一致性。arch/x86/mm/
(x86 架构):
pgtable.c
:x86 页表结构(如 4 级页表)的创建和维护。mmu.c
:x86 MMU 控制(如 CR3 寄存器设置,指向页表基地址)。fault.c
:x86 缺页异常的中断处理。3. 关键头文件(
include/linux/
)
mm_types.h
:定义struct page
、struct mm_struct
、struct vm_area_struct
等核心结构体。mmzone.h
:定义struct zone
、struct pg_data_t
、struct free_area
等物理内存区域相关结构。gfp.h
:定义内存分配标志(如GFP_KERNEL
(可睡眠)、GFP_ATOMIC
(不可睡眠))。slab.h
:kmem_cache
结构体及 slab 分配器接口(kmem_cache_create()
、kmem_cache_alloc()
)。vmscan.h
:内存回收相关结构(如struct scan_control
)和函数声明。总结
Linux 内存管理的关键数据结构(如
struct page
、struct mm_struct
、struct zone
)构成了内存管理的逻辑基础,而mm/
目录下的文件实现了通用管理逻辑(伙伴系统、缺页处理、内存回收等),arch/<架构>/mm/
则负责硬件适配。理解这些结构和文件的作用,是深入掌握 Linux 内存管理机制的核心。
伙伴系统
伙伴系统(Buddy System)是 Linux 内核中用于管理物理内存页框分配与释放的核心机制,主要解决连续内存块的高效分配和内存碎片问题。它将物理内存划分为大小为 2^n 个页框的块(称为 “页块”),通过合并相邻空闲块的方式减少碎片,确保系统能快速分配不同大小的连续内存。
一、伙伴系统的核心思想
内存块的 “阶”(Order)划分
物理内存被划分为若干个 “阶”,每个阶对应一种大小的连续页框块:
- 0 阶(order 0):1 个页框(通常 4KB)
- 1 阶(order 1):2 个连续页框(8KB)
- 2 阶(order 2):4 个连续页框(16KB)
- ...
- 最大阶(通常为 11 阶):2048 个连续页框(8MB)
“伙伴” 的定义
两个大小相同(同阶)且物理地址连续的块互为 “伙伴”。例如,两个 0 阶块(各 1 页)若地址连续,则互为伙伴,可合并为 1 个 1 阶块(2 页)。
分配与释放的核心逻辑
- 分配:当请求 n 阶块时,先检查 n 阶是否有空闲块,有则直接分配;若无,则向更高阶(n+1)查找,找到后分裂为两个 n 阶伙伴块,分配其中一个,另一个加入 n 阶空闲链表。
- 释放:释放块时,检查其伙伴是否空闲,若是则合并为更高阶块,重复此过程直到无法合并或达到最大阶。
二、关键数据结构
伙伴系统的核心数据结构用于跟踪各阶空闲块的状态,主要包括:
struct free_area
(定义于include/linux/mmzone.h
)
每个内存区域(struct zone
)包含一个free_area
数组,数组索引对应 “阶”,用于管理该阶的空闲块:struct free_area { struct list_head free_list; // 空闲块链表(同阶块通过链表连接) unsigned long nr_free; // 该阶的空闲块数量 };
struct zone
(部分字段)
内存区域(如ZONE_DMA
、ZONE_NORMAL
)中包含伙伴系统的核心信息:struct zone { // ... 其他字段 ... struct free_area free_area[MAX_ORDER]; // 各阶空闲块管理结构 unsigned long nr_free_pages; // 该区域的总空闲页数量 // ... 其他字段 ... };
struct page
(部分字段)
每个物理页框的描述符中,与伙伴系统相关的字段用于标记块的阶和状态:struct page { // ... 其他字段 ... unsigned long private; // 对于空闲块,存储块的阶(order) struct list_head lru; // 用于链接到free_area的free_list链表 // ... 其他字段 ... };
三、分配与释放流程
1. 内存分配(以
__alloc_pages()
为例)
__alloc_pages()
是伙伴系统的核心分配函数,流程如下:
- 根据分配标志(如
GFP_KERNEL
)和阶数,确定目标内存区域(zone
)。- 检查目标阶(
order
)的free_area
是否有空闲块:
- 若有,从
free_list
中取出第一个块,减少nr_free
,返回该块的起始页框。- 若没有,向更高阶(
order+1
)查找,直到最大阶:
- 找到高阶空闲块后,将其分裂为两个同阶的伙伴块。
- 一个块用于分配,另一个加入低一阶的
free_list
。- 若所有阶均无可用块,触发内存回收(如通过
kswapd
)后重试,最终失败返回NULL
。2. 内存释放(以
__free_pages()
为例)
__free_pages()
用于释放页框块,流程如下:
- 将待释放块加入对应阶的
free_list
,增加nr_free
。- 检查该块的伙伴是否为空闲块(通过地址计算伙伴位置,验证其状态):
- 若伙伴空闲,将两者从当前阶链表中移除,合并为一个更高阶块。
- 重复合并过程,直到伙伴不空闲或达到最大阶。
- 更新内存区域的
nr_free_pages
。四、优势与局限性
优势
- 高效的连续内存分配:通过预划分阶数和分裂机制,快速满足不同大小的连续内存需求。
- 减少内存碎片:释放时合并伙伴块,避免小块内存碎片化导致无法分配大块内存。
- 低开销:基于链表操作,分配和释放的时间复杂度为 O (log n)(n 为最大阶)。
局限性
- 仅管理物理页框:伙伴系统面向物理内存,用户态的虚拟内存分配需结合虚拟内存管理(如缺页异常)。
- 大页分配效率低:高阶块(如 11 阶)数量少,频繁分配 / 释放可能导致碎片。
- 不适合小内存分配:对于小于 1 页的内存(如几十字节),效率低下,需依赖 slab/slub 分配器。
五、相关代码实现
伙伴系统的核心代码位于
mm/page_alloc.c
,关键函数包括:
__alloc_pages_nodemask()
:分配入口函数,处理 NUMA 节点和内存区域选择。get_page_from_freelist()
:从空闲链表中查找可用块。__free_pages()
:释放页框块的入口。__free_one_page()
:执行实际的释放和合并逻辑。expand()
:分裂高阶块为低阶块。总结
伙伴系统是 Linux 物理内存管理的基石,通过阶数划分和伙伴合并机制,高效管理连续页框的分配与释放,平衡了性能和内存利用率。它与 slab 分配器(小内存)、虚拟内存管理(用户态)共同构成了 Linux 内存管理的核心体系。
slab分配器
slab 分配器是 Linux 内核中用于高效管理小内存块分配的机制,专门解决伙伴系统在分配小于一页(通常 4KB)内存时效率低下的问题。它基于对象缓存的思想,为频繁分配和释放的同类型内核对象(如进程描述符、inode 节点等)建立专用内存池,从而快速分配与回收。
一、slab 分配器的核心思想
对象缓存
为每种频繁使用的内核对象(如task_struct
、inode
)创建一个专用缓存(kmem_cache
),缓存中预先分配多个相同大小的对象,避免重复初始化。基于页框的划分
缓存中的对象从伙伴系统分配的连续页框( slab 块)中划分,每个 slab 块包含多个同类型对象(如一个 4KB 页框可划分 8 个 512 字节的对象)。三级结构管理
采用kmem_cache
(缓存)→slab
(页框块)→object
(对象)三级结构,通过状态链表(满、部分满、空)跟踪对象的使用情况。二、关键数据结构
slab 分配器的核心数据结构定义于
include/linux/slab.h
:
struct kmem_cache
(缓存描述符)
每个内核对象类型对应一个kmem_cache
,管理该类型对象的所有 slab 块:struct kmem_cache { unsigned int object_size; // 每个对象的大小 unsigned int size; // 每个slab块的总大小(含元数据) unsigned int align; // 对象对齐要求 unsigned int num; // 每个slab块可容纳的对象数 // 状态链表:管理不同使用状态的slab块 struct list_head slabs_full; // 所有对象均被使用的slab struct list_head slabs_partial; // 部分对象被使用的slab struct list_head slabs_free; // 无对象被使用的slab unsigned long num_active; // 正在使用的对象总数 unsigned long num_slabs; // 总slab块数量 // 构造/析构函数(用于对象初始化/清理) void (*ctor)(void *obj); };
struct slab
(slab 块描述符)
描述一个由连续页框组成的 slab 块(通常为 1 页),跟踪块内对象的使用状态:struct slab { struct list_head list; // 链接到kmem_cache的状态链表(full/partial/free) unsigned long inuse; // 已使用的对象数 kmem_bufctl_t *freelist; // 空闲对象链表(指向第一个空闲对象) void *s_mem; // slab块中第一个对象的地址 unsigned int colour; // 用于地址对齐的偏移量(避免缓存冲突) };
三、分配与释放流程
1. 对象分配(
kmem_cache_alloc()
)
- 从
kmem_cache
的slabs_partial
链表中查找有空闲对象的 slab 块。- 若找到,从该 slab 的
freelist
中取出第一个空闲对象,更新inuse
计数,返回对象地址。- 若
slabs_partial
为空,检查slabs_free
链表:
- 若有空闲 slab 块,将其移至
slabs_partial
,再分配对象。- 若无,通过伙伴系统分配新页框,创建新 slab 块(划分对象、初始化
freelist
),加入slabs_partial
后分配对象。2. 对象释放(
kmem_cache_free()
)
- 将释放的对象加入对应 slab 块的
freelist
,减少inuse
计数。- 根据 slab 块的使用状态更新链表:
- 若释放后所有对象均空闲,将 slab 从
slabs_partial
移至slabs_free
。- 若仍有对象使用,保持在
slabs_partial
。- 当
slabs_free
中的 slab 块数量过多时,回收部分 slab(释放页框给伙伴系统)。四、优化机制
着色(Colouring)
为避免不同 slab 块中的对象映射到 CPU 缓存的同一位置(导致缓存冲突),通过colour
字段为每个 slab 块设置偏移量,使对象在缓存中均匀分布。对齐优化
确保对象地址满足硬件对齐要求(如 4 字节、8 字节对齐),避免访问未对齐内存导致的性能损失。构造函数
缓存创建时可指定ctor
函数,新对象分配时自动执行初始化(如清零、设置默认值),避免重复初始化开销。五、slab 的变种:slub 与 slob
Linux 内核提供三种 slab 分配器变种,可通过编译选项选择:
- slab:传统实现,功能完善但代码复杂,适合通用场景。
- slub:简化版 slab,移除冗余元数据,性能更优,现为多数系统的默认选择。
- slob:极简实现,内存开销小(适合嵌入式系统),但性能较低。
六、核心接口与代码位置
主要接口
kmem_cache_create()
:创建一个新的对象缓存。kmem_cache_alloc()
:从缓存中分配一个对象。kmem_cache_free()
:释放对象回缓存。kmem_cache_destroy()
:销毁缓存(释放所有 slab 块)。kmalloc()
/kfree()
:基于 slab 的通用接口(自动选择合适的缓存)。代码位置
- 传统 slab:
mm/slab.c
- slub 分配器:
mm/slub.c
- slob 分配器:
mm/slob.c
总结
slab 分配器通过对象缓存和精细管理,解决了小内存块分配的效率问题,与伙伴系统(管理页框)形成互补:伙伴系统负责大内存(整页及以上)分配,slab 负责小内存(小于一页)分配。这种分层设计使 Linux 内核能高效应对不同场景的内存需求。
虚拟内存管理
虚拟内存管理是 Linux 内核中负责将进程的虚拟地址空间与物理内存映射,并实现内存高效利用、隔离与保护的核心机制。它通过硬件(MMU,内存管理单元)和软件(内核页表管理、缺页处理等)的协同,为进程提供了 “连续且独立” 的虚拟地址空间,同时隐藏了物理内存的实际分布。
一、虚拟内存的核心作用
地址空间隔离
每个进程拥有独立的虚拟地址空间,进程间无法直接访问彼此的内存,确保安全性(例如,一个进程的崩溃不会影响其他进程)。
地址空间扩展
虚拟地址空间大小不受物理内存限制(可通过 swap 分区 / 文件扩展),允许进程使用比实际物理内存更大的地址空间。
内存高效利用
- 仅将进程当前使用的虚拟内存页映射到物理内存(“按需分配”),未使用的部分无需占用物理内存。
- 支持内存共享(如共享库、进程间共享内存),减少重复内存消耗。
简化编程模型
进程看到的是连续的虚拟地址,无需关心物理内存的碎片化或分布,简化了程序内存管理逻辑。
二、虚拟地址空间划分
Linux 的虚拟地址空间按 “用户空间” 和 “内核空间” 划分,具体范围取决于处理器位数:
32 位系统
虚拟地址空间共 4GB,常见划分方式为 “3:1”:
- 用户空间:0~3GB(进程私有,每个进程独立)。
- 内核空间:3GB~4GB(所有进程共享,内核代码 / 数据在此区域)。
64 位系统
虚拟地址空间通常为 48 位(支持 256TB),划分更灵活(如用户空间占低 39 位,内核空间占高 9 位),避免了 32 位系统的地址空间瓶颈。
用户空间内部结构
每个进程的用户虚拟地址空间包含以下区域(从低到高):
- 代码段(.text):存放可执行指令。
- 数据段(.data/.bss):存放全局变量和静态变量。
- 堆(Heap):动态内存分配区域(如
malloc()
),向上增长。- 共享库:动态链接库(如
libc
)的代码和数据。- 栈(Stack):函数调用和局部变量存储,向下增长。
- 内核映射区:如
mmap()
映射的文件或匿名内存。三、关键数据结构
虚拟内存管理的核心数据结构用于描述地址空间、虚拟区域和页表映射:
struct mm_struct
(进程地址空间描述符)
每个进程(或线程组)对应一个mm_struct
,描述整个虚拟地址空间:struct mm_struct { struct pgd *pgd; // 页全局目录(页表的根) struct vm_area_struct *mmap; // 虚拟内存区域(VMA)链表头 struct rb_root mm_rb; // VMA的红黑树(加速查找) struct semaphore mmap_sem; // 保护VMA操作的信号量 unsigned long total_vm; // 总虚拟页数 unsigned long locked_vm; // 被锁定的虚拟页数(不被换出) // 其他字段:如代码段/数据段的地址范围、页表操作统计等 };
struct vm_area_struct
(VMA,虚拟内存区域)
描述用户空间中一段连续的虚拟地址区域(如堆、栈、共享库),每个区域有独立的权限和属性:struct vm_area_struct { unsigned long vm_start; // 区域起始虚拟地址 unsigned long vm_end; // 区域结束虚拟地址(不包含) struct vm_area_struct *vm_next; // 下一个VMA(链表) struct rb_node vm_rb; // 红黑树节点(用于快速查找) struct mm_struct *vm_mm; // 所属的地址空间 pgprot_t vm_page_prot; // 页保护属性(如读写、执行权限) unsigned long vm_flags; // 区域标志(如VM_READ、VM_WRITE、VM_EXEC、VM_SHARED) struct vm_operations_struct *vm_ops; // 区域操作函数集 struct file *vm_file; // 映射的文件(NULL表示匿名映射) // 其他字段:如私有数据、偏移量等 };
struct vm_operations_struct
定义 VMA 的操作函数(如缺页处理、内存释放),核心是fault
回调(处理该区域的缺页异常):struct vm_operations_struct { void (*open)(struct vm_area_struct *vma); void (*close)(struct vm_area_struct *vma); vm_fault_t (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf); // 缺页处理 // 其他操作:如页换入、共享内存同步等 };
页表结构
虚拟地址到物理地址的映射通过多级页表实现(如 x86 的 4 级页表、ARMv8 的 4 级页表),核心结构包括:
- 页全局目录(PGD):页表的根节点。
- 页上级目录(PUD)、页中间目录(PMD):中间级页表。
- 页表项(PTE):最终映射到物理页框,包含权限(如读写、用户 / 内核访问)和缓存策略等标志。
四、核心机制:地址映射与缺页异常
1. 地址转换流程(MMU 的作用)
当进程访问虚拟地址时,MMU 按以下步骤将其转换为物理地址:
- 从
mm_struct->pgd
获取页全局目录(PGD)的基地址。- 解析虚拟地址的高几位,作为 PGD 索引,找到对应的 PUD 条目。
- 依次解析中间级页表(PUD→PMD→PTE),最终通过 PTE 找到物理页框的基地址。
- 结合虚拟地址的低几位(页内偏移),得到完整的物理地址。
- 若 PTE 无效(未映射物理页),MMU 触发缺页异常。
2. 缺页异常处理(
do_page_fault()
)缺页异常是虚拟内存 “按需分配” 的核心,流程如下:
- 内核通过
vmalloc()
查找引发异常的虚拟地址所属的 VMA(若不存在则触发段错误SIGSEGV
)。- 检查访问权限(如写一个只读 VMA 会触发
SIGSEGV
)。- 调用 VMA 的
vm_ops->fault
回调处理缺页:
- 文件映射(如共享库):从磁盘读取对应的数据块到物理页,建立映射。
- 匿名映射(如堆 / 栈):通过伙伴系统分配物理页,清零后建立映射。
- 交换页:从 swap 分区 / 文件换入数据到物理页,更新映射。
- 更新 PTE,将虚拟地址与物理页绑定,允许进程继续访问。
五、内存共享与 Copy-on-Write(写时复制)
内存共享
- 多个进程可通过共享 VMA(
VM_SHARED
标志)映射同一块物理内存(如共享库、shmget()
创建的共享内存),实现数据共享。写时复制(COW)
- 进程创建(
fork()
)时,内核不为子进程复制物理页,而是让父子进程共享物理页,并将 PTE 标记为 “只读”。- 当任一进程写入共享页时,触发缺页异常,内核为写进程分配新的物理页(复制原页数据),更新 PTE 为 “可写”,实现 “按需复制”,减少内存开销。
六、内存回收与交换(Swap)
当物理内存紧张时,内核通过以下机制释放内存:
- 页缓存回收:未修改的文件页(如共享库代码)可直接释放,再次访问时从磁盘重加载。
- 匿名页交换:修改过的匿名页(如堆 / 栈数据)被写入 swap 分区 / 文件,释放物理页,需要时再换入。
- LRU 算法:内核通过 LRU(最近最少使用)链表跟踪页的访问频率,优先回收不常用的页(
vmscan.c
实现)。七、核心代码与接口
核心代码位置
- 虚拟内存管理:
mm/memory.c
(缺页处理、页表操作)、mm/mmap.c
(VMA 管理)。- 页表操作:
mm/pgtable.c
(通用逻辑)、arch/<架构>/mm/pgtable.c
(架构相关)。- 内存回收:
mm/vmscan.c
(LRU 管理、页扫描)。关键接口
- 用户态:
malloc()
/free()
(基于brk()
或mmap()
)、mmap()
/munmap()
(内存映射)。- 内核态:
vmalloc()
/vfree()
(内核虚拟内存分配)、kmap()
(高端内存映射)。总结
虚拟内存管理通过 “虚拟地址→物理地址” 的映射、按需分配(缺页异常)、内存共享和回收机制,实现了高效、安全、灵活的内存使用。它是 Linux 多任务并发和大型程序运行的基础,也是内核中最复杂的子系统之一。
页表定义和管理
在 Linux 中,页表(Page Table)是实现虚拟地址到物理地址转换的核心数据结构,由硬件(MMU)和软件(内核)共同管理。页表通过多级结构实现地址映射,同时存储内存访问权限、缓存策略等关键信息,是虚拟内存管理的基础。
一、页表的基本概念与结构
页表的核心作用是建立虚拟地址(VA)到物理地址(PA)的映射,其结构与处理器架构密切相关(如 32 位 / 64 位、ARM/x86 等),但遵循多级页表的通用设计思想:
页表级数
级数越多,可支持的虚拟地址空间越大,但地址转换的硬件开销也略增。
- 32 位系统(如 ARMv7、x86):通常采用二级页表(如 ARMv7 的 PUD+PT,x86 的 PDE+PTE)。
- 64 位系统(如 ARMv8、x86_64):为支持更大的地址空间(如 48 位虚拟地址),采用四级页表(ARMv8 的 Level 0~3,x86_64 的 PGD→PUD→PMD→PTE)。
页表项(PTE,Page Table Entry)
每个页表项是页表中的最小单位,存储映射关系和属性,主要包含:
- 物理地址字段:指向低一级页表的基地址(中间级页表项)或物理页框的基地址(最后一级页表项)。
- 权限标志:如读写(R/W)、执行(X)、用户 / 内核访问(U/S)等,用于内存保护。
- 状态标志:如存在(Present)、脏页(Dirty)、访问过(Accessed)等,辅助内存管理。
- 缓存策略:如是否启用 Cache(C)、写回 / 写透(WB/WT)等(与架构相关)。
页大小
页表映射的基本单位是 “页”,Linux 支持多种页大小(由硬件和内核配置决定):
- 标准页:4KB(最常用)。
- 大页(Huge Page):2MB、1GB 等,减少页表级数,提升转换效率(适用于大型程序)。
二、Linux 中的页表数据结构
Linux 内核通过一系列数据结构抽象不同架构的页表实现,核心定义在
include/asm-generic/pgtable.h
和架构相关的arch/<架构>/include/asm/pgtable.h
中。1. 页表各级结构(以 64 位 ARMv8 为例)
ARMv8(AArch64)采用四级页表,各级结构如下:
- Level 0(PGD,Page Global Directory):页全局目录,最高级页表,每个表项指向 Level 1 页表。
- Level 1(PUD,Page Upper Directory):页上级目录,表项指向 Level 2 页表。
- Level 2(PMD,Page Middle Directory):页中间目录,表项指向 Level 3 页表或大页(如 2MB)。
- Level 3(PTE,Page Table Entry):页表项,最终指向物理页框(4KB 页)。
各级页表在 Linux 中通过指针类型抽象:
// ARMv8架构的页表指针类型(arch/arm64/include/asm/pgtable.h) typedef struct { pgdval_t pgd; } pgd_t; // Level 0 typedef struct { pudval_t pud; } pud_t; // Level 1 typedef struct { pmdval_t pmd; } pmd_t; // Level 2 typedef struct { pteval_t pte; } pte_t; // Level 3
2. 页表项操作宏
内核定义了一系列宏用于操作页表项(屏蔽架构差异):
pgd_val(pgd)
:获取 pgd_t 的原始值(物理地址 + 标志)。pgd_set(pgd, val)
:设置 pgd_t 的值。pgd_none(pgd)
:判断 pgd_t 是否为空(未映射)。pgd_present(pgd)
:判断 pgd_t 是否有效(存在映射)。- 类似宏:
pud_*()
、pmd_*()
、pte_*()
(如pte_write(pte)
判断是否可写)。3. 进程页表的根
每个进程的
struct mm_struct
(地址空间描述符)中,pgd
字段指向该进程页表的根(PGD):struct mm_struct { pgd_t *pgd; // 进程页表的根目录(PGD) // 其他字段... };
三、页表的管理流程
Linux 内核负责页表的创建、更新、销毁等管理操作,核心流程如下:
1. 页表的创建(以进程初始化为例)
- 进程创建(
fork()
)时,内核为子进程复制父进程的mm_struct
,并通过pgd_alloc(mm)
分配新的 PGD 页表。- 对于用户空间的虚拟地址区域(VMA),内核在首次访问时通过缺页异常动态创建下级页表(PUD、PMD、PTE)。
- 内核空间的页表(如直接映射区)在系统启动时初始化(
paging_init()
),通过early_pgtable_init()
建立固定映射。2. 地址映射的建立(
set_pte_at()
)当需要为虚拟地址分配物理页并建立映射时(如缺页异常处理),内核执行以下步骤:
- 通过
pgd_offset(mm, addr)
从 PGD 中找到对应虚拟地址的 PGD 项。- 若 PGD 项为空,通过
pud_alloc(mm, pgd, addr)
分配 PUD 页表,并更新 PGD 项。- 依次处理 PUD→PMD→PTE,最终通过
set_pte_at(mm, addr, ptep, pte)
设置 PTE,建立虚拟地址到物理页的映射。示例代码片段(简化):
// 为虚拟地址addr分配物理页并建立映射 struct page *page = alloc_page(GFP_KERNEL); // 分配物理页 pte_t pte = pte_mkdirty(pte_mkwrite(pfn_pte(page_to_pfn(page), PAGE_USER))); // 构造PTE(可写、脏页标志) pgd_t *pgd = pgd_offset(mm, addr); // 查找PGD项 pud_t *pud = pud_alloc(mm, pgd, addr); // 分配PUD(若为空) pmd_t *pmd = pmd_alloc(mm, pud, addr); // 分配PMD(若为空) pte_t *ptep = pte_alloc_map(mm, pmd, addr); // 分配PTE(若为空) set_pte_at(mm, addr, ptep, pte); // 设置PTE,完成映射
3. 页表的销毁(
pgd_free()
)
- 进程退出时,内核通过
exit_mm()
释放其地址空间,最终调用pgd_free(mm, mm->pgd)
销毁 PGD。- 销毁过程中,内核递归释放下级页表(PUD→PMD→PTE),并将物理页框归还给伙伴系统。
4. 页表的刷新(TLB 操作)
页表项更新后(如映射变更、权限修改),需刷新 CPU 的 TLB( Translation Lookaside Buffer,页表缓存),否则 CPU 可能使用旧的映射信息。内核通过以下接口实现:
flush_tlb_page(vma, addr)
:刷新单个页的 TLB。flush_tlb_range(vma, start, end)
:刷新地址范围内的 TLB。flush_tlb_mm(mm)
:刷新整个进程的 TLB。这些接口最终调用架构相关的汇编指令(如 ARM 的
tlbi
,x86 的invlpg
)。四、内核空间与用户空间的页表
Linux 的内核空间和用户空间使用不同的页表管理策略:
用户空间页表
- 每个进程有独立的用户空间页表(由
mm_struct->pgd
指向),进程切换时通过更新页表基地址寄存器(如 ARM 的 TTBR0)切换。- 动态建立:用户空间的映射(如堆、栈、文件映射)通过缺页异常动态创建。
内核空间页表
- 所有进程共享内核空间页表,映射内核代码、数据、直接映射的物理内存等。
- 静态初始化:内核启动时通过
paging_init()
建立固定映射(如直接映射区),无需动态分配。- 高端内存映射:32 位系统中,内核通过临时映射(
kmap()
)动态将高端内存映射到内核虚拟地址。五、关键代码位置
页表管理的核心代码分布在通用目录和架构相关目录:
- 通用逻辑:
mm/pgtable-generic.c
(通用页表操作)、mm/memory.c
(缺页处理中的页表更新)。- 架构相关实现:
- ARM:
arch/arm/mm/pgtable.c
、arch/arm64/mm/pgtable.c
- x86:
arch/x86/mm/pgtable.c
- 头文件:
include/asm-generic/pgtable.h
(通用定义)、arch/<架构>/include/asm/pgtable.h
(架构特定定义)。总结
Linux 的页表管理通过多级结构实现虚拟地址到物理地址的高效映射,结合硬件 MMU 和内核软件逻辑,提供了内存保护、权限控制和缓存策略管理。页表的动态创建(缺页异常)和共享(内核空间)机制,既保证了进程隔离,又提高了内存利用率。理解页表的结构和管理流程,是掌握 Linux 虚拟内存机制的关键。