107

高端内存

 6 years ago
source link: http://mp.weixin.qq.com/s/mKblqfM2uG-k_xz6pDM1mw
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一、为什么我们需要高端内存

我们知道在x86_32架构下,linux中的进程的虚拟地址空间大小是4GB,其中的用户空间占用其中的低3GB,而内核空间占用其中的高1GB。而实际上内核的物理空间是从地址0开始的。所以内核空间的物理地址和虚拟地址可以根据右式转换 PA = VA - 0xC000 0000。根据这种计算方式,我们可以得到以下的表格:

虚拟地址             物理地址
0xC000 0000       0x0000 0000
0xFFFF 8FFF       0x3FFF 8FFF

0xFFFF FFFF       0x4000 0000

Image

图1-1 无高端内存的映射关系

这里就出现了问题,内核空间只能映射到前1GB的物理空间,为了解决这个问题。内核将每个节点的物理内存空间分成了三个部分:zone_dma zone_normal zone_highmem。zone_dma和zone_normal占用其中的896MB,而 zone_highmem占用的是>896MB的空间。而内核虚拟地址空间的高128MB用来专门映射高端内存。不过这种映射是动态的,也就是说该区域没有办法永久映射到内核的虚拟地址空间。

Image

图1-2  有高端内存之后的映射关系

二、建立高端内存的映射

Image

图2-1 内核虚拟地址空间的结构

1.永久内核映射

1.1数据结构

1 page_address_htable
在函数page_address()中,为了加速从页框指针到线性地址的转换,内核使用哈希表保存页框指针和线性地址的关系。桶中的每一项都是一个page_address_map结构。

Image

图2-2 page_address_htable

Image

2 pkmap_count数组
永久映射区间的起始线性地址为 PKMAP_BASE。内核利用主内核页目录表( swapper_pg_dir)的中的一个页表项所指向的页表来建立永久映射,该页表由指针
pte_t *  pkmap_page_table 来表示。页表中的页表项个数有宏LAST_PKMAP表示。PAE开启时,页表项个数为1024,反之则为512。

与临时映射不同为了保证映射的持久,内核建立了一个数组 int pkmap_count[LAST_PKMAP],该数组元素的个数就是页表项的个数。数组是一个计数器的集合。

count = 0 : 表示该页表项可用,相关映射还未建立,在TLB刷新前,TLB还没有相关页表项的存在。

count = 1 : 表示该页目前没有映射到任何页框,但是TLB中的上次映射的表项还没有被flush。所以该页的映射无法创建。简单来说,就是该映射还存储在TLB中。

count = n : 表示相关的页表项已经建立,并且有 n-1 个进程在使用该映射。
当然,仅仅一个计数器还不够,为了防止对页表项的并发访问,创建映射的过程需要用锁进行控制。

永久映射由 kmap(struct page *page) 创建,该函数接收参数page作为被映射的页框指针。该函数返回一个线性地址。kmap的核心是函数kmap_high()。
3  kmap_high
kmap_high 先调用page_address得到页框对应的线性地址,如果该页框还没有被映射,则调用 map_new_virtual。在map_new_virtual中,如果发现一个count为0的映射,则将count置为1,随后将count加一,此时,count值等于2。

否则不调用map_new_virtual,直接将count加一,此时count应该大于2。

Image

4 map_new_virtual
当一个页框还没有被映射到一个虚拟页时,就会调用map_new_virtual。为了防止对pkmap_count数组的重复遍历,函数使用last_pkmap_nr记录上次映射结束时页表项的索引。map_new_virtual其实大致上做了三件事:
第一,如果pkmap_count中有计数器为0的索引,则建立映射并令其count = 1。
第二,如果last_pkmap_nr=0,也就是整个页表没有可用的页表项了,则调用flush_all_zero_pkmaps 将所有的计数器为1 的映射(也就是说映射仅仅在TLB中)的计数器置为0,冲刷TLB。
第三,如果pkmap_count都大于1,则阻塞当前进程,将当前进程状态置为 TASK_UNINTERRUPTIBLE 并加入等待队列。之后调度其他进程,其他进程的时间片完后,再将原进程从等待队列移出 。如果当前没有其他进程映射该页框,则进行下次循环。
map_new_virtual等价于以下代码(摘自ULK)

Image

2.临时内核映射

2.1 临时内核映射

临时内核映射又称为原子映射,这里先抛个问题:为什么临时映射要称作原子映射。

