内核用 struct page 结构体来表示系统中的每个物理页面,该结构体用来跟踪和管理这些物理页面的使用情况。
page_address函数根据给定的struct page 结构体返回该物理页面的内核起始虚拟地址:
// 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)
对于内核高版本:
// 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)
# cat /boot/config-3.10.0-1160.el7.x86_64 | grep CONFIG_SPARSEMEM_VMEMMAP
CONFIG_SPARSEMEM_VMEMMAP=y
//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。
# cat /boot/config-3.10.0-1160.el7.x86_64 | grep CONFIG_PHYS_ADDR_T_64BIT
CONFIG_PHYS_ADDR_T_64BIT=y
#ifdef CONFIG_PHYS_ADDR_T_64BIT
typedef u64 phys_addr_t;
/* 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)x86_64:
在x86_64等64位架构中,因为内核虚拟空间较大,所以直接把所有物理内存直接线性映射到内核虚拟地址当中,物理地址和内核虚拟地址仅仅一个PAGE_OFFSET的偏移:
#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,就可以将物理地址转换为对应的虚拟地址。
备注:
内核虚拟地址空间都是直接映射区,地址连续,和物理地址空间是简单的线性映射关系。虽然内核虚拟地址空间是直接映射区,但还是会建立页表。
ffff880000000000 - ffffc7ffffffffff (=64 TB) direct mapping of all phys. memory
(2)arm64架构:
# 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 的虚拟地址空间。
// /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))
// linux-5.4.18/arch/arm64/mm/init.c
void __init arm64_memblock_init(void)
{
......
physvirt_offset = PHYS_OFFSET - PAGE_OFFSET;
......
}
// 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 可能因架构和环境而异,它表示物理地址与虚拟地址之间的差距。
s64 physvirt_offset __ro_after_init;
EXPORT_SYMBOL(physvirt_offset);
# cat /proc/kallsyms | grep physvirt_offset
ffffffc01123d2e8 R physvirt_offset
通过伙伴系统接口分配得到一个struct page以后,调用page_address函数过程:
(1)page_to_pfn宏根据给定struct page 结构体获取该物理页面的页帧号pfn。
(2)PFN_PHYS宏根据给定的物理页面的页帧号pfn获取其该物理页面的物理起始地址。
(3)__va() 宏用于将给定的物理地址转换为对应的内核虚拟地址。
struct page --> 页帧号pfn --> 物理页面的物理起始地址 --> 内核虚拟起始地址
page_to_virt 和 page_address 功能一样:
(1)3.10.0内核版本:
#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内核版本:
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和之前一样:
#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:
# cat /boot/config-5.4.18-74-generic | grep CONFIG_SPARSEMEM_VMEMMAP
CONFIG_SPARSEMEM_VMEMMAP=y
#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));\
})
# include
# include
# include
# include
# include
# include
//内核模块初始化函数
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");
[159158.806456] virtual addr = 0xffff9d991dc92000
[159158.806463] pfn = 384146
[159158.806467] phys addr = 0x5dc92000
[159158.806471] virtual addr1 = 0xffff9d991dc92000
可以看到 virtual addr 和 virtual addr1 值一样。