6.      2.5页表的建立

   前面已经建立了为内存页面管理所需的数据结构,现在是进一步完善页面映射机制,并且建立起内存页面映射管理机制的时候了,与此相关的主要函数有:

·      paging_init() 函数

·      pagetable_init() 函数

1.paging_init() 函数

这个函数仅被调用一次,即由setup_arch()调用以建立页表,对此函数的具体描述如下:

 

           pagetable_init();

 

   这个函数实际上才真正地建立页表,后面会给出详细描述.

 

__asm__( "movl %%ecx,%%cr3\n" ::"c"(__pa(swapper_pg_dir)));

    

   因为pagetable_init()已经建立起页表,因此把swapper_pg_dir(页目录)的地址装入CR3寄存器。

    

            #if CONFIG_X86_PAE

            /*

            * We will bail out later - printk doesnt work right now so

            * the user would just see a hanging kernel.

            */

            if (cpu_has_pae)

                       set_in_cr4(X86_CR4_PAE);

            #endif

 

            __flush_tlb_all();

   

上面这一句是个宏,它使得转换旁路缓冲区(TLB)无效。TLB总是要维持几个最新的虚地址到物理地址的转换。每当页目录改变时,TLB就需要被刷新。

    

            #ifdef CONFIG_HIGHMEM

            kmap_init();

            #endif

 

如果使用了CONFIG_HIGHMEM选项, 就要对大于896MB的内存进行初始化,我们不准备对这部分内容进行详细讨论。

 

            {

                   unsigned long zones_size[MAX_NR_ZONES] = {0, 0, 0};

                   unsigned int max_dma, high, low;

    

                   max_dma = virt_to_phys((char *)MAX_DMA_ADDRESS)

                                      >> PAGE_SHIFT;

    

     低于16MB的内存只能用于DMA,因此,上面这条语句用于存放16MB的页面

                   low = max_low_pfn;

                   high = highend_pfn;

    

            if (low < max_dma)

                       zones_size[ZONE_DMA] = low;

            else {

                       zones_size[ZONE_DMA] = max_dma;

                       zones_size[ZONE_NORMAL] = low - max_dma;

            #ifdef CONFIG_HIGHMEM

                       zones_size[ZONE_HIGHMEM] = high - low;

            #endif

               }

 

       计算三个管理区的大小,并存放在zones_size数组中。三个管理区是:

      ZONE_DMA:从016MB分配给这个区

  ZONE_NORMAL:从16MB896MB分配给这个区

 ZONE_DMA 896MB以上分配给这个区

 

                   free_area_init(zones_size);

                   }

    

                   return;

 

      这个函数用来初始化内存管理区并创建内存映射表,详细介绍参见后面内容。  

2pagetable_init()函数

   这个函数真正地在页目录swapper_pg_dir中建立页表,描述如下:

              unsigned long vaddr, end;

              pgd_t *pgd, *pgd_base;

              int i, j, k;

              pmd_t *pmd;

              pte_t *pte, *pte_base;

 

              /*

              * This can be zero as well - no problem, in that case we exit

              * the loops anyway due to the PTRS_PER_* conditions.

              */

              end = (unsigned long)__va(max_low_pfn*PAGE_SIZE);

 

    计算max_low_pfn 的虚拟地址,并把它存放在end.中。

              pgd_base = swapper_pg_dir;

 

    pgd_base (页目录基地址) 指向 swapper_pg_dir.

      

              #if CONFIG_X86_PAE

              for (i = 0; i < PTRS_PER_PGD; i++)

                         set_pgd(pgd_base + i, __pgd(1 + __pa(empty_zero_page)));

              #endif

 

      如果PAE 被激活, PTRS_PER_PGD就为4,且变量 swapper_pg_dir 用作页目录指针表,宏set_pgd() 定义于 include/asm-i386/pgtable-3level.h.中。

      

              i = __pgd_offset(PAGE_OFFSET);

              pgd = pgd_base + i;

 

__pgd_offset()在给定地址的页目录中检索相应的下标。因此__pgd_offset(PAGE_OFFSET)返回0x300 ( 十进制768 ),即内核地址空间开始处的下标。因此,pgd现在指向页目录表的第768项。

 

              for (; i < PTRS_PER_PGD; pgd++, i++) {

                         vaddr = i*PGDIR_SIZE;

                         if (end && (vaddr >= end))

               break;

 

如果使用了CONFIG_X86_PAE选项, PTRS_PER_PGD 就为4,否则,一般情况下它都为1024,即页目录的项数。PGDIR_SIZE给出一个单独的页目录项所能映射的RAM总量,在两级页目录中它为4MB,当使用CONFIG_X86_PAE选项时,它为1GB。计算虚地址vaddr,并检查它是否到了虚拟空间的顶部。

 

              #if CONFIG_X86_PAE

                         pmd = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);

                         set_pgd(pgd, __pgd(__pa(pmd) + 0x1));

              #else

                    pmd = (pmd_t *)pgd;

              #endif

 

