前面已經分析過了Intel的記憶體映射和linux的基本使用情況,已知head_32.S僅是建立臨時頁表,內核還是要建立內核頁表,做到全面映射的,下面就基于RAM大于896MB,而小于4GB ,切CONFIG_HIGHMEM配置了高端記憶體的環境情況進行分析,
建立內核頁表前奏,了解兩個很關鍵的變數:
- max_pfn:最大物理記憶體頁面幀號;
- max_low_pfn:低端記憶體區(直接映射空間區的記憶體)的最大可用頁幀號;
max_pfn 的值來自setup_arch()中,setup_arch()函式中有:
max_pfn = e820_end_of_ram_pfn();
那么接下來看一下e820_end_of_ram_pfn()的實作:
804762
【file:/arch/x86/kernel/e820.c】
unsigned long __init e820_end_of_ram_pfn(void)
{
return e820_end_pfn(MAX_ARCH_PFN, E820_RAM);
}
e820_end_of_ram_pfn()直接封裝呼叫e820_end_pfn(),而其入參為MAX_ARCH_PFN和E820_RAM,其中MAX_ARCH_PFN的定義(x86的32bit環境)為:
# define MAX_ARCH_PFN (1ULL<<(32-PAGE_SHIFT))
最終值為0x100000,它表示的是4G物理記憶體的最大頁面幀號;而E820_RAM為:
#define E820_RAM 1
接下來看一下e820_end_pfn()函式實作:
【file:/arch/x86/kernel/e820.c】
/*
* Find the highest page frame number we have available
*/
static unsigned long __init e820_end_pfn(unsigned long limit_pfn, unsigned type)
{
int i;
unsigned long last_pfn = 0;
unsigned long max_arch_pfn = MAX_ARCH_PFN;
for (i = 0; i < e820.nr_map; i++) {
struct e820entry *ei = &e820.map[i];
unsigned long start_pfn;
unsigned long end_pfn;
if (ei->type != type)
continue;
start_pfn = ei->addr >> PAGE_SHIFT;
end_pfn = (ei->addr + ei->size) >> PAGE_SHIFT;
if (start_pfn >= limit_pfn)
continue;
if (end_pfn > limit_pfn) {
last_pfn = limit_pfn;
break;
}
if (end_pfn > last_pfn)
last_pfn = end_pfn;
}
if (last_pfn > max_arch_pfn)
last_pfn = max_arch_pfn;
printk(KERN_INFO "e820: last_pfn = %#lx max_arch_pfn = %#lx\n",
last_pfn, max_arch_pfn);
return last_pfn;
}
這個函式用來查找最大物理的頁面幀號,通過對e820圖的記憶體塊資訊得到記憶體塊的起始地址,將起始地址右移PAGE_SHIFT,算出其起始地址對應的頁面幀號,如果足夠大,超出了limit_pfn則設定最大頁面幀號為limit_pfn,否則則設定為遍歷中找到的最大的last_pfn,
e820_end_of_ram_pfn()函式的呼叫位置:
start_kernel() #init/main.c
└─>setup_arch() #arch/x86/kernel/setup.c
├─>e820_end_of_ram_pfn() #arch/x86/kernel/e820.c
└─>find_low_pfn_range() #arch/x86/kernel/e820.c
其中find_low_pfn_range()用于查找低端記憶體的最大頁面數的 ,max_low_pfn則在這里面初始化,
find_low_pfn_range()代碼實作:
【file:/arch/x86/mm/init_32.c】
/*
* Determine low and high memory ranges:
*/
void __init find_low_pfn_range(void)
{
/* it could update max_pfn */
if (max_pfn <= MAXMEM_PFN)
lowmem_pfn_init();
else
highmem_pfn_init();
}
函式實作很簡單,根據max_pfn是否大于MAXMEM_PFN,從而判斷是否初始化高端記憶體,也可以認為是啟用,那么來看一下MAXMEM_PFN的宏定義:
(file:/arch/x86/include/asm/setup.h)
#define MAXMEM_PFN PFN_DOWN(MAXMEM)
其中PFN_DOWN(x)的定義為:
(file:/include/linux/pfn.h)
#define PFN_DOWN(x) ((x) >> PAGE_SHIFT)
PFN_DOWN(x)是用來回傳小于x的最后一個頁面號,對應的還有個PFN_UP(x)是用來回傳大于x的第一個頁面號,此外有個PFN_PHYS(x)回傳的是x的物理頁面號,接著看MAXMEM的定義:
(file:arch/x86/include/asm/pgtable_32_types.h)
#define MAXMEM (VMALLOC_END - PAGE_OFFSET - __VMALLOC_RESERVE)
那么VMALLOC_END的定義則為:
(file:arch/x86/include/asm/pgtable_32_types.h)
#define VMALLOC_END (PKMAP_BASE - 2 * PAGE_SIZE)
//永久記憶體映射
#define PKMAP_BASE ((FIXADDR_BOOT_START - PAGE_SIZE * (LAST_PKMAP + 1)) & PMD_MASK)
其中PKMAP_BASE是永久映射空間的起始地址,LAST_PKMAP則是永久映射空間的映射頁面數,定義為:
#define LAST_PKMAP 1024
另外PAGE_SHIFT和PAGE_SIZE的定義為:
#define PAGE_SHIFT 12
#define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT)
而FIXADDR_BOOT_START是臨時固定映射空間起始地址,其的相關宏定義:
臨時記憶體映射:
#define FIXADDR_BOOT_SIZE (__end_of_fixed_addresses << PAGE_SHIFT)
#define FIXADDR_BOOT_START (FIXADDR_TOP - FIXADDR_BOOT_SIZE)
unsigned long __FIXADDR_TOP = 0xfffff000;
extern unsigned long __FIXADDR_TOP;
#define FIXADDR_TOP ((unsigned long)__FIXADDR_TOP)
這里其中的__end_of_fixed_addresses是來自fixed_addresses列舉值,是固定映射的一個標志,此外這里的FIXADDR_TOP是固定映射區末尾,而另外還有一個這里未列出的FIXADDR_START,是固定映射區起始地址,
既然到此,順便介紹一下內核空間映射情況,

