Linux 内核内存管理 page_address 函数

文章目录

  • 一、page_address
    • [1.1 page_address](#1.1 page_address)
    • [1.2 page_to_pfn](#1.2 page_to_pfn)
    • [1.3 PFN_PHYS](#1.3 PFN_PHYS)
    • [1.4 __va(x)](#1.4 __va(x))
    • [1.5 总结](#1.5 总结)
    • [1.6 page_to_virt](#1.6 page_to_virt)
  • 二、使用demo

一、page_address

1.1 page_address

内核用 struct page 结构体来表示系统中的每个物理页面,该结构体用来跟踪和管理这些物理页面的使用情况。

page_address函数根据给定的struct page 结构体返回该物理页面的内核起始虚拟地址:

c 复制代码
// linux-3.10/include/linux/mm.h

static __always_inline void *lowmem_page_address(const struct page *page)
{
	return __va(PFN_PHYS(page_to_pfn(page)));
}

#if !defined(HASHED_PAGE_VIRTUAL) && !defined(WANT_PAGE_VIRTUAL)
#define page_address(page) lowmem_page_address(page)

对于内核高版本:

c 复制代码
// linux-5.4.18/include/linux/mm.h

static __always_inline void *lowmem_page_address(const struct page *page)
{
	return page_to_virt(page);
}

#if !defined(HASHED_PAGE_VIRTUAL) && !defined(WANT_PAGE_VIRTUAL)
#define page_address(page) lowmem_page_address(page)

1.2 page_to_pfn

c 复制代码
# cat /boot/config-3.10.0-1160.el7.x86_64 | grep CONFIG_SPARSEMEM_VMEMMAP
CONFIG_SPARSEMEM_VMEMMAP=y
c 复制代码
//linux-3.10/include/asm-generic/memory_model.h

if defined(CONFIG_SPARSEMEM_VMEMMAP)

/* memmap is virtually contiguous.  */
#define __page_to_pfn(page)	(unsigned long)((page) - vmemmap)

#define page_to_pfn __page_to_pfn

page_to_pfn宏根据给定struct page 结构体获取该物理页面的页帧号pfn。

1.3 PFN_PHYS

c 复制代码
# cat /boot/config-3.10.0-1160.el7.x86_64 | grep CONFIG_PHYS_ADDR_T_64BIT
CONFIG_PHYS_ADDR_T_64BIT=y
c 复制代码
#ifdef CONFIG_PHYS_ADDR_T_64BIT
typedef u64 phys_addr_t;
c 复制代码
/* PAGE_SHIFT determines the page size */
#define PAGE_SHIFT	12

#define PFN_PHYS(x)	((phys_addr_t)(x) << PAGE_SHIFT)

PFN_PHYS宏根据给定的物理页面的页帧号pfn获取其该物理页面的物理起始地址。

参数 x 是一个页框号(PFN),PAGE_SHIFT 是一个常量,表示页的大小的位移值(通常为 12,在 x86 架构上表示 4KB 大小的页面)。

这个宏通过将页框号左移 PAGE_SHIFT 位来计算物理地址。由于页框号是以页为单位计数的,每个页的大小为 2^PAGE_SHIFT 字节,所以将页框号左移 PAGE_SHIFT 位相当于将其乘以页的大小,得到物理地址。

1.4 __va(x)

(1)x86_64:

在x86_64等64位架构中,因为内核虚拟空间较大,所以直接把所有物理内存直接线性映射到内核虚拟地址当中,物理地址和内核虚拟地址仅仅一个PAGE_OFFSET的偏移:

c 复制代码
#define __PAGE_OFFSET           _AC(0xffff880000000000, UL)

#define PAGE_OFFSET		((unsigned long)__PAGE_OFFSET)

// linux-3.10/arch/x86/include/asm/page.h
#define __va(x)			((void *)((unsigned long)(x)+PAGE_OFFSET))

__va() 宏用于将给定的物理地址转换为对应的内核虚拟地址。

这个宏将物理地址转换为虚拟地址的过程是将物理地址转换为无符号长整型,然后加上 PAGE_OFFSET 值。PAGE_OFFSET 是一个常量,表示内核虚拟地址的偏移量,是内核代码和数据在虚拟地址空间中的起始位置。

通过将物理地址加上 PAGE_OFFSET,就可以将物理地址转换为对应的虚拟地址。

备注:

内核虚拟地址空间都是直接映射区,地址连续,和物理地址空间是简单的线性映射关系。虽然内核虚拟地址空间是直接映射区,但还是会建立页表。

c 复制代码
ffff880000000000 - ffffc7ffffffffff (=64 TB) direct mapping of all phys. memory

(2)arm64架构:

c 复制代码
# cat /boot/config-5.4.18-74-generic | grep CONFIG_ARM64_VA_BITS
CONFIG_ARM64_VA_BITS_39=y
# CONFIG_ARM64_VA_BITS_48 is not set
CONFIG_ARM64_VA_BITS=39

CONFIG_ARM64_VA_BITS_39 是一个内核配置选项,用于指定 ARM64 架构中虚拟地址的位数。该选项设置为 y 表示启用 39 位虚拟地址。

ARM64 架构支持不同的虚拟地址位数配置,可以根据系统需求进行调整。虚拟地址位数决定了虚拟地址空间的大小。

其中,39 位配置是较为常见的配置,适用于大多数 ARM64 架构的系统。它提供了 512 GB 的虚拟地址空间。

c 复制代码
// /arch/arm64/include/asm/memory.h

/* PHYS_OFFSET - the physical address of the start of memory. */
#define PHYS_OFFSET		({ VM_BUG_ON(memstart_addr & 1); memstart_addr; })

/*
 * PAGE_OFFSET - the virtual address of the start of the linear map, at the
 *               start of the TTBR1 address space.
 * PAGE_END - the end of the linear map, where all other kernel mappings begin.
 * KIMAGE_VADDR - the virtual address of the start of the kernel image.
 * VA_BITS - the maximum number of bits for virtual addresses.
 */
#define VA_BITS			(CONFIG_ARM64_VA_BITS)
#define _PAGE_OFFSET(va)	(-(UL(1) << (va)))
#define PAGE_OFFSET		(_PAGE_OFFSET(VA_BITS))
c 复制代码
// linux-5.4.18/arch/arm64/mm/init.c

void __init arm64_memblock_init(void)
{
	......
	physvirt_offset = PHYS_OFFSET - PAGE_OFFSET;
	......
}
c 复制代码
// linux-5.4.18/arch/arm64/include/asm/memory.h

#define __phys_to_virt(x)	((unsigned long)((x) - physvirt_offset))

#define __va(x)			((void *)__phys_to_virt((phys_addr_t)(x)))

参数 x 是一个物理地址,physvirt_offset 是一个常量,表示物理地址和虚拟地址之间的偏移量。

这个宏通过将给定的物理地址减去 physvirt_offset 来计算对应的虚拟地址。偏移量 physvirt_offset 可能因架构和环境而异,它表示物理地址与虚拟地址之间的差距。

c 复制代码
s64 physvirt_offset __ro_after_init;
EXPORT_SYMBOL(physvirt_offset);
c 复制代码
# cat /proc/kallsyms | grep physvirt_offset
ffffffc01123d2e8 R physvirt_offset

1.5 总结

通过伙伴系统接口分配得到一个struct page以后,调用page_address函数过程:

(1)page_to_pfn宏根据给定struct page 结构体获取该物理页面的页帧号pfn。

(2)PFN_PHYS宏根据给定的物理页面的页帧号pfn获取其该物理页面的物理起始地址。

(3)__va() 宏用于将给定的物理地址转换为对应的内核虚拟地址。

c 复制代码
struct page --> 页帧号pfn --> 物理页面的物理起始地址 --> 内核虚拟起始地址

1.6 page_to_virt

page_to_virt 和 page_address 功能一样:

(1)3.10.0内核版本:

c 复制代码
#define __va(x)			((void *)((unsigned long)(x)+PAGE_OFFSET))

// linux-3.10/include/asm-generic/page.h

#define pfn_to_virt(pfn)	__va((pfn) << PAGE_SHIFT)

#define page_to_virt(page)	pfn_to_virt(page_to_pfn(page))

(2)5.4.18内核版本:

c 复制代码
static __always_inline void *lowmem_page_address(const struct page *page)
{
	return page_to_virt(page);
}

#if !defined(HASHED_PAGE_VIRTUAL) && !defined(WANT_PAGE_VIRTUAL)
#define page_address(page) lowmem_page_address(page)

对于 x86_64和之前一样:

c 复制代码
#define __va(x)			((void *)((unsigned long)(x)+PAGE_OFFSET))

#define pfn_to_virt(pfn)	__va((pfn) << PAGE_SHIFT)

#define page_to_virt(page)	pfn_to_virt(page_to_pfn(page))

对于aarch64:

c 复制代码
# cat /boot/config-5.4.18-74-generic | grep CONFIG_SPARSEMEM_VMEMMAP
CONFIG_SPARSEMEM_VMEMMAP=y
c 复制代码
#if defined(CONFIG_SPARSEMEM_VMEMMAP)

#define page_to_virt(x)	({						\
	__typeof__(x) __page = x;					\
	u64 __idx = ((u64)__page - VMEMMAP_START) / sizeof(struct page);\
	u64 __addr = PAGE_OFFSET + (__idx * PAGE_SIZE);			\
	(void *)__tag_set((const void *)__addr, page_kasan_tag(__page));\
})

二、使用demo

c 复制代码
# include <linux/init.h>
# include <linux/kernel.h>
# include <linux/module.h>
# include <linux/mm_types.h>
# include <linux/mm.h>
# include <linux/gfp.h>

//内核模块初始化函数
static int __init lkm_init(void)
{
    struct page *page = alloc_pages(GFP_KERNEL, 0);
    
    unsigned long virt_address = (unsigned long)page_address(page);

    printk("virtual addr = 0x%lx\n", virt_address);

    unsigned int pfn = page_to_pfn(page);
    printk("pfn = %d\n", pfn);

    unsigned long phys_address = PFN_PHYS(pfn);
    printk("phys addr = 0x%lx\n", phys_address);

    unsigned long virt_address1 = (unsigned long)__va(phys_address);

    printk("virtual addr1 = 0x%lx\n", virt_address1);

    free_pages(virt_address, 0);

    return 0;

}

//内核模块退出函数
static void __exit lkm_exit(void)
{
	printk("Goodbye\n");
}


module_init(lkm_init);
module_exit(lkm_exit);

MODULE_LICENSE("GPL");
c 复制代码
[159158.806456] virtual addr = 0xffff9d991dc92000
[159158.806463] pfn = 384146
[159158.806467] phys addr = 0x5dc92000
[159158.806471] virtual addr1 = 0xffff9d991dc92000

可以看到 virtual addr 和 virtual addr1 值一样。

相关推荐
海岛日记28 分钟前
centos一键卸载docker脚本
linux·docker·centos
AttackingLin1 小时前
2024强网杯--babyheap house of apple2解法
linux·开发语言·python
学Linux的语莫2 小时前
Ansible使用简介和基础使用
linux·运维·服务器·nginx·云计算·ansible
踏雪Vernon2 小时前
[OpenHarmony5.0][Docker][环境]OpenHarmony5.0 Docker编译环境镜像下载以及使用方式
linux·docker·容器·harmonyos
学Linux的语莫3 小时前
搭建服务器VPN,Linux客户端连接WireGuard,Windows客户端连接WireGuard
linux·运维·服务器
legend_jz3 小时前
【Linux】线程控制
linux·服务器·开发语言·c++·笔记·学习·学习方法
Komorebi.py3 小时前
【Linux】-学习笔记04
linux·笔记·学习
黑牛先生3 小时前
【Linux】进程-PCB
linux·运维·服务器
友友马3 小时前
『 Linux 』网络层 - IP协议(一)
linux·网络·tcp/ip