如果使用了CONFIG_X86_PAE选项,则分配一页(4K)的内存给bootmem分配器用,以保存中间页目录,并在总目录中设置它的地址。否则,没有中间页目录,就把中间页目录直接映射到总目录。

 

                  if (pmd != pmd_offset(pgd, 0))

                             BUG();

                         for (j = 0; j < PTRS_PER_PMD; pmd++, j++) {

                             vaddr = i*PGDIR_SIZE + j*PMD_SIZE;

                             if (end && (vaddr >= end))

                                break;

                       if (cpu_has_pse) {

                                unsigned long __pe;

                                set_in_cr4(X86_CR4_PSE);

                                boot_cpu_data.wp_works_ok = 1;

                                __pe = _KERNPG_TABLE + _PAGE_PSE + __pa(vaddr);

                                /* Make it "global" too if supported */

                                if (cpu_has_pge) {

                                    set_in_cr4(X86_CR4_PGE);

                                    __pe += _PAGE_GLOBAL;

                                }

                                set_pmd(pmd, __pmd(__pe));

                           continue;

                             }

 

现在,开始填充页目录(如果有PAE,就是填充中间页目录)。计算表项所映射的虚地址,如果没有激活PAE PMD_SIZE大小就为0,因此,vaddr = i * 4MB。例如,表项0x300所映射的虚地址为0x300 * 4MB = 3GB。接下来,我们检查PSEPage Size Extension)是否可用,如果是,就要避免使用页表而直接使用4MB的页。宏cpu_has_pse()用来检查处理器是否具有扩展页,如果有,则宏set_in_cr4()就启用它。

PentiumII处理器开始,就可以有附加属性PGE (Page Global Enable)。当一个页被标记为全局的,且设置了PGE,那么,在任务切换发生或CR3被装入时,就不能使该页所在的页表(或页目录项)无效。这将提高系统性能,也是让内核处于3GB以上的原因之一。选择了所有属性后,设置中间页目录项。

 

               pte_base = pte = (pte_t *)

                                alloc_bootmem_low_pages(PAGE_SIZE);

 

       如果PSE可用,就执行这一句,它为一个页表(4K)分配空间。

 

               for (k = 0; k < PTRS_PER_PTE; pte++, k++) {

                   vaddr = i*PGDIR_SIZE + j*PMD_SIZE + k*PAGE_SIZE;

                   if (end && (vaddr >= end))

                       break;

 

       在一个页表中有1024个表项(如果启用PAE,就是512个),每个表项映射4K1页)。

                   *pte = mk_pte_phys(__pa(vaddr), PAGE_KERNEL);

               }

 

mk_pte_phys()创建一个页表项,这个页表项的物理地址为__pa(vaddr)。属性PAGE_KERNEL表示只有在内核态才能访问这一页表项。

 

               set_pmd(pmd, __pmd(_KERNPG_TABLE + __pa(pte_base)));

               if (pte_base != pte_offset(pmd, 0))

                   BUG();

           }

       }

 

通过调用set_pmd()把该页表追加到中间页目录中。这个过程一直继续,直到把所有的物理内存都映射到从PAGE_OFFSET开始的虚拟地址空间。

 

       /*

              * Fixed mappings, only the page table structure has to be

              * created - mappings will be set by set_fixmap():

              */

              vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK;

              fixrange_init(vaddr, 0, pgd_base);

 

在内存的最高端(4GB - 128MB),有些虚地址直接用在内核资源的某些部分中,这些地址的映射定义在/include/asm/fixmap.h中,枚举类型__end_of_fixed_addresses用作索引,宏__fix_to_virt()返回给定索引的虚地址。函数fixrange_init()为这些虚地址创建合适的页表项。注意,这里仅仅创建了页表项,而没有进行映射。这些地址的映射是由set_fixmap()函数完成的。

      

              #if CONFIG_HIGHMEM

              /*

              * Permanent kmaps:

              */

              vaddr = PKMAP_BASE;

              fixrange_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base);

              pgd = swapper_pg_dir + __pgd_offset(vaddr);

              pmd = pmd_offset(pgd, vaddr);

              pte = pte_offset(pmd, vaddr);

              pkmap_page_table = pte;

              #endif

 

如果使用了CONFIG_HIGHMEM选项,我们就可以访问896MB以上的物理内存,这些内存的地址被暂时映射到为此目的而保留的虚地址上。PKMAP_BASE 的值为0xFE000000(即4064MB),LAST_PKMAP 的值为 1024。因此,从4064MB开始,由fixrange_init()在页表中创建的表项能覆盖4MB的空间。接下来,把覆盖4MB内存的页表项赋给pkmap_page_table

 

              #if CONFIG_X86_PAE

              /*

              * Add low memory identity-mappings - SMP needs it when

              * starting up on an AP from real-mode. In the non-PAE

              * case we already have these mappings through head.S.

              * All user-space mappings are explicitly cleared after

              * SMP startup.

              */

              pgd_base[0] = pgd_base[USER_PTRS_PER_PGD];

              #endif