临时内核映射区域位于固定映射区内,固定映射区内的线性地址可以随意映射到任意一个物理地址,而不是使用 物理地址 = 线性地址 - 0xC000_0000 得到。

临时内核映射区的起始和终止的线性地址的索引(关于什么是线性地址的索引,后面会说明)由 enum fixed_address 中的常量 FIX_KMAP_BEGIN  FIX_KMAP_END 分别指定。其中FIX_KMAP_END  = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1。内核根据CPU核心数划分临时映射区。

Image

图2-3 临时内核映射区的结构

而在每个CPU独有的块内部又根据页面的用途分成了13个窗口,举个例子 KM_USER0 和 KM_USER1就是内核用来存储来自用户上下文的(通常是系统调用传递的局部变量和参数)。 每个窗口其实就是一个页面。这13个窗口在内核中由 enum km_type 表示,而每个窗口的线性地址由km_type 中的常量作为索引来计算。KM_TYPE_NR是窗口的分类个数,等于13。于是,临时映射区变成了这样:

Image

图2-4 细化的临时内核映射区结构
临时映射由kmap_atomic 创建相比于 kmap,kmap_atomic 不阻塞当前进程,不刷新TLB,从而带来了速度上的提升。但是由于kmap_atomic并不阻塞当前进程,如果同一个CPU 上先后有两个进程都要在同一个window上建立映射,并且前一个进程还没有释放映射,那么后一个进程创建的映射就会覆盖前一个进程所创建的映射(其实质是页表项的覆盖)。所以必须原子性的创建和释放映射,这就是kmap_atomic名字的由来。

2.2 kmap_atomic

kmap_atomic接收两个参数,page是被映射的页面指针,type表明此次映射位于临时区间的那个window。

函数返回一个线性地址。

Image

2.3 __fix_to_virt 宏

关于fix_to_virt需要重点说明一下,fix_to_virt宏将索引转换为线性地址,注意此处使用的是位于固定映射区间的绝对索引FIXADDR_TOP 是固定映射区间的结束线性地址。固定映射位于线性地址 FIXADDR_START 与 FIXADDR_TOP之间,FIXADDR_TOP = 0xFFFF_F000 。在固定映射区间与虚拟地址空间的顶端(4G)之间还有一个1个页大小的空洞称为 FIX_HOLE ,更重要的是固定映射区间是向下拓展的(类似于栈)。

内核中使用宏 #define __fix_to_virt(x)    (FIXADDR_TOP - ((x) << PAGE_SHIFT)) 完成从索引到线性地址的转换,结合下图可得区域 FIX_VSYSCALL的起始线性地址为
0xFFFF_E000 = 0xFFFF_F000 - 1 * 0x1000

Image

图2-5 固定映射区间的结构图

enum km_type {
D(0)            KM_BOUNCE_READ,
D(1)            KM_SKB_SUNRPC_DATA,
D(2)            KM_SKB_DATA_SOFTIRQ,
D(3)            KM_USER0,
D(4)            KM_USER1,
D(5)            KM_BIO_SRC_IRQ,
D(6)            KM_BIO_DST_IRQ,
D(7)            KM_PTE0,
D(8)            KM_PTE1,
D(9)            KM_IRQ0,
D(10)            KM_IRQ1,
D(11)            KM_SOFTIRQ0,
D(12)            KM_SOFTIRQ1,
D(13)            KM_TYPE_NR
};

fixmap.h

#ifdef CONFIG_HIGHMEM
FIX_KMAP_BEGIN,    /* reserved pte's for temporary kernel mappings */
FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,
#define __FIXADDR_SIZE    (__end_of_permanent_fixed_addresses << PAGE_SHIFT)
#define FIXADDR_START        (FIXADDR_TOP - __FIXADDR_SIZE)
#define __fix_to_virt(x)    (FIXADDR_TOP - ((x) << PAGE_SHIFT))
#define FIXADDR_TOP    ((unsigned long)__FIXADDR_TOP)
#define __FIXADDR_TOP    0xfffff000

以上所有的内容都基于linux-2.6.11

深入理解Linux内核
http://bbs.chinaunix.net/thread-1920551-1-1.html 关于pkmap_count的讨论
https://yq.aliyun.com/articles/130909 关于pkmap_count很直观的描述
http://bbs.chinaunix.net/thread-1938084-1-1.html  关于高端内存的讨论


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK