2013-06-08 17:39:57 qianlong4526888 阅读数 22022
  • 多任务的裸机实现(下)

    通过本课程学习,让嵌入式学员对对整个嵌入式系统、CPU内部、操作系统都有一个全局的把握和认识,为后续的Linux内核、驱动开发等高阶课程打下良好的理论基础。掌握这些知识后,会让我们后续的课程更加轻松、学习效率...

    9064课时 0分钟 33人学习 王利涛
    免费试看


 

目录

文件修订记录...2

目录...3

1ARMv8存储管理...4

1.1Aarch64 Linux中的内存布局... 4

1.2AArch64的虚拟地址格式...4

1.2.14K页时的虚拟地址...4

1.2.264K页时的虚拟地址...5

2head.S页表建立过程分析...6

2.1页表建立函数__create_page_tables.6

2.1.1pgtbl       x25,x26, x24分析... 7

2.1.2MM_MMUFLAGS解释...8

2.1.3create_pgd_entry/create_block_map宏解释...8

3问题解答...12

3.1TLB打开之前,内存物理内存大小如何通知OS内核?...12

3.2PGD及PTE的填写过程...12

3.2.1map_mem()12

3.2.2create_mapping()12

3.2.3alloc_init_pud()13

3.2.4alloc_init_pmd()14

3.2.5set_pmd()15

3.2.6alloc_init_pte()15

3.2.7set_pte()15

3.3ARMv8三级页表情况下,全部把页表放到内存中是放不下的(1G大小),是不是部分放到硬盘中?...16

 

1 ARMv8存储管理

1.1 Aarch64 Linux中的内存布局

ARMv8架构可以支持48位虚拟地址,并配置成4级页表(4K页),或者3级页表(64K页)。而本Linux系统只使用39位虚拟地址(512G内核,512G用户),配置成3级页表(4K页)或者2级页表(64K页)

用户空间的地址63:39位都置零,内核空间地址63:39都置一,虚拟地址的第63位可以用来选择TTBRx。swapper_pg_dir只包含内核全局映射,用户的pgd包含用户(非全局)映射。swapper_pg_dir地址在TTBR1中,不会写入TTBR0中。

AArch64Linux内存布局:

Start                                 End                             Size                     Use

--------------------------------------------------------------------------------------------------

0000000000000000         0000007fffffffff      512GB          user

 

ffffff8000000000             ffffffbbfffcffff           ~240GB       vmalloc

 

ffffffbbfffd0000               ffffffbcfffdffff         64KB            [guardpage]

 

ffffffbbfffe0000               ffffffbcfffeffff         64KB            PCII/O space

 

ffffffbbffff0000               ffffffbcffffffff          64KB            [guard page]

 

ffffffbc00000000             ffffffbdffffffff       8GB               vmemmap

 

ffffffbe00000000             ffffffbffbffffff         ~8GB           [guard,future vmmemap]

 

ffffffbffc000000                     ffffffbfffffffff            64MB           modules

 

ffffffc000000000             ffffffffffffffff                  256GB          memory

1.2 AArch64的虚拟地址格式

1.2.1 4K页时的虚拟地址

 

1.2.2 64K页时的虚拟地址


 


2 head.S页表建立过程分析

2.1 页表建立函数__create_page_tables

该函数用于在内核启动时,为FDT(设备树)、内核镜像创建启动所必须的页表。等内核正常运行后,还需运行create_mapping为所有的物理内存创建页表,这将覆盖__create_page_tables所创建的页表。

内核开始运行时创建页表源文件:arm64/kernel/head.Sline345

/*

 * Setup the initial page tables. We only setup the barest amount which is

 * required to get the kernel running. The following sections are required:

 *   -identity mapping to enable the MMU (low address, TTBR0)

 *   -first few MB of the kernel linear mapping to jump to once the MMU has

 *    been enabled, including the FDT blob (TTBR1)

 */

__create_page_tables:

          pgtbl     x25, x26,x24                         //idmap_pg_dir and swapper_pg_dir addresses

 

          /*

           * 清空新建的两个页表TTBR0,TTBR1

           */

          mov       x0,x25

          add       x6,x26, #SWAPPER_DIR_SIZE

1:        stp       xzr,xzr, [x0], #16

          stp       xzr,xzr, [x0], #16

          stp       xzr,xzr, [x0], #16

          stp       xzr,xzr, [x0], #16

          cmp       x0,x6

          b.lo      1b

 

          ldr       x7,=MM_MMUFLAGS

 

          /*

           * Create the identity mapping.

           */

          add       x0, x25,#PAGE_SIZE                   // sectiontable address

          adr       x3, __turn_mmu_on            // virtual/physical address

          create_pgd_entry x25, x0, x3, x5, x6  //展开见1.1.3

          create_block_map x0, x7, x3, x5, x5, idmap=1

 

          /*

           * Map the kernel image (starting withPHYS_OFFSET).

           */

          add       x0,x26, #PAGE_SIZE                   //section table address

          mov       x5,#PAGE_OFFSET

          create_pgd_entry x26, x0, x5, x3, x6

          ldr       x6,=KERNEL_END - 1

          mov       x3,x24                               // physoffset

          create_block_map x0, x7, x3, x5, x6

 

          /*

           * Map the FDT blob (maximum 2MB; must bewithin 512MB of

           * PHYS_OFFSET).

           */

          mov       x3,x21                               // FDTphys address

          and       x3,x3, #~((1 << 21) - 1)    // 2MBaligned

          mov       x6,#PAGE_OFFSET

          sub       x5,x3, x24                           //subtract PHYS_OFFSET

          tst       x5,#~((1 << 29) - 1)                 //within 512MB?

          csel      x21,xzr, x21, ne            // zero the FDTpointer

          b.ne      1f

          add       x5,x5, x6                            // __va(FDTblob)

          add       x6,x5, #1 << 21             // 2MB forthe FDT blob

          sub       x6,x6, #1                            //inclusive range

          create_block_map x0, x7, x3, x5, x6

1:

          ret

ENDPROC(__create_page_tables)

 

 

2.1.1  pgtbl   x25, x26, x24分析

pgtbl是个宏,定义如下:

arm64/kernel/head.S line55

          .macro    pgtbl,ttb0, ttb1, phys

          add       \ttb1,\phys, #TEXT_OFFSET - SWAPPER_DIR_SIZE

          sub       \ttb0,\ttb1, #IDMAP_DIR_SIZE

          .endm

pgtbl               x25,x26, x24  //展开后如下

add                x26,x24, #TEXT_OFFSET -SWAPPER_DIR_SIZE

sub                x25,x26,#IDMAP_DIR_SIZE

 

其中各变量定义如下:

#defineSWAPPER_DIR_SIZE     (3 * PAGE_SIZE)

#defineIDMAP_DIR_SIZE                (2 *PAGE_SIZE)

说明:

1、关于TTBR0、TTBR1的介绍见ARM ARM 手册的Page B4-1708

2、x25中存TTBR0(TTBR0 holds the base address of translation table 0)的地址;

3、X26存TTBR1(TTBR1 holds the base address of translation table 1)地址;

4、X24 存PHYS_OFFSET,/* PHYS_OFFSET- the physical address of the start of memory. */

    #definePHYS_OFFSET      ({ memstart_addr; })

5、TEXT_OFFSET是Bootloader启动时传进来的参数,是内核Image加载时相对于RAM起始地址的偏移量

6、PAGE_OFFSEST:the virtual address of the start of the kernel image.

 

 

 


图1  pgtbl宏分析

 

2.1.2 MM_MMUFLAGS解释

在文件arm64/kernel/head.S line71:

/*

 * Initial memory map attributes.

 */

#ifndefCONFIG_SMP

#definePTE_FLAGS      PTE_TYPE_PAGE | PTE_AF

#definePMD_FLAGS     PMD_TYPE_SECT | PMD_SECT_AF

#else

#definePTE_FLAGS      PTE_TYPE_PAGE | PTE_AF |PTE_SHARED

#definePMD_FLAGS     PMD_TYPE_SECT | PMD_SECT_AF| PMD_SECT_S

#endif

 

#ifdefCONFIG_ARM64_64K_PAGES

#defineMM_MMUFLAGS   PTE_ATTRINDX(MT_NORMAL) |PTE_FLAGS

#defineIO_MMUFLAGS      PTE_ATTRINDX(MT_DEVICE_nGnRE)| PTE_XN | PTE_FLAGS

#else

#defineMM_MMUFLAGS   PMD_ATTRINDX(MT_NORMAL) |PMD_FLAGS

#defineIO_MMUFLAGS      PMD_ATTRINDX(MT_DEVICE_nGnRE)| PMD_SECT_XN | PMD_FLAGS

#endif

 

根据以上宏定义能明确,MM_MMUFLAGS就是根据你编译内核时选定的页大小(64K or 4K),设置MMU。

2.1.3 create_pgd_entry/create_block_map宏解释

1、create_pgd_entry

