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

13 - linux 内存子系统

---- 整理自 王利涛老师 课程
实验环境:宅学部落 www.zhaixue.cc

文章目录

  • 1. 什么是内存管理
  • 2. 内存的硬件电路与接口
  • 3. 物理内存管理:page、zone、node
    • 3.1 物理页帧:struct page
    • 3.2 内存区域:struct zone
    • 3.3 内存节点:struct node
    • 3.4 物理内存管理架构
  • 4. 伙伴系统:buddy system
  • 5. 物理页面的迁移类型:migratetype
    • 5.1 枚举类型:migratetype
    • 5.2 为什么要引入迁移类型?
  • 6. Per-CPU 页帧缓存
  • 7. 页分配器接口:alloc_pages
    • 7.1 编程示例:使用页分配器接口申请内存
    • 7.2 内核源码分析
  • 8. 连续内存分配器:CMA
  • 9. 伙伴系统初始化
    • 9.1 伙伴系统初始化:memblock 管理器
      • 9.1.1 早期的内存管理
      • 9.1.2 memblock 接口
      • 9.1.3 memblock 的初始化
    • 9.2 伙伴系统初始化:memblock 内存释放
    • 9.3 伙伴系统初始化:.init 内存释放
      • 9.3.1 memblock 中的 reserved memory
      • 9.3.2 .init 段的内存释放
    • 9.4 伙伴系统初始化:CMA 内存释放
  • 10. slab、slob 和 slub 分配器
    • 10.1 slab 工作原理
    • 10.2 slab 核心数据结构关联
    • 10.3 slab 编程接口
  • 11. kmalloc 机制实现分析
  • 12. 虚拟地址和 MMU 工作原理
  • 13. 二级页表的工作原理
  • 14. 页表
  • 15. TLB 和 Table Walk Unit
  • 16. Linux 虚拟内存管理
    • 16.1 32 位 X86 系统下的虚拟内存经典布局
    • 16.2 虚拟内存划分
    • 16.3 ARM 32 下的内存布局
  • 17. 虚拟内存管理:线性映射区
  • 18. 低端内存和高端内存的边界划分
    • 18.1 实验
      • 18.1.1 PAGE_OFFSET 为 0x80000000,DDR 256MB
      • 18.1.2 PAGE_OFFSET 为 0x80000000,DDR 512MB
      • 18.1.3 PAGE_OFFSET 为 0x80000000,DDR 1GB
      • 18.1.4 PAGE_OFFSET 为 0xC0000000 呢?
    • 18.2 小结
  • 19. 二级页表的创建过程分析
  • 20. 虚拟内存管理:vmalloc 区
  • 21. 寄存器映射:ioremap
  • 22. 高端内存映射
  • 23. 虚拟内存管理:pkmap 区
  • 24. 虚拟内存管理:fixmap 区
  • 25. 虚拟内存管理:modules 区
  • 26. 用户进程的页表
  • 27. 缺页异常机制
  • 28. 用户页表的刷新
  • 29. mmap 映射机制
    • 29.1 编程示例
    • 29.2 remap_pfn_range
    • 29.3 文件映射
    • 29.4 文件缺页异常
    • 29.5 设备映射缺页异常
    • 29.6 匿名映射
    • 29.7 私有映射和共享映射
  • 30. 系统调用 brk 实现机制
  • 31. 反向映射

1. 什么是内存管理

  • 内存管理相关
    • 为什么需要虚拟内存和物理内存
    • 用户空间、内核空间
    • 物理地址、虚拟地址
    • 页表是什么?谁在维护?存在哪里?什么格式?
    • MMU、TLB
    • 映射:文件映射、匿名映射、IO 内存映射
    • 驱动如何申请内存?
    • 缺页中断、伙伴系统
  • 主要内容:
    • 物理内存管理:zone、page、伙伴系统
    • 虚拟内存管理
    • MMU、页表、TLB
    • 内存申请与释放接口
    • 映射机制底层实现
    • 预期收获:
      物理内存、虚拟内存的划分
      深入理解页表、地址转换、映射
      学会使用内核提供的接口申请内存
      构建一个完整的内存管理框架

2. 内存的硬件电路与接口

  • 内存硬件实现
    Data Latch
    D 触发器
    - 使用 D 触发器构建寄存器

在这里插入图片描述

  • 使用 D 触发器构建内存

在这里插入图片描述

  • SRAM & DDR SDRAM

在这里插入图片描述

  • 内存泄漏
    • 广义的内存泄漏
    • 狭义的内存泄漏
    • 解决之道
      • 裸机环境
      • RTOS 平台
      • Linux/Android 平台

在这里插入图片描述

3. 物理内存管理:page、zone、node

  • 页:struct page
  • 分区:struct zone
  • 内存节点:struct node

在这里插入图片描述

3.1 物理页帧:struct page

  • 定义头文件:include/linux/mm_types.h
  • 每个物理页帧(page frame)使用结构体 struct page 表示
  • 结构体 struct page 核心成员分析
  • 思考:
    • 物理页帧和 struct page 之间的关系
      • 物理页帧(Page Frame)是 RAM 中固定大小(通常 4KB)的存储单位,每个物理页帧都有一个对应的 struct page 结构体。
      • 可以认为 struct page 是对物理页帧的“元数据描述符”,它不会存储实际的数据,而是提供对该页的管理信息,如引用计数、状态标志、映射关系等。
    • 物理页帧号(page frame number,pfn)和物理地址的关系
      • 物理页帧号是对物理页的索引,pfn= 物理地址 / PAGE_SIZE
    • struct page 存储在哪里?
      • struct page 结构体通常存储在 mem_map 数组中。mem_map 是一个全局变量,指向 struct page 数组的起始地址,每个 struct page 对应一个物理页帧。

在这里插入图片描述

3.2 内存区域:struct zone

  • 在 Linux 内核的内存管理体系中,物理内存被划分为多个内存区域(Memory Zones),每个区域由 struct zone 结构体描述。struct zone 主要用于管理不同类型的物理内存,并提供分配与回收的机制。
  • 定义:include/linux/mmzone.h
  • 结构体 struct zone 核心成员解读
  • 初始化:zone_sizes_init

在这里插入图片描述

enum zone_type {
	ZONE_DMA,
	ZONE_DMA32,
	ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
	ZONE_HIGHMEM,
#endif
	ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
	ZONE_DEVICE,
#endif
	__MAX_NR_ZONES
}

3.3 内存节点:struct node

  • 内存模型:UMA(统一内存架构)和 NUMA(非统一内存架构)
  • struct pglist_data:表示 node 中的内存资源
  • 定义:include/linux/mmzone.h
  • 结构体: struct pglist_data 核心成员解析
  • node_data 数组:保存所有 node 的 pglist_data 结构

UMA(Uniform Memory Architecture,统一 / 一致性内存访问),就是在多 CPU(多核)系统中,每个 CPU 都通过同一根总线访问物理内存,而且访问的方式和时延是一样的,没有区别。NUMA(Non-Uniform Memory Architecture,非统一 / 一致性内存访问),相当于在多 CPU(多核)系统中,系统给每个 CPU 都分配了一块物理内存,这就意味着每个 CPU 可以并发访问各自的内存,当然每个 CPU 也可以访问其他 CPU 对应的物理内存或者公共物理内存,此时,这就出现了一个新的情况,由于各种物理内存空间所处的位置不同,于是访问它们的时间长短也就各异,而且比访问各自内存的时间都要更长,这就是 NUMA 系统跟 UMA 系统的区别。原文链接:https://blog.csdn.net/weixin_45337360/article/details/126940438

struct pglist_data {
    struct zone node_zones[MAX_NR_ZONES]; /* 该节点包含的内存区域(ZONE_DMA, ZONE_NORMAL, etc.) */
    struct zonelist node_zonelists[MAX_ZONELISTS]; /* 页分配时的 zone 选择策略 */
    int nr_zones;                 /* 该节点包含的 zone 数量 */
    struct page *node_mem_map;     /* 该节点的 struct page 数组(mem_map) */
    unsigned long node_start_pfn;  /* 该节点管理的物理页帧号起始值 */
    unsigned long node_present_pages; /* 该节点上已存在的物理页数 */
    unsigned long node_spanned_pages; /* 该节点的地址范围跨度 */
    int node_id;                   /* 该 NUMA 节点的 ID */
    struct pglist_data *next;      /* 指向下一个 NUMA 节点 */
};

