arm-linux内存管理学习笔记(3)-页表前戏

start_kernel之前的汇编代码建立了内核临时页表,完成了内核区域的静态线性映射,保证内核可以在舒适的虚拟地址空间(运行地址和链接地址一致)运行。进入start_kernel之后就要准备建立完整的页表映射,这部分工作是在paging_init中完成。
不过在建立完整页表映射之前还需要进行一些准备工作,本文来分析下。
为了简化整个代码流程,便于分析,我的设备内核配置为不使用高端内存,不配置CONFIG_HIGHMEM。bootargs中传给内核的mem=256.
内核版本号:3.4.55
paging_init是在start_kernel的setup_arch中调用,这里按照先后顺序对setup_arch跟页表相关的各个函数功能做个介绍,重点函数paging_init进行详细分析。
setup_processor:调用lookup_processor_type,跟head.S中__lookup_processor_type一样,获取存储在.proc.info.init段中与cpu id一致的proc_info_list结构体,该结构体中存储着处理器的一些特性。打印出cpu的一些相关信息(如版本号 cache属性等)。

setup_machine_tags:对uboot传递来的tags进行解析,获取mem cmdline等信息,具体过程可以参考我分析kernel传参的博文,链接:http://blog.csdn.net/skyflying2012/article/details/35787971

parse_early_param:对boot_command_line进行早期的解析,具体解析原理可以参考我分析kernel参数解析的博文,链接:http://blog.csdn.net/skyflying2012/article/details/41142801
与页表相关的是对mem的解析,相应的解析函数如下。

{
    static int usermem __initdata = 0;
    unsigned long size;
    phys_addr_t start;
    char *endp;

    /*
     * If the user specifies memory size, we
     * blow away any automatically generated
     * size.
     */
    if (usermem == 0) {
        usermem = 1;
        meminfo.nr_banks = 0;
    }

    start = PHYS_OFFSET;
    size  = memparse(p, &endp);
    if (*endp == '@')
        start = memparse(endp + 1, NULL);

    arm_add_memory(start, size);

    return 0;
}
early_param("mem", early_mem);

我的设备内存起始物理地址是0x80000000,即PHYS_OFFSET = 0x80000000,cmdline中mem=256m,
early_mem最终调用arm_add_memory将0x80000000起始的256MB内存添加到meminfo的membank数组中。meminfo中记录着系统有多少块连续内存,用membank数组记录,这里我们仅使用1个membank表示0x80000000起始的256MB内存空间。

sanity_check_meminfo:对meminfo中所有的membank进行范围检查,不能覆盖最小的vmalloc区域,将lowmem_limit设置为最高membank的顶端,我的设备只有一个membank,因此lowmem_limit为0x90000000,这是内存物理地址。最后将high_memory设置为lowmem_limit的虚拟地址,lowmem是线性映射到0xc0000000,因此high_memory=0xd0000000。
static void * __initdata vmalloc_min = (void *)(VMALLOC_END - (240 << 20) - VMALLOC_OFFSET);
我的设备VMALLOC_END=0xfc000000,这是vmalloc区域的上限,VMALLOC_OFFSET=0x800000,是vmalloc区域与lowmem之间8MB的隔离带。这样计算vmalloc_min是0xec800000,这是一个最小(240MB)的vmalloc区域.

arm_memblock_init:全局结构体memblock用来记录内存中可用和保留的区域,memblock.memory代表可用区域,memblock.reserved代表保留区域。arm_memblock_init中首先将meminfo中记录的membank添加到可用区域中。将kernel的代码段 数据段以及ramdisk区域都添加到保留区域,再将一级页目录的16KB区域添加到保留区域。
最后如果在板级结构体machine_desc定义了reserve函数,则会调用该函数完成板级相关的一些内存区域的保留。
为了方便调试,可以在cmdline中加入memblock=debug,会将memblock中可用和保留的区域全部打印出来。
我的设备是将0x80000000起始的256MB区域添加到可用区域memblock.memory中。
以上完成了对meminfo memblock的初始化,需要注意,其中存储的都是物理地址。

接下来调用paging_init,其中与页表建立相关的函数如下。在arch/arm/mm/mmu.c中

 build_mem_type_table();
    prepare_page_table();
    map_lowmem();
    devicemaps_init(mdesc);
    kmap_init();

    top_pmd = pmd_off_k(0xffff0000);

其中map_lowmem devicemaps_init kmap_init完成了完整页表的建立,下篇文章再详细分析,这里先不说了。
build_mem_type_table:完成对kernel中mem_types数组的修复和补充。mem_types数组是kernel记录当前系统映射不同地址空间类型(普通内存 设备内存 IO空间等)的页表属性,其中页表属性还包括section-mapping的属性prot_sect,以及page-mapping的一级页目录属性prot_l1,二级页表属性prot_pte.
不过要注意的是,mem_types中prot_sect prot_l1都与硬件属性一一对应。但二级页表属性值prot_pte是linux定义的软件页表属性,而不是硬件页表属性,本文最后说明linux与arm适配问题会再详说。
在kernel下页表建立函数create_mapping中会根据选定的mem_types成员来设置当前映射所需的二级软件页表属性,最后会调用处理器相关的set_pte_ext,首先设置软件页表项,然后在根据软件页表项值配置硬件页表项。(linux在建立完整第二级页表时使2个硬件页表相连,并且在其后添加2个软件页表,保证这一页空间被充分利用,文末再细说)
build_mem_type_table中跟处理器特性对mem_types数组成员(各个类型的地址空间)的各个页表属性进行修改补充。软件开发人员一般不需要修改,除非处理器核做过修改。
接下来我们来分析下本文的关键函数prepare_page_table。在arch/arm/mm/mmu.c中

static inline void prepare_page_table(void)
{
    unsigned long addr;
    phys_addr_t end;

    /*
     * Clear out all the mappings below the kernel image.
     */
    for (addr = 0; addr < MODULES_VADDR; addr += PMD_SIZE)
        pmd_clear(pmd_off_k(addr));

#ifdef CONFIG_XIP_KERNEL
    /* The XIP kernel is mapped in the module area -- skip over it */
    addr = ((unsigned long)_etext + PMD_SIZE - 1) & PMD_MASK;
#endif
    for ( ; addr < PAGE_OFFSET; addr += PMD_SIZE)
        pmd_clear(pmd_off_k(addr));

    /*
     * Find the end of the first block of lowmem.
     */
    end = memblock.memory.regions[0].base + memblock.memory.regions[0].size;
    if (end >= lowmem_limit)
        end = lowmem_limit;

    /*
     * Clear out all the kernel space mappings, except for the first
     * memory bank, up to the vmalloc region.
     */
    for (addr = __phys_to_virt(end);
         addr < VMALLOC_START; addr += PMD_SIZE)
        pmd_clear(pmd_off_k(addr));
}

该函数是在建立完整页表前对一级页目录(swapper_pg_dir)进行清空,便于建立页表时对空页目录项进行判断然后分配。不过并不是所有页目录项都清空,我们知道虚拟地址的高12bit是16K一级页目录的索引ndex,prepare_page_table中对于第一块membank(lowmem部分)虚拟地址空间映射的页目录项跳过,不进行清空操作。
以我的设备为例,第一块也是唯一一块membank是0x80000000起始的256MB。lowmem_limit也是0x90000000。这块空间是线性映射,由0x80000000映射到0xc0000000。因此在4KB个页目录项中,第0xc00到第0xd00这256个页目录项,prepare_page_table不进行清空。
根据上一篇内核临时页表建立的分析,head.S中建立的临时页表,主要完成了3个地址空间的映射。
(1)turn_mmu_on所在1M空间的平映射
(2)kernel image的线性映射
(3)atags所在1M空间的线性映射
我计算过我的设备中,临时页表中kernel image线性映射了大约12MB空间,也就是第0xc00之后的12个页目录项。prepare_page_table中跳过0xc00开始的256个,临时页表建立的kernel image线性映射没有破坏,kernel的运行环境得到了保证。
不过为了保证mmu使能后的平滑跳转而建立的1MB平映射(vaddr从0x80000000到0x8100000),在prepare_page_table中就被清空了。
prepare_page_table整个流程如下。
1 对虚拟地址0到MODULES_VADDR(0xc0000000以下8MB或16MB的地址)的一级页目录项进行清空
2 对MODULES_VADDR到PAGE_OFFSET(0xc0000000)的一级页目录项进行清空
3 对lowmem顶端到VMALLOC_START的一级页目录项进行清空
我们来分析下具体的清空过程,以流程1为例。
从0到MODULES_VADDR,对每2MB空间,调用pmd_clear(pmd_off_k(addr))。
pmd_off_k定义如下。在arch/arm/mm/mm.h中