內核空間如上圖,分為直接記憶體映射區(低端記憶體,線性)和高端記憶體映射區,其中直接記憶體映射區是指3G到3G+896M的線性空間,直接對應物理地址就是0到896M(前提是有超過896M的物理記憶體),其中896M是high_memory值,使用kmalloc()/kfree()介面操作申請釋放;
而高端記憶體映射區則是至超多896M物理記憶體的空間,它又分為動態映射區、永久映射區和固定映射區,
- 動態記憶體映射區,又稱之為vmalloc映射區或非連續映射區,是指VMALLOC_START到VMALLOC_END的地址空間,申請釋放操作介面是vmalloc()/vfree(),通常用于將非連續的物理記憶體映射為連續的線性地址記憶體空間;
- 而永久映射區,又稱之為KMAP區或持久映射區,是指自PKMAP_BASE開始共LAST_PKMAP個頁面大小的空間,操作介面是kmap()/kunmap(),用于將高端記憶體長久映射到記憶體虛擬地址空間中;
- 最后的固定映射區,也稱之為臨時內核映射區,是指FIXADDR_START到FIXADDR_TOP的地址空間,操作介面是kmap_atomic()/kummap_atomic(),用于解決持久映射不能用于中斷處理程式而增加的臨時內核映射,
下圖是根據個人的實驗環境繪制的一張關于內核空間映射情況,

