Chinaunix首页 | 论坛 | 博客
  • 博客访问: 313759
  • 博文数量: 100
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 665
  • 用 户 组: 普通用户
  • 注册时间: 2015-02-02 12:43
文章分类

全部博文(100)

文章存档

2015年(100)

我的朋友

分类: LINUX

2015-05-16 21:31:45

 linux的内存(正式)页表是在内核代码执行到start_kernel函数后执行paging _init函数建立的,这里要注意一个事情就是说,这里paging_init函数可以正常创建内存页表的条件有两个:

1、              meminfo已初始化:即初始化物理内存各个node的各个bank,一般对于小型arm嵌入式设备,不涉及多个内存就是一个node和一个bank;这部分初始化是在paging_init函数前面的对uboot所传参数的解析中完成的(可在内核的arm_add_memory函数中加入打印信息验证)

2、              全局变量init_mm的代码段首尾、数据段首尾四个成员已初始化:在paging_init前面有对这四个成员的初始化,它们规定了内核镜像的代码段起始、代码段结尾、数据段起始、数据段结尾(数据段结尾也是整个内核镜像的结尾),给这四个成员赋的地址值都是在vmlinux.lds.S链接脚本中规定的(即虚拟地址),界定它们的意义在于能够正确的界定内核镜像的运行需要在虚拟地址占用的空间位置及大小,以利于其他内容在内核空间位置的确定。

Paging_init函数首先调用的是build_mem_type_table,这个函数做的事情就是给静态全局变量mem_types赋值,这个变量就在本文件(arch/arm/mm/mmu.c)定义,它的用处就是在create_mapping函数创建映射时配置MMU硬件时需要;build_mem_type_table函数里面是完全与本arm芯片自身体系结构相关的配置,我还没完全搞明白。。。后续再补充吧。

接下来调用的是sanity_check_meminfo,这个函数主要做两件事情,首先是确定本设备物理内存的各个node各个bank中到底有没有高端内存,根据是否存在高端内存决定每个bankhighmem成员值;然后是对于每个bank的正确性进行检测;下面分别描述:

由下面代码判断每个物理内存bank是否属于高端内存:

if (__va(bank->start) > VMALLOC_MIN || __va(bank->start) < (void *)PAGE_OFFSET)

         highmem = 1;

即:该bank的物理内存起始虚拟地址大于VMALLOC_MIN,或者小于PAGE_OFFSETPAGE_OFFSET是内核用户空间的交界,在这里定义为0xc0000000也就是arm-linux普遍适用值3G/1GVMALLOC_MIN就定义在本文件(arch/arm/mm/mmu.c),如下:

#define VMALLOC_MIN  (void *)(VMALLOC_END - vmalloc_reserve)

VMALLOC_ENDarch/arm/mach-XXX/include/mach/vmalloc.h文件中定义,可见是不同arm设备可以不同的,它标志着vmalloc区域的结尾在哪里,这里定义为3G+768M,如下:

#define VMALLOC_END       (PAGE_OFFSET + 0x30000000)

静态全局变量vmalloc_reserve定义在本文件(arch/arm/mm/mmu.c),它是可以由用户指定的,指示了vmalloc区域的大小,默认值为128M,用户指定的方式是通过uboot中指定“命令行”参数的vmalloc参数(“vmalloc=”,通过paging_init前的__early_param方式指定)修改内核中该变量的值,这里采用的就是默认值128M

vmalloc区域的结尾(3G+768M)减去该区域的大小(128M)即得到了vmalloc区域的起始,3G + 768M - 128M = 3G + 640M(0xe8000000)

所以,一个bank的物理内存属于高端内存的条件是:

1、  起始地址不大于vmalloc区域的起始虚拟地址;

2、  起始地址不小于内核用户交界的虚拟地址;

当属于高端内存时,该bankhighmem成员将置1

除了界定物理内存bank是否属于高端内存,sanity_check_meminfo函数还对每个物理内存bank的正确性进行检测,这部分个人认为不是重点,主要注意下在存在高端内存情况下(代码中定义宏CONFIG_HIGHMEM情况下),若低端内存太大(起始位置在VMALLOC_MIN之前,结尾位置超过VMALLOC_MIN),则超过VMALLOC_MIN的部分将被算进另一个bank并且判定为高端内存。

接下来调用的是prepare_page_table,它的作用是清除在内核代码执行到start_kernel之前时创建的大部分临时内存页表,这里需要对arm-linux内存页表的机制原理进行理解:

