HighMemory介绍

Linux一般把整个4GB可以map的内存中的1GB用于低端内存。从0xC0000000开始的话(CONFIG_PAGE_OFFSET配置),低端内存的地址范围就是0xC0000000到high_memory地址。
high_memory = __va(arm_lowmem_limit - 1) + 1,arm_lowmem_limit也是0xff00000减去vmalloc大小什么的算出来的,和vmalloc_min一样。所以可以直接map的lowmemory小于1GB。如果vmalloc区域等于340MB的话,大小一般也就600多MB。

high_memory = __va(arm_lowmem_limit - 1) + 1static void * __initdata vmalloc_min =(void *)(VMALLOC_END - (240 << 20) - VMALLOC_OFFSET);#define VMALLOC_OFFSET      (8*1024*1024)
#define VMALLOC_START       (((unsigned long)high_memory + VMALLOC_OFFSET) & ~(VMALLOC_OFFSET-1))
#define VMALLOC_END     0xff000000UL

high_memory保存了高端内存的开始地址。

那如果用大于1GB内存怎么办呢?这时候如果不想把大于1GB的内存浪费掉,就需要定义CONFIG_HIGHMEM。
但Highmemory区域是不会在内核初始化的时候,直接map到内存可以访问的。
内核采用三种不同的机制将高端内存的页框映射过来。分别叫做临时内核映射,永久内核映射以及非连续内存分配(vmalloc)。

永久内核映射

http://blog.csdn.net/xiaojsj111/article/details/11817587
kmap()函数建立永久内核映射。
这种映射的建立,可能会发生阻塞,不适用于中断上下文(中断处理程序及可延迟函数)
函数kmap()被用来建立永久内核映射,如下所示,该代码的核心在函数kunmap_high()中,所用到的数据结构有

  • 哈希数组page_address_htable,数组每个元素都是一个链表,链表的每个节点中都存放了一个页描述符指针及相应的虚拟地址
  • pte_t * pkmap_page_table,主内核页表中专门用于永久内核映射的页表项数组,其大小由LAST_PKMAP指出,如果不使用扩展物理内存,其值为1024,正好是占一页页表。
  • int pkmap_count[LAST_PKMAP],与每个用于永久内核映射的页表项是一对一的关系
    • 0,对应页表项没有映射任何高端内存,并且是可用的
    • 1,对应页表项没有映射任何高端内存,但不可用,因最后一次被使用后,相应的TLB表还没有被刷新
    • 大于1,n-1个内核成分在使用对应的页表项
  • PKMAP_BASE,数组pkmap_page_table中第一个页表项所对应的虚拟地址

函数kunmap_high()调用了函数page_address(),对该页描述符所代表的高端内存,在哈希数组page_address_htable中,查找其相应的虚拟地址,找不到,则返回NULL,然后调用函数map_new_virtual()。

函数map_new_virtual()全部代码如下,其使用数组pkmap_count查找空闲可用的页表项,找到后,设置该页表项指向函数void *kmap(struct page *page)的实参所引用的页框,同时,在哈希数组page_address_htable的某个元素链表上添加节点,以记录该页描述符所对应的虚拟地址。另外,要注意的是,找不到空闲可用的页表项时,会睡眠等待。

如果是通过 alloc_page() 获得了高端内存对应的 page,如何给它找个线性空间?
内核专门为此留出一块线性空间,从 PKMAP_BASE 到 FIXADDR_START,用于映射高端内存。在 2.4 内核上,这个地址范围是 4G-8M 到 4G-4M 之间。这个空间起叫“内核永久映射空间”或者“永久内核映射空间”。
这个空间和其它空间使用同样的页目录表,对于内核来说,就是 swapper_pg_dir,对普通进程来说,通过 CR3 寄存器指向。
通常情况下,这个空间是 4M 大小,因此仅仅需要一个页表即可,内核通过来 pkmap_page_table 寻找这个页表。通过 kmap(), 可以把一个 page 映射到这个空间来。由于这个空间是 4M 大小,最多能同时映射 1024 个 page。因此,对于不使用的的 page,及应该时从这个空间释放掉(也就是解除映射关系),通过 kunmap() ,可以把一个 page 对应的线性地址从这个空间释放出来。

PKMAP_BASE,PKMAP_SIZE这两个定义了PKMAP区域的开始地址和大小

在Highmemory区域里分配的page,需要map一下内核才能访问。以下看一下几个相关的接口函数。

//这个函数检查当前page是否是lowmemory,如果是的话就调用page_address()把当前page的地址转成虚拟地址返回。但如果是高端内存的page,则需要用到PKMAP区域重新map一下,才能让内核进行访问。这样map的page释放的时候必须使用kumap来释放。由于这种map用到的PKMAP区域大小有限,建议不要占据这种内存太长时间(ldd3书里边写)?
void *kmap(struct page *page)
{might_sleep();//当前内存可能会进入睡眠,如果在atomic上下文调用kmap函数,//migh_sleep函数会打印stack trace//这个函数需要使能CONFIG_DEBUG_ATOMIC_SLEEPif (!PageHighMem(page))return page_address(page);return kmap_high(page);
}void *kmap_high(struct page *page)
{unsigned long vaddr;/** For highmem pages, we can't trust "virtual" until* after we have the lock.*/lock_kmap();vaddr = (unsigned long)page_address(page);if (!vaddr)vaddr = map_new_virtual(page);pkmap_count[PKMAP_NR(vaddr)]++;BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);unlock_kmap();return (void*) vaddr;
}//如果是highmemory的page,则会调用如下函数
static inline unsigned long map_new_virtual(struct page *page)
{unsigned long vaddr;int count;start:count = LAST_PKMAP;/* Find an empty entry */for (;;) {//last_pkmap_nr保存上一次map过的值,从这个值开始寻找。如果二级页表是512的话,最大只能到512//所以与了一个LAST_PKMAP_MASK。//如果二级页表是512的话,就可以猜到pkmap区域的大小应该至少是2MB,//http://blog.csdn.net/hongzg1982/article/details/47341881 //pkmap_count[]标志总共512个里边,哪个是可以map的。 last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;if (!last_pkmap_nr) {flush_all_zero_pkmaps();count = LAST_PKMAP;}if (!pkmap_count[last_pkmap_nr]) break;  /* Found a usable entry */if (--count)continue;/** Sleep for somebody else to unmap their entries*/{ //如果找不到剩余的,则先进入睡眠,等有kumap的时候再醒来继续寻找。//这里用的waitqueue的方式,也可以看一下 DECLARE_WAITQUEUE(wait, current);__set_current_state(TASK_UNINTERRUPTIBLE);add_wait_queue(&pkmap_map_wait, &wait);unlock_kmap();schedule();remove_wait_queue(&pkmap_map_wait, &wait);lock_kmap();/* Somebody else might have mapped it while we slept */if (page_address(page))return (unsigned long)page_address(page);/* Re-start */goto start;}}vaddr = PKMAP_ADDR(last_pkmap_nr);//highmemory的虚拟地址是PKMAP_BASE +last_pkmap_nr<<PAGE_SHIFT,所以很容从虚拟地址看出来当前页是否是highmemory。因为最多就可以只map 512个page,也就是2MB大小的highmemory,所以最好不要使用太长时间,导致highmemory分配失败!!这个也是上面说过的//下面的函数就简单配置一下页表就可以了set_pte_at(&init_mm, vaddr,&(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));//标记当前地方的已经被分配出去了。pkmap_count[last_pkmap_nr] = 1;set_page_address(page, (void *)vaddr);return vaddr;
}

kmap()可以看到,在分配不到的时候,可能会进入睡眠。
那还有一种kmap_atomic()接口是比kmap更为高效且不进入睡眠的,可以在atomic上下文进行调用的。
下面看一下其实现:

void *kmap_atomic(struct page *page)
{unsigned int idx;unsigned long vaddr;void *kmap;int type;pagefault_disable();if (!PageHighMem(page))return page_address(page);#ifdef CONFIG_DEBUG_HIGHMEM/** There is no cache coherency issue when non VIVT, so force the* dedicated kmap usage for better debugging purposes in that case.*/if (!cache_is_vivt())kmap = NULL;else
#endifkmap = kmap_high_get(page);if (kmap)return kmap;type = kmap_atomic_idx_push();idx = type + KM_TYPE_NR * smp_processor_id();vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
#ifdef CONFIG_DEBUG_HIGHMEM/** With debugging enabled, kunmap_atomic forces that entry to 0.* Make sure it was indeed properly unmapped.*/BUG_ON(!pte_none(get_top_pte(vaddr)));
#endif/** When debugging is off, kunmap_atomic leaves the previous mapping* in place, so the contained TLB flush ensures the TLB is updated* with the new mapping.*/set_top_pte(vaddr, mk_pte(page, kmap_prot));return (void *)vaddr;
}

临时内核映射

临时内核映射比永久内核映射的实现要简单;此外它可以在中断处理程序和可延迟函数的内部,因为这个临时内核映射函数从来不阻塞当前进程。为了建立临时内核映射,内核调用kmap_atomic()函数。
内核在 FIXADDR_START 到 FIXADDR_TOP 之间保留了一些线性空间用于特殊需求。这个空间称为“固定映射空间”
在这个空间中,有一部分用于高端内存的临时映射。

分配page或者free page的时候会判断是不是highmemory。

highmemory的大小:
memblock或者meminfo()里边,highmemory的最大地址减去arm_lowmem_limit的地址之后算出来的大小就是highmemory的大小。比如像下面log这样,最后一段count = 3的区域为highmemory区域。其size为768MB,但highmemory开始地址小于arm_lowmem_limit,所以减去arm_lowmem_limit - reg->base的虚拟地址(18MB),highmemory的大小就是750MB。这个大小与/proc/meminfo里边读出来的HighTotal的大小是一致的。

<5>[0.000000]  [0:swapper:0] arm_lowmem_limit = 0xf1200000
<6>[0.000000]  [0:swapper:0] count = 1 , reg->base =0x80000000 , reg->size =0x5500000
<6>[0.000000]  [0:swapper:0] count = 2 , reg->base =0x8cb00000 , reg->size =0x23200000
<6>[0.000000]  [0:swapper:0] count = 3 , reg->base =0xb0000000 , reg->size =0x30000000

然后再arm_bootmem_free里边

#ifdef CONFIG_HIGHMEMzone_size[ZONE_HIGHMEM] = max_high - max_low;//可以在find_limits函数里边找到max_high和max_low的定义
#endif
//max_high就是high memory最大地址对应的pfn,max_low是lowmemory的最大地址对应的pfn

用户空间映射:
虽然内核地址空间有限,但是每个进程的用户地址空间都可以达到3G,Highmem的页框可以不受限制的映射到用户线性地址空间。当访问用户地址空间地址发生缺页异常时,内核的page allocator会优先从highmem zone分配页面,只有当highmem zone没有足够的空闲页面时,才会选择Normal或者DMA zone进行分配。

因此highmem内存的主要使用者是应用进程的页面映射,内核kernel通过pkmap fixmap方式,同时使用的Highmem内存,理论上最多2MB/4MB + 3.xMB;由于Highmem的存在,使得应用地址空间缺页异常处理,文件映射,堆分配等操作优先使用highmem zone的内存,减轻了Normal zone的分配压力,某种程度上避免了Normal区的碎片化。我们甚至可以禁止用户空间地址的HIGH_MEM分配使用Normal zone和 DMA zone,使得Normal DMA只用于内核地址空间内存的分配,尽量减少碎片化,避免内存分配失败。我想这就是HighMem存在的意义吧。

非连续内存区管理 vmalloc

如果一段内存不是很频繁访问,那么通过连续的线性地址来访问非连续的页框这样一种分配模式就会有很大的意义。这种模式的优点是避免了外碎片(??),而缺点是必须重新建立页表。
显然通过vmalloc分配的内存大小必须是4096字节,也就是page大小的倍数!!!

如下图可以看到vmalloc开始和结束分别与high_memory与PKMAP_BASE大小相关,但一般都会插入一个8MB的区域,目的是为了”捕获”对内存的越界访问。
出于同样理由,vmalloc区域之间也会留下4kB的安全区来隔离非连续的内存区

这种方式很简单,因为通过 vmalloc() ,在”内核动态映射空间”(上图的VMALLOC_START到VMALLOC_END)申请内存的时候,就可能从高端内存获得页面(参看 vmalloc 的实现),因此说高端内存有可能映射到”内核动态映射空间” 中。
vmalloc区域的起始线性地址是VMALLOC_START,结束线性地址是VMALLOC_END。每个vmalloc内存区都对应着一个vm_struct数据结构。

struct vm_struct {struct vm_struct    *next;void            *addr;unsigned long       size;unsigned long       flags;struct page     **pages;unsigned int        nr_pages;phys_addr_t     phys_addr;const void      *caller;
};- addr,第一个内存单元的线性地址
 - size,内存区的大小加4096的安全区
 - pages,这个数组中存放的是被映射的物理页的页框描述符
 - nr_pages,数组pages的维数
 - phy_addr,除非内存被用来映射一个硬件设备的IO共享内存,否则为0
 - next,所有的非连续内存区的vm_struct描述符都通过该字段链接在全局变量vmlist中
 - flags,
     - VM_ALLOC,使用vmalloc()分配的非连续内存区
     - VM_MAP,使用vmap()映射已经分配好的页框
     - VM_IOREMAP,使用ioremap()映射的硬件设备上的内存

使用vmalloc(size)分配非连续的内存区,该函数的核心是__vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL),下面介绍该函数

1) 使用kmalloc(sizeof(vm_struct), GFP_KERNEL),根据vm_struct数据结构的大小,从几何分布的
slab中为vm_struct分配内存,并在线性地址从VMALLOC_START到VMALLOC_END之间寻找一块空闲区域,大小至
少为size+4096,找到后,用该段线性地址的起始值初始化vm_struct->addr,并初始化vm_struct->flags,且将vm_struct->size记为size+40962)  初始化vm_struct->nr_pages为(size >> PAGE_SHIFT),然后,为页描述符指针数组
vm_struct->pages分配空间,需要分配的大小是
(vm_struct->nr_pages*sizeof(struct page *)),若数组占用内存大于一页,则使用__vmalloc(array_size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL)分配,否则,使用
kmalloc(array_size, (GFP_KERNEL | __GFP_HIGHMEM& ~__GFP_HIGHMEM))分配。这
里需注意的是,所谓一页的安全区只是指线性地址需要,实际的页框中并没有3) 使用函数alloc_page(GFP_KERNEL | __GFP_HIGHMEM)(单个页框分配函数)分配
vm_struct->nr_pages个页框,将页描述符记录在数组vm_struct->pages中4)接下来,就是要修改内核页表,以表明非连续内存区的每个页框都对应着一个线性地址,使用的
函数是map_vm_area(area, PAGE_KERNEL,&vm_struct->pages),该函数先是通过
pgd_offset(&init_mm, vm_struct->addr)得到主内核页全局目录pgd,然后,为每段4KB大小的线性地址(除却最后4KB用作安全区的的线性地址),
分配所需的各级页中间目录项,并最终建立页表项,并将每个页描述符对应的页的物理地址,
连同PAGE_KERNEL标志设置到相应的页表项中。注意,PAGE_KERNEL等同于(_PAGE_PRESENT | _PAGE_RW | _PAGE_DIRTY | _PAGE_ACCESSED | _PAGE_NX)需要注意的是,vmalloc(size)并未触及当前进程的页表,因此,内核态的进程访问非连续内存区时,由于在进程页表中找不到对应的表项,所以,缺页异常发生,然后,缺页处理程序发现这个缺页线性地址在主内核页表中,所以,就把主内核页表中相应的值拷贝到进程的页表中,最后恢复进程的执行,详见 缺页异常??????
使用vfree(void *addr)来释放非连续的内存区,该函数- 调用remove_vm_area(void *addr),根据线性地址addr在vmlist中找到vm_struct,并清除该非连续内存区中的线性地址对应的所有的内核的页表项,注意,这里只清楚了页表项,不清楚各级页中间目录项,因为内核永远也不会回收扎根于主内核页全局目录中的页上级目录、页中间目录和页表。
- 调用函数__free_page(),将vm_struct->pages数组中的每个页归还到页框分配器,然后,调用vfree(area->pages)(该数组占用空间大于4096KB时)或kfree(vm_struct->pages)释放这个数组本身
- 调用kfree(vm_struct),释放数据结构vm_struct