static inline pmd_t *pmd_off_k(unsigned long virt)
{
    return pmd_offset(pud_offset(pgd_offset_k(virt), virt), virt);
}

arm-linux仅使用2级页表,pud_offset
pmd_offset不做任何操作,直接返回参数,直接来看pgd_offset_k,如下。

extern pgd_t swapper_pg_dir[PTRS_PER_PGD];

/* to find an entry in a page-table-directory */
#define pgd_index(addr)     ((addr) >> PGDIR_SHIFT)

#define pgd_offset(mm, addr)    ((mm)->pgd + pgd_index(addr))

/* to find an entry in a kernel page-table-directory */
#define pgd_offset_k(addr)  pgd_offset(&init_mm, addr)

PGDIR_SHIFT定义为21,pgd_offset_k获取到了一级页目录项的虚拟地址,但是需要注意的是,该地址是8字节对齐。再来看pmd_clear,如下。在./arch/arm/include/asm/pgtable-2level.h

#define pmd_clear(pmdp)         \
    do {                \
        pmdp[0] = __pmd(0); \
        pmdp[1] = __pmd(0); \
        clean_pmd_entry(pmdp);  \
    } while (0)

将连续的2个页目录项清空,并且清空相应的TLB缓存。
综上,prepare_page_table清空具体流程是,对指定地址空间以2MB为一次操作单元,每次操作连续清空对应的2个一级页目录。
这就奇怪了,按照之前我们介绍的arm硬件页表机制,一级页目录共4096个,每个映射1MB空间,不应该一次操作1MB吗,为啥要以2MB为单位?

理解内核的这种做法,文章最后,就要来说明下linux页表机制与arm页表机制的适配问题了,主要有2点。
1 页表深度的适配
2 软件页表和硬件页表配合

1 页表深度的适配
arm硬件页表机制之前详细分析过,这里不再细说了,可以看我这系列的第一篇文章。
linux内核为了适配各种各样的处理器MMU,特别是64位处理器。2.6.11之前使用三级页表,之后开始使用四级页表。
linux内核定义的标准是这样,最高级pgd为页目录表,它找到每个进程mm_struct结构的pgd成员,用它定位到下一级pud,第二级pud定位到下一级pud,第三级pud定位到pte,pte是页表,它就能定位到哪个页了。最后虚拟地址的最后12位定位的是该页的偏移量。大体流程如下:

virt addr —> pgd —> pud —> pmd —> pte —> phy addr

首先需要理解,内存多级页表,是给mmu来解析的,因此针对不同的处理器mmu页表机制,linux需要在配置多级页表时进行调整。
为了能够符合arm mmu的页表解析机制(2级页表),arm-linux实现时砍掉了中间的pud pmd。如上面分析pmd_off_k时pud_offset pmd_offset中不做任何操作,直接返回参数。大体流程如下。

virt addr —> pgd —> pte —> phy addr

这样就保证了linux配置页表时虽然表面上看是4级页表,但是内存中建立却是一个2级页表。arm-mmu可以正确的解析页表,进行地址翻译。

2 软件页表和硬件页表配合
arm硬件页表机制中,每个一级页目录项对应的二级页表空间都是独立分配的,虽然连续2个一级页目录项所映射2MB地址空间是连续的虚拟地址空间,但是用来存储二级页表的空间之间是没有什么关系的。
不过arm-linux内核为了实现高效的内存管理,做了一个很巧妙的安排。
我们知道1个一级页目录项对应的二级页表是256x4 = 1024字节。arm-linux将2个连续的一级页目录项对应的2个二级页表分配在一起。而且还在这2个二级硬件页表之下在建立2个对应的二级软件页表,一共是4KB,正好占用1页空间。如下。