3.4 物理内存管理架构

  • 每个 struct page 结构体都属于某个 struct zone,而 struct zone 归属于 struct pglist_data。

在这里插入图片描述
在这里插入图片描述

  • 核心结构体关联

在这里插入图片描述

4. 伙伴系统:buddy system

  • 物理内存由页分配器(page allocator)接管,页分配器使用伙伴系统(Buddy System) 来高效管理空闲页
  • 内存块的申请、释放过程
  • 伙伴算法、阶数

在这里插入图片描述

  • 空闲页按照 2 的幂次(2^order)进行管理,形成不同阶(order)的块。
  • 释放页时,内核会尝试合并相邻的空闲块,以减少碎片。
  • 申请页时,若目标阶数的块不可用,则从更大的块拆分。
  • order 代表分配的页数:
    order = 0:1 个页(4KB)
    order = 1:2 个连续页(8KB)
    order = 2:4 个连续页(16KB)

    order = MAX_ORDER-1:最大连续块(通常为 10 阶,即 4MB)

在这里插入图片描述

# cat /proc/buddyinfo
Node 0, zone      DMA      0      0      0      0      0      0      0      0      1      2      2
Node 0, zone    DMA32  11396   7054   6280   5125   3865   2626   1399    564    151    138     61
Node 0, zone   Normal     12     15    189    577    427    682   5448   2163    650    353    252
  • 核心结构体关联
    • 在 struct zone 中,内核维护了 free_area[MAX_ORDER] 数组,每个索引对应 order 级别的空闲页链表
struct free_area {
    struct list_head free_list[MIGRATE_TYPES];  /* 维护不同类型的空闲页链表 */
    unsigned long nr_free;                      /* 该阶空闲块数量 */
};

在这里插入图片描述
在这里插入图片描述

struct page {
	unsigned long private; //page_order=1
	atomic_t _mapcount;
	atomic_t _refcount;
};

5. 物理页面的迁移类型:migratetype

在这里插入图片描述
在这里插入图片描述

5.1 枚举类型:migratetype

在这里插入图片描述

  • 查看页面迁移类型:cat /proc/pagetypeinfo
    • 可移动的:用户进程申请的内存
    • 可回收的:文件系统的 page cache
    • 不可移动的:内核镜像区的物理内存
# cat /proc/pagetypeinfo
Page block order: 9
Pages per block:  512

Free pages count per migrate type at order       0      1      2      3      4      5      6      7      8      9     10
Node    0, zone      DMA, type    Unmovable      0      0      0      0      0      0      0      0      1      1      0
Node    0, zone      DMA, type      Movable      0      0      0      0      0      0      0      0      0      1      2
Node    0, zone      DMA, type  Reclaimable      0      0      0      0      0      0      0      0      0      0      0
Node    0, zone      DMA, type   HighAtomic      0      0      0      0      0      0      0      0      0      0      0
Node    0, zone      DMA, type      Isolate      0      0      0      0      0      0      0      0      0      0      0
Node    0, zone    DMA32, type    Unmovable    402    198    208    153     49     17      3      4      1      0      0
Node    0, zone    DMA32, type      Movable    652    201     77     37     12   1108   1319    525    140    131     62
Node    0, zone    DMA32, type  Reclaimable   2893    965    840     24    145    163     71     37     13      7      0
Node    0, zone    DMA32, type   HighAtomic      0      0      0      0      0      0      0      0      0      0      0
Node    0, zone    DMA32, type      Isolate      0      0      0      0      0      0      0      0      0      0      0
Node    0, zone   Normal, type    Unmovable   2691   1893    654     50     47     10      3      0      2      0      0
Node    0, zone   Normal, type      Movable   4147   2405   1853    913    449    157     11     24      1      0      0
Node    0, zone   Normal, type  Reclaimable      0      0      0     10      5      2      0      0      0      1      0
Node    0, zone   Normal, type   HighAtomic      0      0      0      0      0      0      0      0      0      0      0
Node    0, zone   Normal, type      Isolate      0      0      0      0      0      0      0      0      0      0      0

Number of blocks type     Unmovable      Movable  Reclaimable   HighAtomic      Isolate
Node 0, zone      DMA            3            5            0            0            0
Node 0, zone    DMA32           23         1252          171            0            0
Node 0, zone   Normal          281         5305         1058            0            0

5.2 为什么要引入迁移类型?

  • 伙伴系统存在的问题:
    • 由于伙伴系统的内存分配是按 2 的幂次划分的,可能会导致一些空闲内存不能被充分利用。如果请求的内存大小不刚好是 2 的幂,则可能浪费一部分内存。随着时间的推移和分配/释放的内存块数量增加,内存中可能会留下许多无法合并的碎片区域,导致内存浪费。
  • 对伙伴系统的改进:
    • page migratetype(页面迁移):指将虚拟内存中的页面从一个物理内存页框(Page Frame)迁移到另一个物理内存页框
    • memory compaction(内存压实):内存压实的基本思想是在内存中移动已分配的内存块,使它们尽可能集中在内存的一个区域,同时将空闲的内存区域移到另一块区域。这样,通过将碎片化的内存“压实”在一起,就可以恢复出大块的空闲内存区域。

在这里插入图片描述

  • 伙伴系统如何使用 free_list[MIGRATE_TYPES]
    • 分配页时:
      当 alloc_pages() 需要分配 order=3(32KB)大小的页:
      • 根据 GFP 标志确定 migratetype(例如 MIGRATE_MOVABLE)。
      • 查找 free_area[3].free_list[MIGRATE_MOVABLE] 是否有空闲块:
        – 若有,直接分配。
        – 若没有,尝试在 MIGRATE_RECLAIMABLE 和 MIGRATE_UNMOVABLE 里寻找。
        – 若仍无可用块,向更高 order 请求并拆分。
    • 释放页时:
      • 释放页时,伙伴系统会检查该页的 migratetype,然后插入到对应的 free_list[MIGRATE_TYPES] 链表。
      • 若释放的页可以与其伙伴页合并,则尝试合并到更高阶。

6. Per-CPU 页帧缓存

  • 在多核系统中,多个处理器核心可能会频繁访问内存中的某些页面。为了避免各个核心频繁访问共享的内存资源(如全局页表或系统级缓存)并发生竞争,Per-CPU 页帧缓存机制将每个 CPU 的常用内存页缓存在该 CPU 本地的缓存中。每个 CPU 会维护自己的“局部缓存”,从而减少访问共享内存的需求。
  • 这种技术通常用于在多个 CPU 核心之间减少内存访问的冲突,提高内存的局部性和访问效率。

在这里插入图片描述

  • free_area 管理了大部分的公共伙伴系统内存
  • lowmem_reserve 预留了一部分
  • __percpu *pagset 对每个 CPU 都分配一部分管理起来:
    • 每个进程在申请页面的时候都需要加锁解锁等操作,极大的引入了开销。
    • 为了提高效率就引入页帧缓存,为每个 CPU 提供一个变量指针 __percpu *pageset,这样每个 CPU 就不用去加锁解锁申请,直接使用本地物理页面。
    • 把单个物理页面的申请和释放做成缓存,每个 CPU 都有这个链表。给每个 CPU 本地定义一个页表,维护这样一个变量。因此,不需要去全局伙伴系统上去申请释放
    • __percpu 是一个特殊的类型修饰符,用于表示一个变量在每个 CPU 上都有一个独立的副本

在这里插入图片描述

7. 页分配器接口:alloc_pages

  • alloc_pages 是 Linux 内核中用于分配物理内存页面的一个重要接口。它是内核内存管理中的核心部分,负责分配指定数量的物理页面。
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);
#define __get_free_page(gfp_mask) __get_free_pages((gfp_mask), 0)


void free_pages(unsigned long addr, unsigned int order);
void __free_pages(struct page *page, unsigned int order);
#define __free_page(page) __free_pages((page), 0)
#define free_page(addr) free_pages((addr), 0)
  • alloc_pages vs. kmalloc
    • alloc_pages 分配的是一个或多个物理页面,即它分配的是 连续的内存页框(page frame)。每个页面通常有 4KB(在 x86 和 ARM 架构中),而通过 order 参数可以控制分配的页面数量。order = 0 时表示分配 1 个页面,order = 1 时表示分配 2 个页面,order = 2 时表示分配 4 个页面,依此类推。
    • kmalloc 是用于分配小块内存的函数,它分配的是一块连续的内存块,内存大小由传入的参数决定,通常是以 字节 为单位。kmalloc 分配的内存可以是较小的内存块,而不一定是以页为单位。

