arm的2級頁表在Linux核心建立過程解析
使用者空間/核心空間劃分為2G/2G。
create_mapping:pgd = 0x80007f98, addr = 0xfe700000, phys =0xffff0000, next = 0xfe710000
pte =0x3fffd000, pmdval=0x3fffd801 //這裡的pte已經轉成實體地址了
*pte = 0xffff045f, *(pte+(2048>>2))=0xffff045e
*pte = 0xffff145f, *(pte+(2048>>2))=0xffff145e
*pte = 0xffff245f, *(pte+(2048>>2))=0xffff245e
*pte = 0xffff345f, *(pte+(2048>>2))=0xffff345e
*pte = 0xffff445f, *(pte+(2048>>2))=0xffff445e
*pte = 0xffff545f, *(pte+(2048>>2))=0xffff545e
*pte = 0xffff645f, *(pte+(2048>>2))=0xffff645e
*pte = 0xffff745f, *(pte+(2048>>2))=0xffff745e
*pte = 0xffff845f, *(pte+(2048>>2))=0xffff845e
*pte = 0xffff945f, *(pte+(2048>>2))=0xffff945e
*pte = 0xffffa45f, *(pte+(2048>>2))=0xffffa45e
*pte = 0xffffb45f, *(pte+(2048>>2))=0xffffb45e
*pte = 0xffffc45f, *(pte+(2048>>2))=0xffffc45e
*pte = 0xffffd45f, *(pte+(2048>>2))=0xffffd45e
*pte = 0xffffe45f, *(pte+(2048>>2))=0xffffe45e
*pte = 0xfffff45f, *(pte+(2048>>2))=0xfffff45e
讀取:level 1 頁表
0x00007F98: 3FFFD801 3FFFDC01
讀取: level 2 頁表
0x3FFFD400: FFFF045F FFFF145F FFFF245F FFFF345F
0x3FFFD410: FFFF445F FFFF545F FFFF645F FFFF745F
0x3FFFD420: FFFF845F FFFF945F FFFFA45F FFFFB45F
0x3FFFD430: FFFFC45F FFFFD45F FFFFE45F FFFFF45F /* 這個位置是linux 頁表,也就是所謂的軟體頁表 */
0x3FFFDC00: FFFF045E FFFF145E FFFF245E FFFF345E
0x3FFFDC10: FFFF445E FFFF545E FFFF645E FFFF745E
0x3FFFDC20: FFFF845E FFFF945E FFFFA45E FFFFB45E
0x3FFFDC30: FFFFC45E FFFFD45E FFFFE45E FFFFF45E /* 這個位置是硬體頁表,是給ARM硬體MMU使用的 */
0x3FFFD000: 00000000 00000000 00000000 00000000
0x3FFFD010: 00000000 00000000 00000000 00000000
0x3FFFD020: 00000000 00000000 00000000 00000000
0x3FFFD030: 00000000 00000000 00000000 00000000 /* 這個位置是linux 頁表,也就是所謂的軟體頁表 */
0x3FFFD800: 00000000 00000000 00000000 00000000
0x3FFFD810: 00000000 00000000 00000000 00000000
0x3FFFD820: 00000000 00000000 00000000 00000000
0x3FFFD830: 00000000 00000000 00000000 00000000 /* 這個位置是硬體頁表,是給ARM硬體MMU使用的 */
static inline void __pmd_populate(pmd_t *pmdp, phys_addr_t pte,
pmdval_t prot)
{
pmdval_t pmdval = (pte + PTE_HWTABLE_OFF) | prot;/* HW page table offset is 512*4 = 2048 */
printk("pte =0x%x, pmdval=0x%x\n", pte, pmdval);
pmdp[0] = __pmd(pmdval); //第1個頁表項
#ifndef CONFIG_ARM_LPAE //這個巨集沒有定義,所以1次要填充PGD的8個位元組的頁表
pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t)); //第2個頁表項
#endif
flush_pmd_entry(pmdp);
}
static pte_t * __init early_pte_alloc(pmd_t *pmd, unsigned long addr, unsigned long prot)
{
if (pmd_none(*pmd)) {
pte_t *pte = early_alloc(PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE);
__pmd_populate(pmd, __pa(pte), prot);
}
BUG_ON(pmd_bad(*pmd));
return pte_offset_kernel(pmd, addr);
}
static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,
unsigned long end, unsigned long pfn,
const struct mem_type *type)
{
pte_t *pte = early_pte_alloc(pmd, addr, type->prot_l1);
do {
set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);
printk("*pte = 0x%x, *(pte+(2048>>2))=0x%x\n", *pte, *(pte + (2048>>2)));
pfn++;
} while (pte++, addr += PAGE_SIZE, addr != end);
}
void __init create_mapping(struct map_desc *md)
{
unsigned long addr, length, end;
phys_addr_t phys;
const struct mem_type *type;
pgd_t *pgd;
if (md->virtual != vectors_base() && md->virtual < TASK_SIZE) {
printk(KERN_WARNING "BUG: not creating mapping for 0x%08llx"
" at 0x%08lx in user region\n",
(long long)__pfn_to_phys((u64)md->pfn), md->virtual);
return;
}
if ((md->type == MT_DEVICE || md->type == MT_ROM) &&
md->virtual >= PAGE_OFFSET &&
(md->virtual < VMALLOC_START || md->virtual >= VMALLOC_END)) {
printk(KERN_WARNING "BUG: mapping for 0x%08llx"
" at 0x%08lx out of vmalloc space\n",
(long long)__pfn_to_phys((u64)md->pfn), md->virtual);
}
type = &mem_types[md->type];
#ifndef CONFIG_ARM_LPAE
/*
* Catch 36-bit addresses
*/
if (md->pfn >= 0x100000) {
create_36bit_mapping(md, type);
return;
}
#endif
addr = md->virtual & PAGE_MASK;
phys = __pfn_to_phys(md->pfn);
length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK));
if (type->prot_l1 == 0 && ((addr | phys | length) & ~SECTION_MASK)) {
printk(KERN_WARNING "BUG: map for 0x%08llx at 0x%08lx can not "
"be mapped using pages, ignoring.\n",
(long long)__pfn_to_phys(md->pfn), addr);
return;
}
pgd = pgd_offset_k(addr);
end = addr + length;
do {
unsigned long next = pgd_addr_end(addr, end);
//if(phys == 0xffff0000)
printk("%s:pgd = 0x%x, addr = 0x%x, phys =0x%x, next = 0x%x\n", __func__, (u32)pgd, addr, phys, next);
alloc_init_pud(pgd, addr, next, phys, type);
phys += next - addr;
addr = next;
} while (pgd++, addr != end);
}
一級頁表的內容是:3FFFD801 3FFFDC01
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
按照linux的方式對映:
pgd_index = (0xfe7 >> 1) = 0x7f3
所以0xfe600000和0xfe700000組成的2MB空間是放到一個pgd[2]數組裡的。0xfe6對應的是pgd[0],0xfe7對應的是pgd[1]。
一級頁表的在TTB中的TTB_offset = 0x7f3 * 8 = 0x3f98
所以1級頁表的地址為0x4000+0x3f98 = 0x7f98
所以二級頁表的地址計算過程為:
第1個表項0x3fffd801 ----> 0xfe600000虛擬地址
0x3fffd| 1|000 0000 0001
hex bin bin bin
bit31 bit11 |bit10 bit9 ...... bit2 bit1 bit0
0x3fffd| 1 0 0 0 0 0 0 0 0 0 0 0 即 0x3fffd800 就是上面的硬體二級物理頁表地址。
第2個表項0x3fffdc01 ----> 0xfe700000虛擬地址
0x3fffd| 1|100 0000 0001
hex bin bin bin
bit31 bit11 |bit10 bit9 ...... bit2 bit1 bit0
0x3fffd| 1 1 0 0 0 0 0 0 0 0 0 0 即 0x3fffdc00 就是上面的硬體二級物理頁表地址。
所以從linux頁表角度來看,bit31-bit11為2級頁表的基地址,而bit10-bit2為二級頁表的索引(偏移)。而bit10-bit2來自虛擬地址(MVA)的bit20-bit12。
虛擬地址0xfe600000: bit20-bit12 是0b0 0000 0000
虛擬地址0xfe700000: bit20-bit12 是0b1 0000 0000
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
下面是從硬體角度做頁錶轉換:
虛擬地址0xfe700000
pgd_index = 0xfe7
TTB_offset = 0xfe7 * 4 = 0x3f9c
所以1級頁表的地址為0x4000+0x3f9c = 0x7f9c,所以硬體找到的1級表項內容是3FFFDC01
下面是找2級頁表的過程
0x3fffd| 11|00 0000 0001
hex bin bin bin
bit31 bit10 bit9 ...... bit2 bit1 bit0
0x3fffd| 1 1 |0 0 0 0 0 0 0 0 0 0 即 0x3fffdc00 就是上面的硬體二級物理頁表地址。
假定MMU要訪問第1個1MB空間,即虛擬地址為0xfe60000,即pgd_index = 0xfe6
TTB_offset = 0xfe6 * 4 = 0x3f98
0x3fffd| 10|00 0000 0001
hex bin bin bin
bit31 bit10 bit9 ...... bit2 bit1 bit0
0x3fffd| 1 0 |0 0 0 0 0 0 0 0 0 0 即 0x3fffd800 就是上面的硬體二級物理頁表地址。
bit9...bit2 來自虛擬機器地址的bit19~bit12。
虛擬地址0xfe600000: bit19-bit12 是0b0 0000 0000
虛擬地址0xfe700000: bit19-bit12 是0b0 0000 0000
---------------------------------------------------------------------------------------------------------------------------------------------------
總結:
bit21 bit20
pg_index = 0xfe7-->0b 1111 1110 0 1 1 1 xxxx xxxx xxxx xxxx xxxx
對於linux來說實際是以2MB為單位對映的,pgd一定是8個位元組對齊的,比如這裡的0x7f98而不是0x7f9c。bit21為0則對映到這兩2MB第1MB空間,而bit21為1的話則對映到這2MB第1MB空間,我們舉的這個例子,是對映到第2個1MB空間。我們知道硬體的二級頁表本來是256項,即256*4KB = 1MB空間,現在明顯Linux把它改成512項, 512*4KB = 2MB。所以linux是這樣處理的,通過定義1級pgd為8個位元組,即有兩個表項,2級頁表pte有512個表項,L1的第1個表項,指向L2的前256個表項,L1的第2個表項,指向L2的後256個表項,
總體來看和硬體是保持一致的。
pmdp[0] = __pmd(pmdval); //第1個頁表項, 對應第1個1MB空間 (bit21 = 0)
pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t)); //第2個頁表項, 跳過256個表項,即跳過1MB地址空間,進入到第2個1MB空間 (bit21 = 1)
一次性把L1的pgd的2個表項都填充完畢,linux的對映是以2MB空間為單位對映的。
bit21在linux中只是前256個表項還是後256個表項。
#define PTRS_PER_PTE 512
#define pte_index(addr) (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
最後2級頁表的佈局:
0x3fffd000--> Linux page table offset: 0
256個表項(1st 1MB)
0x3fffd400--> Linux page table
256個表項(2st 1MB)
0x3fffd800--> HW page table offset: 2048
256個表項(1st 1MB)
0x3fffdc00--> HW page table
256個表項(2st 1MB)
=============================arm-linux記憶體頁表建立 ================================
linux的記憶體(正式)頁表是在核心程式碼執行到start_kernel函式後執行paging _init函式建立的,這裡要注意一個事情就是說,這裡paging_init函式可以正常建立記憶體頁表的條件有兩個:
1、 meminfo已初始化:即初始化實體記憶體各個node的各個bank,一般對於小型arm嵌入式裝置,不涉及多個記憶體就是一個node和一個bank;這部分初始化是在paging_init函式前面的對uboot所傳引數的解析中完成的(可在核心的arm_add_memory函式中加入列印資訊驗證);
2、 全域性變數init_mm的程式碼段首尾、資料段首尾四個成員已初始化:在paging_init前面有對這四個成員的初始化,它們規定了核心映象的程式碼段起始、程式碼段結尾、資料段起始、資料段結尾(資料段結尾也是整個核心映象的結尾),給這四個成員賦的地址值都是在vmlinux.lds.S連結指令碼中規定的(即虛擬地址),界定它們的意義在於能夠正確的界定核心映象的執行需要在虛擬地址佔用的空間位置及大小,以利於其他內容在核心空間位置的確定。
Paging_init函式首先呼叫的是build_mem_type_table,這個函式做的事情就是給靜態全域性變數mem_types賦值,這個變數就在本檔案(arch/arm/mm/mmu.c)定義,它的用處就是在create_mapping函式建立對映時配置MMU硬體時需要;build_mem_type_table函式裡面是完全與本arm晶片自身體系結構相關的配置,我還沒完全搞明白。。。後續再補充吧。
接下來呼叫的是sanity_check_meminfo,這個函式主要做兩件事情,首先是確定本裝置實體記憶體的各個node各個bank中到底有沒有高階記憶體,根據是否存在高階記憶體決定每個bank的highmem成員值;然後是對於每個bank的正確性進行檢測;下面分別描述:
由下面程式碼判斷每個實體記憶體bank是否屬於高階記憶體:
if (__va(bank->start) > VMALLOC_MIN || __va(bank->start) < (void *)PAGE_OFFSET)
highmem = 1;
即:該bank的實體記憶體起始虛擬地址大於VMALLOC_MIN,或者小於PAGE_OFFSET;PAGE_OFFSET是核心使用者空間的交界,在這裡定義為0xc0000000也就是arm-linux普遍適用值3G/1G;VMALLOC_MIN就定義在本檔案(arch/arm/mm/mmu.c),如下:
#define VMALLOC_MIN (void *)(VMALLOC_END - vmalloc_reserve)
VMALLOC_END在arch/arm/mach-XXX/include/mach/vmalloc.h檔案中定義,可見是不同arm裝置可以不同的,它標誌著vmalloc區域的結尾在哪裡,這裡定義為3G+768M,如下:
#define VMALLOC_END (PAGE_OFFSET + 0x30000000)
靜態全域性變數vmalloc_reserve定義在本檔案(arch/arm/mm/mmu.c),它是可以由使用者指定的,指示了vmalloc區域的大小,預設值為128M,使用者指定的方式是通過uboot中指定“命令列”引數的vmalloc引數(“vmalloc=”,通過paging_init前的__early_param方式指定)修改核心中該變數的值,這裡採用的就是預設值128M;
用vmalloc區域的結尾(3G+768M)減去該區域的大小(128M)即得到了vmalloc區域的起始,3G + 768M - 128M = 3G + 640M(0xe8000000);
所以,一個bank的實體記憶體屬於高階記憶體的條件是:
1、 起始地址不大於vmalloc區域的起始虛擬地址;
2、 起始地址不小於核心使用者交界的虛擬地址;
當屬於高階記憶體時,該bank的highmem成員將置1。
除了界定實體記憶體bank是否屬於高階記憶體,sanity_check_meminfo函式還對每個實體記憶體bank的正確性進行檢測,這部分個人認為不是重點,主要注意下在存在高階記憶體情況下(程式碼中定義巨集CONFIG_HIGHMEM情況下),若低端記憶體太大(起始位置在VMALLOC_MIN之前,結尾位置超過VMALLOC_MIN),則超過VMALLOC_MIN的部分將被算進另一個bank並且判定為高階記憶體。
接下來呼叫的是prepare_page_table,它的作用是清除在核心程式碼執行到start_kernel之前時建立的大部分臨時記憶體頁表,這裡需要對arm-linux記憶體頁表的機制原理進行理解:
首先一個是,什麼是記憶體頁表,都有哪些屬於記憶體(注意這裡的記憶體是廣義上的記憶體,不單單是實體記憶體)?
具體的說,記憶體頁表,更應該叫記憶體對映,對於有MMU的CPU來說,CPU訪問實體記憶體或某個SOC硬體暫存器,所做的操作並非是直接把它們的實體地址放在CPU的地址匯流排,而是把一個虛擬地址交給MMU,如果MMU硬體存在這個虛擬地址對應的實體地址(這個對映關係就是需要建立的內容,也就是記憶體對映!),那麼它就會把對應的實體地址放在地址總線上。這樣做最大的好處是,避免了軟體程式直接訪問一個不存在的地址導致出現問題 + 使用者程式可使用的“記憶體”很大。
再說哪些東西屬於記憶體,很簡單,不僅僅實體記憶體,所有通過CPU地址匯流排連線的都屬於記憶體,比如SOC的硬體暫存器,這裡有個方法驗證這個道理,函式create_mapping是最終建立記憶體對映的函式,看看它都被哪些函式呼叫:
map_memory_bank:這是為實體記憶體建立記憶體對映
devicemaps_init:這是為中斷向量建立記憶體對映
iotable_init:這是為SOC硬體暫存器建立記憶體對映
就以上三個呼叫需求。
函式create_mapping是最終建立記憶體對映的函式,先不管哪些需求去呼叫這個函式,先看這個函式本身:
這個函式只需要一個引數struct map_desc *md,這個結構體的定義在檔案arch/arm/include/asm/mach/map.h中,只有4個引數非常簡單:
unsigned long virtual; /*虛擬地址起始*/
unsigned long pfn; /*實體地址起始*/
unsigned long length; /*長度*/
unsigned int type; /*說明這個區間所屬的域,以及是否可讀、寫、可快取記憶體等屬性,arm硬體相關*/
第一個引數非常好理解,虛擬地址起始;第二個引數是實體地址起始;第三個引數是要對映的長度,最後一個比較複雜,但實際用到的往往只有MT_MEMORY(代表實體記憶體)、MT_HIGH_VECTORS/MT_LOW_VECTORS(代表中斷向量)、MT_DEVICE(代表硬體IO暫存器),實際含義和arm硬體相關,指的是MMU不同頁表的對映許可權暫可先不關心。
那麼給定這四個引數,create_mapping函式是怎麼建立對映呢?現在必須描述一下arm-linux的分頁機制:
32位的arm晶片的定址能力就是2^32 = 4G,地址範圍即0-0xffffffff,如果按照1M大小為單位進行對映,則4G = 4096 * 1M也就是需要4096個條目,每個條目負責1M大小的地址範圍;比如需要對映一個大小為128M的實體記憶體,那麼就需要填寫128個條目即可;事實上這就是所謂的段式對映或所謂一級對映,使用1MB的粗頁表,優點是佔用條目較少僅4096個條目,每個條目4位元組即每個程序(包括核心程序自己)僅佔用16K空間,但缺點是粒度太大了不利於linux記憶體管理,比如某程序或某核心程式碼(如模組)申請一些較小的空間不足1M,卻也得分配這麼大空間,當申請頻繁時,實體記憶體消耗將很快。這個缺點是必須要克服的,arm-linux肯定要引入二級頁表,但首先要理解這種段式對映或稱為一級對映的頁表是怎麼樣的,理解了一級對映才能理解二級對映,如下:
現在正式看核心程序的頁表建立,先分析道理,再對應原始碼(後面有足量的程式碼註釋),在mm/init-mm.c檔案中,有全域性變數init_mm,如下:
struct mm_struct init_mm = {
.mm_rb = RB_ROOT,
.pgd = swapper_pg_dir,
.mm_users = ATOMIC_INIT(2),
.mm_count = ATOMIC_INIT(1),
.mmap_sem = __RWSEM_INITIALIZER(init_mm.mmap_sem),
.page_table_lock = __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
.mmlist = LIST_HEAD_INIT(init_mm.mmlist),
.cpu_vm_mask = CPU_MASK_ALL,
};
每個程序都有描述自己記憶體使用情況的結構mm_struct,核心程序也不例外,從這個檔案的目錄可見這個變數是不以平臺區分的,現在重點看第二個成員pgd,這是該程序記憶體頁表的虛擬地址,值為swapper_pg_dir,這是個在head.S中定義的變數,值設定為KERNEL_RAM_VADDR - 0x4000 = 0xc0004000,由前面已知段式記憶體頁表的大小為16K,所以核心的記憶體頁表的虛擬地址範圍是[0xc0004000: 0xc0008000]。
核心的記憶體頁表,在原始碼中paging_init函式都映射了什麼?由前面已知,映射了三方面內容,分別是:實體記憶體、中斷向量、硬體IO暫存器,先看實體記憶體的情況,這裡marvell裝置實體記憶體為256M,PHYS_OFFSET為0,
函式呼叫順序是:bootmem_init-> bootmem_init_node-> map_memory_bank,關注函式map_memory_bank,把實體記憶體的引數填充到struct map_desc結構體變數map,並用它呼叫函式create_mapping,正式開始:
1、首先一個判斷(md->virtual != vectors_base() && md->virtual < TASK_SIZE),這是為了防止虛擬地址不是中斷表地址並且在使用者區(0~3G)的情況;然後又是一個判斷((md->type == MT_DEVICE || md->type == MT_ROM) && md->virtual >= PAGE_OFFSET && md->virtual < VMALLOC_END),這是為了防止記憶體型別為IO型或ROM但虛擬地址為低端記憶體申請區(3G~3G + 768MB)的情況;這些判斷暫無需關注;
2、type = &mem_types[md->type];由前面可知這是獲取所對映記憶體區間所屬的域,以及是否可讀、寫、可快取記憶體等屬性,暫無需關注;
3、判斷(md->pfn >= 0x100000)的情況,這個就不要關注了(超過4G的情況);
4、判斷(type->prot_l1 == 0 && ((addr | phys | length) & ~SECTION_MASK))的情況,意思是本區間不為段式對映(type->prot_l1 == 0)但該區間可以按1M對齊((addr | phys | length) & ~SECTION_MASK),記住這是一個原則,能按1M對齊的對映空間就按段式對映,不足1M的空間才需要二級對映;
5、pgd = pgd_offset_k(addr);,一級一級的看這個函式的實現,最後這個函式相當於(init_mm)->pgd + ((addr) >> 21) = (pgd_t *)0xc0004000 + (addr >> 21),這是在找這個虛擬地址在核心程序的頁表中的位置,由前面已知核心程序的頁表從虛擬地址0xc0004000開始,到0xc0008000結束,頁表大小為0x4000,這個範圍標識0-0xffffffff這4G的範圍,現在addr的值為0xc0000000,那麼它在核心程序的頁表中的位置可以算出來是0xc0004000 + 0x4000*3/4 = 0xc0007000,為什麼是3/4?因為0xc0000000在0-0xffffffff這4G的範圍是整好3/4的位置即4G中的最後一個G,addr值為0xc0000000,它右移21位值為0x600,(pgd_t *)0xc0004000 + 0x600 = 0xc0007000,貌似很奇怪為什麼不是0xc0004600?這是因為前面有強制型別轉換(pgd_t *),而這個結構的定義是兩個ulong,所以(pgd_t *)0xc0004000 + 0x600 =(pgd_t *)0xc0004000 + 0x600*8 = 0xc0007000,至於為什麼addr要右移21位,以及為什麼有強制型別轉換(pgd_t *),一會再說先關注段式對映的情況;
6、end = addr + length;,end指的是要對映的虛擬地址的結尾,它的值為0xc0000000 + 0x10000000(256M) = 0xd0000000;
7、至此,要對映的空間的虛擬地址起始值addr為0xc0000000,虛擬地址結尾值end為0xd0000000,長度為0x10000000,實體地址起始值為0,對映型別為變數type的值(暫不關心細節),接下來的do while迴圈是真正映射了:
8、do {
unsigned long next = pgd_addr_end(addr, end);
alloc_init_section(pgd, addr, next, phys, type);
phys += next - addr;
addr = next;
} while (pgd++, addr != end);
第一行的意思是,只要不超過end,就獲得下一個2MB的虛擬起始地址,所以傳給alloc_init_section的next引數,要麼與addr相差一整段(2MB),要麼是end則不足一整段(2MB),我們這裡的記憶體256M,不存在end與addr相差不足2M的情況;
進入函式alloc_init_section,它的引數分別是“一級頁表(段頁表)地址pgd、虛擬起始地址addr、虛擬結尾地址end(現在就是addr + 2MB)、物理起始地址phys、記憶體型別type”;
9、pmd_t *pmd = pmd_offset(pgd, addr);,這個pmd_t結構就只是一個ulong大小了,這裡函式pmd_offset的實現就是pmd = (pmd_t *)pgd,地址不變,但型別轉變,意思很明顯;
10、判斷(((addr | end | phys) & ~SECTION_MASK) == 0),我們這裡的都是2M對齊的,必然1M也對齊,底下的(addr & SECTION_SIZE)對於我們這裡不會成立,我們這裡都是2M對齊,即addr值的第21位一直都會是偶數;
11、下面的內容是配置段式頁表的值和寫頁表:
do {
*pmd = __pmd(phys | type->prot_sect);
phys += SECTION_SIZE;
} while (pmd++, addr += SECTION_SIZE, addr != end);
flush_pmd_entry(p);
第一行,頁表的這個條目pmd,寫入的值是什麼,可見,把實體地址和type的對映方式(prot_sect)寫進去了;
第二行,累加1M的實體地址值;
第三行,只要虛擬地址起始值addr再累加1M,沒有超過end(這裡是addr+2M),那麼寫下一個頁表條目(pmd+1)的值,很明顯,我們這裡會再迴圈一次,即do裡的內容總共執行過兩次;
第四行,最終寫入MMU,這個p指向pmd。
先不要管上面為什麼這麼實現,先看結果,總共256M的實體記憶體,最後結果是:
頁表條目索引 |
條目所在地址 |
頁表填的內容 |
對應的虛擬地址 |
0xfff |
0xc0008000 |
0xffffffff |
|
…… |
…… |
…… |
…… |
0x3fc |
0xc00073fc |
0x10000000+type |
0xd0000000 |
…… |
…… |
…… |
…… |
0xc05 |
0xc0007014 |
0x00500000+type |
0xc0500000 |
0xc04 |
0xc0007010 |
0x00400000+type |
0xc0400000 |
0xc03 |
0xc000700c |
0x00300000+type |
0xc0300000 |
0xc02 |
0xc0007008 |
0x00200000+type |
0xc0200000 |
0xc01 |
0xc0007004 |
0x00100000+type |
0xc0100000 |
0xc00 |
0xc0007000 |
0x00000000+type |
0xc0000000 |
…… |
…… |
…… |
…… |
0x000 |
0xc0004000 |
0x00000000 |
上面就是對實體記憶體對映後的情況(紅色部分),第一列是頁表的索引,256M的實體記憶體的的對映部分在第0xc00到0x3fc部分,對應頁表本身所在地址從0xc0007000到0xc00073fc,這部分頁表填充的內容是實體記憶體和對映型別的或運算結果,它們實際上對應虛擬地址的0xc0000000到0xd0000000。
如果把一個虛擬地址0xc1234567給CPU去訪問,那麼CPU把它發給MMU,MMU會根據已經建立的對映關係發現這個地址對應的是0x012這個段,然後把後面低20位的部分0x34567和0x012拼接起來,結果是0x01234567。
事實上這裡還有很多細節,比如MMU到底是怎麼能夠識別是0x012段的細節,這牽扯到arm硬體體系結構內容,如果不是特殊需要可不特別關心,關注到核心這步即可。
上面是實體記憶體頁表建立的結果,但還有個問題沒有說,就是arm頁表在linux中的融合問題,這部分不理解將影響對全域性的理解:
要知道,linux要實現高效的記憶體管理,是不可能按1M的區間管理的,這樣很容易產生問題,前面說過這個問題,事實上linux是以4K為一頁作為管理單位即粒度,這是怎麼定的呢?在arch/arm/include/asm/page.h檔案中規定巨集PAGE_SHIFT為12導致巨集PAGE_SIZE為4096即4K;
既然是4K為一頁,那麼按理說linux需要描述4G虛擬記憶體的話,需要多少個這樣的4K呢?很簡單:2^20 * 2^12 = 4G,所以是2^20為1M,即頁表的條目個數為1M個,每個條目佔用4位元組,即頁表大小為4M,每個程序包括核心程序都需要一個記憶體頁表,這就大量消耗實體記憶體在頁表上;
所以這裡將引入多級頁表的概念,linux核心定義的標準是這樣的:最高階pgd為頁目錄表,它找到每個程序mm-_struct結構的pgd成員,用它定位到下一級pmd;第二級pmd為中間頁表,它定位到下一級pte;第三級pte是頁表,它就能定位到哪個頁了;最後虛擬地址的最後12位定位的是該頁的偏移量;
為什麼搞的這麼麻煩?因為linux還要適配64位處理器,到那時是真的需要這麼麻煩,因為否則頁表佔用空間太大了,所以核心必須搞的級數多一些。
那麼arm呢,arm體系結構的MMU實際上支援兩級頁表,一級是剛才描述的段式對映即一級對映,再就是支援第二級對映,包括1K、4K、64K的頁實際上使用的是4K頁,這裡就牽扯到arm頁表機制和linux頁表機制融合的問題;這裡記住,arm的第一級頁表條目數為4096個,對於4K頁第二級目錄條目個數為256個,一級二級條目都是每個條目4位元組;
像這種物理的級數支援少的,砍掉中間目錄pmd就可以,從本質看就是函式pmd_offset(pgd, addr)的實現是pgd,即可什麼pmd在arm-linux形同虛設。ARM在linux下二級分頁如下:
虛擬地址——> PGD轉換——> PTE轉換——>實體地址
此外linux的記憶體管理中,有對頁的置屬性為“access”、“dirty”的需求,可是arm的MMU沒有提供這種屬性可以設定;
綜合各種原因,最終arm-linux假裝第一級目錄只有2048個條目,但其實每個條目是2個ulong大小即8位元組,所以最終設定MMU的還是4096個條目,只是每訪問1個pgd條目將可以訪問到2個pte條目,linux為了實現其記憶體管理功能又在後面加上2個對應的假pte表,這個假pte表專門給linux核心程式碼自己用的,不會影響arm硬體(事實上還有一個重要原因是,linux要求pte表長度為4K即一頁),在arch/arm/include/asm/pgtable.h中:
* pgd pte
* | |
* +--------+ +0
* | |-----> +------------+ +0
* +- - - - + +4 | h/w pt 0 |
* | |-----> +------------+ +1024
* +--------+ +8 | h/w pt 1 |
* | | +------------+ +2048
* +- - - - + | Linux pt 0 |
* | | +------------+ +3072
* +--------+ | Linux pt 1 |
* | | +------------+ +4096
這個東西可以看看英文註釋,比較容易懂。
看懂這個就很容易弄懂所有的建立記憶體對映的內容了,像為什麼PTRS_PER_PTE定義為512、為什麼PTRS_PER_PGD是2048、為什麼PGDIR_SHIFT是21等等,下面是完全原始碼註釋:
void __init paging_init(struct machine_desc *mdesc)
{
void *zero_page;
/*根據不同的arm版本,設定不同mem_type*/
build_mem_type_table();
/*判斷各個bank是否有highmem即高階記憶體,根據是否存在高階記憶體置位或復位各個bank的highmem,
同時檢查是否存在某bank的記憶體與vmalloc區域重合現象,如存在則需要去除(通過削減該bank的size的方式)*/
sanity_check_meminfo();
/*清除臨時的記憶體對映(不包括vmalloc_end以上的高階記憶體區域)*/
prepare_page_table();
/*為主(物理)記憶體建立對映;建立bootmem分配器;初始化重要全域性變數*/
bootmem_init();
/*為裝置I/O記憶體建立對映,附帶首先要清除vmalloc_end以上的高階記憶體區域的臨時頁表表項*/
devicemaps_init(mdesc);
/*對於marvell,沒有高階記憶體,無需關心,
對於高階記憶體對映初始化,也是從bootmem獲取頁表表項佔用的空間,
大小為1頁(4K),並在表項中填入表項地址值和屬性內容即可*/
kmap_init();
/*給全域性變數top_pmd賦值,值為0xffff0000在一級頁表中的表項位置,
地址0xffff0000到4G之間為copy_user_page/clear_user_page等函式使用*/
top_pmd = pmd_off_k(0xffff0000);
/*
* allocate the zero page. Note that this always succeeds and
* returns a zeroed result.
*/
/*由bootmem分配器獲取一頁實體記憶體,轉換為虛擬地址後賦給zero_page,
把對應的物理頁地址賦值給empty_zero_page並重新整理該頁*/