一步一步学linux操作系统: 23 内存管理_内核态内存映射

内核页表

和用户态页表不同,在系统初始化的时候,就要创建内核页表

swapper_pg_dir
arch/x86/include/asm/pgtable_64.h


extern pud_t level3_kernel_pgt[512];
extern pud_t level3_ident_pgt[512];
extern pmd_t level2_kernel_pgt[512];
extern pmd_t level2_fixmap_pgt[512];
extern pmd_t level2_ident_pgt[512];
extern pte_t level1_fixmap_pgt[512];
extern pgd_t init_top_pgt[];


#define swapper_pg_dir init_top_pgt

在这里插入图片描述

swapper_pg_dir 指向内核最顶级的目录 pgd

64 位系统的虚拟地址空间的布局,其中 XXX_ident_pgt 对应的是直接映射区,XXX_kernel_pgt 对应的是内核代码区,XXX_fixmap_pgt 对应的是固定映射区。

内核页表定义

arch/x86/kernel/head_64.S


__INITDATA


NEXT_PAGE(init_top_pgt)
  .quad   level3_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE
  .org    init_top_pgt + PGD_PAGE_OFFSET*8, 0
  .quad   level3_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE
  .org    init_top_pgt + PGD_START_KERNEL*8, 0
  /* (2^48-(2*1024*1024*1024))/(2^39) = 511 */
  .quad   level3_kernel_pgt - __START_KERNEL_map + _PAGE_TABLE


NEXT_PAGE(level3_ident_pgt)
  .quad  level2_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE
  .fill  511, 8, 0
NEXT_PAGE(level2_ident_pgt)
  /* Since I easily can, map the first 1G.
   * Don't set NX because code runs from these pages.
   */
  PMDS(0, __PAGE_KERNEL_IDENT_LARGE_EXEC, PTRS_PER_PMD)


NEXT_PAGE(level3_kernel_pgt)
  .fill  L3_START_KERNEL,8,0
  /* (2^48-(2*1024*1024*1024)-((2^39)*511))/(2^30) = 510 */
  .quad  level2_kernel_pgt - __START_KERNEL_map + _KERNPG_TABLE
  .quad  level2_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE


NEXT_PAGE(level2_kernel_pgt)
  /*
   * 512 MB kernel mapping. We spend a full page on this pagetable
   * anyway.
   *
   * The kernel code+data+bss must not be bigger than that.
   *
   * (NOTE: at +512MB starts the module area, see MODULES_VADDR.
   *  If you want to increase this then increase MODULES_VADDR
   *  too.)
   */
  PMDS(0, __PAGE_KERNEL_LARGE_EXEC,
    KERNEL_IMAGE_SIZE/PMD_SIZE)


NEXT_PAGE(level2_fixmap_pgt)
  .fill  506,8,0
  .quad  level1_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE
  /* 8MB reserved for vsyscalls + a 2MB hole = 4 + 1 entries */
  .fill  5,8,0


NEXT_PAGE(level1_fixmap_pgt)
  .fill  51

在这里插入图片描述

init_top_pgt

quad 是声明了一项的内容
org 是跳到了某个位置
init_top_pgt 有三项:

第一项 ,指向的是 level3_ident_pgt,也即直接映射区页表的三级目录。
为什么要减去__START_KERNEL_map
1、因为level3_ident_pgt是定义在内核代码里的,写代码的时候,写的都是虚拟地址,谁写代码的时候页不知道将来加载的物理地址是多少呀,对不对
2、因为level3_ident_pgt是在虚拟地址的内核代码里的,而__START_KERNEL_map正是虚拟地址空间内核代码段的其实地址,
3、level3_ident_pgt 减去 __START_KERNEL_map才是物理地址
第二项,跳到 PGD_PAGE_OFFSET 的位置,再定义一项

这一项就应该是 __PAGE_OFFSET_BASE 对应的,__PAGE_OFFSET_BASE 是虚拟地址空间里面内核的起始地址

arch/x86/kernel/head_64.S


PGD_PAGE_OFFSET = pgd_index(__PAGE_OFFSET_BASE)
PGD_START_KERNEL = pgd_index(__START_KERNEL_map)
L3_START_KERNEL = pud_index(__START_KERNEL_map)

在这里插入图片描述
第三项,指向 level3_kernel_pgt,内核代码区

接下来的代码就很类似了,就是初始化个表项,然后指向下一级目录,最终形成下面这张图

图片来自极客时间趣谈linux操作系统
图片来自极客时间趣谈linux操作系统

如果是用户态进程页表,会有 mm_struct 指向进程顶级目录 pgd,对于内核来讲,也定义了一个 mm_struct,指向 swapper_pg_dir。

\mm\init-mm.c


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),
  .user_ns  = &init_user_ns,
  INIT_MM_CONTEXT(init_mm)
};

在这里插入图片描述

初始化内核页表

在系统启动的时候 start_kernel 会调用 setup_arch。

setup_arch 函数

\arch\x86\kernel\setup.c


void __init setup_arch(char **cmdline_p)
{
  /*
   * copy kernel address range established so far and switch
   * to the proper swapper page table
   */
  clone_pgd_range(swapper_pg_dir     + KERNEL_PGD_BOUNDARY,
      initial_page_table + KERNEL_PGD_BOUNDARY,
      KERNEL_PGD_PTRS);


  load_cr3(swapper_pg_dir);
  __flush_tlb_all();
......
  init_mm.start_code = (unsigned long) _text;
  init_mm.end_code = (unsigned long) _etext;
  init_mm.end_data = (unsigned long) _edata;
  init_mm.brk = _brk_end;
......
  init_mem_mapping();
......
}

在这里插入图片描述
load_cr3(swapper_pg_dir) 说明内核页表要开始起作用了,并且刷新了 TLB,初始化 init_mm 的成员变量
最重要的就是 init_mem_mapping。最终它会调用 kernel_physical_mapping_init。

kernel_physical_mapping_init 函数

\arch\x86\mm\init_64.c


/*
 * Create page table mapping for the physical memory for specific physical
 * addresses. The virtual and physical addresses have to be aligned on PMD level
 * down. It returns the last physical address mapped.
 */
unsigned long __meminit
kernel_physical_mapping_init(unsigned long paddr_start,
           unsigned long paddr_end,
           unsigned long page_size_mask)
{
  unsigned long vaddr, vaddr_start, vaddr_end, vaddr_next, paddr_last;


  paddr_last = paddr_end;
  vaddr = (unsigned long)__va(paddr_start);
  vaddr_end = (unsigned long)__va(paddr_end);
  vaddr_start = vaddr;


  for (; vaddr < vaddr_end; vaddr = vaddr_next) {
    pgd_t *pgd = pgd_offset_k(vaddr);
    p4d_t *p4d;


    vaddr_next = (vaddr & PGDIR_MASK) + PGDIR_SIZE;


    if (pgd_val(*pgd)) {
      p4d = (p4d_t *)pgd_page_vaddr(*pgd);
      paddr_last = phys_p4d_init(p4d, __pa(vaddr),
               __pa(vaddr_end),
               page_size_mask);
      continue;
    }


    p4d = alloc_low_page();
    paddr_last = phys_p4d_init(p4d, __pa(vaddr), __pa(vaddr_end),
             page_size_mask);


    p4d_populate(&init_mm, p4d_offset(pgd, vaddr), (pud_t *) p4d);
  }
  __flush_tlb_all();


  return paddr_l

在这里插入图片描述
在 kernel_physical_mapping_init 里,先通过 __va 将物理地址转换为虚拟地址,然后再创建虚拟地址和物理地址的映射页表。

1、既然对于内核来讲,我们可以用 __va 和 __pa 直接在虚拟地址和物理地址之间转来转去?
因为这是CPU和内存的硬件的需求,也就是说。CPU在保护模式下访问虚拟地址的时候,就会用CR3这个寄存器,这个寄存器是CPU定义的。作为操作系统,要按照硬件的要求来。

2、按照咱们将初始化的时候的过程,系统早早就进入了保护模式,到了 setup_arch 里面才 load_cr3,如果使用 cr3 是硬件的要求,那之前是怎么办的呢?
arch\x86\kernel\head_64.S,这里面除了初始化内核页表之外,在这之前,还有另一个页表 early_top_pgt。这个页表就是专门用在真正的内核页表初始化之前,为了遵循硬件的要求而设置的

vmalloc 和 kmap_atomic 原理

用户态可以通过malloc函数分配内存,当然malloc在分分配比较大的内存的时候,底层调用的是mmap,当然也可以直接通过mmap做内存映射、在内核里面也有相应的函数

在虚拟地址空间里,有个vmalloc区域,从VMALLOC_START开始VMALLOC_END,可以用于映射一段物理内存

vmalloc函数


/**
 *  vmalloc  -  allocate virtually contiguous memory
 *  @size:    allocation size
 *  Allocate enough pages to cover @size from the page level
 *  allocator and map them into contiguous kernel virtual space.
 *
 *  For tight control over page level allocator and protection flags
 *  use __vmalloc() instead.
 */
void *vmalloc(unsigned long size)
{
  return __vmalloc_node_flags(size, NUMA_NO_NODE,
            GFP_KERNEL);
}


static void *__vmalloc_node(unsigned long size, unsigned long align,
          gfp_t gfp_mask, pgprot_t prot,
          int node, const void *caller)
{
  return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END,
        gfp_mask, prot, 0, node, caller);
}

