高端内存

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

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

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

0xFFFF FFFF       0x4000 0000

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

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


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

二、建立高端内存的映射


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

1.永久内核映射

1.1数据结构

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

2-2 page_address_htable


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中,如果发现一个count0的映射,则将count置为1,随后将count加一,此时,count值等于2

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


4map_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


2.临时内核映射

2.1 临时内核映射

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

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

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


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

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

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

2.2 kmap_atomic

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

函数返回一个线性地址。

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

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

enumkm_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

#ifdefCONFIG_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)

#defineFIXADDR_START        (FIXADDR_TOP - __FIXADDR_SIZE)

#define__fix_to_virt(x)    (FIXADDR_TOP - ((x) << PAGE_SHIFT))

#defineFIXADDR_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  
关于高端内存的讨论

 

<think>好的,我现在需要帮助用户理解高端内存映射的概念及其实现方式和原理。首先,我要回顾一下用户提供的引用资料,看看有哪些相关信息可以利用。 根据引用[1],高端内存被定义为不能通过直接偏移映射的虚拟地址空间,而低端内存可以通过直接映射。例如,在ARM架构中,永久映射可能在high_memory之下,但仍属于高端内存。这说明高端内存的界定更侧重于映射方式而非物理位置。 引用[4]提到Linux的直接映射区是线性空间从3G开始的896MB区域,而永久内核映射区用于访问高端内存。这帮助我理解不同内存区域的结构。此外,引用[3]中的系统调用如vmalloc()和kmap()可能涉及高端内存的管理,但需要进一步确认。 用户的问题集中在实现技术和原理上,所以我需要结合这些引用,整理出高端内存存在的背景、具体实现方式以及相关技术。可能需要解释为什么需要高端内存,比如32位系统的地址空间限制,导致无法直接映射所有物理内存,从而引入高端内存的概念。 然后,实现方式方面,引用[1]提到永久映射和动态映射,可能对应kmap和vmalloc等方法。同时,引用[4]中的永久内核映射区和动态内存映射区应详细说明,包括它们如何工作,比如页表动态管理、临时映射等。 还需要注意区分不同体系结构下的差异,如ARM和x86的可能不同处理,这能帮助用户理解为何高端内存的定义不单纯依赖物理位置。 最后,确保回答结构清晰,分点解释,并结合原理和实现技术。同时生成相关问题,比如应用场景、与低端内存的区别、32/64位系统的影响等,这些能帮助用户深入理解。</think>### 高端内存映射技术实现与原理 #### 1. **背景与定义** 高端内存(High Memory)主要用于解决 **32位系统虚拟地址空间不足** 的问题。在32位架构中,内核虚拟地址空间通常被划分为: - **低端内存(Low Memory)**:直接映射区(如Linux中3G~3G+896MB),通过固定偏移(如`PAGE_OFFSET`)实现物理地址到虚拟地址的线性映射,访问效率高[^4]。 - **高端内存(High Memory)**:无法通过直接偏移映射的物理内存,需动态管理虚拟地址与物理页的映射关系[^1]。 #### 2. **实现原理** 高端内存的核心是 **动态虚拟地址分配**,通过以下技术实现: - **页表动态管理**:内核维护临时映射(如`kmap()`),将高端内存页临时映射到内核虚拟地址空间的小块区域(如永久映射区或固定映射区)[^4]。 - **按需映射**:仅在需要时建立映射,使用后释放虚拟地址资源。例如,`vmalloc()`从动态内存映射区分配虚拟地址,支持非连续物理内存的访问[^4]。 - **体系结构适配**:ARM等架构可能通过永久映射区(位于低端内存之上)实现高端内存访问,打破“高端内存=高地址”的误解[^1]。 #### 3. **关键技术** - **永久映射(kmap)**:通过`kmap()`为高端内存页分配固定虚拟地址,需手动释放(`kunmap()`),适用于长期访问的场景。 - **临时映射(kmap_atomic)**:使用CPU的TLB快速建立短期映射,映射后需尽快释放,常用于中断上下文[^4]。 - **非连续内存分配(vmalloc)**:将离散的物理页映射到连续的虚拟地址空间,适合大块内存请求,但访问效率低于直接映射区[^3]。 #### 4. **典型应用场景** - **物理内存超过内核直接映射容量**:如32位系统物理内存超过896MB时,超出部分通过高端内存管理。 - **动态内核模块加载**:模块代码可能分配到高端内存区域。 - **用户空间与内核空间的数据交换**:如`get_user_pages()`将用户内存页映射到内核高端内存区域。 --- ### 示例:Linux内核中的高端内存映射 ```c // 使用kmap映射高端内存页 struct page *high_mem_page = alloc_pages(GFP_HIGHUSER, 0); // 分配高端内存页 void *vaddr = kmap(high_mem_page); // 建立虚拟地址映射 memcpy(vaddr, src_data, PAGE_SIZE); // 操作映射后的内存 kunmap(high_mem_page); // 释放映射 ``` ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值