首先一个是,什么是内存页表,都有哪些属于内存(注意这里的内存是广义上的内存,不单单是物理内存)

具体的说,内存页表,更应该叫内存映射,对于有MMUCPU来说,CPU访问物理内存或某个SOC硬件寄存器,所做的操作并非是直接把它们的物理地址放在CPU的地址总线,而是把一个虚拟地址交给MMU,如果MMU硬件存在这个虚拟地址对应的物理地址(这个映射关系就是需要创建的内容,也就是内存映射!),那么它就会把对应的物理地址放在地址总线上。这样做最大的好处是,避免了软件程序直接访问一个不存在的地址导致出现问题 + 用户程序可使用的“内存”很大。

再说哪些东西属于内存,很简单,不仅仅物理内存,所有通过CPU地址总线连接的都属于内存,比如SOC的硬件寄存器,这里有个方法验证这个道理,函数create_mapping是最终创建内存映射的函数,看看它都被哪些函数调用:

map_memory_bank:这是为物理内存创建内存映射

devicemaps_init:这是为中断向量创建内存映射

iotable_init:这是为SOC硬件寄存器创建内存映射

就以上三个调用需求

函数create_mapping是最终创建内存映射的函数,先不管哪些需求去调用这个函数,先看这个函数本身:

这个函数只需要一个参数struct map_desc *md,这个结构体的定义在文件arch/arm/include/asm/mach/map.h中,只有4个参数非常简单:

unsigned long virtual;  /*虚拟地址起始*/

unsigned long pfn;    /*物理地址起始*/

unsigned long length;  /*长度*/

unsigned int type;    /*说明这个区间所属的域,以及是否可读、写、可高速缓存等属性,arm硬件相关*/

第一个参数非常好理解,虚拟地址起始;第二个参数是物理地址起始;第三个参数是要映射的长度,最后一个比较复杂,但实际用到的往往只有MT_MEMORY(代表物理内存)MT_HIGH_VECTORS/MT_LOW_VECTORS(代表中断向量)MT_DEVICE(代表硬件IO寄存器),实际含义和arm硬件相关,指的是MMU不同页表的映射权限暂可先不关心。

那么给定这四个参数,create_mapping函数是怎么创建映射呢?现在必须描述一下arm-linux的分页机制:

32位的arm芯片的寻址能力就是2^32 = 4G,地址范围即0-0xffffffff,如果按照1M大小为单位进行映射,则4G = 4096 * 1M也就是需要4096个条目,每个条目负责1M大小的地址范围;比如需要映射一个大小为128M的物理内存,那么就需要填写128个条目即可;事实上这就是所谓的段式映射或所谓一级映射,使用1MB的粗页表,优点是占用条目较少仅4096个条目,每个条目4字节即每个进程(包括内核进程自己)仅占用16K空间,但缺点是粒度太大了不利于linux内存管理,比如某进程或某内核代码(如模块)申请一些较小的空间不足1M,却也得分配这么大空间,当申请频繁时,物理内存消耗将很快。这个缺点是必须要克服的,arm-linux肯定要引入二级页表,但首先要理解这种段式映射或称为一级映射的页表是怎么样的,理解了一级映射才能理解二级映射,如下:

现在正式看内核进程的页表创建,先分析道理,再对应源码(后面有足量的代码注释),在mm/init-mm.c文件中,有全局变量init_mm,如下:

struct mm_struct init_mm = {

         .mm_rb             = RB_ROOT,

         .pgd          = swapper_pg_dir,  

         .mm_users       = ATOMIC_INIT(2),

         .mm_count      = ATOMIC_INIT(1),

         .mmap_sem    = __RWSEM_INITIALIZER(init_mm.mmap_sem),

         .page_table_lock =  __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),

         .mmlist              = LIST_HEAD_INIT(init_mm.mmlist),

         .cpu_vm_mask         = CPU_MASK_ALL,

};

每个进程都有描述自己内存使用情况的结构mm_struct,内核进程也不例外,从这个文件的目录可见这个变量是不以平台区分的,现在重点看第二个成员pgd,这是该进程内存页表的虚拟地址,值为swapper_pg_dir,这是个在head.S中定义的变量,值设置为KERNEL_RAM_VADDR - 0x4000 = 0xc0004000,由前面已知段式内存页表的大小为16K,所以内核的内存页表的虚拟地址范围是[0xc0004000: 0xc0008000]

