Linux核心原始碼分析之setup_arch (三)

機器感知發表於2020-12-31

1. 前言

Linux核心原始碼分析之setup_arch (二) 中介紹了當前啟動階段的記憶體分配函式memblock_alloc,該記憶體分配函式在本篇將要介紹paging_init中用於頁表和記憶體的分配,paging_init函式大致流程如下圖所示。

Linux核心原始碼分析之setup_arch (三)

2. paging_init

2.1 build_mem_type_table

該函式根據具體的CPU架構對靜態定義的mem_types陣列中定義的屬性進行調整。

2.2 prepare_page_table

該函式的作用是把頁目錄項清零,原始碼大致如下。首先是把虛擬地址範圍[0, MODULES_VADDR]的頁目錄項清零,如果核心是模組區域以XIP方式執行的,則跳過核心部分的頁目錄,然後繼續對區域[addr,PAGE_OFFSET]的頁目錄項清零,此時使用者空間的頁目錄項已經全部清零;最後,把除了第一塊記憶體條之外的核心空間[__phys_to_virt(end), VMALLOC_START]對應的頁目錄項清零。

/* arch/arm/mm/mmu.c */
static inline void prepare_page_table(void)
{
 unsigned long addr;
 phys_addr_t end;
  /* <--(1)--> */
 for (addr = 0; addr < MODULES_VADDR; addr += PMD_SIZE)
  pmd_clear(pmd_off_k(addr));

#ifdef CONFIG_XIP_KERNEL
 addr = ((unsigned long)_etext + PMD_SIZE - 1) & PMD_MASK;
#endif
  /* <--(2)--> */
 for ( ; addr < PAGE_OFFSET; addr += PMD_SIZE)
  pmd_clear(pmd_off_k(addr));

 end = memblock.memory.regions[0].base + memblock.memory.regions[0].size;
 if (end >= lowmem_limit)
  end = lowmem_limit;
  /* <--(3)--> */
 for (addr = __phys_to_virt(end);
      addr < VMALLOC_START; addr += PMD_SIZE)
  pmd_clear(pmd_off_k(addr));
}

2.3 map_lowmem

該函式將實體記憶體地址小於lowmem_limit的記憶體對映到核心空間,實際的記憶體對映工作在create_mapping中完成。

/* arch/arm/mm/mmu.c */
static void __init map_lowmem(void)
{
 ...
 for_each_memblock(memory, reg) {
  start = reg->base;
  end = start + reg->size;

  if (end > lowmem_limit)
   end = lowmem_limit;
  if (start >= end)
   break;

  map.pfn = __phys_to_pfn(start);
  map.virtual = __phys_to_virt(start);
  map.length = end - start;
  map.type = MT_MEMORY;

  create_mapping(&map, false);
 }
}

create_mapping函式的大致流程如下圖所示,這裡需要提一下,linux核心使用的是四級頁表,即PGD、PUD、PMD、PTE;而ARM32使用的是二級頁表,即PMD、PTE。同時由於記憶體管理是以頁為單位進行的,如果按照ARM硬體MMU的分頁機制,一個PMD對應的PTE並不能完全佔用完整個頁,為了避免記憶體浪費,會在軟體層面上將兩個PMD對應的PTE放在一個頁內,具體細節可以參考檔案arch/arm/include/asm/pgtable-2level.h中的註釋部分。最終會呼叫alloc_init_pte函式對指定範圍的記憶體區域進行對映,其中的early_pte_alloc函式最終也會去呼叫 Linux核心原始碼分析之setup_arch (二) 中介紹的memblock_alloc函式來分配記憶體,最後將PTE所在頁寫入到PMD中即可完成對映。

Linux核心原始碼分析之setup_arch (三)
/* arch/arm/mm/mmu.c */
static void __init alloc_init_pte(...)
{
 pte_t *start_pte = early_pte_alloc(pmd);
 pte_t *pte = start_pte + pte_index(addr);

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

2.4 devicemaps_init

該函式大致流程如下圖所示,首先呼叫early_alloc分配一個頁,然後呼叫early_trap_init將向量表複製到新的頁內,最後呼叫create_mapping將這個頁對映到0xffff0000處,如果mdesc->map_io存在,還會對裝置相關的IO進行對映。

Linux核心原始碼分析之setup_arch (三)

2.5 kmap_init

這個函式非常簡單,把大小為2MB的區間[PKMAP_BASE,PAGE_OFFSET]對映到核心空間。

/* arch/arm/mm/mmu.c */
static void __init kmap_init(void)
{
#ifdef CONFIG_HIGHMEM
 pkmap_page_table = early_pte_alloc_and_install(pmd_off_k(PKMAP_BASE),
  PKMAP_BASE, _PAGE_KERNEL_TABLE);
#endif
}

3. 總結

本文主要介紹了核心啟動階段頁表初始化部分的內容,其中,build_mem_type_table負責根據不同CPU架構對mem_types進行調整,prepare_page_table負責將待初始化區域的頁目錄項清零,然後通過map_lowmem建立低端記憶體區域的頁表對映,最後呼叫devicemaps_init建立對向量表和裝置IO的對映。至此,除了bootmem_init函式沒有分析之外,paging_init基本算是分析完了,bootmem_init的分析將在下一篇中給出。

相關文章