深入理解Linux虚拟内存管理(四)(中)

简介: 深入理解Linux虚拟内存管理(四)

深入理解Linux虚拟内存管理(四)(上):https://developer.aliyun.com/article/1597778

3、释放内存

(1)free_bootmem

// mm/bootmem.c
void __init free_bootmem (unsigned long addr, unsigned long size)
{
  // 调用核心函数,以 contig_page_data 的启动内存数据为参数。
  return(free_bootmem_core(contig_page_data.bdata, addr, size));
}

(2)free_bootmem_core

static void __init free_bootmem_core(bootmem_data_t *bdata, unsigned long addr, unsigned long size)
{
  unsigned long i;
  unsigned long start;
  /*
   * round down end of usable mem, partially free pages are
   * considered reserved.
   */
  unsigned long sidx;
  // 计算受影响的末端索引 eidx。
  unsigned long eidx = (addr + size - bdata->node_boot_start)/PAGE_SIZE;
  // 如果末端地址不是页对齐,则它为受影响区域的末端向下取整到最近页面。
  unsigned long end = (addr + size)/PAGE_SIZE;
  // 如果释放了大小为 0 的页面,则调用 BUG()。
  if (!size) BUG();
  // 如果末端 PFN 在该节点可寻址内存之后,则这里调用 BUG()。
  if (end > bdata->node_low_pfn)
    BUG();

  /*
   * Round up the beginning of the address.
   */
  // 如果起始地址不是页对齐的,则将其向上取整到最近页面。  
  start = (addr + PAGE_SIZE-1) / PAGE_SIZE;
  // 计算要释放的起始索引。
  sidx = start - (bdata->node_boot_start/PAGE_SIZE);
  // 释放全部满页面,这里清理在启动位图中的位。如果已经为 0,则表示是一次重
  // 复释放或内存从未使用,这里调用 BUG()。
  for (i = sidx; i < eidx; i++) {
    if (!test_and_clear_bit(i, bdata->node_bootmem_map))
      BUG();
  }
}

(3)总结

    由上分析可知:函数 free_bootmem_core 主要就是把对应页帧号的位图(bootmem_data_t ->node_bootmem_map)设置为 0, 来表示对应的页是空闲的。

4、释放引导内存分配器

    在系统启动后,引导内存分配器就不再需要了,这些函数负责销毁不需要的引导内存分配器结构,并将其余的页面传入到普通的物理页面分配器中。

(1)mem_init

    这个函数的调用图如图 5.2 所示。引导内存分配器的这个函数的重要部分是它调用 free_pages_init() 。这个函数分成如下几部分:


函数前面部分为高端内存地址设置在全局 mem_map 中的 PFN,并将系统范围的 0 页面清零。

调用 free_pages_init() 。

打印系统中可用内存的提示信息。

如果配置项可用,则检查 CPU 是否支持 PAE,并测试 CPU 中的 WP 位。这很重要,如果没有 WP 位,就必须调用 verify_write() 对内核到用户空间的每一次写检查。这仅应用于像 386 一样的老处理器。

填充 swapper_pg_dir 的 PGD 用户空间部分的表项,其中有内核页表。0 页面映射到所有的表项。

// arch/i386/mm/init.c
void __init mem_init(void)
{
  int codesize, reservedpages, datasize, initsize;
  // ...
  // 这个函数记录从 mem_map(highmem_start_page) 处高端内存开始的 PFN,系统中最
  // 大的页面数 (max_mapnr 和 num_physpages),以及最后是可被内核映射的最大页面数
  // (num_mappedpages)。
  set_max_mapnr_init();
  // high_memory 是高端内存开始处的虚拟地址。
  high_memory = (void *) __va(max_low_pfn * PAGE_SIZE);

  /* clear the zero-page */
  // 将系统范围内的 0 页面清 0。
  memset(empty_zero_page, 0, PAGE_SIZE);

  // 调用 free_pages_init ,在那里告知引导内存分配器释放它自身以
  // 及初始化高端内存的所有页面,以用于伙伴分配器。
  reservedpages = free_pages_init();

  // 计算用于初始化代码和数据的代码段、数据段和内存大小。(所有标识为 __init 的函
  // 数都将在这一部分) 。  
  codesize =  (unsigned long) &_etext - (unsigned long) &_text;
  datasize =  (unsigned long) &_edata - (unsigned long) &_etext;
  initsize =  (unsigned long) &__init_end - (unsigned long) &__init_begin;
  // ...
  if (boot_cpu_data.wp_works_ok < 0)
    test_wp_bit();
    
#ifndef CONFIG_SMP
  // 遍历 swapper_pg_dir 的用户空间部分用到的每个 PGD,并将 0 页面与之映射。
  zap_low_mappings();
#endif
}
(a)⇐ set_max_mapnr_init
// mm/memory.c
unsigned long max_mapnr;
unsigned long num_physpages;
unsigned long num_mappedpages;
void * high_memory;
struct page *highmem_start_page;

// ==============================================================================
// arch/i386/mm/init.c
static void __init set_max_mapnr_init(void)
{
#ifdef CONFIG_HIGHMEM
        highmem_start_page = mem_map + highstart_pfn;
        max_mapnr = num_physpages = highend_pfn;
        num_mappedpages = max_low_pfn;
#else
        max_mapnr = num_mappedpages = num_physpages = max_low_pfn;
#endif
}


(2)free_pages_init

    这个函数有 3 个重要的功能:调用 free_all_bootmem(),销毁引导内存分配器,以及释放伙伴分配器的所有高端内存。

 // arch/i386/mm/init.c
static int __init free_pages_init(void)
{
  extern int ppro_with_ram_bug(void);
  int bad_ppro, reservedpages, pfn;
  
  // 在奔腾 Pro 版本中有一个 bug,阻止高端内存中的某些页被使用。
  // 函数 ppro_with_ram_bug() 检查是否存在这个 bug。
  bad_ppro = ppro_with_ram_bug();

  /* this will put all low memory onto the freelists */
  // 调用 free_all_bootmem() 来销毁引导内存分配器。
  totalram_pages += free_all_bootmem();

  // 遍历所有的内存,计数保留给引导内存分配器的页面数。
  reservedpages = 0;
  for (pfn = 0; pfn < max_low_pfn; pfn++) {
    /*
     * Only count reserved RAM pages
     */
    if (page_is_ram(pfn) && PageReserved(mem_map+pfn))
      reservedpages++;
  }
#ifdef CONFIG_HIGHMEM
  // 对高端内存中的每一页,这里调用 one_highpage_init()。这个
  // 函数清除 PG_reserved 位,设置 PG_high 位,设置计数为 1,调用 __free_pages() 来给
  // 伙伴分配器分配页面,增加 totalhigh_pages 计数。杀死有 bug 的奔腾 Pro 的页面将被跳过。
  for (pfn = highend_pfn-1; pfn >= highstart_pfn; pfn--)
    one_highpage_init((struct page *) (mem_map + pfn), pfn, bad_ppro);
  totalram_pages += totalhigh_pages;
#endif
  return reservedpages;
}

传送门 free_all_bootmem

(3)one_highpage_init

    这个函数初始化高端内存中的页面信息,并检查以保证页面不会在某些奔腾 Pro 上报 bug。它仅在编译时指定了 CONFIG_HIGHMEM 的情况下存在。

// arch/i386/mm/init.c
#ifdef CONFIG_HIGHMEM
void __init one_highpage_init(struct page *page, int pfn, int bad_ppro)
{
  // 如果在 PFN 处不存在页面,则这里标记 struct page 为保留的,所以不会使用该页面。
  if (!page_is_ram(pfn)) {
    SetPageReserved(page);
    return;
  }
  
  // 如果当前运行的 CPU 是有奔腾 Pro bug 的,而这个页面会导致崩责 (page_kill_ppro()
  // 进行这项检查) ,这里就标记页面为保留的,它也不会被分配。
  if (bad_ppro && page_kills_ppro(pfn)) {
    SetPageReserved(page);
    return;
  }
  
  // 从这里开始,就会使用高端内存的页面,所以这里首先清除保留位,然后将它们分配
  // 给伙伴分配器。
  ClearPageReserved(page);
  // 设置 PG_highmem 位表示它是一个高端内存页面。
  set_bit(PG_highmem, &page->flags);
  // 初始化页面的使用计数为 1,它由伙伴分配器设为 0。
  atomic_set(&page->count, 1);
  // 利用 __free_page() 来释放页面,这样伙伴分配器会将高端内存页面
  // 加到它的空闲链表中。
  __free_page(page);
  // 将可用的高内存页面总数 (totalhigh_pages) 加 1。
  totalhigh_pages++;
}
#endif /* CONFIG_HIGHMEM */