内核的内存页表,在源码中paging_init函数都映射了什么?由前面已知,映射了三方面内容,分别是:物理内存、中断向量、硬件IO寄存器,先看物理内存的情况,这里marvell设备物理内存为256MPHYS_OFFSET0

函数调用顺序是:bootmem_init-> bootmem_init_node-> map_memory_bank,关注函数map_memory_bank,把物理内存的参数填充到struct map_desc结构体变量map,并用它调用函数create_mapping,正式开始:

1、首先一个判断(md->virtual != vectors_base() && md->virtual < TASK_SIZE),这是为了防止虚拟地址不是中断表地址并且在用户区(0~3G)的情况;然后又是一个判断((md->type == MT_DEVICE || md->type == MT_ROM) && md->virtual >= PAGE_OFFSET && md->virtual < VMALLOC_END),这是为了防止内存类型为IO型或ROM但虚拟地址为低端内存申请区(3G~3G + 768MB)的情况;这些判断暂无需关注;

2type = &mem_types[md->type];由前面可知这是获取所映射内存区间所属的域,以及是否可读、写、可高速缓存等属性,暂无需关注;

3、判断(md->pfn >= 0x100000)的情况,这个就不要关注了(超过4G的情况)

4、判断(type->prot_l1 == 0 && ((addr | phys | length) & ~SECTION_MASK))的情况,意思是本区间不为段式映射(type->prot_l1 == 0)但该区间可以按1M对齐((addr | phys | length) & ~SECTION_MASK)记住这是一个原则,能按1M对齐的映射空间就按段式映射,不足1M的空间才需要二级映射

5pgd = pgd_offset_k(addr);,一级一级的看这个函数的实现,最后这个函数相当于(init_mm)->pgd + ((addr) >> 21) = (pgd_t *)0xc0004000 + (addr >> 21),这是在找这个虚拟地址在内核进程的页表中的位置,由前面已知内核进程的页表从虚拟地址0xc0004000开始,到0xc0008000结束,页表大小为0x4000,这个范围标识0-0xffffffff4G的范围,现在addr的值为0xc0000000,那么它在内核进程的页表中的位置可以算出来是0xc0004000 + 0x4000*3/4 = 0xc0007000,为什么是3/4?因为0xc00000000-0xffffffff4G的范围是整好3/4的位置即4G中的最后一个Gaddr值为0xc0000000,它右移21位值为0x600(pgd_t *)0xc0004000 + 0x600 = 0xc0007000,貌似很奇怪为什么不是0xc0004600?这是因为前面有强制类型转换(pgd_t *),而这个结构的定义是两个ulong,所以(pgd_t *)0xc0004000 + 0x600 =(pgd_t *)0xc0004000 + 0x600*8 = 0xc0007000,至于为什么addr要右移21位,以及为什么有强制类型转换(pgd_t *),一会再说先关注段式映射的情况;

6end = addr + length;end指的是要映射的虚拟地址的结尾,它的值为0xc0000000 + 0x10000000(256M) = 0xd0000000

7、至此,要映射的空间的虚拟地址起始值addr0xc0000000,虚拟地址结尾值end0xd0000000,长度为0x10000000,物理地址起始值为0,映射类型为变量type的值(暂不关心细节),接下来的do while循环是真正映射了:

8do {

        unsigned long next = pgd_addr_end(addr, end);

        alloc_init_section(pgd, addr, next, phys, type);

                   phys += next - addr;

        addr = next;

         } while (pgd++, addr != end);

第一行的意思是,只要不超过end,就获得下一个2MB的虚拟起始地址,所以传给alloc_init_sectionnext参数,要么与addr相差一整段(2MB),要么是end则不足一整段(2MB),我们这里的内存256M,不存在endaddr相差不足2M的情况;

进入函数alloc_init_section,它的参数分别是“一级页表(段页表)地址pgd、虚拟起始地址addr、虚拟结尾地址end(现在就是addr + 2MB)、物理起始地址phys、内存类型type”;

9pmd_t *pmd = pmd_offset(pgd, addr);,这个pmd_t结构就只是一个ulong大小了,这里函数pmd_offset的实现就是pmd = (pmd_t *)pgd,地址不变,但类型转变,意思很明显;

10、判断(((addr | end | phys) & ~SECTION_MASK) == 0),我们这里的都是2M对齐的,必然1M也对齐,底下的(addr & SECTION_SIZE)对于我们这里不会成立,我们这里都是2M对齐,即addr值的第21位一直都会是偶数;

11、下面的内容是配置段式页表的值和写页表:

