当前位置: 首页 > news >正文

不连续页分配器补充

vmalloc流程

1. 背景:vmalloc() 要解决的问题

  • kmalloc() 要求 虚拟地址连续,物理页也连续。大块内存分配可能失败。
  • vmalloc() 只保证 虚拟地址连续,物理内存可以由很多不连续的页拼接。

实现的关键就是:

  1. vmalloc 区域 找一块空闲的虚拟地址。
  2. 分配若干物理页(可能不连续)。
  3. 建立虚拟地址 → 物理页的映射。

这三个步骤里,数据结构的角色就是:

  • **vmap_area**:负责管理 vmalloc 区域里的虚拟地址范围。
  • **vm_struct**:描述一个具体的 vmalloc 内存块(和用户返回的 addr 对应)。

2. 关键数据结构解析

struct vmap_area

表示 vmalloc 区域中的一个虚拟地址段

struct vmap_area {unsigned long va_start;unsigned long va_end;unsigned long flags;struct rb_node rb_node;         /* address sorted rbtree */struct list_head list;          /* address sorted list */struct llist_node purge_list;    /* "lazy purge" list */struct vm_struct *vm;struct rcu_head rcu_head;
};
  • 内核全局维护一棵红黑树和链表来管理所有的 vmap_area,保证虚拟地址分配不冲突。
  • 每次 vmalloc() 会新建一个 vmap_area,挂到这棵树里。

struct vm_struct

表示 一个具体的 vmalloc 块,用户代码拿到的就是 vm_struct->addr

struct vm_struct {struct vm_struct	*next;void			*addr;unsigned long		size;unsigned long		flags;struct page		**pages;unsigned int		nr_pages;phys_addr_t		phys_addr;const void		*caller;
};
  • **pages[]**** 是核心**:记录了 vmalloc 这片区域实际映射到哪些物理页。
  • addrvmap_area->va_start,两者一一对应。
  • vm_struct 通过 vmap_area->vm 与虚拟地址区间关联。

3. vmalloc() 的流程

vmalloc(size) 为例,流程大致是:

(1) 计算所需页数

nr_pages = (size + PAGE_SIZE - 1) >> PAGE_SHIFT;

(2) 在 vmalloc 区域找虚拟地址

  • 调用 alloc_vmap_area()
    • 通过 红黑树,在vmalloc区域中查找一块足够大的空闲虚拟地址区间;
    • 建立一个新的 struct vmap_area,填好 va_start/va_end
    • 挂到全局红黑树/链表里。

这一步解决:虚拟地址空间的分配。


(3) 分配物理页

  • 调用 alloc_page()(实际走伙伴系统),分配 nr_pages 个物理页。
  • 这些页可能离散。
  • 把它们存进 vm_struct->pages[]

这一步解决:物理内存的获取。


(4) 建立映射

  • 调用 map_vm_area() 或更底层的 vmap_page_range()
    • 遍历 pages[]
    • 在页表里把 va_start ~ va_end 的虚拟页,依次映射到对应的物理页。

这样,就实现了 虚拟地址连续 → 物理页不连续 的映射。

如何找到内核线程的页表?后面解释


(5) 返回给用户

  • vm_struct->addr = (void *)vmap_area->va_start
  • 返回给调用者。

调用者得到的是一段看起来“连续”的内存。


4. vmalloc() 与 vmap() 的关系

  • vmalloc() = 自动分配物理页 + 申请虚拟地址 + 调用 vmap 建立映射
  • vmap(pages[], nr_pages, ...) = 自己提供物理页数组,直接建立虚拟映射。

所以:

  • **vmalloc()**** 面向使用者**(只要给我一段内存);
  • **vmap()**** 面向更底层**(我已有页,帮我拼接)。

5. 小结

vmalloc() 的机制可以归纳为三步:

  1. 地址管理
    vmap_area 负责在 vmalloc 区域找一段空闲虚拟地址,并放到全局红黑树。
  2. 块描述
    vm_struct 保存这段虚拟内存的元数据(起始地址、大小、物理页数组)。
  3. 页表映射
    把虚拟地址区间映射到 vm_struct->pages[] 里记录的实际物理页。

vfree释放过程

vfree() 被调用时:

  1. 根据 addr 找到对应的 vmap_area
  2. 从红黑树和链表删除。
  3. 把物理页释放回伙伴系统。
  4. 延迟释放 vmap_area(放到 purge_list,用 RCU 机制安全回收)。

linux中常用内存分配函数

用户态 vs 内核态

  • 用户态 API
    malloc(), brk(), mmap()
    这是 C 库(glibc)或系统调用提供的接口,进程使用。
    本质上是通过 VMA 管理 + 缺页时分配物理页
  • 内核态 API
    alloc_pages(), kmalloc(), vmalloc()
    这是 Linux 内核给自己用的内存分配器接口,驱动/内核子系统用。
    本质上是 直接操作伙伴系统/SLAB/vmalloc 子系统

各方法机制对比

接口使用场景内核实现方式地址连续性使用者
malloc()用户程序最常用的内存申请glibc 封装,底层调用 brk()
mmap()
扩展堆/映射匿名页
用户虚拟地址连续(物理不一定连续)用户空间
brk()扩展/收缩 heap(sbrk
系统调用)
修改进程的堆 VMA 边界,缺页时由 alloc_pages()
分配物理页
用户虚拟地址连续(物理不一定连续)用户空间
mmap()大块内存/文件映射/共享内存创建新的 VMA,缺页时用 alloc_pages()
或从文件读取到物理页
用户虚拟地址连续(物理不一定连续)用户空间
alloc_pages()分配页粒度内存伙伴系统分配 struct page物理连续,内核虚拟地址也连续(线性映射区)内核
kmalloc()内核小块内存(字节/KB 级)SLAB/SLUB 分配器,底层基于 alloc_pages()物理连续 + 内核虚拟连续内核
vmalloc()内核大块内存(MB 级)从 vmalloc 区找虚拟地址区间,分配不连续物理页(底层基于alloc_pages()),建立页表映射虚拟地址连续,物理地址不连续内核

关系梳理

  1. 用户空间
    • malloc() → 封装,可能走 brk()mmap()
    • brk()/mmap() → 修改 mm_struct 和 VMA;
    • 缺页时 → 最终用 alloc_pages() 分配物理页。
  2. 内核空间
    • alloc_pages() → 最底层接口,直接伙伴系统;
    • kmalloc() → 面向小对象,使用slab分配器,底层用 alloc_pages()
    • vmalloc() → 面向大块虚拟地址空间,物理页不连续。底层用 alloc_pages()

总结

  • 用户态用 malloc()(底层 brk/mmap),本质是修改虚拟内存布局,缺页时通过 **伙伴系统 ****alloc_pages()** 分配物理页;
  • 内核态直接用 alloc_pages()kmalloc()(小块)、vmalloc()(大块,物理不连续)。

如何找到内核线程的页表?

“内核线程没有用户空间”就会怀疑:那页表怎么办?是不是有个“内核专用页表”?
其实 Linux 内核线程并不是共享一个“内核页表”,而是借用普通进程的页表


1. 页表的基本事实

  • x86/ARM 等架构上,CPU 访问内存都要走页表转换。页表的基地址存放在控制寄存器(x86 的 CR3,ARM64 的 TTBR0/TTBR1)。
  • Linux 设计:所有进程的页表都包含了同一份内核态映射(高地址部分的 linear mapping、vmalloc 等)
    • 换句话说,每个进程的 mm_struct->pgd 不同,但其中“内核地址区”是一致的。
    • 所以,只要有一份用户进程的页表,就能保证内核地址区始终可用。

2. 普通进程 vs 内核线程

普通用户进程

  • 每个进程有自己的 mm_struct,里面有独立的 pgd(页全局目录)。
  • 切换进程时,调度器会把 mm->pgd 加载到 CR3
  • 这样用户态地址空间不同,但内核态地址映射相同。

内核线程

  • task_struct->mm = NULL,说明它没有独立的 mm_structpgd
  • 调度器在切换到内核线程时:
    • 如果发现 mm == NULL,会把 prev->active_mm 借给内核线程,保存到 next->active_mm
    • 并且在切换时 不会切换 CR3,继续使用原进程的页表。
  • 内核线程只在内核态执行,不会访问用户空间地址,所以根本不在意用户空间页表部分。

3. 也就是说:

  • 每个内核线程并没有单独的页表
  • 它们 借用上一个普通进程的页表,只是用其中的内核映射部分。
  • 这就是 task_struct->active_mm 的意义。

4. “内核页表”的保存与使用

  • 并不存在一个独立的“全局内核页表”。
  • 取而代之:每个进程的页表都自带了内核映射部分
  • 内核线程调度时,就继续使用借来的页表的内核部分。

总结

内核线程没有独立的页表,它们不会切换到某个“内核专用页表”。调度到内核线程时,Linux 内核会让它们 借用上一个进程的页表(通过 active_mm),只使用其中的内核地址映射部分。由于所有进程的内核区页表一致,内核线程就能安全运行。

测试验证

代码实现