/*

 * Macro to populate the PGD for thecorresponding block entry in the next

 * level (tbl) for the given virtual address.

 *

 * Preserves:     pgd,tbl, virt

 * Corrupts:       tmp1,tmp2

 */

      .macro   create_pgd_entry,pgd, tbl, virt, tmp1, tmp2

      lsr   \tmp1,\virt, #PGDIR_SHIFT

      and \tmp1,\tmp1, #PTRS_PER_PGD - 1    // PGD index

      orr  \tmp2,\tbl, #3                   // PGD entrytable type

      str   \tmp2,[\pgd, \tmp1, lsl #3]

      .endm

 

根据以上定义,create_pgd_entry x25, x0, x3, x5, x6展开后如下:

lsr   x5, x3,# PGDIR_SHIFT      //X3中存放的是__turn_mmu_on的地址,右移PGDIR_SHIFT(30)位

and  x5, x5, #PTRS_PER_PGD – 1//将<38:30>置位

orr  x6, x0, #3         //x0存放PGD entry(即下级页表地址),低三位用于表项的有效位

str  x6, [ x25, x5, lsl #3] //将entry存放到TTBR0(x25)中偏移为x5左移3位(乘8,因为entry为8byte)的位置处。

为了便于理解,如下图所示:


 

                                                                                                                          图2  4K页面时48位虚拟地址组成


注意:上图中虚拟地址对应的表格名称是:

PGD:全局描述符表

PUD:折合到PGD中,Linux中不使用

PMD:页表中间描述符表

PTE:页表

Linux内核只使用了39位虚拟地址


 

 

图3  64位页表项格式

 

 

 

 

图4

同理,create_pgd_entry x26, x0, x5, x3, x6展开后如下:

lsr   x3, x5,#PGDIR_SHIFT //X5中存放的是PAGE_OFFSET= 0xffffffc000000000,右移PGDIR_SHIFT位存入X3

and  x3, x3,#PTRS_PER_PGD – 1//将<38:30>置位

orr  x6, x0, #3       //x0存放TTBR1指向的页的下一页,低三位用于表项的有效位,存入x6

str  x6, [ x26, x3,lsl #3] //将entry存放到TTBR0(x25)中偏移为x5左移3位的位置处

以上内容就是,填写TTBR1指向的页表中偏移为x3*8(因为一个entry是8byte)的页表项,内容为x6(即图4中x0所指的位置)

 

 

2、create_block_map

/*

 * Macro to populate block entries in the pagetable for the start..end

 * virtual range (inclusive).

 *

 * Preserves:     tbl,flags

 * Corrupts:       phys,start, end, pstate

 */

      .macro   create_block_map,tbl, flags, phys, start, end, idmap=0

      lsr   \phys,\phys, #BLOCK_SHIFT

      .if   \idmap

      and \start,\phys, #PTRS_PER_PTE - 1 // table index

      .else

      lsr   \start,\start, #BLOCK_SHIFT

      and \start,\start, #PTRS_PER_PTE - 1 // table index

      .endif

      orr  \phys,\flags, \phys, lsl #BLOCK_SHIFT     //table entry

      .ifnc       \start,\end

      lsr   \end,\end, #BLOCK_SHIFT

      and \end,\end, #PTRS_PER_PTE - 1          // tableend index

      .endif

9999:    str   \phys,[\tbl, \start, lsl #3]         // storethe entry

      .ifnc       \start,\end                          //ifnc:如果string1!=string2

      add \start,\start, #1                  // next entry

      add \phys,\phys, #BLOCK_SIZE         // next block

      cmp       \start,\end

      b.ls 9999b

      .endif

      .endm

 

根据以上宏定义,create_block_map x0, x7, x3,x5, x5, idmap=1,翻译后如下:

lsr  x3, x3, # BLOCK_SHIFT

and  x5, x3 # PTRS_PER_PTE – 1

orr  x3, x7, x3, lsl  # BLOCK_SHIFT

9999:

      str x3, [x0, x5, lsl #3]

 

同理,create_block_map x0, x7, x3, x5,x6展开后如下:

      lsr   x3,x3, #BLOCK_SHIFT

      lsr   x5,x5, #BLOCK_SHIFT

      and x5,x5, #PTRS_PER_PTE - 1  // table index

      orr  x3,x7, x3, lsl #BLOCK_SHIFT     // tableentry

      lsr   x6,x6, #BLOCK_SHIFT

      and x6,x6, #PTRS_PER_PTE - 1         // table endindex

9999:    str   x3,[x0, x5, lsl #3]            // store the entry

      add x5,x5, #1                   // next entry

      add x3,x3, #BLOCK_SIZE            // next block

      cmp       x5, x6

      b.ls 9999b

create_block_mapx0, x7, x3, x5, x6宏的作用就是创建内核镜像所有的映射关系

     

     

3 问题解答

3.1  TLB打开之前,内存物理内存大小如何通知OS内核?

Bootloader通过设备树(devicetree.dts文件)将物理内存起始地址及大小传给Linux 内核。物理内存的大小需要在bootloader即dts文件中写明。dts文件中内存声明如下:

      memory {

             device_type= "memory";

             reg= <0x00000000 0x80000000>;

      };

Reg字段:<地址 大小>

以上声明一段内存:从地址0x开始,大小为2G

 

3.2 PGD及PTE的填写过程

内核初始化时,会调用map_mem对所有内存建立页表,进行映射,函数执行流程是:

start_kernel()àsetup_arch()àpaging_init()àmap_mem()àcreate_mapping()

下面我们从map_mem()函数开始分析。

 

3.2.1 map_mem()

arm64/mm/mmu.cline254

staticvoid __init map_mem(void)

{

      struct memblock_region *reg;

 

      // 按照内存块进行映射,映射所有内存bank

      for_each_memblock(memory, reg) {

             phys_addr_t start = reg->base;

             phys_addr_t end = start +reg->size;

             if (start >= end)  //如果end不大于start 则退出

                    break;

             create_mapping(start,__phys_to_virt(start), end - start);//创建映射

      }

}

3.2.2 create_mapping()

arm64/mm/mmu.cline 230

/*

 * Create the page directory entries and anynecessary page tables for the

 * mapping specified by 'md'.

 */

staticvoid __init create_mapping(phys_addr_t phys, unsigned long virt,

                             phys_addr_t size)

{

      unsigned long addr, length, end, next;

      pgd_t *pgd;

 

      if (virt < VMALLOC_START) {          //对虚拟地址进行检查

             pr_warning("BUG: not creatingmapping for 0x%016llx at 0x%016lx - outside kernel range\n",

                       phys, virt);

             return;

      }

 

      addr = virt & PAGE_MASK;  // PAGE_MASK=(~(PAGE_SIZE-1)),将虚拟地址的低位偏移掩掉

 

//计算需要映射的内存长度,对齐到下一页的边界

      length= PAGE_ALIGN(size + (virt & ~PAGE_MASK));

 

//一级数组中addr对应的段在init_mm->pgd的下标,即在内核的pgd中获得一个entry

      pgd = pgd_offset_k(addr);

      end = addr + length; //计算需要映射的结束地址

      do {

             next = pgd_addr_end(addr, end);//获得下一段开始地址

 

//申请并初始化一个段

//段码,虚拟地址,结束地址,物理地址,内存类型

             alloc_init_pud(pgd,addr, next, phys);

             phys += next - addr;//物理地址累加

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

}

3.2.3 alloc_init_pud()

arm64/mm/mmu.cline213

staticvoid __init alloc_init_pud(pgd_t *pgd, unsigned long addr,

                             unsigned long end, unsigned long phys)

{

//由于Linux内核不使用pud,所以pud折如pgd,这里的pud=pgd  

pud_t *pud = pud_offset(pgd, addr);

      unsigned long next;

 

      do {

             next = pud_addr_end(addr, end);

             alloc_init_pmd(pud, addr, next, phys);

             phys += next - addr;

      } while (pud++, addr = next, addr != end);

}

 

3.2.4 alloc_init_pmd()

arm64/mm/mmu.cline 187

staticvoid __init alloc_init_pmd(pud_t *pud, unsigned long addr,

                             unsigned long end, phys_addr_t phys)

{

      pmd_t *pmd;

      unsigned long next;

 

      /*

       *Check for initial section mappings in the pgd/pud and remove them.

       */

      if (pud_none(*pud) || pud_bad(*pud)) {

             pmd = early_alloc(PTRS_PER_PMD *sizeof(pmd_t));

             pud_populate(&init_mm, pud,pmd);

      }

      pmd = pmd_offset(pud, addr);

      do {

             next = pmd_addr_end(addr, end);

             /* try section mapping first */

//addr,end, phys都是段对齐,则直接进行段映射(大部分情况下应该是满足条件),否则需要进一步填写PTE

//段大小在不同页大小情况下不同,在3级页表时,2M;在2级页表时,512M

             if (((addr | next | phys) &~SECTION_MASK) == 0)

//将物理地址及一些段属性存放到pmd中

                    set_pmd(pmd, __pmd(phys |prot_sect_kernel));

             else

                    alloc_init_pte(pmd, addr, next,__phys_to_pfn(phys));

             phys += next - addr;

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

}

 

3.2.5 set_pmd()

arm64/include/asmline 195

staticinline void set_pmd(pmd_t *pmdp, pmd_t pmd)

{

      *pmdp = pmd;

      dsb();//同步数据进RAM(由于有cache机制,所以数据操作的时候是先保存在cache中的,这里是强制将数据从cache中刷进RAM中)

}

3.2.6 alloc_init_pte()

arm64/mm/mmu.cline 169

staticvoid __init alloc_init_pte(pmd_t *pmd, unsigned long addr,

                             unsigned long end, unsigned long pfn)

{

      pte_t *pte;

 

      if (pmd_none(*pmd)) {

             pte = early_alloc(PTRS_PER_PTE *sizeof(pte_t));

             __pmd_populate(pmd, __pa(pte),PMD_TYPE_TABLE);

      }

      BUG_ON(pmd_bad(*pmd));

 

      pte = pte_offset_kernel(pmd, addr);

      do {

             set_pte(pte,pfn_pte(pfn, PAGE_KERNEL_EXEC));

             pfn++;

      } while (pte++, addr += PAGE_SIZE, addr !=end);

}

3.2.7 set_pte()

arm64/include/asm/pgtable.hline 150

staticinline void set_pte(pte_t *ptep, pte_t pte)

{

      *ptep = pte;

}

3.3 ARMv8三级页表情况下,全部把页表放到内存中是放不下的(1G大小),是不是部分放到硬盘中?

答:ARMv8 OS是根据内存大小建立页表的,例如当物理内存只有1G时,1G=230=218 *4K(页大小),即需要218个PTE,每个页表512个PTE,所以需要29个PT表,共需要29*4K=2M大小的页表(事实上还需要计算PGD和PMD,各4K)。

所以1G内存空间,需要2M大小页表;

2G——4M

100G——200M

512G——1G页表

相对内存大小来说,页表大小还是很小的。

注意:如果是64位机器,就不存在高端内存一说,因为地址空间很大很大,属于内核的空间也不止1G,在aarch64 linux中内核空间是512G,内核完全可以直接管理所有内存。

 

 

 

2019-04-26 18:51:41 geshifei 阅读数 849
  • 多任务的裸机实现(下)

    通过本课程学习,让嵌入式学员对对整个嵌入式系统、CPU内部、操作系统都有一个全局的把握和认识,为后续的Linux内核、驱动开发等高阶课程打下良好的理论基础。掌握这些知识后,会让我们后续的课程更加轻松、学习效率...

    9064课时 0分钟 33人学习 王利涛
    免费试看

 

背景:

在看内核代码初始化arm页表的时候,发现linux内核做了一些特殊处理,引入了硬件页表、linux页表的概念,这篇文章描述为何需要这样处理以及具体的实现方式。

原因:

linux中ARM32采用2级页表映射方式,MMU映射过程如下:

                                                    图1  ARM MMU映射过程

从图1可以得知,ARM MMU页表(称为硬件页表或hw pt)如下:

页表中的每一项称为一个entry,entry存放的是物理地址值,PGD entry值指向2级页表(PTE页表),PTE entry值指向物理页。

由于以下两个原因,linux代码对图2的映射过程做了一些调整:

1)PTE entry中的一些低bit位被硬件使用了,没有linux需要的“accessed”、“dirty”等标志位。

参考内核代码注释: Hardware-wise, we have a two level page table structure, where the first level has 4096 entries, and the second level has 256 entries.  Each entry is one 32-bit word.  Most of the bits in the second level entry are used by hardware, and there aren't any "accessed" and "dirty" bits。

2)linux希望PTE页表本身也是一个页面大小。

参考内核代码注释:However, Linux also expects one "PTE" table per page, and at least a "dirty" bit.,但图1表明PTE页表是256*4 byte=1k大小。

解决方案

针对上面提到的问题,linux做了一些处理,使内核中实现的页表能够满足硬件需求,最终的arm页表见图4。

对于图4,解释如下:

1)软件实现必须符合硬件要求。ARM要求4096个PGD entry、256个PTE entry。

解决:PGD每个entry为8 bytes,定义为pmdval_t pgd[2],故共2048*2=4096 PGD entry。ARM MMU用va的bit[31,20](见图1)在PGD 4096项中找到对应的entry,每个entry指向一个hw页表(见图4中pmdp)。每一个hw页表有256个entry,ARM MMU用va的bit[19,12]在hw页表中找到对应的entry。所以从硬件角度看,linux实现的arm页表,完全符合硬件要求。

2)Linux需要 "accessed" and "dirty"位。

解决:从图3中可以看出,PTE entry的低位已经被硬件占用,所以只能再复制出一份页表(称为linux页表或linux pt),图4的hw pt 0对应Linux pt 0,linux页表的低bit位被linux系统用来提供需要的 "accessed" and "dirty"位。hw pt由MMU使用,linux pt由操作系统使用。

3)Linux期望PTE页表占用1个page。

解决:ARM的hw pt为256*4 bytes=1k,不满一个page大小。内核代码在实现上采用了一个小技巧,让一个PGD entry映射2个连续的hw pt,同时将对应的2个linux pt也组织在一起,共1k*4=4k。

因为linux代码让PGD一次映射2个hw pt,所以软件需要做一些处理来实现这个目的。软件定义PGD表项为pmdval_t pgd[2],pgd[i]指向一个hw pt,所以PGD表项一共有4096/2=2048项,也就是说需要用bit[31,21]来寻址这2048项,所以pgtable-2level.h中定义了:#define PGDIR_SHIFT 21 (注意,图1中PGD偏移20bit,那是给硬件MMU用的,跟我们这里的软件偏移没有关系)。

 

上面部分内容,可以结合代码来仔细品味(从__create_mapping函数看起),代码路径:

start_kernel-->setup_arch-->paging_init-->map_lowmem->

create_mapping-->__create_mapping-->alloc_init_pud-->alloc_init_pmd

-->alloc_init_pte

2013-08-08 15:32:38 u010246947 阅读数 0
  • 多任务的裸机实现(下)

    通过本课程学习,让嵌入式学员对对整个嵌入式系统、CPU内部、操作系统都有一个全局的把握和认识,为后续的Linux内核、驱动开发等高阶课程打下良好的理论基础。掌握这些知识后,会让我们后续的课程更加轻松、学习效率...

    9064课时 0分钟 33人学习 王利涛
    免费试看

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没有提供这种属性可以设置;

综合各种原因,最终arm-linux假装第一级目录只有2048个条目,但其实每个条目是2ulong大小即8字节,所以最终设置MMU的还是4096个条目,只是每访问1pgd条目将可以访问到2pte条目,linux为了实现其内存管理功能又在后面加上2个对应的假pte表,这个假pte表专门给linux内核代码自己用的,不会影响arm硬件(事实上还有一个重要原因是,linux要求pte表长度为4K即一页),在arch/arm/include/asm/pgtable.h中:

*    pgd             pte

 * |        |

 * +--------+ +0

 * |        |-----> +------------+ +0

 * +- - - - + +4    |  h/w pt 0  |

 * |        |-----> +------------+ +1024

 * +--------+ +8    |  h/w pt 1  |

 * |        |       +------------+ +2048

 * +- - - - +       | Linux pt 0 |

 * |        |       +------------+ +3072

 * +--------+       | Linux pt 1 |

 * |        |       +------------+ +4096

这个东西可以看看英文注释,比较容易懂。

看懂这个就很容易弄懂所有的创建内存映射的内容了,像为什么PTRS_PER_PTE定义为512、为什么PTRS_PER_PGD2048、为什么PGDIR_SHIFT21等等,下面是完全源码注释:

void __init paging_init(struct machine_desc *mdesc)

{

         void *zero_page;

    /*根据不同的arm版本,设置不同mem_type*/

         build_mem_type_table();

    /*判断各个bank是否有highmem即高端内存,根据是否存在高端内存置位或复位各个bankhighmem

      同时检查是否存在某bank的内存与vmalloc区域重合现象,如存在则需要去除(通过削减该banksize的方式)*/

         sanity_check_meminfo();

    /*清除临时的内存映射(不包括vmalloc_end以上的高端内存区域)*/

         prepare_page_table();

    /*为主(物理)内存创建映射;建立bootmem分配器;初始化重要全局变量*/

         bootmem_init();

    /*为设备I/O内存创建映射,附带首先要清除vmalloc_end以上的高端内存区域的临时页表表项*/

         devicemaps_init(mdesc);

    /*对于marvell,没有高端内存,无需关心,

      对于高端内存映射初始化,也是从bootmem获取页表表项占用的空间,

      大小为1(4K),并在表项中填入表项地址值和属性内容即可*/

         kmap_init();

 

    /*给全局变量top_pmd赋值,值为0xffff0000在一级页表中的表项位置,

      地址0xffff00004G之间为copy_user_page/clear_user_page等函数使用*/

         top_pmd = pmd_off_k(0xffff0000);

 

         /*

          * allocate the zero page.  Note that this always succeeds and

          * returns a zeroed result.

          */

         /*bootmem分配器获取一页物理内存,转换为虚拟地址后赋给zero_page

      把对应的物理页地址赋值给empty_zero_page并刷新该页*/

         zero_page = alloc_bootmem_low_pages(PAGE_SIZE);

         empty_zero_page = virt_to_page(zero_page);

         flush_dcache_page(empty_zero_page);

}

***************************************************************************

/*清除汇编阶段创建的临时页表的全部的段映射设置(16K一级页表全部写0)*/

static inline void prepare_page_table(void)

{

         unsigned long addr;

 

         /*

          * Clear out all the mappings below the kernel image.

          */

         /*MODULES_VADDR(模块区)值为(PAGE_OFFSET - 16*1024*1024)即内核下16MB,其实就是内核以下的映射,

      前两个for循环将清除内核空间以下的映射

      pmd_clear是将()中的页表项地址中的Cache无效,以及清除段开始的两个字节

      pmd_off_k为获取addr在一级页表中的位置

      要注意pmd_clear的实现,注意传进来的地址是什么,为什么操作其后两个成员*/

         for (addr = 0; addr < MODULES_VADDR; addr += PGDIR_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 + PGDIR_SIZE - 1) & PGDIR_MASK;

#endif

    /*再完成模块区MODULES_VADDR到内核起始的这16MB的清除*/

         for ( ; addr < PAGE_OFFSET; addr += PGDIR_SIZE)

                   pmd_clear(pmd_off_k(addr));

 

         /*

          * Clear out all the kernel space mappings, except for the first

          * memory bank, up to the end of the vmalloc region.

          */

         /*从物理内存对应的虚拟起始地址开始,直到VMALLOC区结尾,清除*/

         for (addr = __phys_to_virt(bank_phys_end(&meminfo.bank[0]));

              addr < VMALLOC_END; addr += PGDIR_SIZE)

                   pmd_clear(pmd_off_k(addr));

}

***************************************************************************

/*为主(物理)内存创建映射;建立bootmem分配器;初始化重要全局变量*/

void __init bootmem_init(void)

{

    /*对于marvell,此时meminfo已初始化,start = 0size = 128MBnr_banks = 1,其他应该是均为0*/

         struct meminfo *mi = &meminfo;

         unsigned long min, max_low, max_high;

         int node, initrd_node;

 

         sort(&mi->bank, mi->nr_banks, sizeof(mi->bank[0]), meminfo_cmp, NULL);

 

         /*

          * Locate which node contains the ramdisk image, if any.

          */

         /*绝大多数情况下,无用*/

         initrd_node = check_initrd(mi);

 

         max_low = max_high = 0;

 

         /*

          * Run through each node initialising the bootmem allocator.

          */

         /*查看for_each_node的实现,也就理解了底下的代码是什么意思,如果有多于一个的内存node

      如何判断内存node个数?就是宏NODES_SHIFT(其引申宏MAX_NUMNODES,单node时这两个宏值均为1)

      在单node情况下,就是个for(node = 0 node == 0node = 1){底下代码}的结果,即底下代码只执行一次*/

         for_each_node(node) {

                   unsigned long node_low, node_high;

 

        /*由该bank参数,给minnode_lownode_high参数赋值startendend,正常情况下:

          min: start,物理的页地址起始,0

          node_low: end,物理的页地址结尾,0x8000

          node_high: end,,物理的页地址结尾,0x8000*/

                   find_node_limits(node, mi, &min, &node_low, &node_high);

 

        /*max_low = max_high = node_low = node_high = 0x8000*/

                   if (node_low > max_low)

                            max_low = node_low;

                   if (node_high > max_high)

                            max_high = node_high;

 

                   /*

                    * If there is no memory in this node, ignore it.

                    * (We can't have nodes which have no lowmem)

                    */

                   /*这个node里面实际没有物理内存,返回*/

                   if (node_low == 0)

                            continue;

        /*为主(物理)内存的指定节点创建映射,

          node = 0mimeminfo的地址,min = 0node_low = 0x8000*/

        /*为主(物理)内存的指定节点node创建映射,start_pfn是物理起始地址,end_pfn是物理结尾地址

          这时mimeminfonode成员等于0highmem0nr_banks1

          node = 0mimeminfo的地址,start_pfn = 0end_pfn = 0x8000

          此外,最重要的是建立了bootmem分配器,在mm_init初始化伙伴系统之前用于分配物理内存的工具,

          这里将所有node的每个bank都做了映射并设为可用,并且把bootmem自身的bitmap分配表设为已用*/

                   bootmem_init_node(node, mi, min, node_low);

 

                   ……………………后面的是和bootmem分配器相关,下一专题讨论

***************************************************************************

/*为主(物理)内存的指定节点node创建映射,start_pfn是物理起始地址,end_pfn是物理结尾地址

  这时mimeminfonode成员等于0highmem0nr_banks1

  node = 0mimeminfo的地址,start_pfn = 0end_pfn = 0x8000

  此外,最重要的是建立了bootmem分配器,在mm_init初始化伙伴系统之前用于分配物理内存的工具,

  这里将所有node的每个bank都做了映射并设为可用,并且把bootmem自身的bitmap分配表设为已用*/

static void __init bootmem_init_node(int node, struct meminfo *mi,

         unsigned long start_pfn, unsigned long end_pfn)

{

         unsigned long boot_pfn;

         unsigned int boot_pages;

         pg_data_t *pgdat;

         int i;

 

         /*

          * Map the memory banks for this node.

          */

         /*就是对该node的每个bank进行遍历循环*/

         for_each_nodebank(i, mi, node) {

                   struct membank *bank = &mi->bank[i];

 

        /*为一个Bank创建映射*/

                   if (!bank->highmem)

                            map_memory_bank(bank);

         }

………………………………后面的是bootmem分配器相关,下一专题讨论

***************************************************************************

/*arm体系的创建页表,为一个Bank创建映射*/

static inline void map_memory_bank(struct membank *bank)

{

#ifdef CONFIG_MMU

         struct map_desc map;

 

    /*物理页地址,对于marvell,为0*/

         map.pfn = bank_pfn_start(bank);

    /*虚拟地址,对于marvell,为0xc0000000*/

         map.virtual = __phys_to_virt(bank_phys_start(bank));

    /*长度,对于marvell,为128MB*/

         map.length = bank_phys_size(bank);

    /*类型*/

         map.type = MT_MEMORY;

    /*实际开始创建页表,为一个物理存储空间创建映射*/

         create_mapping(&map);

#endif

}

***************************************************************************

/*实际开始创建页表,为一个物理存储空间创建映射*/

void __init create_mapping(struct map_desc *md)

{

         unsigned long phys, addr, length, end;

         const struct mem_type *type;

         pgd_t *pgd;

 

    /*虚拟地址不是中断表地址并且在用户区(0~3G),出错*/

         if (md->virtual != vectors_base() && md->virtual < TASK_SIZE) {

                   printk(KERN_WARNING "BUG: not creating mapping for "

                          "0x%08llx at 0x%08lx in user region\n",

                          __pfn_to_phys((u64)md->pfn), md->virtual);

                   return;

         }

 

    /*内存类型为IO型或ROM并且虚拟地址为低端内存申请区(3G~3G + 768MB),出错*/

         if ((md->type == MT_DEVICE || md->type == MT_ROM) &&

             md->virtual >= PAGE_OFFSET && md->virtual < VMALLOC_END) {

                   printk(KERN_WARNING "BUG: mapping for 0x%08llx at 0x%08lx "

                          "overlaps vmalloc space\n",

                          __pfn_to_phys((u64)md->pfn), md->virtual);

         }

 

    /*mem_typestype成员在前面配置(build_mem_type_table)

      说明这个区间所属的域,以及是否可读、写、可高速缓存等属性

      得到映射类型和属性

      根据内存类型获取内存信息,主要是CP15中的内存操作权限*/

         type = &mem_types[md->type];

 

         /*

          * Catch 36-bit addresses

          */

         /*页表号>1M,超过4G,实际上这个情况绝大多数情况下不会出现*/

         if (md->pfn >= 0x100000) {

                   create_36bit_mapping(md, type);

                   return;

         }

    /*对于marvell,内核的虚拟起始地址addr0xc0000000PAGE_MASK = 0xfffff000 = ~(4095)*/

         addr = md->virtual & PAGE_MASK;

    /*对于marvellphys0*/

         phys = (unsigned long)__pfn_to_phys(md->pfn);

    /*按页对齐计算长度(主要是补齐未按页对齐的多余的部分),对于marvell,为128MB*/

         length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK));

 

    /*段类型为0(意为不是段映射)但却能按1MB对齐,前后矛盾,出错*/

         if (type->prot_l1 == 0 && ((addr | phys | length) & ~SECTION_MASK)) {

                   printk(KERN_WARNING "BUG: map for 0x%08lx at 0x%08lx can not "

                          "be mapped using pages, ignoring.\n",

                          __pfn_to_phys(md->pfn), addr);

                   return;

         }

 

    /*下面等于 = pgd_offset(&init_mm, addr) = (init_mm)->pgd + pgd_index(addr)

                                            = (init_mm)->pgd + ((addr) >> 21)

                                            = (pgd_t *)0xc0004000 + (addr >> 21)

      (init_mm)->pgd = swapper_pg_dir,该变量在head.S中定义,定义为KERNEL_RAM_VADDR - 0x4000 = 0xc0004000

      含义是:得到虚拟地址addr在一级页表PGD中的位置,即addr对应的段在init_mm->pgd的下标*/

         pgd = pgd_offset_k(addr);

    /*end是结尾地址*/

         end = addr + length;

         do {

        /*只要不超过end,就获得下一段(2MB)的虚拟起始地址

          所以传给alloc_init_sectionnext参数,要么与addr相差一整段(2MB),要么是end则不足一整段(2MB)*/

                   unsigned long next = pgd_addr_end(addr, end);

 

        /*申请并初始化一个段,参数分别是:

          一级页表(段页表)地址、虚拟起始地址、虚拟结尾地址(要么是addr + 2MB,要么是end)、物理起始地址、内存类型*/

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

 

        /*物理地址累加*/

                   phys += next - addr;

        /*更新addr为下一段的虚拟起始地址*/

                   addr = next;

         } while (pgd++, addr != end);   /*下一段,判断条件就是虚拟起始地址addr不等于虚拟结尾地址end*/

}

***************************************************************************

/*一级页表(段页表)地址、虚拟起始地址、虚拟结尾地址(要么是addr + 2MB,要么是end)、物理起始地址、内存类型*/

static void __init alloc_init_section(pgd_t *pgd, unsigned long addr,

                                           unsigned long end, unsigned long phys,

                                           const struct mem_type *type)

{

    /*这里就要真正的进行映射了,可能是直接段映射(end - addr = 2MB),也有可能是二级映射(end - addr < 2MB)

      但无论哪种映射,都要实际写一级页表的表项,由于pgd的处理是一个表项实际带两个条目(2ulong),但最终表项必须每个都要写入,

      所以到了这里,要用pmd(1ulong)定位一级页表表项了,具体说来如下:

      对于段映射,底下的"if (addr & SECTION_SIZE)",就是为了区分是哪个1MB(奇数段or偶数段)

      对于二级映射,在函数alloc_init_pte中会把这21MB的段都写入二级页表地址*/

         pmd_t *pmd = pmd_offset(pgd, addr);

 

         /*

          * Try a section mapping - end, addr and phys must all be aligned

          * to a section boundary.  Note that PMDs refer to the individual

          * L1 entries, whereas PGDs refer to a group of L1 entries making

          * up one logical pointer to an L2 table.

          */

         /*当大小和地址1MB(掩码0x000fffff)对齐时,使用段映射*/

         if (((addr | end | phys) & ~SECTION_MASK) == 0) {

                   pmd_t *p = pmd;

 

        /*奇数段,段的处理总是一次处理2(2MB)SECTION_SIZE = 0x00100000

          之所以一次处理2段,是因为数据结构pgd_t是两个ulong成员的数组的类型,即它大小是8个字节,

          上面的pmd直接取pgd的值,但它是ulong型即4个字节大小

          //重要知识: arm硬件实际的一级页表共4096个条目,其index0-0xfff(4095),每个条目占4个字节,

          对于marvell来说,这个一级页表自身的虚拟地址范围是[0xc00040000xc0007fff]共占用16K空间,形象的表示如下图:

          pgd_t         pmd-t       index    vaddr       value

          0xc0004000    0xc0004000  0x000    0xc0004000  XXXXX

          0xc0004000    0xc0004004  0x001    0xc0004004  XXXXX

          0xc0004008    0xc0004008  0x002    0xc0004008  XXXXX

          0xc0004000    0xc000400c  0x003    0xc000400c  XXXXX

          0xc0004010    0xc0004010  0x004    0xc0004010  XXXXX

          0xc0004000    0xc0004014  0x005    0xc0004014  XXXXX

          ..........

          对于pgd,它就是按2个段(2MB)的幅度递增,这样将无法只能定位到偶数的段,

          pmd则在这里通过判断第21位判断是否是奇数,来定位到奇数的段

          按说这种情况发生概率不大,因为它发生的条件是: pgd参数是某个两段(2MB)的起始,但addr参数却是这2MB的后1MB

          这时确实需要后移pmd,但应该发生概率较低*/

                   if (addr & SECTION_SIZE)

                            pmd++;

 

        /*段页表的值value(即一级页表的值,代码中的*pmd)如果是按段映射则是物理地址的值,如果是按二级映射则是二级映射表地址,

          if这里,先看段映射的情况,pmd指向的地址虽然和pgd一样,但它是ulong

          二级映射的情况将在下面的alloc_init_pte中注释*/

                   do {

            /*(物理地址 |段属性(prot_sect))的值填入段描述符,可见,作为段映射的一级页表,写入的是物理地址(及其属性)

              这就是在写段页表(一级页表)的表项内容了!!! */

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

            /*下一段(1MB)*/

                            phys += SECTION_SIZE;

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

 

        /*填充一层页表的对应项,本质是将内容强行刷进RAM

          实际写段映射(一级映射)表项了!!!*/

                   flush_pmd_entry(p);

         }

    /*对于不满1MB的部分,不能直接段映射(一级映射),需要二级映射,

      alloc_init_pte就是完成二级映射!!!

      下面的注释是在说为什么这里不用循环,因为能整段映射的已经在上面的if中映射了,需要进入这里的肯定是不足1MB的区段

      需要注意的是,在初始化为物理内存node分配页表(bootmem_init_node->map_memory_bank->create_mapping)时,

      不会调用到这里,因为那些都是段映射!!!

      */

    else {

                   /*

                    * No need to loop; pte's aren't interested in the

                    * individual L1 entries.

                    */

                   alloc_init_pte(pmd, addr, end, __phys_to_pfn(phys), type);

         }

}

***************************************************************************

static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,

                                       unsigned long end, unsigned long pfn,

                                       const struct mem_type *type)

{

         pte_t *pte;

 

    /*这里是做二级页表映射,即让一级页表表项pmd中写入二级页表的地址值

      这里是先检查这个一级页表表项是否已写入了东西,如果没有内容(NULL),则说明需要创建一个二级页表

      可见,二级页表不同于一级页表,它是动态创建释放的,在slab分配器可用之前,使用bootmem分配器创建*/

         if (pmd_none(*pmd)) {

        /*为二级页表pte分配4KB空间,参数值为4096*/

                   pte = alloc_bootmem_low_pages(2 * PTRS_PER_PTE * sizeof(pte_t));

        /*写一级页表表项pmd,值是二级页表表项的物理地址及其属性*/

                   __pmd_populate(pmd, __pa(pte) | type->prot_l1);

         }

 

    /*定位到二级页表pte的虚拟地址*/

         pte = pte_offset_kernel(pmd, addr);

         do {

                   set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);

                   pfn++;

         } while (pte++, addr += PAGE_SIZE, addr != end);

}

***************************************************************************

/*为设备I/O内存创建映射*/

static void __init devicemaps_init(struct machine_desc *mdesc)

{

         struct map_desc map;

         unsigned long addr;

         void *vectors;

 

         /*

          * Allocate the vector page early.

          */

         /*bootmem分配器中获取一页(4K)物理空间,赋给vectors,这是用于中断向量的!!!

      注意下获取到的已经转换为虚拟地址*/

         vectors = alloc_bootmem_low_pages(PAGE_SIZE);

 

    /*这里还有一步重要内容,就是彻底清除一级页表项,前面的prepare_page_table已经将0-VMALLOC_END的页表项全部删除,

      这里把VMALLOC_END4G的页表项全部清除

      为什么在这里才清除?因为这里要搞的arm中断向量映射,是处在高端内存区域,所以这里清除比较恰当*/

         for (addr = VMALLOC_END; addr; addr += PGDIR_SIZE)

                   pmd_clear(pmd_off_k(addr));

 

         /*

          * Map the kernel if it is XIP.

          * It is always first in the modulearea.

          */

#ifdef CONFIG_XIP_KERNEL

         map.pfn = __phys_to_pfn(CONFIG_XIP_PHYS_ADDR & SECTION_MASK);

         map.virtual = MODULES_VADDR;

         map.length = ((unsigned long)_etext - map.virtual + ~SECTION_MASK) & SECTION_MASK;

         map.type = MT_ROM;

         create_mapping(&map);

#endif

 

         /*

          * Map the cache flushing regions.

          */

#ifdef FLUSH_BASE

         map.pfn = __phys_to_pfn(FLUSH_BASE_PHYS);

         map.virtual = FLUSH_BASE;

         map.length = SZ_1M;

         map.type = MT_CACHECLEAN;

         create_mapping(&map);

#endif

#ifdef FLUSH_BASE_MINICACHE

         map.pfn = __phys_to_pfn(FLUSH_BASE_PHYS + SZ_1M);

         map.virtual = FLUSH_BASE_MINICACHE;

         map.length = SZ_1M;

         map.type = MT_MINICLEAN;

         create_mapping(&map);

#endif

 

         /*

          * Create a mapping for the machine vectors at the high-vectors

          * location (0xffff0000).  If we aren't using high-vectors, also

          * create a mapping at the low-vectors virtual address.

          */

         /*实际开始创建页表,为一个物理存储空间创建映射*/

    /*先获取vectors的物理页地址*/

         map.pfn = __phys_to_pfn(virt_to_phys(vectors));

    /*0xffff0000arm默认的中断向量所在页,长度为1页,不可轻易修改!

      注意,这里映射大小为4K,小于1M,将使用二级映射!*/

         map.virtual = 0xffff0000;

         map.length = PAGE_SIZE;

         map.type = MT_HIGH_VECTORS;

         create_mapping(&map);

 

         if (!vectors_high()) {

                   map.virtual = 0;

                   map.type = MT_LOW_VECTORS;

                   create_mapping(&map);

         }

 

         /*

          * Ask the machine support to map in the statically mapped devices.

          */

         /*这个很重要,是移植相关重要内容,执行machine_desc结构变量中定义的map_io成员,这里的marvell为函数mv_map_io,分为7个部分*/

         if (mdesc->map_io)

                   mdesc->map_io();

 

         /*

          * Finally flush the caches and tlb to ensure that we're in a

          * consistent state wrt the writebuffer.  This also ensures that

          * any write-allocated cache lines in the vector page are written

          * back.  After this point, we can start to touch devices again.

          */

         /*刷新内存MMU的页表*/

         local_flush_tlb_all();

         flush_cache_all();

}

***************************************************************************

/*对于marvell,没有高端内存,无需关心,

  对于高端内存映射初始化,也是从bootmem获取页表表项占用的空间,

  大小为1(4K),并在表项中填入表项地址值和属性内容即可*/

static void __init kmap_init(void)

{

    /*对于marvell,没有高端内存,无需关心,

      对于高端内存映射初始化,也是从bootmem获取页表表项占用的空间,

      大小为1(4K),并在表项中填入表项地址值和属性内容即可*/

#ifdef CONFIG_HIGHMEM

         pmd_t *pmd = pmd_off_k(PKMAP_BASE);

         pte_t *pte = alloc_bootmem_low_pages(2 * PTRS_PER_PTE * sizeof(pte_t));

         BUG_ON(!pmd_none(*pmd) || !pte);

         __pmd_populate(pmd, __pa(pte) | _PAGE_KERNEL_TABLE);

         pkmap_page_table = pte + PTRS_PER_PTE;

#endif

}

 

最后,附上在源码加入调试打印的调试结果,调试方式是在段式映射函数alloc_init_section和二级映射函数alloc_init_pte中加入打印信息,记录都有哪些东西被映射,level 1标识段式映射,level 2标识二级映射:

//前面有些没有打印出来,下面是从物理内存映射半截开始:

level 1: pmd: c00072ec, addr: cbb00000, phy: 0bc00000, end: cbc00000, addr+secsize: cbc00000

level 1: pmd: c00072f0, addr: cbc00000, phy: 0bd00000, end: cbe00000, addr+secsize: cbd00000

level 1: pmd: c00072f4, addr: cbd00000, phy: 0be00000, end: cbe00000, addr+secsize: cbe00000

level 1: pmd: c00072f8, addr: cbe00000, phy: 0bf00000, end: cc000000, addr+secsize: cbf00000

level 1: pmd: c00072fc, addr: cbf00000, phy: 0c000000, end: cc000000, addr+secsize: cc000000

level 1: pmd: c0007300, addr: cc000000, phy: 0c100000, end: cc200000, addr+secsize: cc100000

level 1: pmd: c0007304, addr: cc100000, phy: 0c200000, end: cc200000, addr+secsize: cc200000

level 1: pmd: c0007308, addr: cc200000, phy: 0c300000, end: cc400000, addr+secsize: cc300000

level 1: pmd: c000730c, addr: cc300000, phy: 0c400000, end: cc400000, addr+secsize: cc400000

level 1: pmd: c0007310, addr: cc400000, phy: 0c500000, end: cc600000, addr+secsize: cc500000

level 1: pmd: c0007314, addr: cc500000, phy: 0c600000, end: cc600000, addr+secsize: cc600000

level 1: pmd: c0007318, addr: cc600000, phy: 0c700000, end: cc800000, addr+secsize: cc700000

level 1: pmd: c000731c, addr: cc700000, phy: 0c800000, end: cc800000, addr+secsize: cc800000

level 1: pmd: c0007320, addr: cc800000, phy: 0c900000, end: cca00000, addr+secsize: cc900000

level 1: pmd: c0007324, addr: cc900000, phy: 0ca00000, end: cca00000, addr+secsize: cca00000

level 1: pmd: c0007328, addr: cca00000, phy: 0cb00000, end: ccc00000, addr+secsize: ccb00000

level 1: pmd: c000732c, addr: ccb00000, phy: 0cc00000, end: ccc00000, addr+secsize: ccc00000

level 1: pmd: c0007330, addr: ccc00000, phy: 0cd00000, end: cce00000, addr+secsize: ccd00000

level 1: pmd: c0007334, addr: ccd00000, phy: 0ce00000, end: cce00000, addr+secsize: cce00000

level 1: pmd: c0007338, addr: cce00000, phy: 0cf00000, end: cd000000, addr+secsize: ccf00000

level 1: pmd: c000733c, addr: ccf00000, phy: 0d000000, end: cd000000, addr+secsize: cd000000

level 1: pmd: c0007340, addr: cd000000, phy: 0d100000, end: cd200000, addr+secsize: cd100000

level 1: pmd: c0007344, addr: cd100000, phy: 0d200000, end: cd200000, addr+secsize: cd200000

level 1: pmd: c0007348, addr: cd200000, phy: 0d300000, end: cd400000, addr+secsize: cd300000

level 1: pmd: c000734c, addr: cd300000, phy: 0d400000, end: cd400000, addr+secsize: cd400000

level 1: pmd: c0007350, addr: cd400000, phy: 0d500000, end: cd600000, addr+secsize: cd500000

level 1: pmd: c0007354, addr: cd500000, phy: 0d600000, end: cd600000, addr+secsize: cd600000

level 1: pmd: c0007358, addr: cd600000, phy: 0d700000, end: cd800000, addr+secsize: cd700000

level 1: pmd: c000735c, addr: cd700000, phy: 0d800000, end: cd800000, addr+secsize: cd800000

level 1: pmd: c0007360, addr: cd800000, phy: 0d900000, end: cda00000, addr+secsize: cd900000

level 1: pmd: c0007364, addr: cd900000, phy: 0da00000, end: cda00000, addr+secsize: cda00000

level 1: pmd: c0007368, addr: cda00000, phy: 0db00000, end: cdc00000, addr+secsize: cdb00000

level 1: pmd: c000736c, addr: cdb00000, phy: 0dc00000, end: cdc00000, addr+secsize: cdc00000

level 1: pmd: c0007370, addr: cdc00000, phy: 0dd00000, end: cde00000, addr+secsize: cdd00000

level 1: pmd: c0007374, addr: cdd00000, phy: 0de00000, end: cde00000, addr+secsize: cde00000

level 1: pmd: c0007378, addr: cde00000, phy: 0df00000, end: ce000000, addr+secsize: cdf00000

level 1: pmd: c000737c, addr: cdf00000, phy: 0e000000, end: ce000000, addr+secsize: ce000000

level 1: pmd: c0007380, addr: ce000000, phy: 0e100000, end: ce200000, addr+secsize: ce100000

level 1: pmd: c0007384, addr: ce100000, phy: 0e200000, end: ce200000, addr+secsize: ce200000

level 1: pmd: c0007388, addr: ce200000, phy: 0e300000, end: ce400000, addr+secsize: ce300000

level 1: pmd: c000738c, addr: ce300000, phy: 0e400000, end: ce400000, addr+secsize: ce400000

level 1: pmd: c0007390, addr: ce400000, phy: 0e500000, end: ce600000, addr+secsize: ce500000

level 1: pmd: c0007394, addr: ce500000, phy: 0e600000, end: ce600000, addr+secsize: ce600000

level 1: pmd: c0007398, addr: ce600000, phy: 0e700000, end: ce800000, addr+secsize: ce700000

level 1: pmd: c000739c, addr: ce700000, phy: 0e800000, end: ce800000, addr+secsize: ce800000

level 1: pmd: c00073a0, addr: ce800000, phy: 0e900000, end: cea00000, addr+secsize: ce900000

level 1: pmd: c00073a4, addr: ce900000, phy: 0ea00000, end: cea00000, addr+secsize: cea00000

level 1: pmd: c00073a8, addr: cea00000, phy: 0eb00000, end: cec00000, addr+secsize: ceb00000

level 1: pmd: c00073ac, addr: ceb00000, phy: 0ec00000, end: cec00000, addr+secsize: cec00000

level 1: pmd: c00073b0, addr: cec00000, phy: 0ed00000, end: cee00000, addr+secsize: ced00000

level 1: pmd: c00073b4, addr: ced00000, phy: 0ee00000, end: cee00000, addr+secsize: cee00000

level 1: pmd: c00073b8, addr: cee00000, phy: 0ef00000, end: cf000000, addr+secsize: cef00000

level 1: pmd: c00073bc, addr: cef00000, phy: 0f000000, end: cf000000, addr+secsize: cf000000

level 1: pmd: c00073c0, addr: cf000000, phy: 0f100000, end: cf200000, addr+secsize: cf100000

level 1: pmd: c00073c4, addr: cf100000, phy: 0f200000, end: cf200000, addr+secsize: cf200000

level 1: pmd: c00073c8, addr: cf200000, phy: 0f300000, end: cf400000, addr+secsize: cf300000

level 1: pmd: c00073cc, addr: cf300000, phy: 0f400000, end: cf400000, addr+secsize: cf400000

level 1: pmd: c00073d0, addr: cf400000, phy: 0f500000, end: cf600000, addr+secsize: cf500000

level 1: pmd: c00073d4, addr: cf500000, phy: 0f600000, end: cf600000, addr+secsize: cf600000

level 1: pmd: c00073d8, addr: cf600000, phy: 0f700000, end: cf800000, addr+secsize: cf700000

level 1: pmd: c00073dc, addr: cf700000, phy: 0f800000, end: cf800000, addr+secsize: cf800000

level 1: pmd: c00073e0, addr: cf800000, phy: 0f900000, end: cfa00000, addr+secsize: cf900000

level 1: pmd: c00073e4, addr: cf900000, phy: 0fa00000, end: cfa00000, addr+secsize: cfa00000

level 1: pmd: c00073e8, addr: cfa00000, phy: 0fb00000, end: cfc00000, addr+secsize: cfb00000

level 1: pmd: c00073ec, addr: cfb00000, phy: 0fc00000, end: cfc00000, addr+secsize: cfc00000

level 1: pmd: c00073f0, addr: cfc00000, phy: 0fd00000, end: cfe00000, addr+secsize: cfd00000

level 1: pmd: c00073f4, addr: cfd00000, phy: 0fe00000, end: cfe00000, addr+secsize: cfe00000

level 1: pmd: c00073f8, addr: cfe00000, phy: 0ff00000, end: d0000000, addr+secsize: cff00000

level 1: pmd: c00073fc, addr: cff00000, phy: 10000000, end: d0000000, addr+secsize: d0000000

//下面这个二级映射是中断映射:

level 2: pmd: c0007ff8, addr:ffff0000, phys: 00c37000

//下面是machine_descmap_io成员,即硬件IO映射:

level 1: pmd: c0007c40, addr: f1000000, phy: f1100000, end: f1100000, addr+secsize: f1100000

level 1: pmd: c0007c80, addr: f2000000, phy: f2100000, end: f2100000, addr+secsize: f2100000

level 1: pmd: c0007c84, addr: f2100000, phy: f2200000, end: f2200000, addr+secsize: f2200000

level 1: pmd: c0007d80, addr: f6000000, phy: f6100000, end: f6200000, addr+secsize: f6100000

level 1: pmd: c0007d84, addr: f6100000, phy: f6200000, end: f6200000, addr+secsize: f6200000

level 1: pmd: c0007d88, addr: f6200000, phy: f6300000, end: f6400000, addr+secsize: f6300000

level 1: pmd: c0007d8c, addr: f6300000, phy: f6400000, end: f6400000, addr+secsize: f6400000

level 1: pmd: c0007d90, addr: f6400000, phy: f6500000, end: f6600000, addr+secsize: f6500000

level 1: pmd: c0007d94, addr: f6500000, phy: f6600000, end: f6600000, addr+secsize: f6600000

level 1: pmd: c0007d98, addr: f6600000, phy: f6700000, end: f6800000, addr+secsize: f6700000

level 1: pmd: c0007d9c, addr: f6700000, phy: f6800000, end: f6800000, addr+secsize: f6800000

level 1: pmd: c0007da0, addr: f6800000, phy: f6900000, end: f6a00000, addr+secsize: f6900000

level 1: pmd: c0007da4, addr: f6900000, phy: f6a00000, end: f6a00000, addr+secsize: f6a00000

level 1: pmd: c0007da8, addr: f6a00000, phy: f6b00000, end: f6c00000, addr+secsize: f6b00000

level 1: pmd: c0007dac, addr: f6b00000, phy: f6c00000, end: f6c00000, addr+secsize: f6c00000

level 1: pmd: c0007db0, addr: f6c00000, phy: f6d00000, end: f6e00000, addr+secsize: f6d00000

level 1: pmd: c0007db4, addr: f6d00000, phy: f6e00000, end: f6e00000, addr+secsize: f6e00000

level 1: pmd: c0007db8, addr: f6e00000, phy: f6f00000, end: f7000000, addr+secsize: f6f00000

level 1: pmd: c0007dbc, addr: f6f00000, phy: f7000000, end: f7000000, addr+secsize: f7000000

level 1: pmd: c0007dc0, addr: f7000000, phy: f7100000, end: f7200000, addr+secsize: f7100000

level 1: pmd: c0007dc4, addr: f7100000, phy: f7200000, end: f7200000, addr+secsize: f7200000

level 1: pmd: c0007dc8, addr: f7200000, phy: f7300000, end: f7400000, addr+secsize: f7300000

level 1: pmd: c0007dcc, addr: f7300000, phy: f7400000, end: f7400000, addr+secsize: f7400000

level 1: pmd: c0007dd0, addr: f7400000, phy: f7500000, end: f7600000, addr+secsize: f7500000

level 1: pmd: c0007dd4, addr: f7500000, phy: f7600000, end: f7600000, addr+secsize: f7600000

level 1: pmd: c0007dd8, addr: f7600000, phy: f7700000, end: f7800000, addr+secsize: f7700000

level 1: pmd: c0007ddc, addr: f7700000, phy: f7800000, end: f7800000, addr+secsize: f7800000

level 1: pmd: c0007de0, addr: f7800000, phy: f7900000, end: f7a00000, addr+secsize: f7900000

level 1: pmd: c0007de4, addr: f7900000, phy: f7a00000, end: f7a00000, addr+secsize: f7a00000

level 1: pmd: c0007de8, addr: f7a00000, phy: f7b00000, end: f7c00000, addr+secsize: f7b00000

level 1: pmd: c0007dec, addr: f7b00000, phy: f7c00000, end: f7c00000, addr+secsize: f7c00000

level 1: pmd: c0007df0, addr: f7c00000, phy: f7d00000, end: f7e00000, addr+secsize: f7d00000

level 1: pmd: c0007df4, addr: f7d00000, phy: f7e00000, end: f7e00000, addr+secsize: f7e00000

level 1: pmd: c0007df8, addr: f7e00000, phy: f7f00000, end: f8000000, addr+secsize: f7f00000

level 1: pmd: c0007dfc, addr: f7f00000, phy: f8000000, end: f8000000, addr+secsize: f8000000

level 1: pmd: c0007c88, addr: f2200000, phy: f2300000, end: f2400000, addr+secsize: f2300000

level 1: pmd: c0007c8c, addr: f2300000, phy: f2400000, end: f2400000, addr+secsize: f2400000

level 1: pmd: c0007c90, addr: f2400000, phy: f2500000, end: f2600000, addr+secsize: f2500000

level 1: pmd: c0007c94, addr: f2500000, phy: f2600000, end: f2600000, addr+secsize: f2600000

level 1: pmd: c0007d40, addr: f5000000, phy: f5100000, end: f5100000, addr+secsize: f5100000

//这个还没有找到!

level 2: pmd: c0007f00, addr:fc000000, phys: fc000000

2017-12-05 10:25:07 weixin_37825861 阅读数 655
  • 多任务的裸机实现(下)

    通过本课程学习,让嵌入式学员对对整个嵌入式系统、CPU内部、操作系统都有一个全局的把握和认识,为后续的Linux内核、驱动开发等高阶课程打下良好的理论基础。掌握这些知识后,会让我们后续的课程更加轻松、学习效率...

    9064课时 0分钟 33人学习 王利涛
    免费试看

厘清了什么是分段, 什么是分页,搞清楚了linux的三种地址,即逻辑地址、线性地址、物理地址, 也梳理了x86 linux的启动过程以及至关重要的页表建立的过程,现在再来看一看ARM linux页表的建立过程,毕竟X86 和ARM 是最为流行的两种架构。

12. 页表机制

12.1. 引言

早期的Linux采用的是三级页表模型,在2.6.11以后采用的是四级模型。对于没有开PAE的32位系统,两级目录项已经够用,将页上目录和页中间目录置0。对于启用PAE的32位系统使用了三级页表,即取消了页上目录。64位系统下,启动四级还是三取决于硬件cpu平台,x86下采用了四级, ARM 则采用了三级所以Linux使用三级页表。


  • 页全局目录(Page Global Directory),即 pgd,是多级页表的抽象最高层。每一级的页表都处理不同大小的内存。每项都指向一个更小目录的低级表,因此pgd就是一个页表目录。当代码遍历这个结构时(有些驱动程序就要这样做),就称为是在遍历页表。
  • 页中间目录 (Page Middle Directory),即pmd,是页表的中间层。在 x86 架构上,pmd 在硬件中并不存在,但是在内核代码中它是与pgd合并在一起的。
  • 页表条目 (Page Table Entry),即pte,是页表的最低层,它直接处理页,该值包含某页的物理地址,还包含了说明该条目是否有效及相关页是否在物理内存中的位。

三级页表由不同的数据结构表示,它们分别是pgd_t,pmd_t和pte_t。注意到它们均被定义为unsigned long类型,也即大小为4bytes,32bits。 

12.2. 一级页表

三级页表由不同的的数据结构表示,它们分别是pgd_t,pmd_t和pte_t。注意到它们均被定义为unsigned long类型,也即大小为4bytes,32bits。

arch/arm/include/asm/page.h

typedef unsigned long pte_t;
typedef unsigned long pmd_t;
typedef unsigned long pgd_t[2];
typedef unsigned long pgprot_t;

以下是页表操作相关的宏定义。

#define pte_val(x)      (x)
#define pmd_val(x)      (x)
#define pgd_val(x)      ((x)[0])
#define pgprot_val(x)   (x)

#define __pte(x)        (x)
#define __pmd(x)        (x)
#define __pgprot(x)     (x)

任何一个用户进程都有自己的页表,与此同时,内核本身就是一个名为init_task的0号进程,每一个进程都有一个mm_struct结构管理进程的内存空间,init_mm是内核的mm_struct。在系统引导阶段,首先通过__create_page_tables在内核代码的起始处_stext向低地址方向预留16K,用于一级页表(主内存页表)的存放,每个进程的页表都通过mm_struct中的pgd描述符进行引用。内核页表被定义在swapper_pg_dir。

arch/arm/kernel/init_task.c

#define INIT_MM(name) {                                                                       .mm_rb          = RB_ROOT,                                      .pgd            = swapper_pg_dir,                               .mm_users       = ATOMIC_INIT(2),                               .mm_count       = ATOMIC_INIT(1),                               .mmap_sem       = __RWSEM_INITIALIZER(name.mmap_sem),           .page_table_lock =  __SPIN_LOCK_UNLOCKED(name.page_table_lock),         .mmlist         = LIST_HEAD_INIT(name.mmlist),                  .cpu_vm_mask    = CPU_MASK_ALL,                         }

struct mm_struct init_mm = INIT_MM(init_mm); 

swapper_pg_dir在head.S中被定义为PAGE_OFFSET向上偏移TEXT_OFFSET。TEXT_OFFSET代表内核代码段的相对于PAGE_OFFSET的偏移。KERNEL_RAM_VADDR的值与_stext的值相同,代表了内核代码的起始地址。swapper_pg_dir为KERNEL_RAM_VADDR - 0x4000,也即向低地址方向偏移了16K。

arch/arm/Makefile

textofs-y       := 0x00008000
......
TEXT_OFFSET := $(textofs-y)

特定系统架构的Makefile中通过textofs-y定义了内核起始代码相对于PAGE_OFFSET的偏移。

arch/arm/kernel/head.S
#define KERNEL_RAM_VADDR	(PAGE_OFFSET + TEXT_OFFSET)
......
.globl  swapper_pg_dir
.equ    swapper_pg_dir, KERNEL_RAM_VADDR - 0x4000

ARM Linux中的主内存页表,使用段表。每个页表映射1M的内存大小,由于16K / 4 * 1M = 4G,这16K的主页表空间正好映射4G的虚拟空间。内核页表机制在系统启动过程中的paging_init函数中使能,其中对内核主页表的初始化等操作均是通过init_mm.pgd的引用来进行的。在系统执行paging_init之前,系统的地址空间如下图所示:

图 61. 内核RAM布局



图中的黄色部分就是内核0号进程的主页表。

arch/arm/mm/mmu.c

void __init paging_init(struct meminfo *mi, struct machine_desc *mdesc)
{
	void *zero_page;

	build_mem_type_table();
	sanity_check_meminfo(mi);
	prepare_page_table(mi);
	
	bootmem_init(mi);
	devicemaps_init(mdesc);

	top_pmd = pmd_off_k(0xffff0000);

	zero_page = alloc_bootmem_low_pages(PAGE_SIZE);
	memzero(zero_page, PAGE_SIZE);
	empty_zero_page = virt_to_page(zero_page);
	flush_dcache_page(empty_zero_page);
}

图 62. ARM内存主页表初始化



paging_init依次完成了以下工作:
  • 调用prepare_page_table初始化虚拟地址[0, PAGE_OFFSET]和[mi->bank[0].start + mi->bank[0].size, VMALLOC_END]所对应的主页表项,所有表项均初始化为0。这里保留了内核代码区,主页表区以及Bootmem机制中的位图映射区对应的主页表。这是为了保证内核代码的执行以及对主页表区和位图区的访问。如果只有一个内存bank,那么mi->bank[0].start + mi->bank[0].size的值和high_memory保持一致,它是当前bank进行物理内存一一映射后的虚拟地址。对于一个内存为256M的系统来说,它只有一个bank,经过prepare_page_table处理后的内存如上图所示。
  • 接着在bootmem_init函数将通过bootmem_init_node对每一个内存bank的页表进行值的填充。bootmem_init_node将通过map_memory_bank间接调用create_mapping,最终由该函数创建页表。
  • 通过devicemaps_init初始化设备I/O对应的相关页表。
  • 最后创建0页表,并在Dcache中清空0页表的缓存信息。

图 63. 页表创建函数调用



12.3. ARM 内存访问

当ARM要访问内存RAM时,MMU首先查找TLB中的虚拟地址表,如果ARM的结构支持分开的地址TLB和指令TLB,那么它用:

  • 取指令使用指令TLB
  • 其它的所有访问类别用数据TLB

指令TLB和数据TLB在ARMv6架构的MMU中被分别称为指令MicroTLB和数据MicroTLB。如果没有命中MicroTLB,那么将查询主TLB,此时不区分指令和数据TLB。

如果TLB中没有虚拟地址的入口,则转换表遍历硬件从存在主存储器中的转换表中获取转换页表项,它包含了物理地址或者二级页表地址和访问权限,一旦取到,这些信息将被放在TLB中,它会放在一个没有使用的入口处或覆盖一个已有的入口。一旦为存储器访问的TLB 的入口被拿到,这些信息将被用于:

  • C(高速缓存)和B(缓冲)位被用来控制高速缓存和写缓冲,并决定是否高速缓存。
  • 首先检查域位,然后检查访问权限位用来控制访问是否被允许。如果不允许,则MMU 将向ARM处理器发送一个存储器异常;否则访问将被允许进行。
  • 对没有或者禁止高速缓存的系统(包括在没有高速缓存系统中的所有存储器访问),物理地址将被用作主存储器访问的地址。

图 64. 高速缓存的MMU存储器系统


12.4. ARM MMU页表

在ARMv6的MMU机制中,提供了两种格式的页表描述符:
  • 兼容ARMv4和ARMv5 MMU机制的页表描述符。这种描述符可以对64K大页面和4K小页面再进一步细分为子页面。
  • ARMv6特有的MMU页表描述符,这种页表描述符内增加了额外的特定比特位:Not-Global(nG),Shared (S),Execute-Never (XN)和扩展的访问控制位APX。

图 65. 向前兼容的一级页表描述符格式



图 66. 向前兼容的二级页表描述符格式



Linux使用ARMv6特有的MMU页表描述符格式,它们的标志位描述如下:

图 67. ARMv6一级页表描述符格式



表 20. ARMv6一级页表描述符比特位含义

标志 含义
b[1:0] 类型 指示页表类型:b00 错误项;b11 保留;b01粗页表,它指向二级页表基址。
b10:1MB大小段页表(b[18]置0)或16M大小超级段页表(b[18]置1)
b[2] B[a] 写缓冲使能[b]
b[4] Execute-Never(XN) 禁止执行标志:1,禁止执行;0:可执行
b[5:8] 域(domain) 指明所属16个域的哪个域,访问权限由CP15的c3寄存器据定
b[9] P(ECC Enable) ECC使能标志,1:该页表映射区使能ECC校验[c]
b[10:11] AP(Access Permissions) 访问权限位,具体见访问权限列表
b[12:14] TEX(Type Extension Field) 扩展类型,与B,C标志协同控制内存访问类型
bit[15] APX(Access Permissions Extension Bit) 扩展访问权限位
bit[16] S(Shared) 共享访问
bit[17] nG(Not-Global) 全局访问
bit[18] 0/1 段页表和超级段页表开关
bit[19] NS  

[a]高速缓存和写缓存的引入是基于如下事实,即处理器速度远远高于存储器访问速度;如果存储器访问成为系统性能的瓶颈,则处理器再快也是浪费,因为处理器需要耗费大量的时间在等待存储器上面。高速缓存正是用来解决这个问题,它可以存储最近常用的代码和数据,以最快的速度提供给CPU处理(CPU访问Cache不需要等待)。

[b]SBZ意味置0,该位在粗页表中置0。

[c]ARM1176JZF-S处理器不支持该标志位。


Linux 在ARM体系架构的Hardware page table头文件中通过宏定义了这些位。
arch/arm/include/asm/pgtable-hwdef.h

/*
 * Hardware page table definitions.
 *
 * + Level 1 descriptor (PMD)
 *   - common
 */
#define PMD_TYPE_MASK           (3 << 0) // 获取一级页表类型的掩码,它取bit[0:1]
#define PMD_TYPE_FAULT          (0 << 0) // 置bit[0:1]为b00,错误项
#define PMD_TYPE_TABLE          (1 << 0) // 置bit[0:1]为b01,粗页表
#define PMD_TYPE_SECT           (2 << 0) // 置bit[0:1]为b10,段页表
#define PMD_BIT4                (1 << 4) // 定义bit[4],禁止执行标志位
#define PMD_DOMAIN(x)           ((x) << 5) // 获取域标志位b[5:8]
#define PMD_PROTECTION          (1 << 9)   // b[9]ECC使能标志
以上定义了一级页表的相关标志位。Linux使用段页表作为一级页表,粗页表作为二级页表的基址页表。段页表的标志位定义如下:
#define PMD_SECT_BUFFERABLE     (1 << 2)	
#define PMD_SECT_CACHEABLE      (1 << 3)
#define PMD_SECT_XN             (1 << 4)        /* v6 */
#define PMD_SECT_AP_WRITE       (1 << 10)
#define PMD_SECT_AP_READ        (1 << 11)
#define PMD_SECT_TEX(x)         ((x) << 12)     /* v5 */
#define PMD_SECT_APX            (1 << 15)       /* v6 */
#define PMD_SECT_S              (1 << 16)       /* v6 */
#define PMD_SECT_nG             (1 << 17)       /* v6 */
#define PMD_SECT_SUPER          (1 << 18)       /* v6 */

图 68. ARMv6二级页表基址格式



二级页表相同标志位的含义与一级页表相同,这里不再单独列出。注意它的b[1]为1时,b[0]表示禁止执行标志。Linux对二级页表中的标志位定义如下:
/*
 * + Level 2 descriptor (PTE)
 *   - common
 */
#define PTE_TYPE_MASK           (3 << 0) // 获取二级页表类型的掩码,它取bit[0:1]
#define PTE_TYPE_FAULT          (0 << 0) // 置bit[0:1]为b00,错误项
#define PTE_TYPE_LARGE          (1 << 0) // 置bit[0:1]为b01,大页表(64K)
#define PTE_TYPE_SMALL          (2 << 0) // 置bit[0:1]为b10,扩展小页表(4K)
#define PTE_TYPE_EXT            (3 << 0) // 使能禁止执行标志的扩展小页表(4K)
#define PTE_BUFFERABLE          (1 << 2) // B标志
#define PTE_CACHEABLE           (1 << 3) // C标志
Linux二级页表使用扩展小页表,这样每个二级页表可以表示通常的1个页面大小(4K)。Linux对二级页表标志位的定义如下:
/*
 *   - extended small page/tiny page
 */
#define PTE_EXT_XN              (1 << 0)        /* v6 */
#define PTE_EXT_AP_MASK         (3 << 4)
#define PTE_EXT_AP0             (1 << 4)
#define PTE_EXT_AP1             (2 << 4)
#define PTE_EXT_AP_UNO_SRO      (0 << 4)
#define PTE_EXT_AP_UNO_SRW      (PTE_EXT_AP0)
#define PTE_EXT_AP_URO_SRW      (PTE_EXT_AP1)
#define PTE_EXT_AP_URW_SRW      (PTE_EXT_AP1|PTE_EXT_AP0)
#define PTE_EXT_TEX(x)          ((x) << 6)      /* v5 */
#define PTE_EXT_APX             (1 << 9)        /* v6 */
#define PTE_EXT_COHERENT        (1 << 9)        /* XScale3 */
#define PTE_EXT_SHARED          (1 << 10)       /* v6 */
#define PTE_EXT_NG              (1 << 11)       /* v6 */
以上两种页表转换机制由CP15协处理器的控制寄存器c1中的bit23来选择。bit23为0时为第一种机制,否则为第二种。在CPU初始化后该位的默认值为0。Linux在系统引导时会设置MMU的控制寄存器的相关位,其中把bit23设置为1,所以Linux在ARMv6体系架构上采用的是ARMv6 MMU页表转换机制。
arch/arm/mm/proc-v6.S

__v6_setup:
......
        adr     r5, v6_crval
        ldmia   r5, {r5, r6}
        mrc     p15, 0, r0, c1, c0, 0           @ read control register
        bic     r0, r0, r5                      @ clear bits them
        orr     r0, r0, r6                      @ set them
        mov     pc, lr                          @ return to head.S:__ret

        /*
         *         V X F   I D LR
         * .... ...E PUI. .T.T 4RVI ZFRS BLDP WCAM
         * rrrr rrrx xxx0 0101 xxxx xxxx x111 xxxx < forced
         *         0 110       0011 1.00 .111 1101 < we want
         */
        .type   v6_crval, #object
v6_crval:
        crval   clear=0x01e0fb7f, mmuset=0x00c0387d, ucset=0x00c0187c
注意到v6_crval定义了三个常量,首先mrc指令读取c1到r0,然后清除clear常量指定的比特位,然后设置mmuset指定的比特位,其中bit23为1。在mov pc, lr跳转后将执行定义在head.S中的__enable_mmu函数,在进一步调节其它的比特位后最终将把r0中的值写回c1寄存器。

12.5. 页面访问控制

在谈到create_mapping之前,必须说明一下Linux是如何实现对页面的访问控制的。它定义了一个类型为struct mem_type的局部静态数组。根据不同的映射类型,它定义了不同的访问权限,它通过md参数中的type成员传递给create_mapping。
arch/arm/include/asm/io.h
/*
 * Architecture ioremap implementation.
 */
#define MT_DEVICE               0
#define MT_DEVICE_NONSHARED     1
#define MT_DEVICE_CACHED        2
#define MT_DEVICE_WC            3

arch/arm/include/asm/mach/map.h
/* types 0-3 are defined in asm/io.h */
#define MT_UNCACHED             4
#define MT_CACHECLEAN           5
#define MT_MINICLEAN            6
#define MT_LOW_VECTORS          7
#define MT_HIGH_VECTORS         8
#define MT_MEMORY               9
#define MT_ROM                  10
系统中定义了多个映射类型,最常用的是MT_MEMORY,它对应RAM;MT_DEVICE则对应了其他I/O设备,应用于ioremap;MT_ROM对应于ROM;MT_LOW_VECTORS对应0地址开始的向量;MT_HIGH_VECTORS对应高地址开始的向量,它有vector_base宏决定。
arch/arm/mm/mm.h

struct mem_type {
        unsigned int prot_pte;
        unsigned int prot_l1;
        unsigned int prot_sect;
        unsigned int domain;
};
尽管Linux在多数系统上实现或者模拟了3级页表,但是在ARM Linux上它只实现了主页表和两级页表。主页表通过ARM CPU的段表实现,段表中的每个页表项管理1M的内存,虚拟地址只需要一次转换既可以得到物理地址,它通常存放在swapper_pg_dir开始的16K区域内。两级页表只有在被映射的物理内存块不满足1M的情况下才被使用,此时它由L1和L2组成。
  • prot_pte和prot_l1分别对应两级页表中的L2和L1,分别代表了页表项的访问控制位,其中prot_l1中还包含内存域
  • prot_sect代表主页表的访问控制位和内存域。
  • domain代表了内存域。在ARM处理器中,MMU将整个存储空间分成最多16个域,记作D0~D15,每个域对应一定的存储区域,该区域具有相同的访问控制属性。
arch/arm/include/asm/domain.h

#define DOMAIN_KERNEL   0
#define DOMAIN_TABLE    0
#define DOMAIN_USER     1
#define DOMAIN_IO       2
ARM Linux 中只是用了16个域中的三个域D0-D2。它们由上面的宏来定义,在系统引导时初始化MMU的过程中将对这三个域设置域访问权限。以下是内存空间和域的对应表:

表 21. 内存空间和域的对应表

内存空间
设备空间 DOMAIN_IO
内部高速SRAM空间/内部MINI Cache空间 DOMAIN_KERNEL
RAM内存空间/ROM内存空间 DOMAIN_KERNEL
高低端中断向量空间 DOMAIN_USER

在ARM处理器中,MMU中的每个域的访问权限分别由CP15的C3寄存器中的两位来设定,c3寄存器的大小为32bits,刚好可以设置16个域的访问权限。下表列出了域的访问控制字段不同取值及含义:

表 22. ARM内存访问控制字

访问类型 含义
0b00 无访问权限 此时访问该域将产生访问失效
0b01 用户(client) 根据CP15的C1控制寄存器中的R和S位以及页表中地址变换条目中的访问权限控制位AP来确定是否允许各种系统工作模式的存储访问
0b10 保留 使用该值会产生不可预知的结果
0b11 管理者(Manager) 不考虑CP15的C1控制寄存器中的R和S位以及页表中地址变换条目中的访问权限控制位AP,在这种情况下不管系统工作在特权模式还是用户模式都不会产生访问失效

Linux定义了其中可以使用的三种域控制:
arch/arm/include/asm/domain.h

#define DOMAIN_NOACCESS 0
#define DOMAIN_CLIENT   1
#define DOMAIN_MANAGER  3
Linux在系统引导设置MMU时初始化c3寄存器来实现对内存域的访问控制。其中对DOMAIN_USER,DOMAIN_KERNEL和DOMAIN_TABLE均设置DOMAIN_MANAGER权限;对DOMAIN_IO设置DOMAIN_CLIENT权限。如果此时读取c3寄存器,它的值应该是0x1f。
arch/arm/include/asm/domain.h
#define domain_val(dom,type)    ((type) << (2*(dom)))

arch/arm/kernel/head.S
    ......	
    mov     r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER)|domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | domain_val(DOMAIN_TABLE, DOMAIN_MANAGER)|domain_val(DOMAIN_IO, DOMAIN_CLIENT))
    mcr     p15, 0, r5, c3, c0, 0           @ load domain access register
    mcr     p15, 0, r4, c2, c0, 0           @ load page table pointer
    b       __turn_mmu_on
ENDPROC(__enable_mmu)
在系统的引导过程中对这3个域的访问控制位并不是一成不变的,它提供了一个名为modify_domain的宏来修改域访问控制位。系统在setup_arch中调用early_trap_init后,DOMAIN_USER的权限位将被设置成DOMAIN_CLIENT,此时它的值应该是0x17。
arch/arm/include/asm/domain.h

#define set_domain(x) do { _asm__ __volatile__( "mcr p15, 0, %0, c3, c0 @ set domain" : : "r" (x));\
			isb();\
			} while (0)