*    pgd             pte
 * |        |
 * +--------+
 * |        |       +------------+ +0
 * +- - - - +       | Linux pt 0 |
 * |        |       +------------+ +1024
 * +--------+ +0    | Linux pt 1 |
 * |        |-----> +------------+ +2048
 * +- - - - + +4    |  h/w pt 0  |
 * |        |-----> +------------+ +3072
 * +--------+ +8    |  h/w pt 1  |
 * |        |       +------------+ +4096

对于这样的安排在./arch/arm/include/asm/pgtable-2level.h有详细的英文解释。
我们告诉linux内核arm一级页目录项是8 bytes,一共是2048个。配置页目录项时,一次分配4KB空间,高2KB空间每1KB的物理基地址依次写入8 bytes一级页目录项的2个word中。
我的理解,arm-linux如此安排的原因有二。
(1)减少空间浪费,一个页目录项仅对应1024字节的页表,这样页表初始化时,按页分配的空间仅能使用1KB,其余3KB空间浪费,如上安排,可以完全利用这4KB页。
(2)linux软件二级页表属性位定义与arm硬件二级页表不一致(软件二级页表位定义在arch/arm/include/asm/pgtable-2level.h,硬件二级页表位定义在arch/arm/include/asm/pgtable-2level-hwdef.h,其中有详细说明),有些属性arm硬件页表没有,如dirty,accessed,young等,因此使用软件页表进行模拟兼容。arm-mmu读取硬件2级页表进行地址翻译,而第二级的软件页表仅仅留给linux来配置和读取。最终配置二级页表的set_pte_ext也会设置完软件页表后,在根据软件页表属性值来配置硬件页表。

所以我们在arm-linux内核中看到各种相关宏定义都表示,linux看到的arm一级页目录项有2048个,每个页目录项8
bytes,二级页表项是512个。不过arm-mmu的硬件机制还是4096个一级页目录项,每个页表有256个页表项。
两种机制是靠2个相邻页目录项的页表存储空间连续来平滑过渡的。

到这里,完整页表建立的准备工作做完了,主要是建立meminfo
memblock,并清空页目录项,但是保留了lowmem所在的线性映射页目录项,保障kernel正常运行。下篇文章就来分析下具体页表建立的过程!

本页内容版权归属为原作者,如有侵犯您的权益,请通知我们删除。

App的打磨之路(下) - 2016-07-25 14:07:48

前言:该文接上两篇博文 App的打磨之路(上) 和 App的打磨之路(中) ,继续描述打包、反编译及加固。 一、打包 每个Android应用在完成后都需要打成APK包,对于单个打包的方式在此就不赘述了,基本IDE都带,只是在对外发布的应用需要配置属于该应用的唯一签名,下文主要讲述需要上传多个市场的情况下怎么批量打包。 1、Maven打包 Maven是一个项目管理工具,它包含了一个项目对象模型(Project Object Model),一组标准集合,一个项目生命周期(ProjectLifecycle),一

PendingIntent的内部机制 - 2016-07-25 14:07:48

摘自;http://my.oschina.net/youranhongcha/blog/196933   1 概述         在Android中,我们常常使用PendingIntent来表达一种“留待日后处理”的意思。从这个角度来说,PendingIntent可以被理解为一种特殊的异步处理机制。不过,单就命名而言,PendingIntent其实具有一定误导性,因为它既不继承于Intent,也不包含Intent,它的核心可以粗略地汇总成四个字——“异步激发”。         很明显,这种异步激发常常
Android framework提供了许多标准的工具,来创建有吸引力的、功能丰富的用户图形界面。但是,如果你想要更多的控制权,比如在应用程序的屏幕上绘图,或者冒险进入三维图形,你需要使用不同的工具。通过Android framework提供的OpenGL ES的API提供了一套显示高端的工具,动画图像超出你的想象,许多Android设备的图像处理单元得到了加速(GPUs)。 这节课主要开发一个OpenGL应用程序、包括设置、画对象、移动对象元素、响应触摸输入事件。 这节课的示例代码使用的是OpenGL

