深入理解Linux内存管理:从进程地址空间到物理页框
深入理解Linux内存管理:从进程地址空间到物理页框
本文将带你深入Linux内核,揭示一个malloc
调用背后,操作系统是如何为你默默耕耘的。我们将沿着以下路径,完整地走过内存管理的旅程:
- 宏观规划(进程视角):
mm_struct
- 进程的“内存总部” - 精细分区(进程视角):
vm_area_struct
- 虚拟内存区域的“地块规划图” - 物理调配(系统视角):
struct zone
- 物理内存的“仓库分区” - 最小单元(系统视角):
struct page
- 物理页框的“身份证”
第一章:进程的“内存总部” - struct mm_struct
想象一下,每个进程都拥有一个独立的4GB(32位系统)虚拟地址空间。mm_struct
就是这个庞大地址空间的总指挥部。
1 | struct mm_struct { |
核心字段精解:
pgd_t *pgd
(考试重点!):- 这是进程页表的根指针。当CPU调度到这个进程时,就是把这个
pgd
加载到CR3控制寄存器,从而切换地址空间。这是进程隔离的硬件基础。
- 这是进程页表的根指针。当CPU调度到这个进程时,就是把这个
mmap
与mm_rb
(设计思想与效率的平衡):- 问题:如何快速判断一个地址(比如
0x40001000
)属于进程的哪个内存区域? - 答案:使用两种数据结构协同工作。
mmap
:一个链表。当需要遍历所有内存区域时(如整个进程的内存dump),链表很高效。mm_rb
:一棵红黑树。当需要根据一个地址快速查找其所属的VMA时(如在缺页异常中),红黑树的O(logN)复杂度远胜于链表的O(N)。
- 问题:如何快速判断一个地址(比如
mm_users
与mm_count
(极易混淆的考试点!):mm_users
:使用该地址空间的用户数(通常是线程数)。fork
创建线程时,这个值会增加。mm_count
:该mm_struct
本身的主引用计数。它计数的对象包括用户线程、内核临时引用等。当mm_count
减为0时,内核才会销毁这个mm_struct
。- 简单比喻:
mm_users
像是租用一套房子的租客数量,而mm_count
是这套房子本身的“生命值”,只有当没有任何租客且房子本身也不再被需要时,才会拆掉房子。
mm_struct
的生命周期:
- 创建:在
fork
系统调用中,为新进程创建。 - 使用:进程的每一次内存访问(读/写/执行)都间接地通过它来查询页表。
- 销毁:当进程的所有线程都退出,且内核引用也释放后(
mm_count=0
),它被销毁。
第二章:虚拟内存的“地块规划图” - struct vm_area_struct
一个进程的地址空间不是一整块,而是被划分为多个连续的、具有相同访问属性(读、写、执行) 的区域。每个这样的区域就是一个vm_area_struct
。
1 | struct vm_area_struct { |
核心字段精解:
vm_start
与vm_end
:定义了一个[vm_start, vm_end)
的虚拟地址区间。vm_file
(重要概念!):- 为NULL:表示这是一个匿名映射。例如进程的堆(heap)、栈(stack) 和匿名共享内存。
- 非NULL:表示这是一个文件映射。例如将动态库(如
libc.so
)加载到内存,或者通过mmap
系统调用将一个文件映射到进程地址空间。
一个典型进程的VMA布局:
1 | VMA1: 0x00400000-0x00401000 (代码段, r-x, 文件映射: /bin/myapp) |
mm_struct
与 vm_area_struct
的关系(必考!):
task_struct
(进程) -> mm_struct
(内存总部) -> mmap
链表/mm_rb
树 -> 多个vm_area_struct
(内存地块)。
第三章:物理内存的“仓库分区” - struct zone
上面讲的是进程视角的虚拟内存。现在我们把目光转向操作系统管理的物理内存。由于硬件限制,物理内存被划分为不同的区(Zone)。
1 | struct zone { |
为什么要分区?(考试点)
- ZONE_DMA (0-16MB):一些老式的DMA设备只能对物理内存的低16MB进行直接内存访问。
- ZONE_NORMAL (16MB-896MB):这部分内存被永久映射到内核的虚拟地址空间,内核可以直接访问。大部分内核操作发生在这里。
- ZONE_HIGHMEM (>896MB,仅在32位系统有):内核无法直接访问,需要动态建立临时映射。64位系统地址空间巨大,没有这个区。
伙伴系统(Buddy System) - free_area[]
(核心考点!)
-
要解决的问题:外部碎片——虽然有大量空闲内存,但都是小碎片,无法满足大的连续内存分配请求。
-
工作原理:将空闲物理页框按块组织,每个块的大小是2的幂次方个页。
free_area[0]
:链接所有单个空闲页(4KB)。free_area[1]
:链接所有2个连续空闲页组成的块(8KB)。- …
free_area[10]
:链接所有1024个连续空闲页组成的块(4MB)。
-
分配过程(以分配8个页为例,即order=3):
- 检查
free_area[3]
链表是否为空。 - 如果非空,直接分配链表的第一个块。
- 如果为空,向上查询
free_area[4]
。 - 如果
free_area[4]
有块,将其分裂成两个order=3
的“伙伴”块。 - 一个用于分配,另一个放入
free_area[3]
链表。
- 检查
-
释放过程:
- 释放一个
order=3
的块。 - 查找它的“伙伴”块(地址相邻、大小相同)是否也是空闲的。
- 如果是,将两个伙伴块合并成一个
order=4
的块,并放入free_area[4]
链表。 - 继续向上尝试合并,直到不能合并为止。
- 释放一个
页框回收 - active_list
与 inactive_list
当系统内存不足时,内核需要回收一些不常用的页框。它使用LRU(最近最少使用) 的近似算法:
- 活跃链表:存放最近被访问过的页。
- 非活跃链表:存放候选被回收的页。
- 内核线程
kswapd
会定期将活跃链表中长时间未访问的页移到非活跃链表,然后优先回收非活跃链表中的页。
第四章:物理页框的“身份证” - struct page
物理内存的最小单位是页框(Page Frame),通常是4KB。内核为系统中的每一个物理页框都创建了一个struct page
结构体,作为它的“身份证”或管理元数据。
1 | struct page { |
核心字段精解:
-
flags
(考试重点):使用位来表示页的多种状态。PG_locked
:页被锁定,正进行I/O操作。PG_dirty
:页的内容已被修改,与磁盘文件不一致。PG_uptodate
:页的内容是有效的。PG_lru
:表示该页在zone的LRU链表上。
-
_count
与_mapcount
(最易混淆的考试点!):_count
:内核引用计数。表示内核中有多少地方正在使用这个物理页。_count = 0
是该页可以被回收的必要条件。get_page()
->_count++
put_page()
->_count--
_mapcount
:页表映射计数。表示这个物理页被多少个进程的页表所映射(即被多少个进程共享)。-1
:未被任何进程映射。0
:被一个进程映射。N
:被N+1个进程映射。
- 简单比喻:一个物理页像一本物理书。
_mapcount
:记录这本书被多少人的“借书卡”(页表)登记了。_count
:记录这本书当前被多少个内核子系统正在“用手拿着”阅读(比如正在被修改、正在做I/O)。
-
mapping
与index
:- 如果页属于文件缓存(page cache),
mapping
指向文件的address_space
,index
表示页在文件中的偏移。 - 如果页是匿名映射(如堆、栈),
mapping
指向匿名地址空间。
- 如果页属于文件缓存(page cache),
全链路整合:一个malloc
的完整旅程
现在,让我们把所有这些结构串联起来,看看当你调用char *buf = malloc(8192);
(分配8KB)时,发生了什么:
- 库函数层:
malloc
调用sbrk
或mmap
系统调用,向内核申请虚拟内存。 - VMA操作:内核在进程的
mm_struct
中,通过红黑树mm_rb
找到堆区域的VMA,并扩展其vm_end
,或者创建一个新的VMA。此时,只是在虚拟地址空间划出了一块地,还没有分配实际的物理内存。 - 触发缺页:当你第一次读写
buf
时,CPU发现该虚拟地址对应的页表项是空的(无效),触发缺页异常。 - 异常处理:内核的缺页处理程序被调用。
- 通过
mm_struct->mm_rb
找到该地址所属的VMA。 - 检查VMA的权限是否合法。
- 通过
- 分配物理页:
- 内核转向物理内存管理。它可能会从ZONE_NORMAL进行分配。
- 首先,尝试从每CPU页缓存(
zone->pageset[]
)中获取一个单独的页框。这很快,因为无需加锁。 - 对于连续页框的请求,调用伙伴系统。伙伴系统在
zone->free_area[order]
中寻找合适的空闲块(这里需要2个连续页,order=1)。如果找到,就从链表中取下,更新zone->free_pages
。
- 建立映射:
- 伙伴系统返回
struct page
指针。 - 内核用该物理页框的地址,填充进程页表中对应虚拟地址的页表项(PTE)。
- 同时,该物理页的
_mapcount
变为0(被一个进程映射),_count
变为1(被内核引用)。
- 伙伴系统返回
- 返回用户态:缺页处理完成,CPU重新执行那条引起异常的指令,此时它成功访问到了新分配的物理内存。
总结与考试重点
数据结构 | 角色 | 核心概念 | 考试重点 |
---|---|---|---|
mm_struct |
进程内存总部 | 整个虚拟地址空间的抽象 | pgd 作用;mmap 链表与mm_rb 树的区别与用途;mm_users vs mm_count |
vm_area_struct |
虚拟内存地块 | 连续的同属性地址区间 | vm_start/vm_end ;vm_file (匿名 vs 文件映射);与mm_struct 的关系 |
struct zone |
物理内存仓库 | 分区管理,应对硬件限制 | 三种Zone的作用;伙伴系统原理(free_area[] ,分配/释放/合并过程);LRU链表 |
struct page |
物理页框身份证 | 物理内存的最小管理单元 | flags 状态位;_count vs _mapcount ;mapping 和index 的作用 |