PMD_MASK涉及的宏定義:
(file:/include/asm-generic/pgtable-nopmd.h)
#define PMD_SHIFT PUD_SHIFT
#define PMD_SIZE (1UL << PMD_SHIFT)
#define PMD_MASK (~(PMD_SIZE-1))
(file:/include/asm-generic/pgtable-nopud.h)
#define PUD_SHIFT PGDIR_SHIFT
(file:arch/x86/include/asm/Pgtable-2level_types.h)
#define PGDIR_SHIFT 22
PMD_MASK計算結果是:0xFFC00000,其實是用于資料對齊而已,
已知PAGE_OFFSET默認的為0xC0000000,而__VMALLOC_RESERVE為:
unsigned int __VMALLOC_RESERVE = 128 << 20;
最后在個人的實驗環境上,得出MAXMEM_PFN的值為0x377fe,
Linux是一個支持多硬體平臺的作業系統,各種硬體芯片的分頁并非固定的2級(頁全域目錄和頁表),僅僅Intel處理器而言,就存在3級的情況(頁全域目錄、頁中間目錄和頁表),而到了64位系統的時候就成了4級分頁, 所以Linux為了保持良好的兼容性和移植性,系統設計成了以下的4級分頁模型,根據平臺環境和配置的情況,通過將頁上級目錄和頁中間目錄的索引位設定為0,從而隱藏了頁三級目錄和頁中間目錄的存在,也就是為什么存在PMD_SHIFT、PUD_SHIFT和PGDIR_SHIFT,還有pgtable-nopmd.h、pgtable-nopud.h和Pgtable-2level_types.h的原因了,

由此管中窺豹,看到了Linux記憶體分頁映射模型的存在和相關設計,暫且也就先了解這么多,
分析宏是一件很乏味的事情,不過以小見大卻是一件很有意思的事情,
【file:/arch/x86/mm/init_32.c】
/*
* We have more RAM than fits into lowmem - we try to put it into
* highmem, also taking the highmem=x boot parameter into account:
*/
static void __init highmem_pfn_init(void)
{
max_low_pfn = MAXMEM_PFN;
if (highmem_pages == -1)
highmem_pages = max_pfn - MAXMEM_PFN;
if (highmem_pages + MAXMEM_PFN < max_pfn)
max_pfn = MAXMEM_PFN + highmem_pages;
if (highmem_pages + MAXMEM_PFN > max_pfn) {
printk(KERN_WARNING MSG_HIGHMEM_TOO_SMALL,
pages_to_mb(max_pfn - MAXMEM_PFN),
pages_to_mb(highmem_pages));
highmem_pages = 0;
}
#ifndef CONFIG_HIGHMEM
/* Maximum memory usable is what is directly addressable */
printk(KERN_WARNING "Warning only %ldMB will be used.\n", MAXMEM>>20);
if (max_pfn > MAX_NONPAE_PFN)
printk(KERN_WARNING "Use a HIGHMEM64G enabled kernel.\n");
else
printk(KERN_WARNING "Use a HIGHMEM enabled kernel.\n");
max_pfn = MAXMEM_PFN;
#else /* !CONFIG_HIGHMEM */
#ifndef CONFIG_HIGHMEM64G
if (max_pfn > MAX_NONPAE_PFN) {
max_pfn = MAX_NONPAE_PFN;
printk(KERN_WARNING MSG_HIGHMEM_TRIMMED);
}
#endif /* !CONFIG_HIGHMEM64G */
#endif /* !CONFIG_HIGHMEM */
}
highmem_pfn_init()看起來很長,貌似很復雜,實際上僅僅是把max_low_pfn設定為MAXMEM_PFN,而highmem_pages設定為max_pfn - MAXMEM_PFN,至于后面的幾乎都是為了防止某些資料過大過小引起翻轉而做的保障性作業,需要說明的是這里的max_low_pfn作為直接映射空間區的記憶體最大可用頁幀號,并不是896M大小記憶體的頁面數,896M只是定義高端記憶體的一個界限,至于直接映射記憶體大小只定義了不超過896M而已,
此外還有一個準備操作,在setup_arch()函式中呼叫的頁表緩沖區申請操作:
early_alloc_pgt_buf():
【file:/arch/x86/mm/init.c】
void __init early_alloc_pgt_buf(void)
{
unsigned long tables = INIT_PGT_BUF_SIZE;
phys_addr_t base;
base = __pa(extend_brk(tables, PAGE_SIZE));
pgt_buf_start = base >> PAGE_SHIFT;
pgt_buf_end = pgt_buf_start;
pgt_buf_top = pgt_buf_start + (tables >> PAGE_SHIFT);
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/38488.html
標籤:嵌入式
