对 于高端内存,可以通过 alloc_page() 或者其它函数获得对应的 page,但是要想访问实际物理内存,还得把 page 转为线性地址才行(为什么?想想 MMU 是如何访问物理内存的),也就是说,我们需要为高端内存对应的 page 找一个线性空间,这个过程称为高端内存映射。
对应高端内存的3部分,高端内存映射有三种方式:
映射到”内核动态映射空间”(noncontiguous memory allocation)
这种方式很简单,因为通过 vmalloc() ,在”内核动态映射空间”申请内存的时候,就可能从高端内存获得页面(参看 vmalloc 的实现),因此说高端内存有可能映射到”内核动态映射空间”中。
持久内核映射(permanent kernel mapping)
如果是通过 alloc_page() 获得了高端内存对应的 page,如何给它找个线性空间?
内核专门为此留出一块线性空间,从 PKMAP_BASE 到 FIXADDR_START ,用于映射高端内存。在 2.6内核上,这个地址范围是 4G-8M 到 4G-4M 之间。这个空间起叫”内核永久映射空间”或者”永久内核映射空间”。这个空间和其它空间使用同样的页目录表,对于内核来说,就是 swapper_pg_dir,对普通进程来说,通过 CR3 寄存器指向。通常情况下,这个空间是 4M 大小,因此仅仅需要一个页表即可(注意理解这句话:一个页表(不是页表项),大小为4K,可以映射4M的空间),内核通过来 pkmap_page_table 寻找这个页表。通过 kmap(),可以把一个 page 映射到这个空间来。由于这个空间是 4M 大小,最多能同时映射 1024 个 page。因此,对于不使用的的 page,及应该时从这个空间释放掉(也就是解除映射关系),通过 kunmap() ,可以把一个 page 对应的线性地址从这个空间释放出来。
临时映射(temporary kernel mapping)
内核在 FIXADDR_START 到 FIXADDR_TOP 之间保留了一些线性空间用于特殊需求。这个空间称为”固定映射空间”在这个空间中,有一部分用于高端内存的临时映射。
这块空间具有如下特点:
(1)每个 CPU 占用一块空间
(2)在每个 CPU 占用的那块空间中,又分为多个小空间,每个小空间大小是 1 个 page,每个小空间用于一个目的,这些目的定义在 kmap_types.h 中的 km_type 中。
当要进行一次临时映射的时候,需要指定映射的目的,根据映射目的,可以找到对应的小空间,然后把这个空间的地址作为映射地址。这意味着一次临时映射会导致以前的映射被覆盖。通过 kmap_atomic() 可实现临时映射。
896M边界以上的页框并不映射在内核线性地址空间的第4个GB,因此内核不能直接访问它们。所以,返回所分配页框线性地址的页分配器函数并不对高端内存可用。
在64位平台上不存在这个问题,因为可以使用的线性地址空间大于能安装的RAM,也就是说这些体系结构的ZONE_HIGHMEM是空的。linux使用如下方法来使用高端内存:
1)高端内存页框的分配只能通过alloc_pages( )函数和它的快捷函数alloc_page( )。这些函数不返回线性地址,而是返回第一分配页框的页描述符的线性地址。
2)没有线性地址的高端内存中的页框不能被内核访问。
内核采用三种不同的机制将页框映射到高端内存:永久内核映射、临时内核映射、非连续内存分配。本节讨论前两种。
建立永久内核映射可能阻塞当前进程;也就是高端内存上没有页表项可以用作页框的窗口的时候。因此,这种方法不能用在中断处理函数和可延迟函数。临时内核映射不会阻塞当前进程,但是只有很少的临时内核映射可以建立起来。
需要注意的是,无论哪种方法,128M的线性地址用于高端内存映射,无法保证寻址范围同时到达的物理内存。
2.永久内核映射:注意,下列多有函数应用的范围是内核空间
宏定义与关键变量定义:
pkmap_page_table:高端内存主内核页表中,一个用于永久内核映射的专用页表锁在的地址
LAST_PKMAP: 上述页表所含有的表项(512或者1024)
PKMAP_BASE:该页表所映射线性地址的start地址
pkmap_count:对页表项提供计数器的数组
page_address_htable:散列表,用于记录高端页框与永久内核映射的线性地址之间的关系
page_address_map:一个数据结构,包含指向页描述符的指针和分配给页框的线性地址;用于为高端内存的每个页框提供当前映射,它被包含在page_address_htable这个hansh表中
关键函数:
page_address( page):返回页框对应的线性地址
Void * kmap(struct page * page):返回对应page的线性地址
Void * kmap_high(struct page * page): 同上,不过接受的参数是高端内存的页框描述符
map_new_virtual( ):插入页框的物理地址到pkmap_page_table,在page_address_htable散列表中加入一个元素
它是高端页框到内核地址空间的长期映射。使用主内核页表中的一个专门页表,地址存放在pkmap_page_table变量中。页表中的表项数由LAST_PKMAP宏产生。页表照样包含512或者1024项,这取决于PAE是否激活,因此,内核一次访问最多2M或者4M的高端内存。
该页表映射的线性地址从PKMAP_BASE开始,pkmap_count数组包涵LAST_PKMAP个计数器,pkmap_page_table页表中的每一个项都有一个。我们区分下列三种情况
计数器为0:对应页表项没有映射任何的高端内存页框,并且是可用的。
计数器为1:对应的页表项没有映射任何内存页框,但是它不可用,因为从它最后一次使用以来,对应的TLB表项还未被刷新。
计数器为n:相应的页表项映射一个高端内存页框,这意味着正好有n-1个内核成分在使用这个页框。
当分配项的值等于0时为自由项,等于1时为缓冲项,大于1时为映射项。映射页面的分配基于分配表的扫描,当所有的自由项都用完时,系统将清除所有的缓冲项,如果连缓冲项都用完时,系统将进入等待状态。
为了记录高端内存页框与永久内核映射的线性地址之间的联系,内核使用了page_address_htable散列表。该表包含一个page_address_map数据结构,用于为高端内存的每个页框进行当前映射。而该数据结构还包涵一个指向页描述符号的指针和分配给该页框的线性地址。
对应数据结构关系图如下:
page_address()函数返回页框对应的线性地址,如果页框在高端内存中并没有被映射,则返回NULL。这个函数接受一个页描述符指针page作为参数,并区分以下两种情况:
1)页框不在高端内存中:
__va( ( unsigned long) (page - meme_map) << 12)
2) 页框在高端内存中,该函数就得到page_address_htable中寻找。如果在散列表中找到页框,page_address()就返回它的线性地址,否则就返回NULL。
代码实现如下:
1.void *kmap(struct page *page)
2.{
3.might_sleep();
4.if (!PageHighMem(page))
5.return page_address(page);
6.return kmap_high(page);
7.}
如果页框确实属于高端内存,那么调用kmap_high()函数如下:
01./* We cannot call this from interrupts, as it may block.
02.*/
03.void *kmap_high(struct page *page)
04.{
05.unsigned long vaddr;
06.
07./*
08.* For highmem pages, we can't trust "virtual" until
09.* after we have the lock.
10.*/
11.lock_kmap();
12.vaddr = (unsigned long)page_address(page);//检查页框是否已经被映射
13.if (!vaddr)//没有被映射
14.vaddr = map_new_virtual(page);//将页框的物理地址插入到pkmap_page_table并在pkmapa_address_table散列表中加入一个元素
15.pkmap_count[PKMAP_NR(vaddr)]++;//页框的线性地址对应的计数器+1
16.BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
17.unlock_kmap();
18.return (void*) vaddr;
19.}
其中的一些宏定义内容如下:
1.#define PKMAP_BASE (PAGE_OFFSET - PMD_SIZE)
2.#define LAST_PKMAP PTRS_PER_PTE
3.#define LAST_PKMAP_MASK (LAST_PKMAP - 1)
4.#define PKMAP_NR(virt) (((virt) - PKMAP_BASE) >> PAGE_SHIFT)
5.#define PKMAP_ADDR(nr) (PKMAP_BASE + ((nr) << PAGE_SHIFT))
map_new_virtual( )函数本质上是两个嵌套循环,完成的工作是:插入物理地址到hashtable和在对应hashtable中增加一个元素,代码如下:
01.static inline unsigned long map_new_virtual(struct page *page)
02.{
03.unsigned long vaddr;
04.int count;
05.
06.start:
07.count = LAST_PKMAP;//固定映射的页表项个数
08./* Find an empty entry */
09.for (;;) {
10.last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;//与掩码进行按位与运算,避免数据过长造成的溢出
11.if (!last_pkmap_nr) {//last_pkmap_nr==0,说明它原来已经到达最大值(注意与运算)
12.flush_all_zero_pkmaps();
13.count = LAST_PKMAP;
14.}
15.if (!pkmap_count[last_pkmap_nr])
16.break; /* Found a usable entry */
17.if (--count)
18.continue;
19.
20./*
21.* Sleep for somebody else to unmap their entries
22.*/
23.{
24.DECLARE_WAITQUEUE(wait, current);
25.
26.__set_current_state(TASK_UNINTERRUPTIBLE);
27.add_wait_queue(&pkmap_map_wait, &wait);
28.unlock_kmap();
29.schedule();
30.remove_wait_queue(&pkmap_map_wait, &wait);
31.lock_kmap();
32./* Somebody else might have mapped it while we slept */
33.if (page_address(page))
34.return (unsigned long)page_address(page);
35.
36./* Re-start */
37.goto start;
38.}
39.}
40.vaddr = PKMAP_ADDR(last_pkmap_nr);
41.set_pte_at(&init_mm, vaddr,
42.&(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));
43.
44.pkmap_count[last_pkmap_nr] = 1;
45.set_page_address(page, (void *)vaddr);
46.
47.return vaddr;
48.}
然后,kunmap()函数撤销原来有kmap()建立的永久内核映射。如果页处在高端内存,调用kunmap_high()函数。代码如下:
01.250 void kunmap_high(struct page *page)
02.251 {
03.252 unsigned long vaddr;
04.253 unsigned long nr;
05.254 unsigned long flags;
06.255 int need_wakeup;
07.256
08.257 lock_kmap_any(flags);
09.258 vaddr = (unsigned long)page_address(page);
10.259 BUG_ON(!vaddr);//嵌入式汇编有关的bug处理
11.260 nr = PKMAP_NR(vaddr);//(((virt) - PKMAP_BASE) >> PAGE_SHIFT)页号
12.261
13.262 /*
14.263 * A count must never go down to zero
15.264 * without a TLB flush!
16.265 */
17.266 need_wakeup = 0;
18.267 switch (--pkmap_count[nr]) {
19.268 case 0:
20.269 BUG();
21.270 case 1://没有进程在使用页
22.271 /*
23.272 * Avoid an unnecessary wake_up() function call.
24.273 * The common case is pkmap_count[] == 1, but
25.274 * no waiters.
26.275 * The tasks queued in the wait-queue are guarded
27.276 * by both the lock in the wait-queue-head and by
28.277 * the kmap_lock. As the kmap_lock is held here,
29.278 * no need for the wait-queue-head's lock. Simply
30.279 * test if the queue is empty.
31.280 */
32.281 need_wakeup = waitqueue_active(&pkmap_map_wait);//唤醒
33.282 }
34.283 unlock_kmap_any(flags);
35.284
36.285 /* do wake-up, if needed, race-free outside of the spin lock */
37.286 if (need_wakeup)
38.287 wake_up(&pkmap_map_wait);//唤醒由map_new_virtual()添加在等待队列中的进程
39.288 }
40.289
3.临时内核映射:和进程控制有关
临时内核映射实现简单,可以用在中断处理程序和可延迟函数的内部(这些函数不能被阻塞),因为临时内核映射从来不阻塞当前进程,因为它被设计成是原子的。对比永久内核映射,发现如果页框暂时没有空闲的虚拟地址可以映射,那么永久内核映射将要被阻塞。
建立临时内核映射禁用内核抢占,这是必须的,因为映射对于每个处理器都是独特的,如果没有禁用抢占,那么哪个任务在哪个CPU上运行是不确定的。(这一段需要结合进程管理加以理解)
撤销临时内核映射的函数实际上可以不进行任何实质性的操作,它仅仅允许内核抢占即可(这样新的进程被调度,可以直接使用临时内核映射区域,覆盖原来的映射关系)。
每个CPU都有它自己的包含13个窗口的集合,它们用enum km_type数据结构表示。该数据结构定义的每个符号,标识了一个窗口的线性地址。
01.7 enum km_type {
02.8 KM_BOUNCE_READ,
03.9 KM_SKB_SUNRPC_DATA,
04.10 KM_SKB_DATA_SOFTIRQ,
05.11 KM_USER0,
06.12 KM_USER1,
07.13 KM_BIO_SRC_IRQ,
08.14 KM_BIO_DST_IRQ,
09.15 KM_PTE0,
10.16 KM_PTE1,
11.17 KM_IRQ0,
12.18 KM_IRQ1,
13.19 KM_SOFTIRQ0,
14.20 KM_SOFTIRQ1,
15.21 KM_L1_CACHE,
16.22 KM_L2_CACHE,
17.23 KM_TYPE_NR
18.24 };
其中,内核要确保同一个窗口永远不会被两个不同的控制路径同时使用。最后一个符号非线性地址,但由每个CPU用来产生不同的可用窗口数。
km_type的每一个符号都是固定映射的线性地址的一个下标。enum fixed_addresses数据结构包含符号FIX——KMAP——BEGIN和FIX_KMP_END;把后者的值赋成下标FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1。在这种方式下,系统中的每个CPU有KM-TYPE-NR个固定映射的线性地址。此外,内核用fix_to
_virt(FIX_KMAP_BEGIN )线性地址对应的页表项的地址初始化kmap_pte变量。
01.39 void *kmap_atomic(struct page *page, enum km_type type)
02.40 {
03.41 unsigned int idx;
04.42 unsigned long vaddr;
05.43 void *kmap;
06.44
07.45 pagefault_disable();//有关锁和内核抢占机制
08.46 if (!PageHighMem(page))
09.47 return page_address(page);
10.48
11.49 debug_kmap_atomic(type);//debug点
12.50
13.51 kmap = kmap_high_get(page);//类似kmap_high的功能,只有这个函数返回非空指针,才可以调用kmap_high()
14.52 if (kmap)
15.53 return kmap;
16.54
17.55 idx = type + KM_TYPE_NR * smp_processor_id();//指明需要使用的线性地址
18.56 vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);//固定映射的线性地址转化成虚拟地址
19.57 #ifdef CONFIG_DEBUG_HIGHMEM
20.58 /*
21.59 * With debugging enabled, kunmap_atomic forces that entry to 0.
22.60 * Make sure it was indeed properly unmapped.
23.61 */
24.62 BUG_ON(!pte_none(*(TOP_PTE(vaddr))));
25.63 #endif
26.64 set_pte_ext(TOP_PTE(vaddr), mk_pte(page, kmap_prot), 0);//设置页表项:线性地址,page 页框信息
27.65 /*
28.66 * When debugging is off, kunmap_atomic leaves the previous mapping
29.67 * in place, so this TLB flush ensures the TLB is updated with the
30.68 * new mapping.
31.69 */
32.70 local_flush_tlb_kernel_page(vaddr);//刷新TLB无效
33.71
34.72 return (void *)vaddr;
35.73 }
01.#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)
02.68 #define cpu_set_pte_ext(ptep,pte,ext) processor.set_pte_ext(ptep,pte,ext)
03.
04.6 #define TOP_PTE(x) pte_offset_kernel(top_pmd, x)
05.311 /* Find an entry in the third-level page table.. */
06.312 extern inline pte_t * pte_offset_kernel(pmd_t * dir, unsigned long address)
07.313 {
08.314 pte_t *ret = (pte_t *) pmd_page_vaddr(*dir)
09.315 + ((address >> PAGE_SHIFT) & (PTRS_PER_PAGE - 1));
10.316 smp_read_barrier_depends(); /* see above */
11.317 return ret;
12.318 }
1.#define mk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot))