Linux内核页表



Linux内核页表

一.    Linux地址空间

ARM的32位系统共支持4G的内存空间,其中0-3G为用户空间,3G-4G是内核空间,

ARM采用2级页表,32位地址空间ADDRESS分别为 PGD|PTE|12Bits, 在内核代码中分别为PGD 11位,PTE 9 位,页内地址12位;但是在MMU系统中对于ARM的二级分页设置分别为PGD 12位,PTE 8位,页内地址为12位。在内核代码层次虽然是11位,但是经过代码中的设置,最后都映射到MMU起作用时的PGD12位,PTE8位。看代码定义。

 

#define PTRS_PER_PGD         2048  //PGD页中的指针数

  #definePGDIR_SHIFT            21    //地址中偏移位数,去前11位,因为要右移21位

#define PGDIR_SIZE              (1UL<< PGDIR_SHIFT)  0x20 0000

#define USER_PTRS_PER_PGD     (TASK_SIZE/ PGDIR_SIZE)

在linux-3.5中,用户空间大小TASK_SIZE定义如下

#define TASK_SIZE             (UL(CONFIG_PAGE_OFFSET)- UL(0x01000000))

#define CONFIG_PAGE_OFFSET 0xC000 0000

在linux-2.6.25中,用户空间大小TASK_SIZE定义

       #define TASK_SIZE 0xC000 0000

 

二.    页表

页表分为用户空间页表和内核空间页表,不同的进程,它用户空间是不同的,所以它的用户空间页表是不同的,但是不同的进程它的内核空间是共享的,它的内核空间页表也是相同的。

在创建一个进程时,会为它创建一个页表指针,即mm_struct数据结构中的pgd_t * pgd;分配的函数是mm_init ()->mm_alloc_pgd(struct mm_struct *mm) ->pgd_alloc(structmm_struct *mm) ->#define __pgd_alloc()     (pgd_t*)__get_free_pages(GFP_KERNEL, 2),分配的空间为16k, 即4096*4bytes,从此处分配的页表空间包括了用户空间页表和内核空间页表。

用户空间页表映射了地址0x0000 0000到0xC000 0000的空间,即为TASK_SIZE的大小。然后

#define USER_PTRS_PER_PGD (TASK_SIZE/PGDIR_SIZE)=0xC0000000/0x20 0000 =0x600=1536

内核空间的页表映射了地址0xC000 0000到0Xffff ffff的空间,为1G,然后

0x40000000/PGDIR_SIZE=0x4000 0000/0x20 0000 = 512,

所以页表的项数共为1536+512 =2048项。

考虑到硬件的地址映射,pgd实际为12位,即只需要右移20位,而不是21位,PGDIR_SHIFT的数值,所以实际上页表的项数实际应该为2048*2的4096个项数,然后每项为4个字节,最后与上文分配的16K地址对应起来,即4096*4bytes=16k.

三.    引导代码的内核页表分配

Linux-2.6的S3C2410上,当系统启动的时候,内核页表的地址是0x3000 4000, 内核的加载地址是0x3000 8000,所以内核页表的最大空间是0x4000,4字节,也为16K。

系统启动的时候,最先执行的是最后会成为空闲进程init_mm(), 该进程在启动结束前会生成内核进程init进程,然后再启动其他进程。在init_mm()结构中定义了.pgd          = swapper_pg_dir,

#define CONFIG_PAGE_OFFSET 0xC000 0000

#definePAGE_OFFSET              UL(CONFIG_PAGE_OFFSET)

#defineKERNEL_RAM_VADDR (PAGE_OFFSET +TEXT_OFFSET)

.equ swapper_pg_dir,KERNEL_RAM_VADDR - PG_DIR_SIZE

#definePG_DIR_SIZE  0x4000

空闲进程的页表初始化在引导代码的汇编中实现,__create_page_tables

四.    进程内核空间页表的分配

上面已经讨论了进程页表分为用户空间页表和内核空间页表,当进程创建的时候会把

new_pgd = __pgd_alloc();

       if (!new_pgd)

              goto no_pgd;

       memset(new_pgd, 0, USER_PTRS_PER_PGD *sizeof(pgd_t));

       /*

        * Copy over the kernel and IO PGD entries

        */

       init_pgd = pgd_offset_k(0);

       memcpy(new_pgd + USER_PTRS_PER_PGD,init_pgd + USER_PTRS_PER_PGD,

                     (PTRS_PER_PGD - USER_PTRS_PER_PGD) *sizeof(pgd_t));

 

//

以下是对pgd_offset_k(0)的解析,最后init_pgd即为swapper_pg_dir的值

#definepgd_index(addr)              ((addr)>> PGDIR_SHIFT)

#definepgd_offset(mm, addr)      ((mm)->pgd +pgd_index(addr))

#definepgd_offset_k(addr)   pgd_offset(&init_mm,addr)

/

Memcpy()函数中参数的解析

#definePTRS_PER_PGD            2048,总的页数(暂时这么说)。

#defineUSER_PTRS_PER_PGD       (TASK_SIZE /PGDIR_SIZE)  1536

前1536为用户空间的地址值,之后才是内核空间的。

五.11位和12位的问题

#define PGDIR_SHIFT         21    //地址中偏移位数,去前11位,因为要右移21位

#define PGDIR_SIZE              (1UL<< PGDIR_SHIFT)  0x20 0000

ARM二级页表中PGD的位数为12, 即只要左移20为就行了,但是代码中却移动了21为,即多除了一个2;软件中相当于11位,那就可以这样理解,11位+1位,最后在11位的基础上补上0和1两个值来补齐12位,这样软件与硬件就对应起来。这样,前文中PGD页表的运算是根据11位来运算的,多除了一个2,现在补上0或者1后,它的数值应该翻倍,所以总的页表项数,应该为:

用户空间页表映射了地址0x0000 0000到0xC000 0000的空间,即为TASK_SIZE的大小。然后

#define USER_PTRS_PER_PGD (TASK_SIZE/PGDIR_SIZE)=0xC0000000/0x20 0000 =0x600=1536    //  1536*2 = 3072

内核空间的页表映射了地址0xC000 0000到0Xffff ffff的空间,为1G,然后

0x40000000/PGDIR_SIZE=0x4000 0000/0x20 0000 = 512,  // 512*2 = 1024

所以页表的项数共为 1536+512=2048项。 //2048*2 =4096

然后每项为4字节, 4096*4 = 16K , 即为每个进程分配的PGD的页表空间大小,这样就对应上了。

补充:

      实际上,内核代码一级页表只用到了16K/2,即8K。每项11bit, 但是硬件是12bit,所以一级页表中每项指向两个pte,最低位是即没有的12bit是0或者1。硬件中中每个pte实际上是8bit,即256项,软件上9bit,512项;所以在每项11位的pgd上就指向了两个pte, 空间大小是256*2=512,每项4字节,即2048byte,内核在分配的时候是分配了4K,即4096byte,多余的2048byte对应硬件不支持的dirty等选项。

static inline void __pmd_populate(pmd_t *pmdp, phys_addr_t pte,
                  pmdval_t prot)
{
        pmdval_t pmdval = (pte + PTE_HWTABLE_OFF) | prot;
        pmdp[0] = __pmd(pmdval);
        #ifndef CONFIG_ARM_LPAE
        pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t));
        #endif
        flush_pmd_entry(pmdp);
}
 

 

六.用户空间页表的分配

七.内核空间页表的分配(以linux-3.5为例)

在初始化内核空间页表时,大部分的内核空间已经映射好了,如内核代码的地址,内核内存地址,影响内核页表的内存分配主要有永久内核映射,临时内核映射,非连续内存分配等,当分配内存的时候,内核更新swapper_pg_dir地址下的页表,当访问该地址时,就会产生取值异常,然后就内核就把相应的内核映射的对应项复制到进程的相应的内核页表项中。

7.1 内核空间地址内存的分配

在系统初始化的时候,内核就会初始化内核内存的内核空间页表,

void __init paging_init(struct machine_desc *mdesc)

{

……

map_lowmem();--àcreate_mapping(&map,false);

……

}

7.2 永久内核映射

pkmap : 0xbfe00000 - 0xc0000000   ( 2 MB)

kmap()    

{

   might_sleep();

       if(!PageHighMem(page))

              returnpage_address(page);

       returnkmap_high(page);

}   

#define PKMAP_BASE (PAGE_OFFSET - PMD_SIZE)   //0xbfe00000

#define PAGE_OFFSET 0xC000 0000

#define PMD_SHIFT 21

#define PMD_SIZE (1UL << PMD_SHIFT)

 