#define modify_domain(dom,type) do { \
				struct thread_info *thread = current_thread_info(); \
				unsigned int domain = thread->cpu_domain; \
				domain &= ~domain_val(dom, DOMAIN_MANAGER); \
				thread->cpu_domain = domain | domain_val(dom, type); \
				set_domain(thread->cpu_domain); \
				} while (0)
访问权限由CP15的c1控制寄存器中的R和S位以及页表项中的访问权限控制位AP[0:1]以及访问权限扩展位APX来确定,通过R和S的组合控制方式在第一项中说明,并且已不被推荐使用。具体说明如下表所示。

表 23. MMU中存储访问权限控制[8]

APX AP[1:0] 特权模式访问权限 用户模式访问权限
0 b00 禁止访问;S=1,R=0或S=0,R=1时只读 禁止访问;S=1,R=0时只读
0 b01 读写 禁止访问
0 b10 读写 只读
0 b11 读写 读写
1 b00 保留 保留
1 b01 只读 禁止访问
1 b10 只读 只读
1 b11 只读 只读

[8]参考ARM1176JZF-S Revision: r0p7->6.5.2 Access permissions


static struct mem_type mem_types[] = {
	......
	[MT_MEMORY] = {
		.prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
		.domain    = DOMAIN_KERNEL,
	},
	......
};
对于MT_MEMORY的内存映射类型,依据段页表的各位功能,定义了如下的宏,显然PMD_TYPE_SECT定义了段页表类型0b10,PMD_SECT_AP_WRITE和PMD_SECT_AP_READ则对应AP[0]和AP[1]访问权限控制位。根据表 23 “MMU中存储访问权限控制”,在使用AP[0:1]进行权限控制时,CP15中的C1寄存器中的S和R标志位不影响权限,根据AP权限位的意义,并不能直接根据宏的后缀名得出是对读还是写的控制。
arch/arm/include/asm/pgtable-hwdef.h

