6. 基础 - 虚拟地址和物理地址

205 阅读6分钟

一、本质区别与核心概念

在这里插入图片描述

二、空间布局

整体虚拟地址分布 在这里插入图片描述

1. 32 位虚拟内存空间分布(x86-32)在这里插入图片描述

896 MB 是直接映射区的上限,物理内存大于 896 MB 的部分称为 高端内存 (highmem),需通过 kmap / vmalloc 访问。

1.1 内核空间 - 直接映射区

  在总共大小 1G 的内核虚拟内存空间中,位于最前边有一块 896M 大小的区域,我们称之为直接映射区或者线性映射区,地址范围为 3G -- 3G + 896M。这块连续的虚拟内存地址会映射到 0 - 896M 这块连续的物理内存上,直接映射区中的映射关系是一比一映射,映射关系是固定的不会改变。   前 1M 已经在系统启动的时候被系统占用。1M 之后的物理内存存放的是内核代码段,数据段,BSS 段(这些信息起初存放在 ELF格式的二进制文件中,在系统启动的时候被加载进内存)。

1.1.1 ZONE_DMA

  在 X86 体系结构下,ISA 总线的 DMA (直接内存存取)控制器,只能对内存的前16M 进行寻址,这就导致了 ISA 设备不能在整个 32 位地址空间中执行 DMA,只能使用物理内存的前 16M 进行 DMA 操作。   直接映射区的前 16M 专门让内核用来为 DMA 分配内存,这块 16M 大小的内存区域我们称之为 ZONE_DMA。用于 DMA 的内存必须从 ZONE_DMA 区域中分配。

1.1.2 ZONE_NORMAL

  而直接映射区中剩下的部分也就是从 16M 到 896M(不包含 896M)这段区域,我们称之为 ZONE_NORMAL。ZONE_NORMAL 由于也是属于直接映射区的一部分,对应的物理内存 16M 到 896M 这段区域也是被直接映射至内核态虚拟内存空间中的 3G + 16M 到 3G + 896M 这段虚拟内存上。

1.2 内核空间 - ZONE_HIGHMEM 高端内存

  物理内存 896M 以上的区域被内核划分为 ZONE_HIGHMEM 区域,称之为高端内存。假设物理内存为 4G,高端内存区域为 4G - 896M = 3200M,那么这块 3200M 大小的 ZONE_HIGHMEM 区域该如何映射到内核虚拟内存空间中?   内核虚拟内存空间中的前 896M 虚拟内存已经被直接映射区所占用,而在 32 体系结构下内核虚拟内存空间总共也就 1G 的大小,这样一来内核剩余可用的虚拟内存空间就变为了 1G - 896M = 128M。很显然物理内存中 3200M 大小无法继续通过直接映射的方式映射到这 128M 大小的虚拟内存空间中,就只能采用动态映射的方式映射到 128M 大小的内核虚拟内存空间中,只能动态的分批映射,先映射正在使用的这部分,使用完毕解除映射,接着映射其他部分。

1.2.2 内存空洞

  内核虚拟内存空间中的 3G + 896M 这块地址在内核中定义为 high_memory,high_memory 往上有一段 8M 大小的内存空洞。空洞范围为:high_memory 到 VMALLOC_START。VMALLOC_START 定义在内核源码 /arch/x86/include/asm/pgtable_32_areas.h 文件中。 在这里插入图片描述

1.2.2 vmalloc动态映射区

  VMALLOC_START 到 VMALLOC_END 之间的这块区域成为动态映射区。这块动态映射区域内核使用vmalloc进行内存分配,vmalloc分配的内存在虚拟内存上连续,物理内存不连续。通过页表来建立物理内存和虚拟内存之间的映射关系,从而可以将不连续的物理内存映射到连续的虚拟内存上。由于 vmalloc 获得的物理内存页是不连续的,因此它只能将这些物理内存页一个一个地进行映射,在性能开销上会比直接映射大得多。 在这里插入图片描述

1.2.3 永久映射区

  PKMAP_BASE 到 FIXADDR_START 之间的这段空间称为永久映射区。   LAST_PKMAP 表示永久映射区可以映射的页数限制。 在这里插入图片描述在这里插入图片描述   内核的这段虚拟地址空间中允许建立与物理高端内存的长期映射关系。比如内核通过 alloc_pages() 函数在物理内存的高端内存中申请获取到的物理内存页,这些物理内存页可以通过调用 kmap 映射到永久映射区中。

1.2.4 固定映射区/临时映射区

  固定映射区,区域范围为:FIXADDR_START 到 FIXADDR_TOP。在固定映射区中的虚拟内存地址可以自由映射到物理内存的高端地址上,与动态映射区以及永久映射区不同的是,在固定映射区中虚拟地址是固定的,而被映射的物理地址是可以改变的。有些虚拟地址在编译的时候就固定下来了,是在内核启动过程中被确定的,而这些虚拟地址对应的物理地址不是固定的。固定虚拟地址的好处是它相当于一个指针常量(常量的值在编译时确定),指向物理地址,如果虚拟地址不固定,则相当于一个指针变量。   临时映射区,作为缓存页临时映射内核空间的区域。

2. 64 位虚拟内存空间分布(ARM64)

   ARM64 架构下的 Linux 内核编译选项中虚拟地址空间有48位和39位,使用39位可以提升 TLB 效率 ,降低页表内存占用,减少页表遍历层级。 在这里插入图片描述

2.1 CONFIG_ARM64_VA_BITS_48=y 用户空间(256 TB) 在这里插入图片描述

内核空间(256 TB) 在这里插入图片描述

2.2 CONFIG_ARM64_VA_BITS_39=y

用户空间(512 GB) 在这里插入图片描述

内核空间(512 GB)

三、内核关键API

1. 物理虚拟地址转换

phys_addr_t virt_to_phys(volatile void *x);
void *phys_to_virt(phys_addr_t x);

2. 页操作

struct page *virt_to_page(void *kaddr);
void *page_to_virt(struct page *page);

3. vmalloc操作

void *vmalloc(unsigned long size);
void vfree(const void *addr);

四、常见问题与调试

1. 地址错误诊断

查看进程地址空间
cat /proc/<pid>/maps

物理页号
cat /proc/$$/pagemap

诊断页错误
perf record -e page-faults -ag

检查非法地址访问
echo 1 > /proc/sys/debug/exception-trace
dmesg | grep -i "segfault"

2. 典型错误类型

在这里插入图片描述

五、实际应用场景

1. 进程内存分配

// 用户空间分配1MB内存
void *ptr = malloc(1024*1024); 

// 内核视角:
// 1. 分配虚拟地址范围
// 2. 仅分配VMA结构,不立即分配物理页
// 3. 首次访问时触发缺页中断
// 4. 内核分配物理页并建立映射

2. DMA操作

// 分配物理连续内存
void *dma_buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);

// 实现机制:
// 1. 从伙伴系统分配物理连续页
// 2. 建立设备可访问的虚拟映射
// 3. 返回虚拟地址和物理地址(dma_handle)

3. 进程间共享内存

// 创建共享内存
int shm_id = shmget(IPC_PRIVATE, size, IPC_CREAT | 0666);
void *shm_addr = shmat(shm_id, NULL, 0);

// 内核实现:
// 1. 分配物理页
// 2. 映射到多个进程的虚拟地址空间
// 3. 不同进程的虚拟地址不同,但指向相同物理页