7.1 编程示例:使用页分配器接口申请内存

#include <linux/init.h>
#include <linux/module.h>
#include <linux/mm.h>
#include <linux/gfp.h>

#define PAGE_ORDER 1    // 定义页面顺序,表示分配2页内存(2的PAGE_ORDER次方)

struct page *page;
unsigned long int virt_addr;

static int __init hello_init(void)
{
    // 分配 2 页物理内存
    page = alloc_pages(GFP_KERNEL, PAGE_ORDER);
    printk("page frame no: %lx\n", page_to_pfn(page));  // 输出分配的页面帧号(物理页号)
    printk("physical addr: %x\n", page_to_phys(page));  // 输出分配的物理地址
    printk("virtual  addr: %x\n", (unsigned int)page_address(page));
    virt_addr = (unsigned long)page_to_virt(page);
    printk("virtual  addr: %lx\n", virt_addr);

    return 0;
}

static void __exit hello_exit(void)
{
    free_pages(virt_addr, PAGE_ORDER);
    //__free_pages(page, PAGE_ORDER);
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
ifneq ($(KERNELRELEASE),)
obj-m := alloc_pages.o
else
EXTRA_CFLAGS += -DDEBUG 
KDIR:=/home/code_folder/uboot_linux_rootfs/kernel/linux-5.10.4
ARCH_ARGS := ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
all:
	make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules
clean:
	make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules clean
endif

在这里插入图片描述
在这里插入图片描述

7.2 内核源码分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

8. 连续内存分配器:CMA

  • CMA(Contiguous Memory Allocator,连续内存分配器) 是 Linux 内核中的一个内存管理机制,旨在分配一块物理连续的内存区域,这在某些硬件或应用场景中是必需的。CMA 主要用于那些要求物理内存必须是连续的场景,尤其是像设备驱动、直接内存访问(DMA)等需要连续内存块的情况下。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • CMA 接口:cma_alloc、cma_release

9. 伙伴系统初始化

9.1 伙伴系统初始化:memblock 管理器

  • memblock 是内核早期启动时用于管理物理内存的一种机制。在内核初始化的早期阶段,memblock 负责分配、保留和管理物理内存,直到 buddy system(伙伴系统)接管管理。

9.1.1 早期的内存管理

  • 内核如何获取内存的地址、大小
  • 全局变量:struct memblock memblock;
    • 可用的物理内存:memblock.memory 数组
    • reserved 的物理内存:memblock.reserved 数组。包括:
      • 内核镜像(.init 段除外)、dtb、uboot、页表
      • GPU、camera、音视频解码

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

9.1.2 memblock 接口

在这里插入图片描述

int memblock_add (phys_addr_t base, phys_addr_t size);
int memblock_remove (phys_addr_t base, phys_addr_t size);
for_each_mem_range
int memblock_reserve (phys_addr_t base, phys_addr_t size);
int memblock_free (phys_addr_t base, phys_addr_t size);

在这里插入图片描述
在这里插入图片描述

9.1.3 memblock 的初始化

  • 获取物理内存的起始地址和大小
  • 初始化全局变量 memblock 的两个数组

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
回到前面:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

9.2 伙伴系统初始化:memblock 内存释放

  • memblock 如何释放内存给伙伴系统?

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

9.3 伙伴系统初始化:.init 内存释放

9.3.1 memblock 中的 reserved memory

  • memblock 中的 reserved memory(保留内存)是指那些在启动过程中被标记为“保留”的内存区域,这些区域通常不用于常规的内存分配。
    • 内核的代码段(.text、.data、.bss)(.init 除外)
    • initrd
    • dtb
    • 设备树中的 reserved-memory 区域(CMA 除外)
    • 临时页表
    • reserved memory 的初始化

9.3.2 .init 段的内存释放

  • 为什么要释放?放到 .init.text 段的一些初始化函数,只调用一次,调用完就不用了,可以把占用的内存释放掉
  • 里面包含了什么内容?
  • 释放到哪里?
  • 函数:free_initmem 分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

9.4 伙伴系统初始化:CMA 内存释放

  • CMA 内存如何释放到伙伴系统?

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

10. slab、slob 和 slub 分配器

10.1 slab 工作原理

  • 为什么要引入 slab:对伙伴系统的改进和补充
  • slab 的工作机制:
    • slab 分配器的内存是以 slab 为单位进行管理的。每个 slab 存储固定大小的内存块,这些内存块可以在系统中进行频繁的分配和释放。
    • Cache(缓存):每个缓存区管理一种特定大小的内存块(例如,若缓存区用于存储大小为 128 字节的对象,则该缓存区管理的就是 128 字节的内存块)。
    • Slab(内存池):一个 slab 是一个内存池,里面存放多个相同大小的内存块。每个 slab 可以包含多个对象。
  • 三种分配器:slab、slob、slub

在这里插入图片描述

10.2 slab 核心数据结构关联

  • kmem_cache、kmem_cache_cpu、kmem_cache_node
  • 内存的申请和释放

在这里插入图片描述
在这里插入图片描述

  • slab_caches 是一个全局的 缓存池列表,它包含了内核中所有已经创建的 kmem_cache。
  • kmem_cache 是 SLAB 分配器中的 核心结构,它管理着一个特定对象类型的内存块池(例如,某个内核对象结构体的内存)。它负责内存对象的分配、释放、缓存和管理。
  • kmem_cache_cpu 是与每个 CPU 核心 相关联的缓存结构。为了减少多核 CPU 上的锁竞争,SLAB 分配器为每个 CPU 保留了 局部缓存池,即每个 CPU 都有自己的 kmem_cache_cpu,这样 CPU 在分配内存时首先从本地缓存池中获取内存,而不是访问全局的内存池。
  • 在 NUMA 系统中,每个 NUMA 节点有自己的内存池,kmem_cache_node 负责管理该节点的缓存池,优化节点间内存分配。
    • NUMA(Non-Uniform Memory Access)是一种计算机内存架构,其中每个处理器或处理器组(通常称为 CPU 节点 或 NUMA 节点)都有自己本地的内存(本地内存),并且可以访问其他处理器的内存(远程内存)。与传统的对称多处理(SMP,Symmetric Multiprocessing)架构不同,在 SMP 中所有处理器都共享相同的物理内存。NUMA 的设计目的是通过提高本地内存访问速度来减少内存访问的延迟,并提高系统的可扩展性。
      • 本地内存:每个 CPU 节点(或 NUMA 节点)有自己的内存。这些内存是本地的,访问速度较快,延迟较低。
      • 远程内存:如果一个 CPU 节点访问另一个节点的内存(即非本地内存),则访问速度较慢,延迟较高。

10.3 slab 编程接口

  • 如何创建一个 kmem_cache?
  • 如何创建一个 slab?
  • 如何申请一个内存 object?
  • 如何释放一个内存 object?

在这里插入图片描述

#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>

struct student {
    int id;
    int age;
    float score;
    void (*print_score)(int id);
    void (*print_age)(int id);
};

struct kmem_cache *stu_cache;
struct student *p;

void stu_ctor(void *p)
{
    ; // init objects here
}

static int __init hello_init(void)
{
    // 创建一个名为 "student" 的 kmem_cache 缓存,管理大小为 struct student 的对象
    stu_cache = kmem_cache_create("student", sizeof(struct student), 0, \
                                SLAB_PANIC|SLAB_ACCOUNT, stu_ctor);
    BUG_ON(stu_cache == NULL);
    printk("stu_cache = %x\n", (unsigned int)&stu_cache);

    // 使用 kmem_cache_alloc 从 stu_cache 缓存中分配一个结构体对象
    p = kmem_cache_alloc(stu_cache, GFP_KERNEL);
    if (p) {
        printk("p object size = %x\n", sizeof(*p));
        printk("p object size = %d\n", sizeof(struct student));
    }

    return 0;
}

static void __exit hello_exit(void)
{
    kmem_cache_free(stu_cache, p);
    kmem_cache_destroy(stu_cache);
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
ifneq ($(KERNELRELEASE),)
obj-m := slab.o
else
EXTRA_CFLAGS += -DDEBUG 
KDIR:=/home/code_folder/uboot_linux_rootfs/kernel/linux-5.10.4
ARCH_ARGS := ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
all:
	make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules
clean:
	make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules clean
endif

在这里插入图片描述

11. kmalloc 机制实现分析

  • 如何在内核驱动中申请和释放内存?
  • kmalloc 实现机制分析
  • kmalloc 和 slab、伙伴系统的关联

在这里插入图片描述

#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>

struct student {
    int age;
    char sex;
    char name[10];
    float score;
};

struct student *p;

static int __init hello_init(void)
{
    p = kmalloc(sizeof(struct student), GFP_KERNEL);
    if (!p) {
        return -ENOMEM;
    }

    printk("p = %x\n", (unsigned int)p);

    return 0;
}

static void __exit hello_exit(void)
{
    kfree(p);
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

ifneq ($(KERNELRELEASE),)
obj-m := kmalloc.o
else
EXTRA_CFLAGS += -DDEBUG
KDIR:=/home/code_folder/uboot_linux_rootfs/kernel/linux-5.10.4
ARCH_ARGS := ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
all:
	make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules
clean:
	make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules clean
endif

在这里插入图片描述
在这里插入图片描述

  • kmalloc 能申请的最大内存是多少?
  • kmalloc 返回的地址对齐方式?
  • kmalloc 返回的是虚拟地址,还是物理地址?
    • kmalloc 返回的内存地址是 内核的虚拟地址,而不是物理地址。内核空间中的所有内存分配(包括通过 kmalloc 分配的内存)都是虚拟地址,而内核会在需要时通过页表转换将其映射到物理内存。虚拟内存技术用于隔离物理内存和进程之间的内存访问。内核通过虚拟地址来管理内存,而物理地址通常在底层硬件和内存管理单元(MMU)之间转换。

12. 虚拟地址和 MMU 工作原理

  • 为什么需要虚拟地址?
    • 提供 内存保护、内存隔离、高效的内存管理 和 支持多任务操作。
  • 虚拟地址、物理地址
  • 线性地址、逻辑地址
  • 总线地址
  • MMU 的作用

在这里插入图片描述

  • MMU 工作原理:
    1. 内存访问请求:
      当 CPU 发出内存访问请求时,它使用虚拟地址。
    2. 检查 TLB:
      MMU 首先在 TLB(TLB,Translation Lookaside Buffer 转换旁路缓存,是一种 高速缓存,用于加速虚拟地址到物理地址的转换过程。TLB 存储了最近使用的虚拟页号到物理页号的映射信息。)中查找虚拟页号。如果找到了匹配的映射(TLB Hit),直接返回物理地址。
    3. TLB 未命中:
      如果 TLB 未命中(TLB Miss),MMU 使用 Table Walk Unit 查找页表。
    4. 页表查找:
      MMU 根据虚拟地址中的虚拟页号逐级查找页表,得到物理页号。
    5. 地址转换:
      一旦找到物理页号,MMU 将物理页号与页内偏移结合,得到最终的物理地址。
    6. 物理内存访问:
      最终,CPU 访问物理内存。

在这里插入图片描述

  • 虚拟地址到物理地址的转换原理:

在这里插入图片描述

13. 二级页表的工作原理

  • 二级页表的优势
    • 不需要大量连续的物理内存
    • 一个进程不会映射所有的虚拟地址空间
    • 随着页表级数增加,可以节省物理内存

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • PGD 是虚拟地址映射结构中的 一级页表,在现代操作系统(如 Linux)中,通常属于 二级页表 或 三级页表 结构中的 顶层页表。
  • PTE 是 页表项,用于在虚拟地址与物理地址之间建立映射。PTE 是页表中的基本单元,表示虚拟页面到物理页面的具体映射关系。

14. 页表

  • 使能 MMU 之前,页表要准备好
  • 页表的创建过程分析:__create_page_tables
    • 页表的大小、用途
    • 页表在内存中的地址
    • 页表的创建过程

在这里插入图片描述

  • 一级页表映射:section

在这里插入图片描述

15. TLB 和 Table Walk Unit

  • TLB,The Translation Lookaside Buffer:(转换旁路缓存)是一种 高速缓存,用于加速虚拟地址到物理地址的转换过程。TLB 存储了最近使用的虚拟页号到物理页号的映射信息。
  • Table Walk Unit:读取内存中的页表到 TLB
  • 页表的地址:通过软件写入 MMU 寄存器中
  • MMU 的作用:CPU 访问虚拟地址的过程:TLB hit、 TLB miss
    1. 内存访问请求:
      当 CPU 发出内存访问请求时,它使用虚拟地址。
    2. 检查 TLB:
      MMU 首先在 TLB(TLB,Translation Lookaside Buffer 转换旁路缓存,是一种 高速缓存,用于加速虚拟地址到物理地址的转换过程。TLB 存储了最近使用的虚拟页号到物理页号的映射信息。)中查找虚拟页号。如果找到了匹配的映射(TLB Hit),直接返回物理地址。
    3. TLB 未命中:
      如果 TLB 未命中(TLB Miss),MMU 使用 Table Walk Unit 查找页表。
    4. 页表查找:
      MMU 根据虚拟地址中的虚拟页号逐级查找页表,得到物理页号。
    5. 地址转换:
      一旦找到物理页号,MMU 将物理页号与页内偏移结合,得到最终的物理地址。
    6. 物理内存访问:
      最终,CPU 访问物理内存。

在这里插入图片描述

16. Linux 虚拟内存管理

16.1 32 位 X86 系统下的虚拟内存经典布局

在这里插入图片描述

16.2 虚拟内存划分

  • 用户空间和内核空间划分(4GB)
    • 3:1
    • 2:2
    • 1:3
    • 思考:为什么会有不同的划分比例?
      • 用户空间和内核空间的划分比例是操作系统设计中的一个重要考量,通常是根据操作系统的用途、硬件资源、性能需求以及安全要求等因素来决定的。常见的划分比例有 3:1、2:2 和 1:3 等,每种比例都有其适用的场景和优缺点。操作系统需要根据具体需求在用户空间和内核空间之间找到一个合理的平衡点。
  • 内核虚拟空间划分:
    线性映射区:直接映射物理内存的低端部分,便于访问。
    vmalloc 区:提供内存的非连续分配。
    fixmap 区:该区域用于映射一些固定的内存地址或硬件资源。将这类资源集中管理,可以避免这些资源对动态内存分配的影响,提高效率。
    pkmap 区:用于映射通过 kmalloc 和 slab 分配的物理内存。
    modules 区:用于存放动态加载的内核模块。内核模块区专门用于动态加载内核模块,这样系统可以在运行时加载新的功能,而不需要重启或重新编译内核。这也增强了内核的扩展性。

16.3 ARM 32 下的内存布局

在这里插入图片描述

  • 思考:为什么要将虚拟内存空间划分为不同的区域?
  • 满足多种操作系统需求,包括内存管理、性能优化、安全性、以及与硬件资源的有效互动。将虚拟内存空间划分为不同的区域,能够使操作系统更好地管理内存,提供更高的效率和安全性。

17. 虚拟内存管理:线性映射区

  • 线性映射区指的是内核虚拟地址空间中的一部分,这些虚拟地址与物理内存的地址之间存在直接的映射关系,即每个虚拟地址对应一个物理地址。这个区域的主要用途是映射系统的物理内存,尤其是内核需要频繁访问的物理内存区域。
  • PAGE_OFFSET 和 PHYS_OFFSET 之间只差一个偏移。
    • PAGE_OFFSET 是 Linux 内核中的一个常量,它定义了内核虚拟地址空间的起始位置。
    • PHYS_OFFSET 是 Linux 内核中的另一个常量,它表示物理内存的起始位置。

在这里插入图片描述

  • 地址转换:

在这里插入图片描述

#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>

struct student {
    int age;
    char sex;
    char name[10];
    float score;
};

struct student *p;

static int __init hello_init(void)
{
    p = kmalloc(sizeof(struct student), GFP_KERNEL);
    if (!p)
        return -ENOMEM;
    printk("p = 0x%x\n", (unsigned int)p);

    printk("phys p = 0x%x\n", (unsigned int)__virt_to_phys((unsigned int)p));

    return 0;
}


static void __exit hello_exit(void)
{
    kfree(p);
}


module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

ifneq ($(KERNELRELEASE),)
obj-m := linear_mapping.o
else
EXTRA_CFLAGS += -DDEBUG
KDIR:=/home/code_folder/uboot_linux_rootfs/kernel/linux-5.10.4
ARCH_ARGS := ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
all:
	make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules
clean:
	make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules clean
endif

在这里插入图片描述

18. 低端内存和高端内存的边界划分

  • 线性映射区的大小
  • 低端内存和高端内存如何划分?
    PAGE_OFFSET 设置不同时,会影响划分吗?PAGE_OFFSET 的设置会影响内核虚拟地址空间的划分。PAGE_OFFSET 是内核启动时定义的虚拟地址偏移,它确定内核空间在虚拟地址中的起始位置。这个偏移的设置决定了内核空间的起始地址,从而影响到低端内存和高端内存的划分。
  • 物理内存 1GB,对应的虚拟内存布局是怎样的?物理内存 3GB,对应的虚拟内存布局是怎样的?
    • 假设物理内存为 1GB,以下是 32 位系统中虚拟内存的典型布局,假设 PAGE_OFFSET 设置为 0xC0000000,虚拟地址空间:
      • 0x00000000 - 0xBFFFFFFF 是用户空间地址区域
      • 0xC0000000 - 0xFFFFFFFF 是内核空间地址区域。
      • 其中,0xC0000000 - 0xCFFFFFFF(1GB)是线性映射区,用来直接映射物理内存的低端部分(最多 1GB),即低端内存。
    • 如果系统物理内存超过 1GB,则超过部分会被归为高端内存,并不能直接映射到虚拟地址空间中,需要通过其他机制(如 vmalloc)来管理。

在这里插入图片描述

18.1 实验

18.1.1 PAGE_OFFSET 为 0x80000000,DDR 256MB

在这里插入图片描述

18.1.2 PAGE_OFFSET 为 0x80000000,DDR 512MB

在这里插入图片描述

18.1.3 PAGE_OFFSET 为 0x80000000,DDR 1GB

在这里插入图片描述

18.1.4 PAGE_OFFSET 为 0xC0000000 呢?

在这里插入图片描述

18.2 小结

  • 线性映射区大小,和 PAGE_OFFSET、物理内存相关
    用户空间 3GB/内核空间 1GB 划分,[3G, 3G+760MB] 为线性映射区范围
    用户空间 2GB/内核空间 2GB 划分,[2G, 2G+1760MB] 为线性映射区范围
    64 位系统虚拟空间足够大,全部映射到物理内存
    ARM 32/64 正渐渐舍弃高端内存的概念

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

19. 二级页表的创建过程分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • PTE:Page Table Entry,页表项
  • PMD:Page Middle Directory,页中目录
  • PGD:Page Global Directory,页全局目录

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

20. 虚拟内存管理:vmalloc 区

  • kmalloc vs vmalloc
    • kmalloc:用于内核空间内分配小块的内存,返回的内存是物理上连续的,并且适用于需要频繁访问的内存。适合存储小型数据结构,如控制块、缓冲区等。kmalloc 适用于最大 4MB 的内存分配。
    • vmalloc:用于内核空间分配 虚拟上连续但物理上可以不连续的内存,适合分配较大的内存块。vmalloc 分配的内存大小没有 kmalloc 的限制,适合用于大型数据结构(如内核缓冲区、大型缓存等)。
  • 内存申请接口:vmalloc/vfree
  • VMALLOC_START 到 VMALLOC_END 之间的一段区域
    • VMALLOC_START 和 VMALLOC_END 是内核中虚拟地址空间的一段范围,用于 vmalloc 分配的内存。该范围的内存虚拟地址是连续的,但物理地址可能不连续。内核通过映射多个物理页帧来确保虚拟地址空间的连续性。
  • vmalloc 最大能申请多大的内存?
  • vmalloc 区的大小怎么计算?默认大小呢?
  • vmalloc 区域的页表映射:内核通过多级页表(如二级或四级页表)来映射 vmalloc 区域的虚拟地址到物理内存。每个页表项指向一个物理页帧,从而实现虚拟地址到物理地址的转换。
  • vmalloc 编程接口:
    • 编程示例:使用 vmalloc 申请和释放内存
    • vmalloc 实现机制分析
      • 从 VMALLOC_START 到 VMALLOC_END 查找一片虚拟地址空间
      • 根据内存的大小从伙伴系统申请多个物理页帧 page
      • 把每个申请到的物理页帧逐页映射到虚拟地址空间

在这里插入图片描述

  • kmalloc 最大申请 4MB
ifneq ($(KERNELRELEASE),)
obj-m := kmalloc_4M.o
obj-m += kmalloc_5M.o
else
EXTRA_CFLAGS += -DDEBUG
KDIR:=/home/code_folder/uboot_linux_rootfs/kernel/linux-5.10.4
ARCH_ARGS := ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
all:
	make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules
clean:
	make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules clean
endif

#include <linux/init.h>
#include <linux/module.h>
#include <linux/vmalloc.h>
#include <linux/slab.h>

unsigned int *p = NULL;
unsigned int *q = NULL;

unsigned long vmalloc_to_pfn(const void *);

static int __init hello_init(void)
{
    unsigned int phys_addr;
    unsigned int pfn;
    int i;

    q = kmalloc(4 * 1024 * 1024, GFP_KERNEL); // 4MB
    if (!q) {
        printk("kmalloc failed\n");
        return -ENOMEM;
    } else {
        for (i = 0; i< 100; i++) {
            q = q + 1024;
            phys_addr = virt_to_phys(q);
            pfn = (unsigned long)phys_addr >> 12;
            printk("virt_addr = %x, pfn = %x, phys_addr = %x.\n",  \
                    (unsigned int)q, pfn, (unsigned int)phys_addr);
        }
    }

    return 0;
}

static void __exit hello_exit(void)
{
    kfree(q);
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

在这里插入图片描述

#include <linux/init.h>
#include <linux/module.h>
#include <linux/vmalloc.h>
#include <linux/slab.h>

unsigned int *p = NULL;
unsigned int *q = NULL;

unsigned long vmalloc_to_pfn(const void *);

static int __init hello_init(void)
{
    unsigned int phys_addr;
    unsigned int pfn;
    int i;

    q = kmalloc(5 * 1024 * 1024, GFP_KERNEL); // 5MB
    if (!q) {
        printk("kmalloc failed\n");
        return -ENOMEM;
    } else {
        for (i = 0; i< 100; i++) {
            q = q + 1024;
            phys_addr = virt_to_phys(q);
            pfn = (unsigned long)phys_addr >> 12;
            printk("virt_addr = %x, pfn = %x, phys_addr = %x.\n",  \
                    (unsigned int)q, pfn, (unsigned int)phys_addr);
        }
    }

    return 0;
}

static void __exit hello_exit(void)
{
    kfree(q);
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

在这里插入图片描述

  • vmalloc
ifneq ($(KERNELRELEASE),)
obj-m := kmalloc_4M.o
obj-m += kmalloc_5M.o
obj-m += vmalloc.o
else
EXTRA_CFLAGS += -DDEBUG
KDIR:=/home/code_folder/uboot_linux_rootfs/kernel/linux-5.10.4
ARCH_ARGS := ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
all:
	make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules
clean:
	make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules clean
endif

#include <linux/init.h>
#include <linux/module.h>
#include <linux/vmalloc.h>
#include <linux/slab.h>

unsigned int *p = NULL;
unsigned int *q = NULL;

unsigned long vmalloc_to_pfn(const void *);

static int __init hello_init(void)
{
    unsigned int phys_addr;
    unsigned int pfn;
    int i;

    p = vmalloc(50 * 1024 * 1024);
    if (!p) {
        printk("vmalloc failed\n");
        return -ENOMEM;
    } else {
        for (i = 0; i < 100; i++) {
            p = p + 1024; // test: change 1024 to 4096 8192...
            pfn = vmalloc_to_pfn(p);
            phys_addr = (pfn<<12) | ((unsigned int)p & 0xfff);
            printk("virt_addr: %x  pfn: %x  phys_addr: %x\n", \
                               (unsigned int)p, pfn, (unsigned int)phys_addr);
        }
    }

    return 0;
}

static void __exit hello_exit(void)
{
    vfree(p);
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

在这里插入图片描述

  • vmalloc 分配器
    核心数据结构:vm_struct、vmap_area
    全局变量:vmap_area_root、vmap_area_list

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

21. 寄存器映射:ioremap

  • 寄存器的 ioremap:ioremap 是 Linux 内核中用于将物理内存地址映射到内核虚拟地址空间的函数。它的主要用途是访问那些物理内存不在内核直接可访问的区域,通常是 I/O 内存区域或设备内存。通过 ioremap,内核可以通过虚拟地址来访问硬件寄存器、设备的内存映射区域等。
    • I/O 端口与 I/O 内存。(设备地址统一编址到物理内存中时,开启 MMU 之后就需要先映射到虚拟地址空间)
    • I/O-mapped 与 Memory-mapped
    • Linux 驱动中寄存器为什么要 ioremap?
    • ioremap 实现机制分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

22. 高端内存映射

  • 为什么会有高端内存区域?
  • 配置:CONFIG_HIGHMEM
  • 配置:memory split。一部分给内核,一部分给用户空间
  • 什么情况下,才会有 ZONE_HIGHMEM?
  • 高端内存的初始化

https://blog.csdn.net/kunkliu/article/details/102703812

  • 在传统的 x86_32 系统中, 当内核模块代码或线程访问内存时,代码中的内存地址都为逻辑地址,而对应到真正的物理内存地址,需要地址一对一的映射,如逻辑地址 0xc0000003 对应的物理地址为 0x3,0xc0000004 对应的物理地址为0x4,… …,
  • 逻辑地址与物理地址对应的关系为
    物理地址 = 逻辑地址 – 0xC0000000
  • 这是内核地址空间的地址转换关系,注意内核的虚拟地址在“高端”,但是它映射的物理内存地址在低端。
    逻辑地址 ------ 物理内存地址
    0xc0000000 – 0x0
    0xc0000001 – 0x1
    0xc0000002 – 0c2
    0xc0000003 – 0x3
    … – …
    0xe0000000 – 0x20000000
    … – …
    0xffffffff – 0x40000000 ??
  • 假设按照上述简单的地址映射关系,那么内核逻辑地址空间访问为 0xc0000000 ~ 0xffffffff,那么对应的物理内存范围就为 0x0 ~ 0x40000000,即只能访问 1G 物理内存。若机器中安装 8G 物理内存,那么内核就只能访问前 1G 物理内存,后面 7G 物理内存将会无法访问,因为内核的地址空间已经全部映射到物理内存地址范围 0x0 ~ 0x40000000。即使安装了 8G 物理内存,那么物理地址为 0x40000001 的内存,内核该怎么去访问呢?代码中必须要有内存逻辑地址的,0xc0000000 ~ 0xffffffff 的地址空间已经被用完了,所以无法访问物理地址 0x40000000 以后的内存。
    显然不能将内核地址空间 0xc0000000 ~ 0xfffffff 全部用来简单的地址映射。因此 x86 架构中将内核地址空间划分三部分:ZONE_DMA、ZONE_NORMAL 和 ZONE_HIGHMEM。ZONE_HIGHMEM 即为高端内存,这就是内存高端内存概念的由来。


在这里插入图片描述
在这里插入图片描述

  • 再一次讨论 vmalloc
    • 当只有低端物理内存时,vmalloc 从哪里申请内存?
      • 在没有高端内存(ZONE_HIGHMEM)的情况下,vmalloc 主要从 低端物理内存(ZONE_NORMAL)中申请内存。
      • 低端物理内存:通常是系统中的常规物理内存,内核可以直接映射到虚拟地址空间中。在 32 位系统中,通常低端内存的范围是内核虚拟地址空间可以直接映射的区域,通常低于 896MB(具体取决于内核的配置和内存映射方式)。
      • 当调用 vmalloc 时,内核会从低端内存区域分配物理页面,并将这些页面映射到非连续的虚拟地址空间。
      • vmalloc 内部会通过页表映射的方式将这些物理页面映射到一个连续的虚拟地址空间,而这些物理页面并不要求在物理地址上是连续的。这种方式允许内核分配一个连续的虚拟内存区域,而背后实际的物理内存页面可能是分散的。
    • vmalloc 建立的映射是否和线性映射冲突?
      • vmalloc 建立的虚拟地址映射通常 不会与线性映射冲突,原因如下:线性映射:通常指的是内核直接映射的物理内存区域,比如 ZONE_NORMAL 中的内存(低端内存)。内核通过线性映射的方式直接访问这些内存;vmalloc 映射:vmalloc 分配的虚拟内存区域是 非连续的,而且这个虚拟内存区域会被映射到不连续的物理内存页面上。这种映射依赖于页表,并不会和线性映射(通常是连续的物理页面映射)产生冲突。
      • 虽然两者都使用虚拟地址,但 vmalloc 分配的虚拟内存并不要求物理内存是连续的。因此,vmalloc 和线性映射的内存区域在虚拟地址空间上可能是不同的区域,它们不会直接冲突。实际上,vmalloc 为了避免与其他映射冲突,会从内核的虚拟地址空间中分配出一个独立的区域。
    • 当有高端内存时,vmalloc 从哪里申请物理内存?
      • 当有高端内存(ZONE_HIGHMEM)时,vmalloc 仍然从 低端物理内存(ZONE_NORMAL)申请内存。尽管高端内存存在,vmalloc 默认情况下并不使用 ZONE_HIGHMEM 中的内存进行分配。
      • 为什么不使用高端内存?高端内存通常是 无法直接映射 的,因为它位于 32 位系统的虚拟地址空间之外。内核需要通过临时映射的方式才能访问这些内存。而 vmalloc 使用的虚拟内存区域需要是 连续的虚拟地址空间,而高端内存的映射并不容易形成连续的虚拟地址映射。因此,vmalloc 通常避免从高端内存中分配内存。如果系统启用了高端内存(ZONE_HIGHMEM),内核一般会使用其他机制(比如 kmalloc 和 alloc_pages)来处理高端内存,而 vmalloc 只会从低端内存中分配内存。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

23. 虚拟内存管理:pkmap 区

  • pkmap 是 Linux 内核中与 高端内存(ZONE_HIGHMEM)相关的机制之一,它用于将高端内存的页面映射到内核的虚拟地址空间。因为在 32 位系统中,内核并不能直接访问 ZONE_HIGHMEM 中的物理内存,内核需要通过 pkmap 这样的机制来访问这些内存。
    • 临时映射:pkmap 是一种临时的虚拟地址映射。内核通过 pkmap 将物理内存中的页面映射到内核虚拟地址空间,内核可以通过这些映射来访问高端内存中的数据。
    • 映射范围:为了提高效率,pkmap 通常会映射 1 页(4KB)大小的高端内存区域。这些映射会通过内核的页表机制动态管理,并且是 临时性的。
  • pkmap 的工作原理是利用内核的页表来实现高端内存页面与内核虚拟地址空间之间的临时映射。具体来说,内核会使用 pkmap 区域,这是一块特殊的虚拟地址空间,内核通过将高端内存页面映射到这块区域来访问高端内存。
  • 编程接口函数:
void *kmap (struct page *page);
void kunmap (struct page *page);
void *kmap_atomic (struct page *page);
  • 编程实验:
    • 在低端内存申请一个物理页帧,使用 kmap 建立映射
    • 在高端内存申请一个物理页帧,使用 kmap 建立映射
    • 内核配置选项:CONFIG_HIGHMEM
    • 打印出映射后的虚拟地址,观察虚拟地址变化
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/highmem.h>

struct page *page_lowmem, *page_highmem;
void  *virt_addr;  // 存储映射后的虚拟地址
unsigned int phys_addr;  // 存储物理地址

static int __init hello_init(void)
{
    // 分配一个低端内存页面,使用 GFP_KERNEL 标志
    page_lowmem = alloc_page(GFP_KERNEL);
    if (!page_lowmem)
        printk("alloc page failed\n");
    
    // 将低端内存页面映射到内核的虚拟地址空间
    virt_addr = kmap(page_lowmem);
    
    // 获取该页面的物理地址
    phys_addr = __page_to_pfn(page_lowmem) << 12;
    printk("phys_addr:%x  virt_addr:%x\n", phys_addr, (unsigned int)virt_addr);

    // 分配一个高端内存页面,使用 __GFP_HIGHMEM 标志
    page_highmem = alloc_page(__GFP_HIGHMEM);
    if (!page_highmem)
        printk("alloc page failed\n");
    
    // 将高端内存页面映射到内核的虚拟地址空间
    virt_addr = kmap(page_highmem);
    
    // 获取该页面的物理地址
    phys_addr = __page_to_pfn(page_highmem) << 12;
    printk("phys_addr:%x  virt_addr:%x\n", phys_addr, (unsigned int)virt_addr);
    
    return 0;
}

static void __exit hello_exit(void)
{
    kunmap(page_lowmem); // 解除低端内存页面的虚拟地址映射
    kunmap(page_highmem); // 解除高端内存页面的虚拟地址映射
    __free_page(page_lowmem); // 释放低端内存页面
    __free_page(page_highmem); // 释放高端内存页面
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

ifneq ($(KERNELRELEASE),)
obj-m := kmap.o
else
EXTRA_CFLAGS += -DDEBUG
KDIR:=/home/code_folder/uboot_linux_rootfs/kernel/linux-5.10.4
ARCH_ARGS := ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
all:
	make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules
clean:
	make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules clean
endif

在这里插入图片描述
在这里插入图片描述

  • 使用注意事项
  • pkmap 区
    • pkmap 实现机制
    • 分配表: int pkmap_count[512];
    • page_address_htable[128]
    • pkmap_page_table[512]

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
一个物理页是 4K,pkmap 区域是 2M 大小,2M / 4K = 512。

24. 虚拟内存管理:fixmap 区

当内核完全启动后,内存管理提供了各种各样的 API 来使各个模块完成物理地址到虚拟地址的映射功能,但是在内核启动的初期,有些模块就需要使用虚拟地址并 mapping 到指定的物理地址上,同时,这些模块也没有办法等到内核的内存管理模块完全初始化之后再进行映射功能,因此,内核就分配了fixmap 这段地址空间,对于 ARM32 的为 0xffc00000 - 0xfff00000 这段虚拟地址空间,这段地址空间就用来实现前期某些特定的模块实现物理内存映射。
————————————————
原文链接:https://blog.csdn.net/u012489236/article/details/104724227

在这里插入图片描述
在这里插入图片描述

25. 虚拟内存管理:modules 区

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

26. 用户进程的页表

  • 用户进程的页表
    • 用户进程的空间描述:mm_struct、vm_area_struct
    • 用户进程的页表与内核的页表

在这里插入图片描述
在这里插入图片描述

  • 用户进程页表的创建过程分析:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

27. 缺页异常机制

  • 缺页异常(Page Fault)是指当一个进程试图访问的内存页不在物理内存中时,操作系统会触发一个中断,进入内核进行异常处理。
  • 缺页异常通常会在以下情况下触发:
    • 访问的内存页不在物理内存中: 当进程试图访问某个虚拟地址对应的内存页时,如果该内存页当前不在物理内存中(即该页可能被交换到磁盘或者从未被加载到内存中),则会触发缺页异常。
    • 页面未被映射: 当进程访问一个尚未映射的虚拟地址(即该地址没有被分配物理页)时,缺页异常会被触发。这通常发生在进程试图访问未初始化的内存区域(如未分配的堆栈空间)时。
    • 访问保护错误: 虚拟内存系统允许不同的内存区域设置不同的访问权限。如果进程试图以不允许的方式访问内存页(例如,尝试写一个只读页),也会触发缺页异常。
    • 硬件相关的缺页异常: 现代处理器(如 x86, ARM)通常具有内存管理单元(MMU),它将虚拟地址映射到物理地址。在该过程中,如果某个虚拟地址没有对应的物理页面,MMU 会发出缺页异常。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

28. 用户页表的刷新

  • 在操作系统中,用户页表的刷新是指在进程访问虚拟地址时,更新页表(或其缓存)的过程,以确保虚拟地址到物理地址的映射是最新的。
  • 页表的同步
    • 用户进程的内核页表
    • 内核空间的内核页表
    • vmalloc/vfree 操作产生的页表更新
    • TLB 刷新
    • 缺页异常分析:页表同步更新

在这里插入图片描述

29. mmap 映射机制

  • 什么是mmap映射?
    • mmap(Memory Mapping)是 Linux 和 Unix 系统中提供的一种系统调用,用于将文件或设备(如设备驱动)映射到进程的虚拟内存空间中。它允许一个文件或设备的内容直接映射到进程的地址空间,进程可以通过内存访问的方式来读取或写入文件中的数据,而不需要通过传统的文件操作接口(如 read 或 write)进行操作。
  • 哪些场合需要mmap内存映射?

在这里插入图片描述
在这里插入图片描述

29.1 编程示例

  • 将文件映射到虚拟地址空间,通过地址读写文件
  • 实现一个字符设备驱动
    • 实现字符设备的 read、write 接口
    • 实现字符设备的 mmap 接口
    • 编写应用程序,通过文件 IO 接口读写字符设备
    • 编写应用程序,通过 mmap 接口读写字符设备
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>

char write_buf[] = "hello world!\n";
char read_buf[200];

int main(void)
{
    int fd;
    char *mmap_addr;

    fd = open("data.txt", O_RDWR);
    if (fd < 0) {
        printf("open failed\n");
        return -1;
    }

    write(fd, write_buf, strlen(write_buf) + 1);
    lseek(fd, 0, SEEK_SET);
    read(fd, read_buf, 100);
    printf("read_buffer: %s\n", read_buf);

    // 将文件的内容映射到内存
    // NULL 表示让操作系统选择映射的虚拟地址,4096 表示映射的字节数(这里是 4096 字节)
    // PROT_READ | PROT_WRITE 表示映射区域的访问权限为读写
    // MAP_SHARED 表示对内存区域的修改会反映到文件中
    mmap_addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    printf("mmap read: %s\n", mmap_addr);

    memcpy(mmap_addr + 5, "hello, zhaixue.cc!\n", 20);
    printf("mmap read: %s\n", mmap_addr);

    // 解除映射,释放映射的内存
    munmap(mmap_addr, 4096);

    return 0;
}

在这里插入图片描述

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/gfp.h>
#include <linux/uaccess.h>

#define PAGE_ORDER 0  // 定义页面的大小,这里为 4KB
#define MAX_SIZE 4096 // 最大映射大小为 4KB

static struct page *page = NULL;  // 内存页面指针
static char   *hello_buf = NULL;  // 用户空间的缓冲区指针

static struct class *hello_class;    // 设备类
static struct device *hello_device;  // 设备
static struct cdev *hello_cdev;      // 字符设备
static dev_t devno;                  // 设备号

static int hello_open(struct inode *inode, struct file *file)
{
    // 分配一个 4KB 的页面
    page = alloc_pages(GFP_KERNEL, PAGE_ORDER);
    if (!page) {
        printk("alloc_page failed\n");
        return -ENOMEM;  // 如果分配失败,返回内存不足错误
    }
    hello_buf = (char *)page_to_virt(page);  // 将页面地址转换为虚拟地址
    printk("data_buf phys_addr: %x, virt_addr: %px\n", page_to_phys(page), hello_buf);

   return 0;
}

static int hello_release(struct inode *inode, struct file *file)
{
    // 释放分配的页面
    __free_pages(page, PAGE_ORDER);
    return 0;
}

static ssize_t hello_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    int len;
    int ret;
    
    len = PAGE_SIZE - *ppos;
    if (count > len)
        count = len;
    
    ret = copy_to_user(buf, hello_buf + *ppos, count);
    if (ret)
        return -EFAULT;
    
    *ppos += count;
    return count;
}

static ssize_t hello_write(struct file *file, const char __user *buf, 
                        size_t count, loff_t *ppos)
{
    int len = 0;
    int ret;

    len = PAGE_SIZE - *ppos;
    if (count > len)
        count = len;

    ret = copy_from_user(hello_buf + *ppos, buf, count);
    if (ret)
        return -EFAULT;

    *ppos += count;
    return count;
}

// 内存映射操作
static int hello_mmap(struct file *file, struct vm_area_struct *vma)
{
    struct mm_struct *mm;
    unsigned long size;
    unsigned long pfn;
    int ret;

    mm = current->mm;  // 获取当前进程的内存管理结构
    pfn = page_to_pfn(page);  // 将内存页的物理页面号转换为物理地址

    size = vma->vm_end - vma->vm_start;  // 计算映射区域的大小
    if (size > MAX_SIZE) {
        printk("map size too large, max size is 0x%x\n", MAX_SIZE);
        return -EINVAL;  // 如果映射的大小超过最大限制,返回无效参数错误
    }

    // 将物理页面映射到虚拟地址空间
    ret = remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot);
    if (ret) {
        printk("remap_pfn_range failed\n");
        return -EAGAIN;
    }
    
    return ret;
}

static loff_t hello_llseek(struct file *file, loff_t offset, int whence)
{
    loff_t pos;
    
    // 根据不同的 seek 类型调整文件指针
    switch(whence) {
        case SEEK_SET: 
            pos = offset;
            break;
        case SEEK_CUR:
            pos = file->f_pos + offset;
            break;
        case SEEK_END:
            pos = MAX_SIZE + offset;
            break;
        default:
            return -EINVAL;
    }

    if (pos < 0 || pos > MAX_SIZE)
        return -EINVAL;  // 文件指针越界,返回无效参数错误
    
    file->f_pos = pos;  // 更新文件指针
    return pos;  // 返回新位置
}

static const struct file_operations hello_chrdev_fops = {
    .owner      = THIS_MODULE,
    .open       = hello_open,
    .release    = hello_release,
    .read       = hello_read,
    .write      = hello_write,
    .mmap       = hello_mmap,
    .llseek     = hello_llseek
};

static int __init hello_init(void)
{
    int ret;

    // 动态分配设备号
    ret = alloc_chrdev_region(&devno, 0, 1, "hello");
    if (ret) {
        printk("alloc char device number failed!\n");
        return ret;
    }

    // 初始化字符设备
    hello_cdev = cdev_alloc();
    cdev_init(hello_cdev, &hello_chrdev_fops);

    // 添加字符设备到内核中
    ret = cdev_add(hello_cdev, devno, 1);
    if (ret < 0) {
        printk("cdev_add failed\n");
        return ret;
    }

    // 创建设备类
    hello_class = class_create(THIS_MODULE, "hello-class");
    if (IS_ERR(hello_class)) {
        printk("create hello class failed!\n");
        return -1;
    }

    // 创建设备文件
    hello_device = device_create(hello_class, NULL, devno, NULL, "hello");
    if (IS_ERR(hello_device)) {
        printk("create hello device failed!\n");
        return -1;
    }

    return 0;
}

static void __exit hello_exit(void)
{
    // 删除字符设备
    cdev_del(hello_cdev);
    // 注销设备
    device_unregister(hello_device);
    // 销毁设备类
    class_destroy(hello_class);
    // 释放设备号
    unregister_chrdev_region(devno, 1);
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

ifneq ($(KERNELRELEASE),)
obj-m := mmap.o
else
EXTRA_CFLAGS += -DDEBUG
KDIR:=/home/code_folder/uboot_linux_rootfs/kernel/linux-5.10.4
ARCH_ARGS := ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
all:
	make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules
clean:
	make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules clean
endif

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>

char write_buf[] = "hello world!\n";
char read_buf[200];

int main(void)
{
    int fd;
    char *mmap_addr;

    fd = open("/dev/hello", O_RDWR);
    if (fd < 0) {
        printf("open failed\n");
        return -1;
    }

    write(fd, write_buf, strlen(write_buf)+1);
    lseek(fd, 0, SEEK_SET);
    read(fd, read_buf, 100);
    printf("read_buffer: %s\n", read_buf);

    mmap_addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    printf("mmap read: %s\n", mmap_addr);

    munmap(mmap_addr, 4096);

    return 0;
}

在这里插入图片描述

29.2 remap_pfn_range

  • 用户空间的虚拟地址映射分析
  • 接口函数实现分析:remap_pfn_remap
  • 虚拟地址是如何分配的?
  • mmap 系统调用流程分析

在这里插入图片描述

29.3 文件映射

在这里插入图片描述
在这里插入图片描述

  • 用户空间的 mmap 映射特点
    • 不会在映射时分配物理页
    • 仅仅是建立一种关联:
      • 虚拟地址和文件地址偏移建立关联
      • 设置好缺页异常回调函数
      • 返回分配的虚拟地址给用户程序
    • 读写的时候触发缺页异常
      • 调用回调函数
      • 在异常处理中分配物理页
      • 建立映射,更新页表
  • 如何建立关联?
    • address_space 与 page cache 的关联
    • 如何定位 page cache?
    • 虚拟地址和文件偏移地址之间的关联
    • 难点:offset 与 vma->vm_pgoff 的关系

在这里插入图片描述
在这里插入图片描述

29.4 文件缺页异常

  • 映射后的逻辑关联:
    • 虚拟地址:vma->vm_start
    • address_space
    • 文件读写偏移:vma->pgoff
    • 异常处理回调:vma->vm_ops
  • 读文件缺页异常分析
    • 读写异常的虚拟地址:vmf
    • 如何查找page:address_space + vma->vm_pgoff
    • 如何建立映射
    • 如何更新页表
  • 文件映射过程分析
    • 用户态系统调用:mmap
    • 内核态 mmap:file_operations->mmap
    • 核心结构体:vm_area_struct、address_space
    • 缺页异常:handle_pte_fault(&vmf);

29.5 设备映射缺页异常

  • 编程示例
    • 编写一个字符设备驱动
    • 编写测试应用程序
  • 要求:
    • 设备设备文件内存的映射功能
    • 完成设备的 mmap 功能
    • 完成缺页异常回调函数
    • 编写应用程序,测试 mmap 功能
  • vmf 在异常处理中扮演色角色:
    保存发生异常的虚拟地址 vma:vmf->vma=vma
    调用在 mmap 时注册的 fault 回调函数分配 page
    保存分配的 page 地址:vmf->page = page
    调用 finish_fault 建立映射,更新页表

29.6 匿名映射

  • 什么是匿名内存?匿名页面,文件页面?
  • 什么是匿名映射?
  • 匿名映射用在什么场合?
    • 使用 malloc 申请的堆内存
    • 使用 mmap 创建的内存匿名映射(注意参数 fd 变化)
    • 如何判断匿名映射?vma->vm_ops
  • 核心结构体
    • struct anon_vma
    • struct anon_vma_chain
    • struct vm_area_struct
  • malloc实现机制分析
    • 匿名映射过程分析
    • 匿名页面缺页异常处理过
    • empty_zero_page
    • do_anonymous_page流程

29.7 私有映射和共享映射

  • 映射组合及应用场合
    • 文件共享映射
    • 文件私有映射
    • 匿名共享映射
    • 匿名私有映射

在这里插入图片描述

30. 系统调用 brk 实现机制

  • 系统调用:brk
    • 用在什么地方?
    • 在什么时候触发 brk 系统调用?
    • brk 系统调用过程分析
    • 思考:为什么堆和栈之间的大片“空间”不能用?

在这里插入图片描述
在这里插入图片描述

31. 反向映射

  • 什么是反向映射?
    • 反向映射与正向映射
    • 反向映射的应用场景
    • 内存回收、页面迁移
    • 碎片整理、CMA 回收、巨型页
  • 匿名页反向映射
  • 文件页反向映射
  • 匿名页的反向映射
    • 核心结构体:anon_vma、anon_vma_chain
    • 核心结构体:page、vm_area_struct

在这里插入图片描述

  • 文件页的反向映射
    • 核心结构体:page、address_space
    • 核心结构体:vm_area_struct

在这里插入图片描述


http://www.kler.cn/a/592267.html

相关文章:

  • redis主从架构和集群---java
  • 【sql靶场】第18-22关-htpp头部注入保姆级教程
  • ELK+Filebeat+Kafka+Zookeeper安装部署
  • 第3章:Docker架构详解:从守护进程到镜像仓库
  • (二)Reactor核心-前置知识1
  • 《心理学与生活》2025最新网课答案
  • puppeteer网络爬虫 “网络爬虫”
  • 【k8s003】k8s与docker的依赖关系
  • 【资源损坏类故障】:详细了解坏块
  • 【从零开始学习计算机科学与技术】计算机网络(三)数据链路层
  • jmeter吞吐量控制器-Throughput Controller
  • 每月更新,提供qiime2兼容库:Mitohelper助力鱼类线粒体序列分析
  • PostgreSQL 14.17 安装 pgvector 扩展
  • Lambda 表达式的语法:
  • 高频SQL 50 题(持续更新)
  • 企业年度经营计划制定与管理方法论(124页PPT)(文末有下载方式)
  • Java:读取中文,read方法
  • WebAssembly 技术在逆向爬虫中的应用研究
  • 网络安全漏洞与修复 网络安全软件漏洞
  • 从位置编码开始手搓transformer框架,transfromer讲解