(4)free_all_bootmem

// mm/bootmem.c
// 对 NUMA 而言,这里仅是简单地调用以特定 pgdat 为参数的核心函数。
unsigned long __init free_all_bootmem_node (pg_data_t *pgdat)
{
  return(free_all_bootmem_core(pgdat));
}

// 对 UMA 而言,这里调用仅以节点 contig_page_data 为参数的核心函数。
unsigned long __init free_all_bootmem (void)
{
  return(free_all_bootmem_core(&contig_page_data));
}

(5)free_all_bootmem_core

    这是销毁引导内存分配器的核心函数。它分为如下两个主要部分:

  • 对该节点已知的未分配页面,它完成如下步骤:
  • 清除结构页面中的 PG_reserved 标志。
  • 置计数为 1
  • 调用 __free_pages(),这样伙伴分配器可以构建它的空闲链表。
  • 释放用于位图的页面,并将其释放给伙伴分配器。
// mm/bootmem.c
static unsigned long __init free_all_bootmem_core(pg_data_t *pgdat)
{
  struct page *page = pgdat->node_mem_map;
  bootmem_data_t *bdata = pgdat->bdata;
  unsigned long i, count, total = 0;
  unsigned long idx;

  // 如果没有映射图,则意味着这个节点已经被释放了,且肯定在依赖于体系结构的代码
  // 中出现了错误,所以这里调用 BUG()。
  if (!bdata->node_bootmem_map) BUG();

  // 将页面数的运行数传给伙伴分配器。
  count = 0;
  // idx 是该节点最后可寻址的索引。
  idx = bdata->node_low_pfn - (bdata->node_boot_start >> PAGE_SHIFT);
// 遍历该节点可寻址的所有页面。
  for (i = 0; i < idx; i++, page++) {
  // 如果该页标记为空闲,则 .....  
    if (!test_bit(i, bdata->node_bootmem_map)) {
  // 将传给伙伴分配器页面数的运行数加 1。    
      count++;
  // 清除 PG_reserved 标志。     
      ClearPageReserved(page);
  // 设置计数为 1,这样伙伴分配器将考虑这是页面的最后一个用户,并将其放入到空闲
  // 链表中。     
      set_page_count(page, 1);
  // 用伙伴分配器的释放函数,这样页面将被加入到空闲链表中。      
      __free_page(page);
    }
  }
  // total 将设为由此函数传递的页面总数。
  total += count;

  /*
   * Now free the allocator bitmap itself, it's not
   * needed anymore:
   */
// 这一块释放分配器位图并返回。  
//
  // 获得在启动内存映射图顶端的 struct page。
  page = virt_to_page(bdata->node_bootmem_map);
  // 位图释放的页面数。
  count = 0;
  // 对该位图使用的所有页面,这里与前面的代码一样,将其释放给伙伴分配器。
  for (i = 0; i < ((bdata->node_low_pfn-(bdata->node_boot_start >> PAGE_SHIFT))/8
                 + PAGE_SIZE-1)/PAGE_SIZE; i++,page++) {
    count++;
    ClearPageReserved(page);
    set_page_count(page, 1);
    __free_page(page);
  }
  total += count;
  // 设启动内存映射图为 NULL,以阻止其意外地被第 2 次释放。
  bdata->node_bootmem_map = NULL;

  // 返回该函数释放的页面总数,或者说,返回加人到伙伴分配器空闲链表的页面总数。
  return total;
}
(a)⇐ mm.h
// include/linux/mm.h
#define get_page(p)   atomic_inc(&(p)->count)
#define put_page(p)   __free_page(p)
#define put_page_testzero(p)  atomic_dec_and_test(&(p)->count)
#define page_count(p)   atomic_read(&(p)->count)
#define set_page_count(p,v)   atomic_set(&(p)->count, v)

#define SetPageReserved(page)   set_bit(PG_reserved, &(page)->flags)
#define ClearPageReserved(page)   clear_bit(PG_reserved, &(page)->flags)
(b)⇒ __free_page

传送门 __free_page

二、页表管理

1、初始化页表

(1)paging_init

    当这个函数返回时,页面已经完全建立完成。注意这里都是与 x86 相关的。

