內核地址空間中後面這128MB的最後一部分,是固定映射 (fixed mappings)。
固定映射是什麼意思?爲什麼要有固定映射?Kernel源代碼的註釋裏有一句話,可謂一語中的:The point is to have a constant address at compile time, but to set the physical address only in the boot process.
一個固定映射的線性地址是個常量,例如0xffffc000,且該常量在編譯階段就可以確定。不過該常量線性地址所映射的物理地址,則需系統啓動之後才能確定。
從某種意義上說,固定映射的線性地址,與指針變量有相同的作用,但是要比指針變量效率高。原因有二:
解析一個指針變量,要比解析固定映射的線性地址多一次內存訪問,畢竟要先從內存中讀出指針變量的值,而固定映射的線性地址本身就是個常量。
作爲一個好的編程習慣,在使用指針變量之前,一般都會檢查一下指針值。而對於地址常量,就沒必要做這種檢查了。
那都有哪些固定映射的線性地址可用呢?Kernel 定義了一個 enum 列表:
54 enum fixed_addresses { 55 FIX_HOLE, 56 FIX_VDSO, 57 FIX_DBGP_BASE, 58 FIX_EARLYCON_MEM_BASE, 59 #ifdef CONFIG_X86_LOCAL_APIC 60 FIX_APIC_BASE, /* local (CPU) APIC) -- required for SMP or not */ 61 #endif 62 #ifdef CONFIG_X86_IO_APIC 63 FIX_IO_APIC_BASE_0, 64 FIX_IO_APIC_BASE_END = FIX_IO_APIC_BASE_0 + MAX_IO_APICS-1, 65 #endif 66 #ifdef CONFIG_X86_VISWS_APIC 67 FIX_CO_CPU, /* Cobalt timer */ 68 FIX_CO_APIC, /* Cobalt APIC Redirection Table */ 69 FIX_LI_PCIA, /* Lithium PCI Bridge A */ 70 FIX_LI_PCIB, /* Lithium PCI Bridge B */ 71 #endif 72 #ifdef CONFIG_X86_F00F_BUG 73 FIX_F00F_IDT, /* Virtual mapping for IDT */ 74 #endif 75 #ifdef CONFIG_X86_CYCLONE_TIMER 76 FIX_CYCLONE_TIMER, /*cyclone timer register*/ 77 #endif 78 #ifdef CONFIG_HIGHMEM 79 FIX_KMAP_BEGIN, /* reserved pte's for temporary kernel mappings */ 80 FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1, 81 #endif 82 #ifdef CONFIG_ACPI 83 FIX_ACPI_BEGIN, 84 FIX_ACPI_END = FIX_ACPI_BEGIN + FIX_ACPI_PAGES - 1, 85 #endif 86 #ifdef CONFIG_PCI_MMCONFIG 87 FIX_PCIE_MCFG, 88 #endif 89 #ifdef CONFIG_PARAVIRT 90 FIX_PARAVIRT_BOOTMAP, 91 #endif 92 __end_of_permanent_fixed_addresses, 93 /* temporary boot-time mappings, used before ioremap() is functional */ 94 #define NR_FIX_BTMAPS 16 95 FIX_BTMAP_END = __end_of_permanent_fixed_addresses, 96 FIX_BTMAP_BEGIN = FIX_BTMAP_END + NR_FIX_BTMAPS - 1, 97 FIX_WP_TEST, 98 __end_of_fixed_addresses 99 };
這些固定映射的線性地址,都位於4GB線性地址空間的最後部分。函數fix_to_virt()用來把上面列表中的一個地址索引轉換爲線性地址。
133 static __always_inline unsigned long fix_to_virt(const unsigned int idx) 134 { 144 if (idx >= __end_of_fixed_addresses) 145 __this_fixmap_does_not_exist(); 146 147 return __fix_to_virt(idx); 148 }
150 unsigned long __FIXADDR_TOP = 0xfffff000; 116 #define FIXADDR_TOP ((unsigned long)__FIXADDR_TOP) 123 #define __fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT))
這個函數有幾個很有意思的地方。
首先,這是一個內聯函數。編譯器會把該函數的代碼直接插入到調用它的地方。
其次,該函數中沒有變量,使用的都是常量。因此,在編譯階段,編譯器即可判斷144行的if語句成不成立。如果成立,則在編譯階段編譯器就會報錯,因爲函數__this_fixmap_does_not_exist()並沒有在Kernel中定義。如果不成立,則編譯器就會把 144 ~ 145 行直接刪掉。
最後,在編譯階段,該函數就可以計算出最後的線性地址值,假如說是0xffffc000。那麼調用該函數的地方就會用常量 0xffffc000 來替代。
雖然線性地址在編譯時可以確定,但是物理地址卻需要系統運行時來映射。Kernel提供了兩個函數來完成這種映射:set_fixmap(idx, phys) 和 set_fixmap_nocache(idx, phys)。當然,這兩個函數也是通過修改 kernel page tables來完成映射。
臨時內核映射 (Temporary Kernel Mappings)
前面講過,創建持久內核映射的函數kmap()可能會阻塞當前進程,因此不能用在中斷上下文中。於是,Kernel在固定映射的基礎上,提供了另一種映射機制 —— 臨時內核映射。與持久內核映射相比,它更快,而且不會阻塞當前進程,因此可以用在中斷上下文中。不過,它也有一個弱點,就是使用它的代碼不能睡眠。
如果仔細觀察前面的fixed_addresses列表,你會發現,在 79 ~ 80 行,有一組地址索引,這些地址索引從 FIX_KMAP_BEGIN 到 FIX_KMAP_END,共有KM_TYPE_NR*NR_CPUS個。這些索引對應的線性地址,正是臨時內核映射之所在。
這些地址索引具體分佈如下:
10 enum km_type { 11 D(0) KM_BOUNCE_READ, 12 D(1) KM_SKB_SUNRPC_DATA, 13 D(2) KM_SKB_DATA_SOFTIRQ, 14 D(3) KM_USER0, 15 D(4) KM_USER1, 16 D(5) KM_BIO_SRC_IRQ, 17 D(6) KM_BIO_DST_IRQ, 18 D(7) KM_PTE0, 19 D(8) KM_PTE1, 20 D(9) KM_IRQ0, 21 D(10) KM_IRQ1, 22 D(11) KM_SOFTIRQ0, 23 D(12) KM_SOFTIRQ1, 24 D(13) KM_TYPE_NR 25 };
在固定映射的地址索引列表中,每個CPU都有13個這樣的地址索引。每一個地址索引代表一種映射類型。分佈如下所示:
建立臨時內核映射是由函數kmap_atomic()完成的。
49 void *kmap_atomic(struct page *page, enum km_type type) 50 { 51 return kmap_atomic_prot(page, type, kmap_prot); 52 }
29 void *kmap_atomic_prot(struct page *page, enum km_type type, pgprot_t prot) 30 { 31 enum fixed_addresses idx; 32 unsigned long vaddr; 33 34 /* even !CONFIG_PREEMPT needs this, for in_atomic in do_page_fault */ 35 pagefault_disable(); 36 37 if (!PageHighMem(page)) 38 return page_address(page); 39 40 idx = type + KM_TYPE_NR*smp_processor_id(); 41 vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx); 42 BUG_ON(!pte_none(*(kmap_pte-idx))); 43 set_pte(kmap_pte-idx, mk_pte(page, prot)); 44 arch_flush_lazy_mmu_mode(); 45 46 return (void *)vaddr; 47 }
40 ~ 41,根據type和當前CPU,計算出臨時內核映射中的地址索引,然後該索引值加上 FIX_KMAP_BEGIN,得到的便是固定映射中的地址索引。最後通過 __fix_to_virt() 轉換成線性地址。
43行,kmap_pte是kernel page tables中的一個頁表,該頁表初始化爲線性地址fix_to_virt(FIX_KMAP_BEGIN)所對應的頁表。
該函數會關閉內核搶佔,這個和調用該函數的代碼不能睡眠是同樣的原因:如果建立了臨時內核映射的進程被調度出去,另一個進程可能會創建相同類型的臨時內核映射,這樣就把之前的映射給覆蓋了。
解除臨時內核映射是由函數kunmap_atomic()完成的。
54 void kunmap_atomic(void *kvaddr, enum km_type type) 55 { 56 unsigned long vaddr = (unsigned long) kvaddr & PAGE_MASK; 57 enum fixed_addresses idx = type + KM_TYPE_NR*smp_processor_id(); 58 59 /* 60 * Force other mappings to Oops if they'll try to access this pte 61 * without first remap it. Keeping stale mappings around is a bad idea 62 * also, in case the page changes cacheability attributes or becomes 63 * a protected page in a hypervisor. 64 */ 65 if (vaddr == __fix_to_virt(FIX_KMAP_BEGIN+idx)) 66 kpte_clear_flush(kmap_pte-idx, vaddr); 67 else { 68 #ifdef CONFIG_DEBUG_HIGHMEM 69 BUG_ON(vaddr < PAGE_OFFSET); 70 BUG_ON(vaddr >= (unsigned long)high_memory); 71 #endif 72 } 73 74 arch_flush_lazy_mmu_mode(); 75 pagefault_enable(); 76 }
如果線性地址確實是對應於type的臨時內核映射地址,則通過修改頁表來解除映射。
最後該函數會遞減當前進程的preempt_count,並檢查是否有pending的調度請求。