前面简单地介绍了三种不同的地址空间,接下来重点讲述线性地址空间到物理地址空间的映射。


我们先从32位系统开始。

在32位系统中,线性地址空间的大小为 2^32,即4GB。Kernel一般会按照 3:1 的比例,把线性地址空间分为两部分:

  • 0~3GB 用户地址空间

  • 3GB~4GB 内核地址空间。

wKiom1W_NS6isOSgAABJiSmORuo084.jpg


用户地址空间的管理和映射是个大的topic。我们后面再详细讲述。


内核地址空间只有1GB大小,最多只能映射1GB的物理内存。那问题来了:1GB之外物理内存怎么办? 

Kernel 给出的解决办法是,把1GB的内核地址空间分成两部分:

  • 前面的896MB:即 3GB ~ (3GB+896MB),与物理内存的 0~896MB 作直接映射。

  • 后面的128MB:即 (3GB+896MB) ~ 4GB,这部分可以动态的映射不同位置的物理内存。


这样,分时复用最后这128MB的线性地址空间,Kernel 就可以访问到全部的物理内存了。正所谓,退一步海阔天空啊。


也正因为如此,大于896MB物理内存,无法直接映射到内核地址空间,通常被称为高端内存。


那内核地址空间中后面的这128MB是如何动态映射不同位置的物理内存的呢?Kernel为此提供了三种机制,如下图所示。


wKiom1W_PXKQDfdtAAHt0J3gQhU569.jpg


虽然机制不同,但是它们都需要通过修改页表来完成映射。我们先介绍一类特殊的页表: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() 中由于等待可用页表项而进入睡眠的进程。