// arch/i386/mm/init.c
/*
 * paging_init() sets up the page tables - note that the first 8MB are
 * already mapped by head.S.
 *
 * This routines also unmaps the page at virtual kernel address 0, so
 * that we can trap those pesky NULL-reference errors in the kernel.
 */
void __init paging_init(void)
{
  // pagetable_init() 负责利用 swapper_pg_dir 设立一个静态页表作为 PGD。
  pagetable_init();
  // 将初始化后的 swapper_pg_dir 载入 CR3 寄存器,这样 CPU 可以使用它。
  load_cr3(swapper_pg_dir); 

#if CONFIG_X86_PAE
  /*
   * We will bail out later - printk doesn't work right now so
   * the user would just see a hanging kernel.
   */
  // 如果 PAE 可用,则在 CR4 寄存器中设置相应的位。  
  if (cpu_has_pae)
    set_in_cr4(X86_CR4_PAE);
#endif
  // 清洗所有 (包括在全局内核中) 的 TLB。
  __flush_tlb_all();

#ifdef CONFIG_HIGHMEM
  // kmap_init() 调用 kmap() 初始化保留的页表区域。
  kmap_init();
#endif
  // zone_sizes_init() (见 B.1.2 小节) 记录每个管理区的大小,然后调用 free_area_init()
  // (见 B.1.3 小节)来初始化各个管理区。
  zone_sizes_init();
}

(2)pagetable_init

    这个函数负责静态初始化一个从静态定义的称为 swapper_pg_dirPDG 开始的页表。不管怎样,PTE 将可以指向在 ZONE_NORMAL 中的每个页面帧。

(a)⇐ pgtable-2level.h
// include/asm-i386/pgtable-2level.h
/*
 * traditional i386 two-level paging structure:
 */

#define PGDIR_SHIFT 22
#define PTRS_PER_PGD  1024

/*
 * the i386 is two-level, so we don't really have any
 * PMD directory physically.
 */
#define PMD_SHIFT 22
#define PTRS_PER_PMD  1

#define PTRS_PER_PTE  1024

/*
 * (pmds are folded into pgds so this doesnt get actually called,
 * but the define is needed for a generic inline function.)
 */
#define set_pmd(pmdptr, pmdval) (*(pmdptr) = pmdval)
#define set_pgd(pgdptr, pgdval) (*(pgdptr) = pgdval)

static inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address)
{
  return (pmd_t *) dir;
}

#define __mk_pte(page_nr,pgprot) __pte(((page_nr) << PAGE_SHIFT) | pgprot_val(pgprot))
(b)⇐ pgtable.h
// ========================================================================
// include/asm-i386/pgtable.h
#define PMD_SIZE  (1UL << PMD_SHIFT)    // 4M
#define PMD_MASK  (~(PMD_SIZE-1))
#define PGDIR_SIZE  (1UL << PGDIR_SHIFT)  // 4M
#define PGDIR_MASK  (~(PGDIR_SIZE-1))

#define page_pte(page) page_pte_prot(page, __pgprot(0))

#define pmd_page(pmd) \
((unsigned long) __va(pmd_val(pmd) & PAGE_MASK))

/* to find an entry in a page-table-directory. */
#define pgd_index(address) ((address >> PGDIR_SHIFT) & (PTRS_PER_PGD-1))

#define __pgd_offset(address) pgd_index(address)

#define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))

/* to find an entry in a kernel page-table-directory */
#define pgd_offset_k(address) pgd_offset(&init_mm, address)

#define __pmd_offset(address) \
    (((address) >> PMD_SHIFT) & (PTRS_PER_PMD-1))

#define mk_pte(page, pgprot)  __mk_pte((page) - mem_map, (pgprot))

/* This takes a physical page address that is used by the remapping functions */
#define mk_pte_phys(physpage, pgprot) __mk_pte((physpage) >> PAGE_SHIFT, pgprot)
(c)⇐ page.h
// ========================================================================
// include/asm-i386/page.h
#define pmd_val(x)  ((x).pmd)
#define pgd_val(x)  ((x).pgd)
#define pgprot_val(x) ((x).pgprot)

#define __pte(x) ((pte_t) { (x) } )
#define __pmd(x) ((pmd_t) { (x) } )
#define __pgd(x) ((pgd_t) { (x) } )
#define __pgprot(x) ((pgprot_t) { (x) } )
(d)pagetable_init

    页地址扩展(PAE,Page Address Extension),页面大小扩展(PSE,大概是 Page Size Extension 的简称),用于扩展 32 位寻址。因此 不研究 PAE 启用情况。