Linux内存管理:HighMemory相关推荐

  1. Linux内存管理基础

    系统启动之Linux内存管理基础 Keywords 非一致内存访问(NUMA)模型.节点(node).内存管理区(Zone).一致内存访问(UMA)模型.内核页表.内存管理区分配器(伙伴系统Buddy ...

  2. Linux内存管理回收机制

    Linux内存管理回收机制 1.Linux内存管理简介     Linux将所管理的内存划分为内存节点(node).内存分区(zone)和页框(page). 1.1.内存节点(node)     依据 ...

  3. Windows内存管理和linux内存管理

    windows内存管理 windows 内存管理方式主要分为:页式管理,段式管理,段页式管理. 页式管理的基本原理是将各进程的虚拟空间划分为若干个长度相等的页:页式管理把内存空间按照页的大小划分成片或 ...

  4. 万字长文,别再说你不懂Linux内存管理了(合辑),30 张图给你安排的明明白白...

    之前写了两篇详细分析 Linux 内存管理的文章,读者好评如潮.但由于是分开两篇来写,而这两篇内容其实是有很强关联的,有读者反馈没有看到另一篇读起来不够不连贯,为方便阅读这次特意把两篇整合在一起,看这 ...

  5. 别再说你不懂Linux内存管理了,10张图给你安排的明明白白!

    来自:后端技术学堂 过去的一周有点魔幻,有印象的有三个新闻:天猫总裁绯闻事件,蘑菇街裁员,不可能打工的周某也放出来了.三件事,两件和互联网行业相关,好像外面的世界很是精彩啊!吃瓜归吃瓜,学习还是不能落 ...

  6. Linux内存管理原理【转】

    转自:http://www.cnblogs.com/zhaoyl/p/3695517.html 本文以32位机器为准,串讲一些内存管理的知识点. 1. 虚拟地址.物理地址.逻辑地址.线性地址 虚拟地址 ...

  7. Linux内存管理【转】

    转自:http://www.cnblogs.com/wuchanming/p/4360264.html 转载:http://www.kerneltravel.net/journal/v/mem.htm ...

  8. Linux内存管理之高端内存映射

    一:引子 我们在前面分析过,在linux内存管理中,内核使用3G->4G的地址空间,总共1G的大小.而且有一部份用来做非连续空间的物理映射(vmalloc).除掉这部份空间之外,只留下896M大 ...

  9. Linux内存管理原理

    本文以32位机器为准,串讲一些内存管理的知识点. 1. 虚拟地址.物理地址.逻辑地址.线性地址 虚拟地址又叫线性地址.linux没有采用分段机制,所以逻辑地址和虚拟地址(线性地址)(在用户态,内核态逻 ...

  10. Linux内存管理 (2)页表的映射过程

    专题:Linux内存管理专题 关键词:swapper_pd_dir.ARM PGD/PTE.Linux PGD/PTE.pgd_offset_k. Linux下的页表映射分为两种,一是Linux自身的 ...

最新文章

  1. CVPR2020论文点评: AdderNet(加法网络)
  2. python的速度问题_python编程如何提升速度篇
  3. lsof 查看一个进程打开哪些fd及对应的文件或套接字操作
  4. easyui datagrid
  5. ArcGIS API for Python(一)开始准备环境
  6. 思维导图分析http之http协议版本
  7. 线条边框简笔画图片大全_超治愈萌系手帐素材大全 美食旅游花草人物花边都备齐了...
  8. tail -f 查找关键字_C语言九种查找算法 | 总有一款适合你
  9. 设置指定ip访问mysql数据库
  10. HTML将广告关闭的JS代码,原生js对联广告代码制作浮动固定层可关闭对联广告横幅...
  11. bandicom录屏音画不同步_bandicam录屏工具
  12. 如何在CSDN上上传资源
  13. 腾讯云轻量应用服务器搭建网站
  14. 7-38 寻找大富翁 (25 分)
  15. 微信企业号接入微信支付
  16. 人民币小写转大写金额(可达千百万亿)
  17. 用户、配额管理 、 云主机类型管理 、 镜像管理 、 网络管理 、 安全和实例管理 、 计算节点扩容案例
  18. 【无标题】C语言连续输出输入语句执行跳过的问题
  19. linux命令引用,Linux下nl命令的用法详解
  20. 亲身经历的 noshow 与 goshow

热门文章

  1. amd cpu 型号大全
  2. Swift(一)语言介绍
  3. 怎么看自己电脑的是几位的操作系统的
  4. xiunobbs装插件
  5. 图书馆图书上架_泉城书房济南市平阴县图书馆锦东分馆图书上架了!
  6. 电脑端(PC)按键精灵2023——入门小白 详细 教程
  7. java POI对word中的表格动态插入固定数据,以及插入不确定数量的的数据
  8. Centos7值得收藏的网站
  9. js字符串格式化方法format
  10. 国标GB/T28181视频流媒体服务器4G摄像头视频无插件直播方案对接过程中前端设备正常上线但视频无法播放问题解决