来仿一仿retrofit - 2016-07-25 14:07:32

为什么要重复造轮子 在开发领域有一句很流行的话就是 不要重复造轮子 , 因为我们在开发中用到的很多东西早已有很多人去实现了, 而且这些实现都是经过时间和开发者检验过的, 一般不会遇到什么坑, 而如果我们自己去实现的话, 那不仅会增加工作量, 最大的隐患还是我们并不能预见以后是否会遇到大坑. 不过大家注意了吗. 上面 不要重复造轮子 的一个前提是 开发中 , 是的, 这句名言在开发中是适用的, 那在学习阶段的? 我可以大概的告诉你- 忘记这句话! , 为什么 不要重复造轮子 不适合在学习阶段使用呢? 如果我
背景 前面一篇总结了Serializable的序列化与反序列化,现在接着总结XML。主要内容:XML基本的序列化与反序列化方法、一些注意事项、以及自定义了一个XML注解框架(简洁代码,解放双手)。 XML的序列化与反序列化 先与Serializable进行简单的对比: Serializable存储的文件,打开后无法正常查看,安全性高。xml文件可通过文本编辑器查看与编辑,可读性高(浏览器会格式化xml文件,更方便查看),安全性低; Serializable文件通过了签名,只能在自己的程序中反序列化,或RM
每日更新关注 : http://weibo.com/hanjunqiang   新浪微博! iOS 开发者交流QQ群 : 446310206   有问题或技术交流可以咨询! 欢迎加入 ! 这篇直接搬了一份官方文档过来看的 由于之前没用markdown搞的乱七八糟的 所以重新做了一份 后面看到官网的中文文档更新不及时看着英文翻译了一点 搞的更乱了 :( 英文好的直接点右边- 官方OC文档 Realm 是一个移动端的数据库, Realm 是 SQLite 和 CoreData 的替代者。它可以节省你成千上万行

2.3.1 存储数据到data目录中 - 2016-07-25 14:07:09

当应用安装到Android后,系统会根据每个应用的包名创建一个/data/data/包名/的文件夹,访问自己包名下的目录是不需要权限的,并且Android已经提供了非常简便的API可以直接去访问该文件夹。 下面以一个案例来演示data目录的使用。 一、需求 如图1-5 所示,在用户将CheckBox选中的前提下点击登录,就将用户名和密码保存在data文件夹中,在用户不勾选CheckBox的前提下点击登录,就不保存用户名和密码,同时删除data文件中的历史数据。 如果数据已经保存到了data目录中,那么下次
本页面更新日期: 2016年07月23日 package 包 前面提到了 包 这个概念. 什么是包? 由于非常多的人参与 Java 的开发, 这难免会遇到一个问题 – 类名冲突 . 也就是说难免会遇到重名的情况. 所以 Java 引入了包这个机制. 提供了类的多层命名空间. 用于解决类的命名冲突 / 类文件管理等问题. Java允许将一组功能相关的类放在同一个 package 下. 从而组成逻辑上的 类库单元 . 如果希望把一个类放在指定的包结构下,应该在Java源程序的第一个非注释行放置如下格式的代码:
目录: APP项目如何与插件化无缝结合(一)  APP项目如何与插件化无缝结合(二)  APP项目如何与插件化无缝结合(三)  搬砖码字不易,转载请注明转自: http://blog.csdn.net/u011176685/article/details/52006474 上面一篇主要介绍了Small的原理,相信大家应该现在心里有个大概的了解。好,我们接下来继续开始! 一、Small的使用 关于Small的使用, Small的使用 这里讲的很详细,关于这里提下我当时遇到的问题和解决办法。 1.Small作
在Windows下试了试用Ionic开发Android应用,试通了。记录了过程。列在下面,供参考。 1. JDK 我用的jdk8,这里下载: http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 。我老早下载的,8u66,i586(32位的),现在是8u102: http://download.oracle.com/otn-pub/java/jdk/8u102-b14/jdk-8u102-wi