内核地址空间中后面这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的调度请求。
转载于:https://blog.51cto.com/richardguo/1682653