do {

        *pmd = __pmd(phys | type->prot_sect);

                  phys += SECTION_SIZE;

         } while (pmd++, addr += SECTION_SIZE, addr != end);

    flush_pmd_entry(p);

第一行,页表的这个条目pmd,写入的值是什么,可见,把物理地址和type的映射方式(prot_sect)写进去了;

第二行,累加1M的物理地址值;

第三行,只要虚拟地址起始值addr再累加1M,没有超过end(这里是addr+2M),那么写下一个页表条目(pmd+1)的值,很明显,我们这里会再循环一次,即do里的内容总共运行过两次;

第四行,最终写入MMU,这个p指向pmd

先不要管上面为什么这么实现,先看结果,总共256M的物理内存,最后结果是:

页表条目索引

条目所在地址

页表填的内容

对应的虚拟地址

0xfff

0xc0008000

 

0xffffffff

……

……

……

……

0x3fc

0xc00073fc

0x10000000+type

0xd0000000

……

……

……

……

0xc05

0xc0007014

0x00500000+type

0xc0500000

0xc04

0xc0007010

0x00400000+type

0xc0400000

0xc03

0xc000700c

0x00300000+type

0xc0300000

0xc02

0xc0007008

0x00200000+type

0xc0200000

0xc01

0xc0007004

0x00100000+type

0xc0100000

0xc00

0xc0007000

0x00000000+type

0xc0000000

……

……

……

……

0x000

0xc0004000

 

0x00000000

上面就是对物理内存映射后的情况(红色部分),第一列是页表的索引,256M的物理内存的的映射部分在第0xc000x3fc部分,对应页表本身所在地址从0xc00070000xc00073fc,这部分页表填充的内容是物理内存和映射类型的或运算结果,它们实际上对应虚拟地址的0xc00000000xd0000000

如果把一个虚拟地址0xc1234567CPU去访问,那么CPU把它发给MMUMMU会根据已经建立的映射关系发现这个地址对应的是0x012这个段,然后把后面低20位的部分0x345670x012拼接起来,结果是0x01234567

事实上这里还有很多细节,比如MMU到底是怎么能够识别是0x012段的细节,这牵扯到arm硬件体系结构内容,如果不是特殊需要可不特别关心,关注到内核这步即可。

上面是物理内存页表创建的结果,但还有个问题没有说,就是arm页表在linux中的融合问题,这部分不理解将影响对全局的理解:

要知道,linux要实现高效的内存管理,是不可能按1M的区间管理的,这样很容易产生问题,前面说过这个问题,事实上linux是以4K为一页作为管理单位即粒度,这是怎么定的呢?在arch/arm/include/asm/page.h文件中规定宏PAGE_SHIFT12导致宏PAGE_SIZE40964K

既然是4K为一页,那么按理说linux需要描述4G虚拟内存的话,需要多少个这样的4K呢?很简单:2^20 * 2^12 = 4G,所以是2^201M,即页表的条目个数为1M个,每个条目占用4字节,即页表大小为4M,每个进程包括内核进程都需要一个内存页表,这就大量消耗物理内存在页表上;

所以这里将引入多级页表的概念,linux内核定义的标准是这样的:最高级pgd为页目录表,它找到每个进程mm-_struct结构的pgd成员,用它定位到下一级pmd;第二级pmd为中间页表,它定位到下一级pte;第三级pte是页表,它就能定位到哪个页了;最后虚拟地址的最后12位定位的是该页的偏移量;

为什么搞的这么麻烦?因为linux还要适配64位处理器,到那时是真的需要这么麻烦,因为否则页表占用空间太大了,所以内核必须搞的级数多一些。

那么arm呢,arm体系结构的MMU实际上支持两级页表,一级是刚才描述的段式映射即一级映射,再就是支持第二级映射,包括1K4K64K的页实际上使用的是4K页,这里就牵扯到arm页表机制和linux页表机制融合的问题;这里记住,arm的第一级页表条目数为4096个,对于4K页第二级目录条目个数为256个,一级二级条目都是每个条目4字节;

像这种物理的级数支持少的,砍掉中间目录pmd就可以,从本质看就是函数pmd_offset(pgd, addr)的实现是pgd,即可什么pmdarm-linux形同虚设。ARMlinux下二级分页如下:

虚拟地址——> PGD转换——> PTE转换——>物理地址

此外linux的内存管理中,有对页的置属性为“access”、“dirty”的需求,可是armMMU没有提供这种属性可以设置;

综合

阅读(2629) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~