实现一个最小可运行的 内核模块 示例,专门用来测试 vmalloc() 申请和释放内存。代码如下:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/vmalloc.h>   // vmalloc/vfree
#include <linux/kernel.h>#define VMALLOC_SIZE (1024 * 1024)  // 申请 1MBstatic void *vmalloc_area = NULL;static int __init vmalloc_test_init(void)
{pr_info("vmalloc_test: module loaded\n");// 使用 vmalloc 申请一块连续虚拟地址的内存vmalloc_area = vmalloc(VMALLOC_SIZE);if (!vmalloc_area) {pr_err("vmalloc_test: vmalloc failed!\n");return -ENOMEM;}pr_info("vmalloc_test: allocated %d bytes at %pK\n",VMALLOC_SIZE, vmalloc_area);// 写入测试数据memset(vmalloc_area, 0xAA, VMALLOC_SIZE);pr_info("vmalloc_test: memory initialized with 0xAA\n");return 0;
}static void __exit vmalloc_test_exit(void)
{if (vmalloc_area) {vfree(vmalloc_area);pr_info("vmalloc_test: freed memory at %pK\n", vmalloc_area);}pr_info("vmalloc_test: module unloaded\n");
}module_init(vmalloc_test_init);
module_exit(vmalloc_test_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("congchp");
MODULE_DESCRIPTION("Simple vmalloc test module");
obj-m += vmalloc_test.oall:make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modulesclean:make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

测试结果

dmesg结果:

/proc/vmallocinfo结果:

参考资料

  1. Professional Linux Kernel Architecture,Wolfgang Mauerer
  2. Linux内核深度解析,余华兵
  3. Linux设备驱动开发详解,宋宝华
  4. linux kernel 4.12

文章转载自:

http://kklOFiQT.qfdyt.cn
http://6MDPARGQ.qfdyt.cn
http://SYYfexCx.qfdyt.cn
http://IKGaRABT.qfdyt.cn
http://oP4EizBI.qfdyt.cn
http://gOtZ8fMR.qfdyt.cn
http://rMj8Tj8T.qfdyt.cn
http://SXZhpTaJ.qfdyt.cn
http://O21cSDe7.qfdyt.cn
http://aZ1OrezY.qfdyt.cn
http://aNwwkLUO.qfdyt.cn
http://8vCYASVu.qfdyt.cn
http://49kBY1UP.qfdyt.cn
http://gCtHbHQI.qfdyt.cn
http://XfUDy3eH.qfdyt.cn
http://GSVFE7Tw.qfdyt.cn
http://gR2sPSjm.qfdyt.cn
http://eQBLHAAa.qfdyt.cn
http://Bq0nMzNy.qfdyt.cn
http://PU02Kjeg.qfdyt.cn
http://namHCikp.qfdyt.cn
http://kfGaRGEc.qfdyt.cn
http://1Hz0C0wq.qfdyt.cn
http://TAHbGbIg.qfdyt.cn
http://X5hVsRdt.qfdyt.cn
http://QmaRZYaZ.qfdyt.cn
http://xUjLjsBB.qfdyt.cn
http://yzbkZFF8.qfdyt.cn
http://6wT5hJgi.qfdyt.cn
http://CQDFm3er.qfdyt.cn
http://www.dtcms.com/a/373043.html

相关文章:

  • Hadoop NameNode内存泄漏与GC停顿问题排查与解决方案
  • 防火墙配置
  • Hydra-SSH 破解安全防范
  • Hadoop(十)
  • C++三种对象实例化在栈或堆的区别
  • 碰一碰系统源码于小程序打通技术开发整合方案,驱动AI技术开发源代码
  • 深入解析TCP核心机制:连接管理、流量与拥塞控制
  • 【混合开发】vue+Android、iPhone、鸿蒙、win、macOS、Linux之dist打包发布在Android工程asserts里
  • 医疗问诊陪诊小程序:以细节创新重塑就医体验的温度与效率
  • [20250908]Android Talkback 自定义合并
  • 【智能融合:增材制造多物理场AI建模与工业应用实战】
  • stm32——独立看门狗,RTC
  • LeetCode 3634.使数组平衡的最少移除数目
  • 106. 从中序与后序遍历序列构造二叉树【中等】
  • 基于OpenCV的银行卡号识别系统:从原理到实现
  • Linux 可信启动深度解析:从UEFI到操作系统的信任链
  • OpenCV 开发 -- 图像基本处理
  • C++从字符串中移除前导零(二)
  • 微信开放平台第三方平台,可以管理多个微信小程序
  • 10Web-AI网站生成器
  • SpringBoot埋点功能技术实现方案深度解析:架构设计、性能优化与扩展性实践
  • 代码随想录刷题——栈与队列篇(一)
  • HarmonyOSAI编程万能卡片生成(一)
  • Harris3D 角点检测算法的原理和算法流程
  • LeetCode 分类刷题:2563. 统计公平数对的数目
  • [前端]1.html基础
  • Griffin|增强现实数据集|无人机数据集
  • MacOS M芯片 运行GPT-SoVITSv2Pro
  • 域名网页加载慢怎么解决:从测速到优化的全链路性能优化实战
  • Http协议+请求响应+分层解耦