Linux内核下sparse内存类型,通过虚拟地址转换为page的接口

在Linux内核的稀疏内存(sparse memory)模型中,虚拟地址到struct page的转换接口与常规模型类似,但底层机制有所不同。以下是关键接口和转换逻辑:


1. 核心转换接口

a. virt_to_page()

1
struct page *virt_to_page(const void *vaddr);
  • 作用:将直接映射区域(低端内存)的虚拟地址转换为对应的struct page
  • 适用场景:虚拟地址位于线性映射区(如kmalloc分配的内存)。
  • 稀疏模型调整:通过vmemmap按需映射的页表访问struct page,而非全局mem_map数组。

b. pfn_to_page()

1
struct page *pfn_to_page(unsigned long pfn);
  • 作用:将物理页帧号(PFN)转换为对应的struct page
  • 稀疏模型实现:根据PFN定位对应的vmemmap区域,计算页结构地址。
  • 关键宏SECTION_ALIGN_UP(pfn)确定vmemmap段,PAGE_SHIFT计算偏移。

c. vmalloc_to_page()

1
struct page *vmalloc_to_page(const void *vaddr);
  • 作用:转换vmalloc分配的虚拟地址到struct page
  • 步骤:通过页表查询物理地址,再调用pfn_to_page()

2. 转换流程