// arch/i386/mm/init.c
static void __init pagetable_init (void)
{
  unsigned long vaddr, end;
  pgd_t *pgd, *pgd_base;
  int i, j, k;
  pmd_t *pmd;
  pte_t *pte, *pte_base;

  // 这里初始化 PGD 的第一块。它把每个表项指向全局 0 页面。需要引用的在 ZONE_NORMAL 
  // 中可用内存的表项将在后面分配。
  /*
   * This can be zero as well - no problem, in that case we exit
   * the loops anyway due to the PTRS_PER_* conditions.
   */
  // 变量 end 标志在 ZONE_NORMAL 中物理内存的末端。  
  end = (unsigned long)__va(max_low_pfn*PAGE_SIZE);
  
  // pgd_base 设立指向静态声明的 PGD 起始位置。
  pgd_base = swapper_pg_dir;
#if CONFIG_X86_PAE
  // 如果 PAE 可用,仅将每个表项设为 0 (实际上,将每个表项指向全局 0 页面) 就
  // 不够了,因为每个 pgd_t 是一个结构。所以,set_pgd 必须在每个 pgd_t 上调用以使每个表项
  // 都指向全局 0 页面。PTRS_PER_PGD(1024)
  for (i = 0; i < PTRS_PER_PGD; i++)
    set_pgd(pgd_base + i, __pgd(1 + __pa(empty_zero_page)));
#endif
  // i 初始化为 PGD 中的偏移,与 PAGE_OFFSET 相对应。或者说,这个函数将仅初始
  // 化线性地址空间的内核部分。可以不用关心这个用户空间部分。
  i = __pgd_offset(PAGE_OFFSET);
  // pgd 初始化为 pgd_t,对应于线性地址空间中内核部分的起点。
  pgd = pgd_base + i;

  // 这个循环开始指向有效 PMD 表项。在 PAE 的情形下,页面由 alloc_bootmem_low_pages()
  // 分配,然后设立相应的 PGD。没有 PAG 时,就没有中间目录,所以就折返到 PGD 以保
  // 留三层页表的假象。
  // 
  // i 已经初始化为线性地址空间的内核部分起始位置,所以这里一直循环到最后
  // PTRS_PER_PGD(1024) 处的 pgd_t。
  for (; i < PTRS_PER_PGD; pgd++, i++) {
  // 计算这个 PGD 的虚拟地址。PGDIR_SIZE(4M),即每个 PGD 代表 4M,PGD 共 1024 个
    vaddr = i*PGDIR_SIZE;
  // 如果到达 ZONE_NORMAL 的末端,则跳出循环,因为不再需要另外的页表项。   
    if (end && (vaddr >= end))
      break;
#if CONFIG_X86_PAE
  // 如果 PAE 可用,则为 PMD 分配一个页面,并利用 set_pgd() 将页面插入到页表中。
    pmd = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);
    set_pgd(pgd, __pgd(__pa(pmd) + 0x1));
#else
  // 如果 PAE 不可用,仅设置 pmd 指向当前 pgd_t。这就是模拟三层页表的 "折返" 策略。
    pmd = (pmd_t *)pgd;
#endif
  // 这是一个有效性检查,以保证 PMD 有效。
    if (pmd != pmd_offset(pgd, 0))
      BUG();

  // 这一块初始化 PMD 中的每个表项。这个循环仅在 PAE 可用时进行。请记住,没有 PAE
  // 时 PTRS_PER_PMD 是 1。    
    for (j = 0; j < PTRS_PER_PMD; pmd++, j++) {
  // 计算这个 PMD 的虚拟地址。PGDIR_SIZE(4M),PMD_SIZE(4M)   
      vaddr = i*PGDIR_SIZE + j*PMD_SIZE;
      if (end && (vaddr >= end))
        break;
  // 如果 CPU 支持 PSE,使用大 TLB 表项。这意味着,对内核页面而言,一个 TLB
  // 项将映射 4 MB 而不是平常的 4 KB,将不再需要三层 PTE。       
      if (cpu_has_pse) {
        unsigned long __pe;

        set_in_cr4(X86_CR4_PSE);
        boot_cpu_data.wp_works_ok = 1;
  // __pe 设置为内核页表的标志(_KERNPG_TABLE),以及表明这是一个映射 4 MB(_PAGE_PSE)
  // 的标志,然后利用__pa()表明这块虚拟地址的物理地址。这意味着 4 MB 的物理
  // 地址不由页表映射。        
        __pe = _KERNPG_TABLE + _PAGE_PSE + __pa(vaddr);
        /* Make it "global" too if supported */
  // 如果 CPU 支持 PGE,则为页表项设置它。它标记表项为全局的,并为所有进程可见。       
        if (cpu_has_pge) {
          set_in_cr4(X86_CR4_PGE);
          __pe += _PAGE_GLOBAL;
        }
  // 由于 PSE 的缘故,所以不需要三层页表。现在利用 set_pmd() 来设置 PMD,并
  // 继续到下一个 PMD。        
        set_pmd(pmd, __pmd(__pe));
        continue;
      }

  // 相反,如果 PSE 不被支持,需要 PTE 的时候,为它们分配一个页面。
      pte_base = pte = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE);

  // 这一块初始化 PTE。
  // 对每个 pte_t, 计算当前被检查的虚拟地址, 创建一个 PTE 来指向相应的物理页面帧。
  // PTRS_PER_PTE(1024),PGDIR_SIZE(4M),PMD_SIZE(4M),PAGE_SIZE(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;
        *pte = mk_pte_phys(__pa(vaddr), PAGE_KERNEL);
      }
  // PTE 已经被初始化, 所以设置 PMD 来指向包含它们的页面。     
      set_pmd(pmd, __pmd(_KERNPG_TABLE + __pa(pte_base)));
  // 保证表项已经正确建立。      
      if (pte_base != pte_offset(pmd, 0))
        BUG();
    }
  }

  // 在这点上,已经设立页面表项来引用 ZONE_NORMAL 的所有部分。需要的其他区域是
  // 那些固定映射以及那些需要利用 kmap() 映射高端内存的区域。
  /*
   * Fixed mappings, only the page table structure has to be
   * created - mappings will be set by set_fixmap():
   */
  // 固定地址空间被认为从 FIXADDR_TOP 开始,并在地址空间前面结束。__fix_to_virt()
  // 将一个下标作为参数,并返回在固定虚拟地址空间中的第 index 个下标后续页面帧 (从
  // FIXADDR_TOP 开始)。 __end_of_fixed_addresses 是上一个由固定虚拟地址空间用到的下
  // 标。或者说,这一行返回的应该是固定虚拟地址空间起始位置的 PMD 虚拟地址。  
  vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK;
  // 这个函数传递 0 作为 fixrange_init() 的结束, 它开始于 vaddr,并创建有效 PGD 和
  // PMD 直到虚拟地址空间的末端,对这些地址而言不需要 PTE。
  fixrange_init(vaddr, 0, pgd_base);

