文章目錄
- 一、page_address
- 1.1 page_address
- 1.2 page_to_pfn
- 1.3 PFN_PHYS
- 1.4 __va(x)
- 1.5 總結
- 1.6 page_to_virt
- 二、使用demo
一、page_address
1.1 page_address
內核用 struct page 結構體來表示系統中的每個物理頁面,該結構體用來跟蹤和管理這些物理頁面的使用情況。
page_address函數根據給定的struct page 結構體返回該物理頁面的內核起始虛擬地址:
// linux-3.10/include/linux/mm.hstatic __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.hstatic __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
# 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.hif 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
# 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.4 __va(x)
(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.cvoid __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
1.5 總結
通過伙伴系統接口分配得到一個struct page以后,調用page_address函數過程:
(1)page_to_pfn宏根據給定struct page 結構體獲取該物理頁面的頁幀號pfn。
(2)PFN_PHYS宏根據給定的物理頁面的頁幀號pfn獲取其該物理頁面的物理起始地址。
(3)__va() 宏用于將給定的物理地址轉換為對應的內核虛擬地址。
struct page --> 頁幀號pfn --> 物理頁面的物理起始地址 --> 內核虛擬起始地址
1.6 page_to_virt
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));\
})
二、使用demo
# 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");
[159158.806456] virtual addr = 0xffff9d991dc92000
[159158.806463] pfn = 384146
[159158.806467] phys addr = 0x5dc92000
[159158.806471] virtual addr1 = 0xffff9d991dc92000
可以看到 virtual addr 和 virtual addr1 值一樣。