kmap_atomic 函数


void *kmap_atomic_prot(struct page *page, pgprot_t prot)
{
......
  if (!PageHighMem(page))
    return page_address(page);
......
  vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
  set_pte(kmap_pte-idx, mk_pte(page, prot));
......
  return (void *)vaddr;
}


void *kmap_atomic(struct page *page)
{
  return kmap_atomic_prot(page, kmap_prot);
}


static __always_inline void *lowmem_page_address(const struct page *page)
{
  return page_to_virt(page);
}


#define page_to_virt(x)  __va(PFN_PHYS(page_to_pfn(x)

1、如果是32位有高端地址的就需要调用set_pte通过内核页表进行临时映射;
2、如果是64位没有高端地址的,就调用page_address,里面会调用lowmen_page_address,
3、其实低端内存的映射,会直接使用_va进行临时映射

内核态缺页异常

vmalloc 和 kmap_atomic 不同
kmap_atomic发现,没有页表的时候,就直接创建也表进行映射了、而vmalloc没有,它只分配了内核的虚拟地址、所以,访问它的时候,会产生缺页异常

内核态的缺页异常还是会调用do_page_fault,进一步会走到vmalloc_fault
vmalloc_fault 函数


/*
 * 32-bit:
 *
 *   Handle a fault on the vmalloc or module mapping area
 */
static noinline int vmalloc_fault(unsigned long address)
{
  unsigned long pgd_paddr;
  pmd_t *pmd_k;
  pte_t *pte_k;


  /* Make sure we are in vmalloc area: */
  if (!(address >= VMALLOC_START && address < VMALLOC_END))
    return -1;


  /*
   * Synchronize this task's top level page-table
   * with the 'reference' page table.
   *
   * Do _not_ use "current" here. We might be inside
   * an interrupt in the middle of a task switch..
   */
  pgd_paddr = read_cr3_pa();
  pmd_k = vmalloc_sync_one(__va(pgd_paddr), address);
  if (!pmd_k)
    return -1;


  pte_k = pte_offset_kernel(pmd_k, address);
  if (!pte_present(*pte_k))
    return -1;


  return 0

总结

1、物理内存管理

在这里插入图片描述

2、内核态内存分配

在这里插入图片描述
3、用户态内存分配

在这里插入图片描述

4、内存分配体系总图

图片来自极客时间趣谈linux操作系统
图片来自极客时间趣谈linux操作系统

参考资料:

趣谈Linux操作系统(极客时间)链接:
http://gk.link/a/10iXZ
欢迎大家来一起交流学习

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墨1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值