#if CONFIG_HIGHMEM
  // 利用 kmap()来设立页表。
  /*
   * Permanent kmaps:
   */
  vaddr = PKMAP_BASE;
  fixrange_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base);

  // 利用 kmap() 获取对应于待用到的区域首址的 PTE。
  pgd = swapper_pg_dir + __pgd_offset(vaddr);
  pmd = pmd_offset(pgd, vaddr);
  pte = pte_offset(pmd, vaddr);
  pkmap_page_table = pte;
#endif

#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.
   */
  // 这里设立一个临时标记来映射虚拟地址 0 和物理地址 0。  
  pgd_base[0] = pgd_base[USER_PTRS_PER_PGD];
#endif
}

(3)⇒ alloc_bootmem_low_pages

    上文 pagetable_init 函数中有调用 alloc_bootmem_low_pages 函数。

static void __init pagetable_init (void) {
  // ...
      pte_base = pte = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE);
  // ...
}

alloc_bootmem_low_pages 函数具体分析可参考 ⇒ alloc_bootmem 一节

(4)fixrange_init

    上文 pagetable_init 函数中有调用 fixrange_init 函数。

static void __init pagetable_init (void) {
  // ...
  fixrange_init(vaddr, 0, pgd_base);
  // ...
}

  这个函数为固定虚拟地址映射创建有效的 PGDPMD

// arch/i386/mm/init.c
static void __init fixrange_init (unsigned long start, unsigned long end, pgd_t *pgd_base)
{
  pgd_t *pgd;
  pmd_t *pmd;
  pte_t *pte;
  int i, j;
  unsigned long vaddr;
  
  // 设置虚拟地址 (vaddr) 作为请求起始地址的一个参数。
  vaddr = start;
  // 获取对应于 vaddr 的 PGD 内部索引。  
  i = __pgd_offset(vaddr);
  // 获取对应于 vaddr 的 PMD 内部索引。
  j = __pmd_offset(vaddr);
  // 获取 pgd_t 的起点。
  pgd = pgd_base + i;

  // 一直循环直到到达 end。当 pagetable_init() 传入 0 时,将继续循环直到 PGD 的末端。
  // PTRS_PER_PGD(1024)
  for ( ; (i < PTRS_PER_PGD) && (vaddr != end); pgd++, i++) {
#if CONFIG_X86_PAE
  // 在有 PAE 时,若没有为 PMD 分配页面,这里就为 PMD 分配一个页面。
    if (pgd_none(*pgd)) {
      pmd = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);
      set_pgd(pgd, __pgd(__pa(pmd) + 0x1));
      if (pmd != pmd_offset(pgd, 0))
        printk("PAE BUG #02!\n");
    }
    pmd = pmd_offset(pgd, vaddr);
#else
  // 没有 PAE 时,也没有 PMD,所以这里把 pgd_t 看作 pmd_t。
    pmd = (pmd_t *)pgd;
#endif
  // 对 PMD 中的每个表项,这里将为 pte_t 表项分配一个页面,并在页表中设置。
  // 注意 vaddr 是以 PMD 大小作为一步增加的。
  // PTRS_PER_PMD(1)。   
    for (; (j < PTRS_PER_PMD) && (vaddr != end); pmd++, j++) {
      if (pmd_none(*pmd)) {
        pte = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE);
        set_pmd(pmd, __pmd(_KERNPG_TABLE + __pa(pte)));
        if (pte != pte_offset(pmd, 0))
          BUG();
      }
      vaddr += PMD_SIZE;
    }
    j = 0;
  }
}

(5)kmap_init

    上文 paging_init 函数中有调用 kmap_init 函数。

// arch/i386/mm/init.c
void __init paging_init(void) {
  // ...
#ifdef CONFIG_HIGHMEM
  // kmap_init() 调用 kmap() 初始化保留的页表区域。
  kmap_init();
#endif
  // ...
}
(a)⇐ fixmap.h
// include/asm-i386/fixmap.h
/*
 * used by vmalloc.c.
 *
 * Leave one empty page between vmalloc'ed areas and
 * the start of the fixmap, and leave one page empty
 * at the top of mem..
 */
#define FIXADDR_TOP (0xffffe000UL)
#define __FIXADDR_SIZE  (__end_of_permanent_fixed_addresses << PAGE_SHIFT)
#define FIXADDR_START (FIXADDR_TOP - __FIXADDR_SIZE)

#define __fix_to_virt(x)  (FIXADDR_TOP - ((x) << PAGE_SHIFT))
(b)⇐ init_task.c

    结构体 mm_struct 可参考 ⇒ 4.3 进程地址空间描述符

// include/linux/sched.h
#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,    \
  mmlist:   LIST_HEAD_INIT(name.mmlist),  \
}

// arch/i386/kernel/init_task.c
struct mm_struct init_mm = INIT_MM(init_mm);
(c)⇐ pgtable.h
// include/asm-i386/page.h
#define pmd_val(x)  ((x).pmd)
#define pgd_val(x)  ((x).pgd)
#define pgprot_val(x) ((x).pgprot)

// =========================================================================
// include/asm-i386/pgtable-2level.h
/*
 * traditional i386 two-level paging structure:
 */

#define PGDIR_SHIFT 22
#define PTRS_PER_PGD  1024

/*
 * the i386 is two-level, so we don't really have any
 * PMD directory physically.
 */
#define PMD_SHIFT 22
#define PTRS_PER_PMD  1

#define PTRS_PER_PTE  1024

static inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address)
{
  return (pmd_t *) dir;
}

// =========================================================================
// include/asm-i386/pgtable.h
#define pmd_page(pmd) \
((unsigned long) __va(pmd_val(pmd) & PAGE_MASK))

/* Find an entry in the third-level page table.. */
#define __pte_offset(address) \
    ((address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
#define pte_offset(dir, address) ((pte_t *) pmd_page(*(dir)) + \
      __pte_offset(address))

/* to find an entry in a page-table-directory. */
#define pgd_index(address) ((address >> PGDIR_SHIFT) & (PTRS_PER_PGD-1))

#define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))

/* to find an entry in a kernel page-table-directory */
#define pgd_offset_k(address) pgd_offset(&init_mm, address)
(d)kmap_init

    这个函数仅存在于如果在编译时设置了 CONFIG_HIGHMEM 的情况下。它负责获取 kma 区域的首址,引用它的 PTE 以及保护页表。这意味着在使用 kmap() 时不一定都需要检查 PGD

/*
 * NOTE: pagetable_init alloc all the fixmap pagetables contiguous on the
 * physical space so we can cache the place of the first one and move
 * around without checking the pgd every time.
 */

#if CONFIG_HIGHMEM
pte_t *kmap_pte;
pgprot_t kmap_prot;

//  由于 fixrange_init() 已经设立了有效的 PGD 和 PMD,所以就不需要再一次检查
// 它们,这样 kmap_get_fixmap_pte() 可以快速遍历页表。
#define kmap_get_fixmap_pte(vaddr)          \
  pte_offset(pmd_offset(pgd_offset_k(vaddr), (vaddr)), (vaddr))

void __init kmap_init(void)
{
  unsigned long kmap_vstart;

  /* cache the first kmap pte */
  // 缓存 kmap_vstart 中 kmap 区域的虚拟地址。
  kmap_vstart = __fix_to_virt(FIX_KMAP_BEGIN);
  // 缓存 PTE 作为 kmap_pte 中 kmap 区域的首址。
  kmap_pte = kmap_get_fixmap_pte(kmap_vstart);
  //  利用 kmap_prot 缓存页表表项的保护项。
  kmap_prot = PAGE_KERNEL;
}
#endif /* CONFIG_HIGHMEM */

(6)⇒ zone_sizes_init

    上文 paging_init 函数中有调用 zone_sizes_init 函数。

// arch/i386/mm/init.c
void __init paging_init(void) {
  // ...
  // zone_sizes_init() (见 B.1.2 小节) 记录每个管理区的大小,然后调用 free_area_init()
  // (见 B.1.3 小节)来初始化各个管理区。
  zone_sizes_init();
  // ...
}

zone_sizes_init 函数具体分析可参考 ⇒ zone_sizes_init 一节

深入理解Linux虚拟内存管理(四)(下):https://developer.aliyun.com/article/1597786

目录
相关文章
|
13天前
|
安全 Linux Shell
Linux上执行内存中的脚本和程序
【9月更文挑战第3天】在 Linux 系统中,可以通过多种方式执行内存中的脚本和程序:一是使用 `eval` 命令直接执行内存中的脚本内容;二是利用管道将脚本内容传递给 `bash` 解释器执行;三是将编译好的程序复制到 `/dev/shm` 并执行。这些方法虽便捷,但也需谨慎操作以避免安全风险。
|
22天前
|
Linux 调度
深入理解Linux虚拟内存管理(七)(下)
深入理解Linux虚拟内存管理(七)
34 4
|
22天前
|
存储 Linux 索引
深入理解Linux虚拟内存管理(九)(中)
深入理解Linux虚拟内存管理(九)
19 2
|
22天前
|
Linux 索引
深入理解Linux虚拟内存管理(九)(上)
深入理解Linux虚拟内存管理(九)
23 2
|
22天前
|
Linux
深入理解Linux虚拟内存管理(七)(中)
深入理解Linux虚拟内存管理(七)
23 2
|
22天前
|
机器学习/深度学习 消息中间件 Unix
深入理解Linux虚拟内存管理(九)(下)
深入理解Linux虚拟内存管理(九)
16 1
|
22天前
|
Linux
深入理解Linux虚拟内存管理(七)(上)
深入理解Linux虚拟内存管理(七)
24 1
|
20天前
|
缓存 Linux 调度
Linux服务器如何查看CPU占用率、内存占用、带宽占用
Linux服务器如何查看CPU占用率、内存占用、带宽占用
58 0
|
22天前
|
Linux API
深入理解Linux虚拟内存管理(六)(下)
深入理解Linux虚拟内存管理(六)
13 0
|
22天前
|
Linux
深入理解Linux虚拟内存管理(六)(中)
深入理解Linux虚拟内存管理(六)
18 0