Kernel那些事儿之内存管理(11) --- 内核映射(上)

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

我们先从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() 中由于等待可用页表项而进入睡眠的进程。

时间: 2024-10-10 04:18:40

Kernel那些事儿之内存管理(11) --- 内核映射(上)的相关文章

Kernel那些事儿之内存管理(13) --- 内核映射(下)

前面讲过,针对于内核地址空间中后面的128MB空间,Kernel提供了三种机制来映射物理内存.之前讲过了两种,即持久内核映射和临时内核映射.这两种机制的目的都是一样的:使Kernel能够访问到高端内存. 今天讲一下第三种机制:非连续内存分配,也就是vmalloc.这个机制同样可以使Kernel能够访问到高端内存,不过这不是该机制的主要目的.该机制的主要目的是:把物理上不连续的页面映射到连续的内核线性地址空间中. 非连续内存区域管理 既然是映射,肯定会涉及到三个元素:集合L,集合P,映射M. 集合

Kernel那些事儿之内存管理(12) --- 内核映射(中)

内核地址空间中后面这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那些事儿之内存管理(7) --- Slab(上)

前面讲的buddy system算法,分配内存的最小单位是一个页面(例如 4K).这对于大的内存申请比较适用.可是实际生活中,Kernel经常需要分配小的内存空间,比如几十个字节,这个时候怎么办呢? 不同的人可能会想到不同的解决办法. 对于财大气粗的富人一族,办法很简单:申请一个页面,只用其中的几十字节,剩下的直接丢掉. 对于锱铢必较的穷人一族,就不敢这么挥霍了:申请一个页面,然后记录好页面内哪些内存区用了,哪些还没用,没用的部分可以用来满足其他的内存分配请求.总之务必做到物尽其用. 很不幸,K

Kernel那些事儿之内存管理(3) --- 久别重逢

上次我们讲到page frame是物理内存的基本组成单位.那Kernel就必须要有一套机制来管理空闲的page frames.这一点不难理解.每个县长必须要把本县可用的劳动力登记在册,这样哪天皇帝要征兵了,你才不至于手忙脚乱. 这个问题看似简单,实则不然.因为这里面有一个外碎片的问题. 在物理内存中,连续的物理内存页有时是很重要的.例如在DMA操作中,由于大部分DMA处理器都没有分页机制,它们会直接访问物理内存地址,因此DMA 所用的缓冲区在物理地址空间必须连续:再例如,使用连续的物理内存页,可

Kernel那些事儿之内存管理(1)

有人的地方就有江湖.要介绍内存管理这个江湖,首先还得从这里面的主要人物讲起. 在NUMA结构中,物理内存首先被分成若干nodes.每一个node进一步被分成若干zones.每一个zone又关联了一个描述page frames的数组,该数组包含了属于该zone的所有page frame的描述符. 不难看出,在这个江湖里主要有三位重要人物:nodes, zones 和 page frames.这三者的关系和地位大体可以用下图来描述(该图取自"Professional Linux Kernel Arc

Kernel那些事儿之内存管理(8) --- Slab(中)

上篇讲了Slab中的数据结构,这篇该讲Slab中的操作了. 既然是内存管理,那操作无非就两点:allocate 和 free. 1. 申请一个object 在Slab中,申请一个object是通过函数 kmem_cache_alloc() 来完成的. 3618 void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags) 3619 { 3620     return __cache_alloc(cachep, flags, __bu

Kernel那些事儿之内存管理(2) --- 百闻不如一见

上次介绍了物理内存管理中三位主要人物中的node 和zone.这两位是当官的,一个是县长,一个是里长,不敢不先介绍啊.接下来出场的就是我们的老百姓了 --- page frame. Page frame是物理内存的基本组成单位,在Kernel中由结构体 struct page 来描述. struct page {     unsigned long flags;          atomic_t _count;          union {         atomic_t _mapcou

Kernel那些事儿之内存管理(9) --- Slab(下)

有了Slab的数据结构和操作方法之后,就可以创建一个指定大小的cache,然后在这个cache中申请和释放object.这个做法很适合这种应用场景:频繁地使用一固定大小的内存空间. 如果只是偶尔使用某个大小的内存空间,为此新建一个cache就有点得不偿失.针对于这种应用场景,内核提供了一系列 general caches.今天我们就来看一看这些general caches是怎么工作的. 其思想很简单:这些general caches会在系统初始化时提前创建好,且其大小按照2的幂次方由小到大分布,

Kernel那些事儿之内存管理(4) --- 未雨绸缪

上次讲的buddy system算法虽然效率很高,但是要从buddy system中分配出一个内存页块来,还是要做不少工作的,有时想想都会觉得很累. 在系统运行过程中,Kernel经常会有单个页面的申请和释放操作.为了进一步提高性能,也为了让生活变得轻松一点,Kernel采用了这样一种cache机制: Memory zone为每个CPU定义了page frame cache.Kernel会在适当的时机提前从buddy system中分配好若干单页,放在这些cache中.以后Kernel若要申请单