定义
永久内核映射允许内核建立高端页框到内核地址空间的长期映射。
内核线性地址空间的最后128MB中的一部分专门用于映射高端内存页框,通过重复使用线性地址,使得整个高端内存能够在不同的时间被访问。
设计
需求:
从分配的页框中,从一组线性地址范围中,得到一个线性地址,以便后续操作。
-
提供一组可供选择的线性地址。
-
需要一个向量数组,每个数组元素的值标记着每个页表项的使用情况。
-
需要一个结构体包含线性地址和物理页地址组成一个节点,然后挂在一个链表中。
这个情景中有1024项,如果在一个链表中遍历1024个项,这比较耗时。可以把1024项目分成256个链表或者128个链表,这样每个链表只有4个项或者8个项,这样遍历时间比较短,可以通过hasb把物理页地址转化为索引,然后通过索引到相应的链表中进行遍历。 -
分配页的时候,先查询下是否存在物理页对应的线性地址,如果存在则返回线性地址,同时设置变量的引用。
. 如果不存在对应的线性地址。
1)首先找一个空闲的页表项
2)把物理页的物理地址和权限写进页表项
3) 把线性地址和物理地址组成的节点挂在相应的链表中。
4) 同时更新页表项的使用情况,设置为1.
- 对外接口
1)kmap_high
2)kunmap_high
1. 永久映射线性地址PKMAP_BASE
1.1 范围``````
#define PKMAP_BASE ( (FIXADDR_BOOT_START - PAGE_SIZE*(LAST_PKMAP + 1)) & PMD_MASK )
#define FIXADDR_BOOT_START (FIXADDR_TOP - __FIXADDR_BOOT_SIZE)
#define __FIXADDR_BOOT_SIZE (__end_of_fixed_addresses << PAGE_SHIFT)
在本机的调试环境中:
__FIXADDR_BOOT_SIZE=0x2e000
FIXADDR_BOOT_START=0xfffd1000
PKMAP_BASE=ff800000
PAGE_SHIFT=4K
FIXADDR_TOP=0xffff f000
PMD_MASK = 00 0000 0000 0000 0000 0000
说明:
1) __FIXADDR_BOOT_SIZE的大小就是固定线性地址映射fixed_addresses的枚举变量值。
2) FIXADDR_BOOT_START就是固定线性映射的最低地址。
3) PKMAP_BASE就是从固定映射的前4M开始(也就是跳过固定映射的前一个页目录项)。
PKMAP_BASE地址是0xff80 0000
1.2 接口
#define PKMAP_ADDR(nr) (PKMAP_BASE + ((nr) << PAGE_SHIFT))
通过参数nr,指向第nr个页表项。
2 页表项的使用情况
2.1 pkmap_count
pkmap_count数组包含LAST_PKMAP个计数器,pkmap_page_table指向的页表中的每一项都有一个。pkmap_count数组元素的值有下面三种情况:
0: 对应的页表项没有映射任何高端内存页框,并且是可用的。
1:
逻辑上线性地址和物理地址没有映射(但是页表项中的内容有物理地址),但是它不能使用,页表项中有物理映射因为相应的TLB还未刷新。
如果线性地址和物理地址没有映射,在调用kmap_high设置pkmap_count为2(kmap_high中调用map_new_virtual设置为1,然后再设为2),下一次对 同样的page调用kmap_high,则给pkmap_count相应元素加1,则设置为3.
如果对这个页调用两次kunmap_high,则pkmap_count对应的数值为1。就会出现这样的情况。
n:
相应的页表映射一个高端内存页框,这意味着正好有n-1个内核成分在使用这个页框。
2.2 pkmap_count值的增加
- kmap_high中调用map_new_virtual设置pkmap_count的值为1,在kmap_high里又增加了pkmap_count的值。
- 再次对同样的页描述符调用kmap_high,则再pkmap_count的值增加1.
2.3 pkmap_count值的减少
- 调用kunmap_high函数pkmap_count相应的值减少1
- 调用flush_all_zero_pkmaps函数把pkmap_count的值从1减少为0.
2.4 pkmap_count值清零
在函数flush_all_zero_pkmaps(void)中pkmap_count值清0
保存已映射的地址的链表
3.1 设计
这个情景中有1024项,如果在一个链表中遍历1024个项,这比较耗时。可以把1024项目分成256个链表或者128个链表,这样每个链表只有4个项或者8个项,这样遍历时间比较短,可以通过hasb把物理页地址转化为索引,然后通过索引到相应的链表中进行遍历。也就是hash桶。
3.2 实现
507 /*
508 * Hash table bucket
509 */
510 static struct page_address_slot {
511 struct list_head lh; /* List of page_address_maps */
512 spinlock_t lock; /* Protect this bucket's list */
513 } ____cacheline_aligned_in_smp page_address_htable[1<<PA_HASH_ORDER];
514
515 static struct page_address_slot *page_slot(struct page *page)
516 {
517 return &page_address_htable[hash_ptr(page, PA_HASH_ORDER)];
518 }
定义了page_address_htable,因为PA_HASH_ORDER=7,所以数组page_address_htable个数是256个,永久映射的页表项数是1024,如果页表项都映射的话,则page_address_htable每个元素不一定挂4个元素。
3.3 节点
挂在链表上的节点的数据结构,至少包含,虚拟地址和物理页地址,链表。定义的结构如下:
493 struct page_address_map {
494 struct page *page;
495 void *virtual;
496 struct list_head list;
497 };
3.4 预分配节点链表
为了方便节点使用,可以把LAST_KMAP个数据节点,然后加入到可用链表中,需要使用时,取出一个节点,不需要使用时候,把这个节点返回可用链表。相当于一个节点池中。
具体实现:
:page_address_maps是一个数组,这个数组元素包含着存page的地址和虚拟地址,把这个数组存放到page_address_pool链表队列中,在散列表page_address_htable需要使用数组元素时候,就从page_address_pool中取出一个元素;不需要的时候就把这个数组元素从page_address_htable删除,然后添加到page_address_pool中。
LAST_PKMAP是1024。
static struct page_address_map page_address_maps[LAST_PKMAP];
static struct list_head page_address_pool; /* freelist */
for (i = 0; i < ARRAY_SIZE(page_address_maps); i++)
list_add(&page_address_maps[i].list, &page_address_pool);
4.物理页与线性地址映射
4.1 物理页映射查询
void *page_address(struct page *page)
{
unsigned long flags;
void *ret;
struct page_address_slot *pas;
if (!PageHighMem(page))
return lowmem_page_address(page);
pas = page_slot(page);
ret = NULL;
spin_lock_irqsave(&pas->lock, flags);
if (!list_empty(&pas->lh)) {
struct page_address_map *pam;
list_for_each_entry(pam, &pas->lh, list) {
if (pam->page == page) {
ret = pam->virtual;
goto done;
}
}
}
done:
spin_unlock_irqrestore(&pas->lock, flags);
return ret;
}
- 先判断是低端页,还是高端页,如果是低端页,直接通过函数lowmem_page_address返回线性地址。
- 如果是高端页,通过对page进行hash,然后确定如果存在,应该在那个链表中。
- 然后在链表中,查询是否存在这个页映射,如果存在则返回相应的线性地址,如果不存在则返回NULL。
4.2 创建页映射函数
100 static inline unsigned long map_new_virtual(struct page *page)
101 {
102 unsigned long vaddr;
103 int count;
104
105 start:
106 count = LAST_PKMAP;
107 /* Find an empty entry */
108 for (;;) {
109 last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;
110 if (!last_pkmap_nr) {
111 flush_all_zero_pkmaps();
112 count = LAST_PKMAP;
113 }
114 if (!pkmap_count[last_pkmap_nr])
115 break; /* Found a usable entry */
116 if (--count)
117 continue;
118
119 /*
120 * Sleep for somebody else to unmap their entries
121 */
122 {
123 DECLARE_WAITQUEUE(wait, current);
124
125 __set_current_state(TASK_UNINTERRUPTIBLE);
126 add_wait_queue(&pkmap_map_wait, &wait);
127 spin_unlock(&kmap_lock);
128 schedule();
129 remove_wait_queue(&pkmap_map_wait, &wait);
130 spin_lock(&kmap_lock);
131
132 /* Somebody else might have mapped it while we slept */
133 if (page_address(page))
134 return (unsigned long)page_address(page);
135
136 /* Re-start */
137 goto start;
138 }
139 }
140 vaddr = PKMAP_ADDR(last_pkmap_nr);
141 set_pte(&(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));
142
143 pkmap_count[last_pkmap_nr] = 1;
144 set_page_address(page, (void *)vaddr);
145
146 return vaddr;
147 }
逻辑:
- 在121行,通过判断pkmap_count[last_pkmap_nr]的值是否为0,判断是否找到空闲线性地址入口。
- 如果找到,直接到140行,主要做了如下工作:
1) 根据索引值last_pkmap_nr转换为线性地址
2) 根据线性地址,把物理地址和属性写进线性地址对应的页表中。
3) 修改索引对应pkmap_count元素的值为1.
4) 调用set_page_address函数。 - 如果未找到空闲地址,则在128行通过schedule调度出去,直到其他线性地址释放,则会带来一个申请线性地址,申请不到,则会阻塞问题。
释放永久地址是在k
172 void fastcall kunmap_high(struct page *page)
173 {
174 unsigned long vaddr;
175 unsigned long nr;
176 int need_wakeup;
177
178 spin_lock(&kmap_lock);
179 vaddr = (unsigned long)page_address(page);
180 if (!vaddr)
181 BUG();
182 nr = PKMAP_NR(vaddr);
183
184 /*
185 * A count must never go down to zero
186 * without a TLB flush!
187 */
188 need_wakeup = 0;
189 switch (--pkmap_count[nr]) {
188 need_wakeup = 0;
189 switch (--pkmap_count[nr]) {
190 case 0:
191 BUG();
192 case 1:
193 /*
194 * Avoid an unnecessary wake_up() function call.
195 * The common case is pkmap_count[] == 1, but
196 * no waiters.
197 * The tasks queued in the wait-queue are guarded
198 * by both the lock in the wait-queue-head and by
199 * the kmap_lock. As the kmap_lock is held here,
200 * no need for the wait-queue-head's lock. Simply
201 * test if the queue is empty.
202 */
203 need_wakeup = waitqueue_active(&pkmap_map_wait);
204 }
205 spin_unlock(&kmap_lock);
206
207 /* do wake-up, if needed, race-free outside of the spin lock */
208 if (need_wakeup)
209 wake_up(&pkmap_map_wait);
210 }
如果pkmap_count的值为2,减少1,则为1,在203行neee_wakeup=1,则在209行wake_up变量pkmap_map_wait,则函数map_new_virtual从129重新执行。
4.3 设置page的虚拟地址
547 void set_page_address(struct page *page, void *virtual)
548 {
549 unsigned long flags;
550 struct page_address_slot *pas;
551 struct page_address_map *pam;
552
553 BUG_ON(!PageHighMem(page));
554
555 pas = page_slot(page);
556 if (virtual) { /* Add */
557 BUG_ON(list_empty(&page_address_pool));
558
559 spin_lock_irqsave(&pool_lock, flags);
560 pam = list_entry(page_address_pool.next,
561 struct page_address_map, list);
562 list_del(&pam->list);
563 spin_unlock_irqrestore(&pool_lock, flags);
564
565 pam->page = page;
566 pam->virtual = virtual;
567
568 spin_lock_irqsave(&pas->lock, flags);
569 list_add_tail(&pam->list, &pas->lh);
570 spin_unlock_irqrestore(&pas->lock, flags);
571 } else { /* Remove */
572 spin_lock_irqsave(&pas->lock, flags);
573 list_for_each_entry(pam, &pas->lh, list) {
574 if (pam->page == page) {
575 list_del(&pam->list);
576 spin_unlock_irqrestore(&pas->lock, flags);
577 spin_lock_irqsave(&pool_lock, flags);
578 list_add_tail(&pam->list, &page_address_pool);
579 spin_unlock_irqrestore(&pool_lock, flags);
580 goto done;
581 }
582 }
583 spin_unlock_irqrestore(&pas->lock, flags);
584 }
585 done:
586 return;
587 }
这个函数提供两个流程,如果vaddr=NULL,则删除从hash链表中删除,有其对应page的节点。
如果vddr不是NULL,则page_address_pool取出一个可用节点,填充page和vaddr数值,然后添加到hash桶链表中。
5 对外接口
5.1 kmap函数
3 void *kmap(struct page *page)
4 {
5 might_sleep();
6 if (!PageHighMem(page))
7 return page_address(page);
8 return kmap_high(page);
9 }
10
5.2 kunmap
11 void kunmap(struct page *page)
12 {
13 if (in_interrupt())
14 BUG();
15 if (!PageHighMem(page))
16 return;
17 kunmap_high(page);
18 }
6. 图解过程
kmap_init
假设,KMAP_BASE=0xff800000,没执行kmap_init,页目录如下图:
执行kmap_init之后,假设分配的页物理地址是0x44b000,属性是0x67,因为KMAP_BASE=0xff800000,修改页目录第0x3fe项。
主要做了如下工作:
1)给KMAP_BASE对应页目录项分配页表,然后把页表物理地址保存到页目录项中。
2)pkmap_page_table指向页表的基地址。
说明:浅绿色框存放的是物理地址,红色框中存的地址是虚拟地址。
使用情景:
kmap(page)
1.在函数page_address中查询映射的page是否有虚拟地址映射。
具体操作:对page=0xc1f67f20做hash值得到0x3f,然后得到链表page_address_htable[0x3f],遍历这个page_address_htable[0x3f]链表,找不到节点page是0xc1f67f20的节点。
2.通过map_new_virutal函数映射虚拟地址
2.1 循环永久高端内存的1024个页表项,循环索引是last_nr整数,通过kmap_count[last_nr]是否为0,判断整个虚 拟地址是否空闲,假设kmap_count[1]=2,kmap_count[2]=3,kmap_count[2]=0,则last_nr=3。
2.2 对应的虚拟地址vaddr=PKMAP_BASE+3<<12=0xff803000可用。
2.3 虚拟地址vaddr对应的页表项地址是pkmap_page_table+3*4=0xc044b000+0xc=0xc044b00c。
2.4 物理页描述数组mem_map的地址是0xc1000000,page描述符对应的地址是0xc1f67f20,page对应物理页框序号是(0xc1f67f20-=0x7b3ed),页表项的属性是0x163,则写入相应页表项的值是:0x7b3ed<<12+0x163=0x7b3ed163.
- 通过函数set_page_address把page=0xc1f67f20,和vaddr=0xff803000写入到相应page_address_htable[0x3f]链表,找不到节点page是0xc1f67f20的节点。
3.1 对page=0xc1f67f20做hash值得到0x3f,然后得到链表page_address_htable[0x3f]。
3.2 从page_address_pool链表去掉一个数据节点,填充vaddr,page,然后把这个节点添加到链表page_address_htable[0x3f]中。
流程图如下,红色框就是对应着上面的流程的序号。
从上面图中可以看出,两个主要工作:
4. 从永久映射的线性地址,找出个没有映射的线性地址,然后在页表项中写入page对应的物理页框地址(包括页属性)。
5. 把页表page和vddr加入page_address_hstable数组元素的链表中。
7. 其他
tlb: 页表缓存,至于什么虚拟地址和物理地址映射是否被加入到tlb,完成由硬件完成,没有软件接口把地址映射加入到tlb中。在函数map_new_virtual中,就是创建映射关系后,默认映射加入到tlb中。
接触映射在函数中:
62 static void flush_all_zero_pkmaps(void)
63 {
64 int i;
65
66 flush_cache_kmaps();
67
68 for (i = 0; i < LAST_PKMAP; i++) {
69 struct page *page;
70
71 /*
72 * zero means we don't have anything to do,
73 * >1 means that it is still in use. Only
74 * a count of 1 means that it is free but
75 * needs to be unmapped
76 */
77 if (pkmap_count[i] != 1)
78 continue;
79 pkmap_count[i] = 0;
80
81 /* sanity check */
82 if (pte_none(pkmap_page_table[i]))
83 BUG();
.......
}
在66行中,清除tlb缓存,同时把pkmap_count的元素等于1的,重新赋值为0.意思说pkmap_count的元素没有了,可以使用了。
想到一个问题