#define PMD_TYPE_TABLE          (1 << 0)
#define PMD_TYPE_SECT           (2 << 0)
#define PMD_BIT4                (1 << 4)
#define PMD_DOMAIN(x)           ((x) << 5)
......
#define PMD_SECT_AP_WRITE       (1 << 10)
#define PMD_SECT_AP_READ        (1 << 11)
一个描述了当前系统mem_types描述的所有内存映射类型权限控制的列表如下所示:

表 24. ARM Linux内存映射权限控制[9]

内存映射类型 域定义 段页表项权限定义 L1页表项权限定义 PTE项权限定义
MT_DEVICE DOMAIN_IO PROT_SECT_DEVICE[a]
PMD_SECT_S
PMD_TYPE_TABLE PROT_PTE_DEVICE[b]
L_PTE_MT_DEV_SHARED
L_PTE_SHARED
MT_DEVICE_NONSHARED DOMAIN_IO PROT_SECT_DEVICE PMD_TYPE_TABLE PROT_PTE_DEVICE
L_PTE_MT_DEV_NONSHARED
MT_DEVICE_CACHED DOMAIN_IO PROT_SECT_DEVICE
PMD_SECT_WB
PMD_TYPE_TABLE PROT_PTE_DEVICE
L_PTE_MT_DEV_CACHED
MT_DEVICE_WC DOMAIN_IO PROT_SECT_DEVICE PMD_TYPE_TABLE ROT_PTE_DEVICE
L_PTE_MT_DEV_WC
MT_UNCACHED DOMAIN_IO PMD_TYPE_SECT
PMD_SECT_XN
PMD_TYPE_TABLE PROT_PTE_DEVICE
MT_CACHECLEAN DOMAIN_KERNEL PMD_TYPE_SECT
PMD_SECT_XN
   
MT_MINICLEAN DOMAIN_KERNEL PMD_TYPE_SECT
PMD_SECT_XN
PMD_SECT_MINICACHE,
   
MT_LOW_VECTORS DOMAIN_USER   PMD_TYPE_TABLE L_PTE_PRESENT
L_PTE_YOUNG
L_PTE_DIRTY
L_PTE_EXEC
MT_HIGH_VECTORS DOMAIN_USER   PMD_TYPE_TABLE L_PTE_PRESENT
L_PTE_YOUNG
L_PTE_DIRTY
L_PTE_USER
L_PTE_EXEC
MT_MEMORY DOMAIN_KERNEL PMD_TYPE_SECT
PMD_SECT_AP_WRITE
   
MT_ROM DOMAIN_KERNEL PMD_TYPE_SECT    

[9]该表描述了Linux2.6.28 ARM体系架构的mem_types内存映射权限控制

[a]在mmu.c中它被定义为 PMD_TYPE_SECT|PMD_SECT_AP_WRITE

[b]在mmu.c中它被定义为 L_PTE_PRESENT|L_PTE_YOUNG|L_PTE_DIRTY|L_PTE_WRITE


build_mem_type_table函数在paging_init执行的开始被调用,它将根据当前CPU的特性进一步调整这些访问权限位。
  • MT_MEMORY被用来映射主存RAM。它只有段页表,对应访问权限中的第二条:特权模式可以读写,用户模式禁止访问。

12.6. create_mapping

create_mapping是完成页表创的核心函数。它的声明如下:

arch/arm/mm/mmu.c

void __init create_mapping(struct map_desc *md);

create_mapping只有一个类型为struct map_desc的参数。这是一个非常简单的参数,但是包含了创建页表相关的所有信息。

  • virtual记录了映射到的虚拟地址的开始。
  • pfn指明了被映射的物理内存的起始页框。
  • length指明了被映射的物理内存的大小,注意这里不是页框大小。对于256M的物理内存,这里的值为0x10000000。
  • type指明映射类型,MT_xxx,决定了相应映射页面的访问权限。

arch/arm/include/asm/mach/map.h

struct map_desc {
        unsigned long virtual;
        unsigned long pfn;
        unsigned long length;
        unsigned int type;
};

在Bootmem机制应用中有提到,系统中所有的内存块都在启动时被注册到meminfo中以struct membank类型的数组形式存在。map_memory_bank的作用就是将以struct membank类型的内存节点转换为struct map_desc类型然后传递给create_mapping。

struct meminfo {
  .nr_banks = 1;
  bank[8] = 
  {
    {
      .start = 0x50000000;
      .size = 0x10000000;
      .node = 0;
    };
   ...
  }
};

对于只有一个大小为256M物理RAM内存的系统来说,如果它具有以上的struct membank类型的内存信息,那么create_mapping得到的参数md如下所示:

map_desc {
    .virtual = 0xc0000000;
    .pfn = 0x50000;
    .length = 0x10000000;
    .type = MT_MEMORY;
};

[MT_MEMORY] = {
	.prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
	.domain    = DOMAIN_KERNEL,
}

create_mapping依次完成了以下工作:

  • 首先根据传入的virtual虚拟地址与中断向量起始地址比较,除特殊的中断向量使用的虚拟地址可能落在0地址外,确保映射到的虚拟地址落在内核空间。通过vectors_base宏,获取中断向量的起始地址,这是由于ARMv4以下的版本,该地址固定为0;ARMv4及以上版本,ARM中断向量表的地址由CP15协处理器c1寄存器中的V位(bit[13])控制,如果V位为1,那么该地址为0xffff0000。考虑到除了ARMv4以下的版本的中断向量所在的虚拟地址为0,其他所有映射到的虚拟地址应该都在地址0xc0000000之上的内核空间,而不可能被映射到用户空间。
  • 接着根据type类型判断是否为普通设备或者ROM,这些设备中的内存不能被映射到RAM映射的区域[PAGE_OFFSET,high_memory),也不能被映射到VMALLOC所在的区域[high_memory,VMALLOC_END),由于这两个区域是连续的,中间隔了8M的VMALLOC_OFFSET隔离区,准确来说是不能映射到[VMALLOC_START,VMALLOC_END),但是这一隔离区为了使能隔离之用也是不能被映射的。
  • 根据pfn与1G内存对应的最大物理页框0x100000比较,如果物理内存的起始地址位于32bits的物理地址之外,那么通过create_36bit_mapping创建36bits长度的页表,对于嵌入式系统来说很少有这种应用。
  • 调整传入的md中的各成员信息:virtual向低地址对齐到页面大小;根据length参数取对齐到页面的大小的长度,并以此计算映射结束的虚拟地址。根据pfn参数计算起始物理地址。
  • 根据虚拟地址和公式pgd = pgd_offset_k(addr)计算页表地址。
  • 根据虚拟地址的起始地址参数以及起始物理地址调用alloc_init_section来生成页表。

pgd_offset_k宏将一个0-4G范围内的虚拟地址转换为内核进程主页表中的对应页表项所在的地址。它首先根据pgd_index计算该虚拟地址对应的页表项在主页表中的索引值这里需要注意PGDIR_SHIFT的值为21,而非20,所以它的偏移是取2M大小区块的索引,这是由于pgd_t的类型为两个长整形的元素。然后根据索引值和内核进程中的init_mm.pgd取得页表项地址。

arch/arm/include/asm/pgtable.h

/* 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)

create_mapping在本质上是对传入参数的检查,并为调用alloc_init_section准备四要素:页表地址,虚拟地址的起止地址和物理地址的起始地址。Linux对create_mapping的调用除了在arch/arm/mm/init.c中通过map_memory_bank初始化主内存页面映射外,对其的调用均集中在arch/arm/mm/mmu.c中,其中iotable_init封装create_mapping用于特定机器架构的设备I/O映射。

12.7. alloc_init_section

页表创建函数调用图中给出了alloc_init_section在页表创建中所处的实现位置,本质上它是与alloc_init_pte并行的函数,alloc_init_section被用来创建段页表(主页表),alloc_init_pte则用来在被映射区长度小于1M时创建2级页表。

static void __init alloc_init_section(pgd_t *pgd, unsigned long addr,
				      unsigned long end, unsigned long phys,
				      const struct mem_type *type);

  • pgd参数指定生成的页表的起始地址,它是一个pgd_t类型,被定义为typedef unsigned long pgd_t[2],所以它是一个2维数组。
  • addr和end分别指明被映射到的虚拟地址的起止地址。
  • phys指明被映射的物理地址的起始地址。
  • type参数指明映射类型,所有映射类型在struct mem_type mem_types[]数组中被统一定义。

alloc_init_section依次完成了以下工作:

  • 首先根据公式(addr | end | phys) & ~SECTION_MASK) == 0依据传入的的addr,end和phys参数判断是否满足地址对齐到1M。
  • 如果满足则直接生成段页表(主页表),并存入pgd指向的地址。由于pgd是一个2维数组,所以每次需要对2个元素赋值,也即一次可以处理2M的内存映射,生成两个页表项。它在一个循环中以SECTION_SIZE为步进单位,通过phys | type->prot_sect来生成和填充页表。主RAM内存就是通过这种方法生成。一个256M内存RAM的生成页表如下图中的棕色部分所示。
  • 调用flush_pmd_entry清空TLB中的页面Cache,以使得新页表起作用。
  • 如不满足直接生成主页表,那么调用alloc_init_pte生成二级页表。

图 69. 内核RAM映射后的页表布局



12.8. alloc_init_pte

alloc_init_pte在初始化非主RAM中起到重要的作用,它尝试创建二级页表。二级页表的L1实际上还是存在于主页表中,只不过此时的主页表项不再是物理地址,而是二级页表,或者称为中间页表(PMD)。

static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,
				  unsigned long end, unsigned long pfn,
				  const struct mem_type *type);

  • pmd参数传递L1页表地址。
  • addr和end分别指明被映射到的虚拟地址的起止地址。
  • pfn是将被映射的物理地址的页框。
  • type参数指明映射类型。

alloc_init_pte依次完成以下工作:

  • 首先判断pmd指向的L1页表中的页表项是否存在,如果不存在则首先使用Bootmem机制中的alloc_bootmem_low_pages函数申请所需的二级页表空间,大小为4K,1个PAGE。

表 25. 页表计算

页表名称 计算公式 说明
主页表项地址 pgd_offset_k(vir_addr) #define PGDIR_SHIFT 21
#define pgd_index(addr) ((addr) >> PGDIR_SHIFT)
#define pgd_offset(mm, addr) ((mm)->pgd+pgd_index(addr))
#define pgd_offset_k(addr) pgd_offset(&init_mm, addr)
主页表项 __pmd(phys | type->prot_sect) #define __pmd(x) (x)
一级页表项地址 pmd = (pmd_t *)pgd_offset_k(vir_addr); 同"主页表项地址"
一级页表项 pte = alloc_bootmem_low_pages(1024 * sizeof(pte_t));
__pmd_populate(pmd, __pa(pte) | type->prot_l1);
static inline void __pmd_populate(pmd_t *pmdp,
unsigned long pmdval) {
pmdp[0] = __pmd(pmdval);
pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t));
flush_pmd_entry(pmdp);
}
二级页表项地址 pte = alloc_bootmem_low_pages(1024 * sizeof(pte_t)); 通过Bootmem机制申请1个页面大小的内存。
二级页表项 pte = pte_offset_kernel(pmd, addr);
set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);
#define __pte_index(addr) (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
#define pte_offset_kernel(dir,addr) (pmd_page_vaddr(*(dir)) + __pte_index(addr))

#define __pgprot(x) (x)
#define pgprot_val(x) (x)
#define __pte(x) (x)
#define pfn_pte(pfn,prot) (__pte(((pfn) << PAGE_SHIFT) | pgprot_val(prot)))


对于pgd_offset_k宏的说明如下:

  • init_mm.pgd类型为pgd_t类型,所以主页表总是以2个页表项为一组,它的大小为2 * sizeof(unsigned long) = 8。
  • 虚拟地址首先右移21位,是为了取高11位作为2个页表项为一组这里数据类型的索引值,在计算(mm)->pgd+pgd_index(addr)时,是指针的相加,等价于(pgd_t *)((unsigned long)init_mm.pgd + 8 * ((addr) >> 21))。
  • 由于每个页表项的大小为4,所以对应于单个页表项的大小,其索引值为8 * ((addr) >> 21) / 4 = 2 * ((addr),地址则为8 * ((addr) >> 21)。右移21然后乘以2,相当于取高12位,并将最后位置0,再乘以4则到达了地址。相当于乘以8。这里取的总是偶数索引,也即create_mapping传递给alloc_init_section的pgd参数永远指向偶数索引。
  • 如果需要取奇数索引的页表项怎么办呢?该值将在alloc_init_section中通过(addr & SECTION_SIZE)对它进行修正。
  • 主页表项的基地址init_mm.pgd加上索引值就是主页表项的地址。

对__pmd_populate内联函数的说明如下:

  • 它同时处理两个一级页表项pmd[0]和pmd[1]。
  • pmd[0]的值即为传入的pmdval,也即通过Bootmem机制获取的地址pte转化为物理地址后加上保护标志。
  • pmd[1]的值是pmd[0]的值的偏移,它偏移了256个PTE页表项,由于每个PTE页表项也是4字节,所以偏移的的物理地址为256 * sizeof(pte_t)。
  • 调用flush_pmd_entry清空TLB中的页面Cache,以使得新页表起作用。

#define PTRS_PER_PTE		512
#define pmd_val(x)      (x)

static inline pte_t *pmd_page_vaddr(pmd_t pmd)
{
        unsigned long ptr;

        ptr = pmd_val(pmd) & ~(PTRS_PER_PTE * sizeof(void *) - 1);
        ptr += PTRS_PER_PTE * sizeof(void *);

        return __va(ptr);
}

12.9. set_pte_ext

set_pte_ext用来填充硬件PTE页表。在create_mapping中被调用,通过一个循环,被传入的物理页帧和大小以PAGE_SIZE步进,进行二级页表的计算和填充。

	do {		
		set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);
		pfn++;
	} while (pte++, addr += PAGE_SIZE, addr != end);

它通过调用特定系统的pte函数完成,对于ARMv6来说,定义如下:

arch/arm/include/asm/pgtable.h
#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)
#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)

arch/arm/include/asm/cpu-single.h
#define cpu_set_pte_ext			__cpu_fn(CPU_NAME,_set_pte_ext)

arch/arm/mm/proc-v6.S

ENTRY(cpu_v6_set_pte_ext)
#ifdef CONFIG_MMU
        armv6_set_pte_ext cpu_v6
#endif
        mov     pc, lr

cpu_v6_set_pte_ext函数中引用了armv6_set_pte_ext宏,并传入cpu_v6参数。该宏定义如下:

arch/arm/mm/proc-macros.S

.macro  armv6_set_pte_ext pfx
str     r1, [r0], #-2048                @ linux version

根据ATPCS规则,C语言函数在调用汇编语言时,分别通过r0-r2来依次传递参数。所以这里的r0代表的是pte参数,也即二级页表的地址;r1为通过pfn_pte计算出的二级页表项;r2为0。这里的str指令首先将人r1存入r0所指向的地址,也即填充二级页表项,然后将r0的值减去2048,相当于下移了2048 / 4 = 512项。这里给出一个一二级页表的全图:

图 70. 内核页表布局全图



注意图中二级页表是在注册中断向量时的一个实例。注册的虚拟地址为0xffff0000,物理地址为0x50740000,大小为0x1000。传递给r0的值即为0xc0741fc0,r1的值为0x5074034b。在经过str操作之后,r0的值为0xc07417c0,相当于下移了512个页表项。

arch/arm/include/asm/pgtable-hwdef.h
#define PTE_TYPE_MASK		(3 << 0)
#define PTE_EXT_AP0     (1 << 4)

bic     r3, r1, #0x000003fc
bic     r3, r3, #PTE_TYPE_MASK
orr     r3, r3, r2
orr     r3, r3, #PTE_EXT_AP0 | 2

bic位清除指令首先将r1中0x3fc对应的位清0,然后对PTE_TYPE_MASK指定的最后两位清0,也即对0x3ff指定的最后11位清零,对于值为0x5074034b的r1来说,存入r3的值为0x50740000。orr按位逻辑或指令通过将r3中的值与r2位或操作放入r3,由于r2的值为0,所以r3的值此时保持不变。最后的orr将r3的值加上PTE_EXT_AP0权限位,或上2是为了指定当前是小页表(4K),此时r3的值为0x50740012。

arch/arm/include/asm/pgtable.h
#define L_PTE_MT_MASK           (0x0f << 2)

adr     ip, \pfx\()_mt_table
and     r2, r1, #L_PTE_MT_MASK
ldr     r2, [ip, r2]

adr伪指令将cpu_v6_mt_table的地址装入ip寄存器,然后取r1中0x5074034b的L_PTE_MT_MASK位作为索引值,这里为8。由于表中每一项的大小为4字节,所以[ip, 8]对应表中的第3项,也即L_PTE_MT_WRITETHROUGH。

arch/arm/include/asm/pgtable.h
#define L_PTE_MT_WRITETHROUGH	(0x02 << 2)	/* 0010 */