步骤 1:区分虚拟地址类型

  • 线性地址(如kmalloc:直接使用virt_to_page()
  • 动态映射地址(如vmalloc:需用vmalloc_to_page()
  • 用户空间地址:需先通过get_user_pages()获取struct page

步骤 2:获取物理地址/PFN

  • 线性地址转PFN
    1
    unsigned long pfn = __pa(vaddr) >> PAGE_SHIFT;
  • 动态映射地址转PFN
    1
    unsigned long pfn = vmalloc_to_pfn(vaddr);

步骤 3:转换为struct page

1
struct page *page = pfn_to_page(pfn);

3. 稀疏内存的底层机制

  • vmemmap优化:将struct page数组按需映射到虚拟地址空间,避免为空洞内存分配结构体。
  • 内存段(Section):物理内存按SECTION_SIZE(如1GB)划分,每个段独立管理vmemmap
  • 页表映射:访问struct page时,通过vmemmap的页表动态映射到实际内存。

4. 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <linux/mm.h>

/* 转换线性地址 */
void *linear_vaddr = kmalloc(1024, GFP_KERNEL);
struct page *page1 = virt_to_page(linear_vaddr);

/* 转换vmalloc地址 */
void *vmalloc_vaddr = vmalloc(2048);
struct page *page2 = vmalloc_to_page(vmalloc_vaddr);

/* 通过PFN转换 */
unsigned long pfn = __pa(linear_vaddr) >> PAGE_SHIFT;
struct page *page3 = pfn_to_page(pfn);

5. 注意事项

  • 地址有效性:确保虚拟地址已分配且有效,否则可能触发oops
  • 上下文限制:部分接口(如virt_to_page())只能在原子上下文或进程上下文中调用。
  • 架构差异:具体实现可能因架构(x86/ARM)有所不同,但接口保持一致。

通过上述接口和逻辑,Linux内核在稀疏内存模型下高效管理虚拟地址到物理页的转换。

如何确定虚拟地址类型

在Linux内核中,虚拟地址的类型可以通过其所属的内存区域来判断。以下是确定虚拟地址类型的关键方法和实现逻辑:


1. 虚拟地址的主要类型

内核虚拟地址分为以下几类:

  • 线性映射地址(Low Memory / Direct Mapping):物理内存的直接映射区域,地址范围通常为 PAGE_OFFSEThigh_memory
  • vmalloc动态映射地址:通过 vmalloc 分配的地址,范围在 VMALLOC_STARTVMALLOC_END
  • 高端内存地址(High Memory):仅在32位系统中有意义,需通过 kmap 动态映射。
  • 模块地址空间:内核模块的代码和数据区域(MODULES_VADDRMODULES_END)。
  • 用户空间地址:用户态进程的地址空间(低于 TASK_SIZE)。

2. 内核提供的判断接口

a. is_vmalloc_addr()

1
2
#include <linux/mm.h>
bool is_vmalloc_addr(const void *x);
  • 作用:判断地址是否属于 vmalloc 区域。
  • 实现逻辑:检查地址是否在 VMALLOC_STARTVMALLOC_END 之间。
  • 示例
    1
    2
    3
    4
    void *addr = vmalloc(4096);
    if (is_vmalloc_addr(addr)) {
    printk("This is a vmalloc address\n");
    }

b. virt_addr_valid()

1
2
#include <linux/mm.h>
int virt_addr_valid(unsigned long addr);
  • 作用:判断地址是否为有效的线性映射地址(低端内存)。
  • 实现逻辑
    • 检查地址是否在 PAGE_OFFSEThigh_memory 之间。
    • 验证对应的物理页是否存在。
  • 示例
    1
    2
    3
    4
    void *addr = kmalloc(1024, GFP_KERNEL);
    if (virt_addr_valid((unsigned long)addr)) {
    printk("This is a linear-mapped address\n");
    }

c. is_kernel_addr()(部分架构特有)

1
2
3
#ifdef CONFIG_PPC64
bool is_kernel_addr(unsigned long addr);
#endif
  • 作用:在特定架构(如PowerPC)中判断地址是否属于内核空间。

3. 手动判断地址范围

a. 直接比较地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <asm/pgtable.h>

// 判断是否是线性映射地址
if (addr >= PAGE_OFFSET && addr < high_memory) {
// 线性映射区域
}

// 判断是否是vmalloc地址
if (addr >= VMALLOC_START && addr < VMALLOC_END) {
// vmalloc区域
}

// 判断是否是模块地址
if (addr >= MODULES_VADDR && addr < MODULES_END) {
// 模块区域
}

b. 使用内核宏

  • **__is_lm_address(addr)**:判断是否为线性映射地址。
  • **__is_kernel_text(addr)**:判断是否在内核代码段。

4. 地址类型判断流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
             +---------------------+
| 虚拟地址 (void *) |
+---------------------+
|
v
+-----------------------------+
| addr >= MODULES_VADDR |
| && addr < MODULES_END? |
+-----------------------------+
|
+----------------+----------------+
| 是 | 否
v v
+----------------------+ +-----------------------------+
| 模块地址空间 | | addr >= VMALLOC_START |
+----------------------+ | && addr < VMALLOC_END? |
+-----------------------------+
|
+-----------------+-----------------+
| 是 | 否
v v
+----------------------+ +-----------------------------+
| vmalloc地址 | | addr >= PAGE_OFFSET |
+----------------------+ | && addr < high_memory? |
+-----------------------------+
|
+-------------+-------------+
| 是 | 否
v v
+----------------------+ +----------------------+
| 线性映射地址 | | 其他(如高端内存、用户空间)|
+----------------------+ +----------------------+

5. 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <linux/mm.h>
#include <linux/vmalloc.h>

void check_address_type(void *addr) {
if (is_vmalloc_addr(addr)) {
printk("Address %p is a vmalloc address\n", addr);
} else if (virt_addr_valid((unsigned long)addr)) {
printk("Address %p is a linear-mapped address\n", addr);
} else if (addr >= MODULES_VADDR && addr < MODULES_END) {
printk("Address %p is in kernel module space\n", addr);
} else if (addr < (void *)TASK_SIZE) {
printk("Address %p is a userspace address\n", addr);
} else {
printk("Address %p is of unknown type (possibly highmem)\n", addr);
}
}

6. 注意事项

  1. 架构差异:地址范围定义(如 PAGE_OFFSETVMALLOC_START)可能因架构(x86/ARM/PowerPC)而不同。
  2. 高端内存(High Memory):仅在32位系统中需要特殊处理,64位系统通常无需考虑。
  3. 性能:直接使用内核接口(如 is_vmalloc_addr)比手动检查更高效且可移植。
  4. 用户空间地址:在内核中访问用户空间地址需先使用 copy_from_userget_user_pages

通过上述方法,可以准确判断虚拟地址的类型,从而选择正确的内存操作接口(如 virt_to_pagevmalloc_to_page)。