前面简单地介绍了三种不同的地址空间,接下来重点讲述线性地址空间到物理地址空间的映射。
我们先从32位系统开始。
在32位系统中,线性地址空间的大小为 2^32,即4GB。Kernel一般会按照 3:1 的比例,把线性地址空间分为两部分:
- 0~3GB 用户地址空间
- 3GB~4GB 内核地址空间。
用户地址空间的管理和映射是个大的topic。我们后面再详细讲述。
内核地址空间只有1GB大小,最多只能映射1GB的物理内存。那问题来了:1GB之外物理内存怎么办?
Kernel 给出的解决办法是,把1GB的内核地址空间分成两部分:
- 前面的896MB:即 3GB ~ (3GB+896MB),与物理内存的 0~896MB 作直接映射。
- 后面的128MB:即 (3GB+896MB) ~ 4GB,这部分可以动态的映射不同位置的物理内存。
这样,分时复用最后这128MB的线性地址空间,Kernel 就可以访问到全部的物理内存了。正所谓,退一步海阔天空啊。
也正因为如此,大于896MB物理内存,无法直接映射到内核地址空间,通常被称为高端内存。
那内核地址空间中后面的这128MB是如何动态映射不同位置的物理内存的呢?Kernel为此提供了三种机制,如下图所示。
虽然机制不同,但是它们都需要通过修改页表来完成映射。我们先介绍一类特殊的页表:kernel page tables。
我们知道,每个进程都有自己的一套页表,用于完成线性地址到物理地址的转换。而内核也维护了一套页表,称为 kernel page tables.
不过,这套页表有点与众不同。系统初始化完成后,kernel page tables不会被任何用户态或内核态进程直接使用。咦?不被任何进程使用,那它到底有什么用呢?
这套页表的页目录表,即所谓的 master kernel Page Global Directory,其最高的一些页目录项 (128/4 = 32),为进程的页表提供了一个参考模型。如果修改了master kernel Page Global Directory中的这些页目录项,这些改动最终会传播到进程所使用的页表中。那这个“传播”是怎么实现的呢?我们在后面会更详细地讲述。
持久内核映射 (Persistent Kernel Mappings)
持久内核映射可以用来比较持久地把高端内存页面映射到内核地址空间中。既然是映射,那就要涉及到三个元素:集合L,集合P,以及这两个集合之间的映射M。
集合L:内核在那128MB的线性地址空间中预留了一块区域。如上图所示,该区域的范围是 PKMAP_BASE ~ FIXADDR_START。
集合P:高端内存页面。
映射M:这个就是下面要讲的内容。
数据结构
持久内核映射使用了 master kernel page tables 中一个供自己专用的页表: pkmap_page_table.
宏 LAST_PKMAP 定义了该页表中有多少页表项。
41 #ifdef CONFIG_X86_PAE 42 #define LAST_PKMAP 512 43 #else 44 #define LAST_PKMAP 1024 45 #endif
没打开PAE的情况下,该页表中有 1024 页表项,所以持久内核映射一次能够映射 4MB 的高端内存。
整型数组 pkmap_count 用来描述页表 pkmap_page_table 中每个页表项的使用情况。
61 static int pkmap_count[LAST_PKMAP];
- counter 等于 0:对应的页表项没有映射高端内存页面,可以使用。
- counter 等于 1:对应的页表项没有映射高端内存页面,但是却不能被使用,因为它对应的TLB项还没有被刷新。
- counter 大于 1:对应的页表项映射了高端内存页面,且被使用了 (n - 1)次。
为了能够方便的找到一个高端内存页面通过持久内核映射机制所映射到的线性地址,内核使用了一个哈希表:page_address_htable.
该哈希表中每一项为 struct page_address_map:
234 /* 235 * Describes one page->virtual association 236 */ 237 struct page_address_map { 238 struct page *page; 239 void *virtual; 240 struct list_head list; 241 };
在介绍内核如何利用这些数据结构创建持久内核映射之前,我们先看看内核如何查找一个物理页面所映射到的线性地址。
查找
262 void *page_address(struct page *page) 263 { 264 unsigned long flags; 265 void *ret; 266 struct page_address_slot *pas; 267 268 if (!PageHighMem(page)) 269 return lowmem_page_address(page); 270 271 pas = page_slot(page); 272 ret = NULL; 273 spin_lock_irqsave(&pas->lock, flags); 274 if (!list_empty(&pas->lh)) { 275 struct page_address_map *pam; 276 277 list_for_each_entry(pam, &pas->lh, list) { 278 if (pam->page == page) { 279 ret = pam->virtual; 280 goto done; 281 } 282 } 283 } 284 done: 285 spin_unlock_irqrestore(&pas->lock, flags); 286 return ret; 287 }
函数 page_address() 区分两种情况:
1.该页面不属于高端内存。那么该页面位于物理内存中的 0 ~ 896MB,该范围的物理内存页面都是直接映射到内核地址空间中的,所以页面的线性地址总是存在,且可通过页面索引计算出来。
536 static __always_inline void *lowmem_page_address(struct page *page) 537 { 538 return __va(page_to_pfn(page) << PAGE_SHIFT); 539 }
2.该页面属于高端内存。这时就用到前面讲的那个哈希表了。如果在哈希表中找到了该页面,说明该页面已经建立了持久内核映射,则返回其线性地址;否则返回NULL。
映射
建立一个持久内核映射是由函数 kmap() 完成的。
4 void *kmap(struct page *page) 5 { 6 might_sleep(); 7 if (!PageHighMem(page)) 8 return page_address(page); 9 return kmap_high(page); 10 }
如果页面不属于高端内存,则直接返回其对应的线性地址。否则利用函数 kmap_high() 来完成映射操作。
166 void fastcall *kmap_high(struct page *page) 167 { 168 unsigned long vaddr; 169 170 /* 171 * For highmem pages, we can‘t trust "virtual" until 172 * after we have the lock. 173 * 174 * We cannot call this from interrupts, as it may block 175 */ 176 spin_lock(&kmap_lock); 177 vaddr = (unsigned long)page_address(page); 178 if (!vaddr) 179 vaddr = map_new_virtual(page); 180 pkmap_count[PKMAP_NR(vaddr)]++; 181 BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2); 182 spin_unlock(&kmap_lock); 183 return (void*) vaddr; 184 }
该函数首先判断页面是否已经映射。如果没有,则通过函数 map_new_virtual()来建立一个新的映射。
最后递增数组 pkmap_count中的计数值。
116 static inline unsigned long map_new_virtual(struct page *page) 117 { 118 unsigned long vaddr; 119 int count; 120 121 start: 122 count = LAST_PKMAP; 123 /* Find an empty entry */ 124 for (;;) { 125 last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK; 126 if (!last_pkmap_nr) { 127 flush_all_zero_pkmaps(); 128 count = LAST_PKMAP; 129 } 130 if (!pkmap_count[last_pkmap_nr]) 131 break; /* Found a usable entry */ 132 if (--count) 133 continue; 134 135 /* 136 * Sleep for somebody else to unmap their entries 137 */ 138 { 139 DECLARE_WAITQUEUE(wait, current); 140 141 __set_current_state(TASK_UNINTERRUPTIBLE); 142 add_wait_queue(&pkmap_map_wait, &wait); 143 spin_unlock(&kmap_lock); 144 schedule(); 145 remove_wait_queue(&pkmap_map_wait, &wait); 146 spin_lock(&kmap_lock); 147 148 /* Somebody else might have mapped it while we slept */ 149 if (page_address(page)) 150 return (unsigned long)page_address(page); 151 152 /* Re-start */ 153 goto start; 154 } 155 } 156 vaddr = PKMAP_ADDR(last_pkmap_nr); 157 set_pte_at(&init_mm, vaddr, 158 &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot)); 159 160 pkmap_count[last_pkmap_nr] = 1; 161 set_page_address(page, (void *)vaddr); 162 163 return vaddr; 164 }
该函数做了两件事情:
- 通过查找数组 pkmap_count,找到一个可用的页表项。通过该页表项,建立线性地址到物理页面的映射。
- 建立好了映射之后,把该映射关系插入哈希表中,以方便以后的查找操作。
在找可用的页表项时,是从上次的位置 (last_pkmap_nr) 开始的。如果已遍历到了数组pkmap_count的尾部,则从数组头部开始接着遍历。不过接着遍历之前,会先调用函数 flush_all_zero_pkmaps()。该函数专门寻找counter为1的页表项,然后对它们做了四件事情:
- 把 counter 重置为0。
- 清除页表 pkmap_page_table 中相应的页表项。
- 删除哈希表中对应的元素。
- 对属于持久内核映射的线性地址空间中的所有TLB项进行刷新。
如果没能找到可用的页表项,当前进程就会进入睡眠,直到其他进程释放了 pkmap_page_table中的一个页表项。也正因为如此,函数kmap()会阻塞当前进程,不能用在中断上下文中。
如果找到了可用的页表项,则计算出其对应的线性地址,在页表pkmap_page_table对应的页表项中建立映射,把pkmap_count中的counter置为1,最后把该映射关系插入到哈希表中。
解除一个持久内核映射是由函数 kunmap() 完成的。
12 void kunmap(struct page *page) 13 { 14 if (in_interrupt()) 15 BUG(); 16 if (!PageHighMem(page)) 17 return; 18 kunmap_high(page); 19 }
如果页面不属于高端内存,就没啥可做的了。否则,利用函数 kunmap_high() 来解除映射。
188 void fastcall kunmap_high(struct page *page) 189 { 190 unsigned long vaddr; 191 unsigned long nr; 192 int need_wakeup; 193 194 spin_lock(&kmap_lock); 195 vaddr = (unsigned long)page_address(page); 196 BUG_ON(!vaddr); 197 nr = PKMAP_NR(vaddr); 203 need_wakeup = 0; 204 switch (--pkmap_count[nr]) { 205 case 0: 206 BUG(); 207 case 1: 218 need_wakeup = waitqueue_active(&pkmap_map_wait); 219 } 220 spin_unlock(&kmap_lock); 221 222 /* do wake-up, if needed, race-free outside of the spin lock */ 223 if (need_wakeup) 224 wake_up(&pkmap_map_wait); 225 }
该函数的实现非常简单。递减数组 pkmap_count 中对应的counter。如果发现counter为1,则说明其对应的页表项已没人使用,于是唤醒在函数 map_new_virtual() 中由于等待可用页表项而进入睡眠的进程。