/*
 * The ARMv6 and ARMv7 set_pte_ext translation function.
 *
 * Permission translation:
 *  YUWD  APX AP1 AP0	SVC	User
 *  0xxx   0   0   0	no acc	no acc
 *  100x   1   0   1	r/o	no acc
 *  10x0   1   0   1	r/o	no acc
 *  1011   0   0   1	r/w	no acc
 *  110x   0   1   0	r/w	r/o
 *  11x0   0   1   0	r/w	r/o
 *  1111   0   1   1	r/w	r/w
 */
	.macro	armv6_mt_table pfx
\pfx\()_mt_table:
	.long	0x00						@ L_PTE_MT_UNCACHED
	.long	PTE_EXT_TEX(1)					@ L_PTE_MT_BUFFERABLE
	.long	PTE_CACHEABLE					@ L_PTE_MT_WRITETHROUGH
	.long	PTE_CACHEABLE | PTE_BUFFERABLE			@ L_PTE_MT_WRITEBACK
	.long	PTE_BUFFERABLE					@ L_PTE_MT_DEV_SHARED
	.long	0x00						@ unused
	.long	0x00						@ L_PTE_MT_MINICACHE (not present)
	.long	PTE_EXT_TEX(1) | PTE_CACHEABLE | PTE_BUFFERABLE	@ L_PTE_MT_WRITEALLOC
	.long	0x00						@ unused
	.long	PTE_EXT_TEX(1)					@ L_PTE_MT_DEV_WC
	.long	0x00						@ unused
	.long	PTE_CACHEABLE | PTE_BUFFERABLE			@ L_PTE_MT_DEV_CACHED
	.long	PTE_EXT_TEX(2)					@ L_PTE_MT_DEV_NONSHARED
	.long	0x00						@ unused
	.long	0x00						@ unused
	.long	0x00						@ unused
	.endm

首先测试二级页表项0x5074034b中的L_PTE_WRITE和L_PTE_DIRTY标志位,如果设置了L_PTE_WRITE,但没有L_PTE_DIRTY,那么设置那么设置PTE_EXT_APX到r3代表的硬件二级页表项0x50740021中。显然这里不会设置该位。

arch/arm/include/asm/pgtable.h
#define L_PTE_DIRTY             (1 << 6)
#define L_PTE_WRITE             (1 << 7)

arch/arm/include/asm/pgtable-hwdef.h
#define PTE_EXT_APX		(1 << 9)	/* v6 */

tst     r1, #L_PTE_WRITE
tstne   r1, #L_PTE_DIRTY
orreq   r3, r3, #PTE_EXT_APX

如果Linux版本的二级页表项设置了L_PTE_USER标志,r3被置PTE_EXT_AP1。如果r3包含PTE_EXT_APX标志,那么同时清除PTE_EXT_APX和 PTE_EXT_AP0。

#define L_PTE_USER              (1 << 8)

tst     r1, #L_PTE_USER
orrne   r3, r3, #PTE_EXT_AP1
tstne   r3, #PTE_EXT_APX
bicne   r3, r3, #PTE_EXT_APX | PTE_EXT_AP0

如果r1没有L_PTE_EXEC标志,则设置PTE_EXT_XN。

tst     r1, #L_PTE_EXEC
orreq   r3, r3, #PTE_EXT_XN

orr     r3, r3, r2

然后再加上L_PTE_MT_WRITETHROUGH标志。然后根据L_PTE_YOUNG标志,确定是否加上L_PTE_PRESENT标志。

tst     r1, #L_PTE_YOUNG
tstne   r1, #L_PTE_PRESENT
moveq   r3, #0

str     r3, [r0]
mcr     p15, 0, r0, c7, c10, 1          @ flush_pte
.endm

str指令将最终的硬件PTE页表值存放到低地址的二级页表中。所以硬件使用的二级页表总是位于低地址处,而高地址处的512项PTE是留给Linux自己使用的。

12.10. Sandbox

表 26. Memory Hierarchy

  位于哪里[a]
   

[a]到底位于哪里呢?


<figure><title>内核RAM布局</title><graphic fileref="images/kernelmap.gif"/></figure>
100=1 100=1
2017-07-03 22:07:44 flaoter 阅读数 1413
  • 多任务的裸机实现(下)

    通过本课程学习,让嵌入式学员对对整个嵌入式系统、CPU内部、操作系统都有一个全局的把握和认识,为后续的Linux内核、驱动开发等高阶课程打下良好的理论基础。掌握这些知识后,会让我们后续的课程更加轻松、学习效率...

    9064课时 0分钟 33人学习 王利涛
    免费试看

本文对arm linux页表创建函数进行说明。在http://blog.csdn.net/flaoter/article/details/73381695中对MMU使能之前的临时页表进行了说明,此文是对kernel中正式页表创建过程进行说明。文中使用的kernel版本为4.4。
arm linux使用两级页表,L1是pgd,L2是pte。其中L1页表共2048项,每项占用8bytes,每项对应2M内存,共占用2048*8=16K bytes。L1项指向的page中放有2个L2页表,每个页表256项,每项占用4 bytes,对应4K内存,共2*256*4=2K bytes,如下面代码中的h/w pt。Linux pt是linux管理用的页表。
arch/arm/include/asm/pgtable-2level.h

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

页表创建通过函数create_mapping实现,在kernel初始化时的paging_init函数中会对memblock进行页表创建。

struct map_desc {
    unsigned long virtual; //虚拟地址
    unsigned long pfn; //page frame
    unsigned long length; //地址的长度
    unsigned int type;
};
struct mem_type {
    pteval_t prot_pte;
    pteval_t prot_pte_s2;
    pmdval_t prot_l1;
    pmdval_t prot_sect;
    unsigned int domain;
};

static void __init create_mapping(struct map_desc *md)
{
    unsigned long addr, length, end;
    phys_addr_t phys;
    const struct mem_type *type;
    pgd_t *pgd;

    if (md->virtual != vectors_base() && md->virtual < TASK_SIZE) { //用户空间返回
        pr_warn("BUG: not creating mapping for 0x%08llx at 0x%08lx in user region\n",
            (long long)__pfn_to_phys((u64)md->pfn), md->virtual);
        return;
    }

    if ((md->type == MT_DEVICE || md->type == MT_ROM) &&
        md->virtual >= PAGE_OFFSET && md->virtual < FIXADDR_START &&
        (md->virtual < VMALLOC_START || md->virtual >= VMALLOC_END)) {   //IO类型内存申请低端内存
        pr_warn("BUG: mapping for 0x%08llx at 0x%08lx out of vmalloc space\n",
            (long long)__pfn_to_phys((u64)md->pfn), md->virtual);
    }

    type = &mem_types[md->type];

    addr = md->virtual & PAGE_MASK;
    phys = __pfn_to_phys(md->pfn);
    length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK));

    if (type->prot_l1 == 0 && ((addr | phys | length) & ~SECTION_MASK)) {  //非段映射,返回  
        pr_warn("BUG: map for 0x%08llx at 0x%08lx can not be mapped using pages, ignoring.\n",
            (long long)__pfn_to_phys(md->pfn), addr);
        return;
    }

    pgd = pgd_offset_k(addr);             //(1)
    end = addr + length;
    do {
        unsigned long next = pgd_addr_end(addr, end);

        alloc_init_pud(pgd, addr, next, phys, type);  //(2)

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

(1) 查找一级页表项

#define PGDIR_SHIFT     21
#define pgd_index(addr)     ((addr) >> PGDIR_SHIFT)
#define pgd_offset(mm, addr)    ((mm)->pgd + pgd_index(addr))
#define pgd_offset_k(addr)  pgd_offset(&init_mm, addr)

在kernel初始化汇编阶段的head.S中可知,一级页表的地址范围是0xC0004000~0xC0008000,即pgd的起始地址是0xC0004000, map_lowmem中addr=0xC0000000, pgd_offset_k(addr)解释为
(pgd_t*)0xC0004000 + (0xC0000000) >> 21

pgtable-2level-types.h

typedef u32 pmdval_t;
typedef pmdval_t pgd_t[2];

pgd_t类型是u32[2]的数组类型,所以(pgd_t*)0xC0004000 + (0xC0000000) >> 21=0xC0007000

(2) 调用alloc_init_pmd

static void __init alloc_init_pmd(pud_t *pud, unsigned long addr,
                      unsigned long end, phys_addr_t phys,
                      const struct mem_type *type)
{
    pmd_t *pmd = pmd_offset(pud, addr);
    unsigned long next;

    do {
        /*
         * With LPAE, we must loop over to map
         * all the pmds for the given range.
         */
        next = pmd_addr_end(addr, end);

        /*
         * Try a section mapping - addr, next and phys must all be
         * aligned to a section boundary.
         */
        if (type->prot_sect &&
                ((addr | next | phys) & ~SECTION_MASK) == 0) {
            __map_init_section(pmd, addr, next, phys, type);  //(a)
        } else {
            alloc_init_pte(pmd, addr, next,
                        __phys_to_pfn(phys), type);   //(b)
        }

        phys += next - addr;

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

(a)段映射,

static void __init __map_init_section(pmd_t *pmd, unsigned long addr,
            unsigned long end, phys_addr_t phys,
            const struct mem_type *type)
{
    pmd_t *p = pmd;

#ifndef CONFIG_ARM_LPAE
    /*
     * In classic MMU format, puds and pmds are folded in to
     * the pgds. pmd_offset gives the PGD entry. PGDs refer to a
     * group of L1 entries making up one logical pointer to
     * an L2 table (2MB), where as PMDs refer to the individual
     * L1 entries (1MB). Hence increment to get the correct
     * offset for odd 1MB sections.
     * (See arch/arm/include/asm/pgtable-2level.h)
     */
    if (addr & SECTION_SIZE)
        pmd++;
#endif
    do {
        *pmd = __pmd(phys | type->prot_sect);        //页表填充项是(phys | type->prot_sect)
        phys += SECTION_SIZE;
    } while (pmd++, addr += SECTION_SIZE, addr != end);

    flush_pmd_entry(p);    //刷新到内存
}
typedef u32 pmdval_t;
typedef pmdval_t pmd_t;

pmd_t是u32类型,指针值就是传进来的pgd_t,*pmd填充的内容即页表项就是(phys | type->prot_sect)。此处地址是按1M累加的,传进来的addr是按2M累加的,所以一般情况此函数的do{}while()循环都是执行两次。
下图是我的平台执行map_lowmem后的结果,一共占用了0x74项内容,建立了虚拟地址0xC0000000~0xC7300000到物理地址0x80000000~0x87300000的映射。
0xC0000000 –> 页表项地址0xC0007000 –>页表项内容0x8001141E –>物理地址0x80000000
map_lowmem_section
(b) 二级页表映射

pte_t * __init early_pte_alloc(pmd_t *pmd, unsigned long addr, unsigned long prot)
{
    if (pmd_none(*pmd)) {  //如果L1页表没映射
        pte_t *pte = early_alloc(PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE);  //PTE_HWTABLE_OFF=512*4 bytes,PTE_HWTABLE_SIZE=512*4bystes,共申请4K
        __pmd_populate(pmd, __pa(pte), prot);   //填充L1页表,使L1页表关联到L2页表
    }
    BUG_ON(pmd_bad(*pmd));
    return pte_offset_kernel(pmd, addr);
}

void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,
                  unsigned long end, unsigned long pfn,
                  const struct mem_type *type)
{
    pte_t *pte = early_pte_alloc(pmd, addr, type->prot_l1);  //pte为二级页表指针
    do {
        set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);  //填充L2页表项
        pfn++;    //L2页表每一项对应4k地址
    } while (pte++, addr += PAGE_SIZE, addr != end);
}

本例中virtual_addr = 0xFFFF0000, virtual_end=0xFFFF1000, pfn=0x000873FC,只进行4K地址的映射,因此只需要填充二级页表的1项内容即可。
二级页表指针pte = 0xC73FF7C0, [0xC73FF7C0] = 0x873FC5DF。建立了虚拟地址0xFFFF0000到物理地址0x873FC000的映射关系。