#define LAST_PKMAP        PTRS_PER_PTE   //512

#define LAST_PKMAP_MASK           (LAST_PKMAP - 1)  //511

last_pkmap_nr = (last_pkmap_nr + 1) &LAST_PKMAP_MASK;

 

512 * 4k = 2 M,如标题所示。

 

7.3临时内核映射

fixmap : 0xfff00000 - 0xfffe0000   ( 896kB)

 

void *kmap_atomic(struct page *page)

4K一个页面

#define FIXADDR_START          0xfff00000UL

#define FIXADDR_TOP              0xfffe0000UL

#define FIXADDR_SIZE             (FIXADDR_TOP - FIXADDR_START)

 

#define FIX_KMAP_BEGIN        0

#define FIX_KMAP_END           (FIXADDR_SIZE >> PAGE_SHIFT)

7.4非连续内存分配

vmalloc : 0xe4800000 - 0xfc000000   ( 376 MB)

vmalloc()  

#define VMALLOC_START (((unsignedlong)high_memory + VMALLOC_OFFSET) & ~

(VMALLOC_OFFSET-1))  

#define VMALLOC_END   0xff000000UL

high_memory = __va(arm_lowmem_limit - 1) +1;  //此处定义有疑义;

 

vmalloc -> __vmalloc_node_flags->__vmalloc_node -> __vmalloc_node_range ->

__vmalloc_area_node -> (此处可能有递归但最终会调用此函数:) map_vm_area ->

vmap_page_range ->vmap_page_range_noflush -> vmap_pud_range -> vmap_pmd_range ->

vmap_pte_range -> pte_alloc_kernel ->__pte_alloc_kernel -> pmd_populate_kernel

(&init_mm, pmd, new)->__pmd_populate(pmdp, __pa(ptep), _PAGE_KERNEL_TABLE)

 

static inline void __pmd_populate(pmd_t*pmdp, phys_addr_t pte,pmdval_t prot)

{

       pmdval_tpmdval = (pte + PTE_HWTABLE_OFF) | prot;

       pmdp[0]= __pmd(pmdval);

#ifndef CONFIG_ARM_LPAE

       pmdp[1]= __pmd(pmdval + 256 * sizeof(pte_t));

#endif

       flush_pmd_entry(pmdp);

}

八.空值异常

当内核访问某个地址发生异常时就会触发取值异常,从中断中进入函数

do_translation_fault(unsigned long addr,unsigned int fsr,

                   struct pt_regs *regs)

{

  if(addr < TASK_SIZE)

              returndo_page_fault(addr, fsr, regs); //用户空间的内存分配

  如果地址大于TASK_SIZE,即 0xC000 0000,是内核空间的地址

  进入内核空间页表的分配。

}

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux x64 中,虚拟地址空间是 2^48 (256 TB)。为了管理这么大的地址空间,Linux 使用了四级页。 四级页包含了四个级别的页,每个页的大小都是 512 个条目。每个条目的大小为 8 字节,因此一个页的大小为 4 KB。因此,一个四级页的大小为 4 KB * 512 * 512 * 512 * 512 = 256 TB。 在四级页中,虚拟地址被划分为五个部分:PGD(Page Global Directory)、PUD(Page Upper Directory)、PMD(Page Middle Directory)、PTE(Page Table Entry)和偏移量。 - PGD:PGD 是最高级的页,它包含了 512 个 PUD 条目。 - PUD:PUD 包含了 512 个 PMD 条目。 - PMD:PMD 包含了 512 个 PTE 条目。 - PTE:PTE 包含了物理页框号和一些标志。 - 偏移量:偏移量用于计算物理地址。 当 CPU 访问一个虚拟地址时,它首先使用 PGD 条目找到对应的 PUD 条目,然后使用 PUD 条目找到对应的 PMD 条目,依此类推。最后,CPU 使用 PTE 条目找到对应的物理页框号,并将虚拟地址中的偏移量添加到物理页框号中,得到物理地址。 Linux x64 中的页是按需分配的。当进程第一次访问某个虚拟地址时,Linux 会分配一个物理页框,并将该页框映射到对应的虚拟地址。如果进程再次访问同一虚拟地址,则直接使用之前分配的物理页框。 这是 Linux x64 中页的基本原理。理解页的工作原理对于理解 Linux 内核的内存管理非常重要。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值