Linux-3.14.12記憶體管理筆記【建立內核頁表(1)】

  • 2019 年 10 月 11 日
  • 筆記

前面已經分析過了Intel的記憶體映射和linux的基本使用情況,已知head_32.S僅是建立臨時頁表,內核還是要建立內核頁表,做到全面映射的。下面就基於RAM大於896MB,而小於4GB ,切CONFIG_HIGHMEM配置了高端記憶體的環境情況進行分析。

建立內核頁表前奏,了解兩個很關鍵的變數:

  • max_pfn:最大物理記憶體頁面幀號;
  • max_low_pfn:低端記憶體區(直接映射空間區的記憶體)的最大可用頁幀號;

max_pfn 的值來自setup_arch()中,setup_arch()函數中有:

max_pfn = e820_end_of_ram_pfn();

那麼接下來看一下e820_end_of_ram_pfn()的實現:

804762

【file:/arch/x86/kernel/e820.c】  unsigned long __init e820_end_of_ram_pfn(void)  {      return e820_end_pfn(MAX_ARCH_PFN, E820_RAM);  }

e820_end_of_ram_pfn()直接封裝調用e820_end_pfn(),而其入參為MAX_ARCH_PFN和E820_RAM,其中MAX_ARCH_PFN的定義(x86的32bit環境)為:

#  define MAX_ARCH_PFN              (1ULL<<(32-PAGE_SHIFT))

最終值為0x100000,它表示的是4G物理記憶體的最大頁面幀號;而E820_RAM為:

#define E820_RAM                 1

接下來看一下e820_end_pfn()函數實現:

【file:/arch/x86/kernel/e820.c】  /*   * Find the highest page frame number we have available   */  static unsigned long __init e820_end_pfn(unsigned long limit_pfn, unsigned type)  {      int i;      unsigned long last_pfn = 0;      unsigned long max_arch_pfn = MAX_ARCH_PFN;        for (i = 0; i < e820.nr_map; i++) {          struct e820entry *ei = &e820.map[i];          unsigned long start_pfn;          unsigned long end_pfn;            if (ei->type != type)              continue;            start_pfn = ei->addr >> PAGE_SHIFT;          end_pfn = (ei->addr + ei->size) >> PAGE_SHIFT;            if (start_pfn >= limit_pfn)              continue;          if (end_pfn > limit_pfn) {              last_pfn = limit_pfn;              break;          }          if (end_pfn > last_pfn)              last_pfn = end_pfn;      }        if (last_pfn > max_arch_pfn)          last_pfn = max_arch_pfn;        printk(KERN_INFO "e820: last_pfn = %#lx max_arch_pfn = %#lxn",               last_pfn, max_arch_pfn);      return last_pfn;  }

這個函數用來查找最大物理的頁面幀號,通過對e820圖的記憶體塊資訊得到記憶體塊的起始地址,將起始地址右移PAGE_SHIFT,算出其起始地址對應的頁面幀號,如果足夠大,超出了limit_pfn則設置最大頁面幀號為limit_pfn,否則則設置為遍歷中找到的最大的last_pfn。

e820_end_of_ram_pfn()函數的調用位置:

start_kernel()                           #init/main.c    └─>setup_arch()                        #arch/x86/kernel/setup.c    ├─>e820_end_of_ram_pfn()              #arch/x86/kernel/e820.c    └─>find_low_pfn_range()               #arch/x86/kernel/e820.c

其中find_low_pfn_range()用於查找低端記憶體的最大頁面數的 ,max_low_pfn則在這裡面初始化。

find_low_pfn_range()程式碼實現:

【file:/arch/x86/mm/init_32.c】  /*   * Determine low and high memory ranges:   */  void __init find_low_pfn_range(void)  {      /* it could update max_pfn */        if (max_pfn <= MAXMEM_PFN)          lowmem_pfn_init();      else          highmem_pfn_init();  }

函數實現很簡單,根據max_pfn是否大於MAXMEM_PFN,從而判斷是否初始化高端記憶體,也可以認為是啟用。那麼來看一下MAXMEM_PFN的宏定義:

(file:/arch/x86/include/asm/setup.h)    #define MAXMEM_PFN               PFN_DOWN(MAXMEM)

其中PFN_DOWN(x)的定義為:

(file:/include/linux/pfn.h)    #define PFN_DOWN(x)              ((x) >> PAGE_SHIFT)

PFN_DOWN(x)是用來返回小於x的最後一個頁面號,對應的還有個PFN_UP(x)是用來返回大於x的第一個頁面號,此外有個PFN_PHYS(x)返回的是x的物理頁面號。接著看MAXMEM的定義:

(file:arch/x86/include/asm/pgtable_32_types.h)    #define MAXMEM                   (VMALLOC_END - PAGE_OFFSET - __VMALLOC_RESERVE)

那麼VMALLOC_END的定義則為:

(file:arch/x86/include/asm/pgtable_32_types.h)    #define VMALLOC_END              (PKMAP_BASE - 2 * PAGE_SIZE)    //永久記憶體映射  #define PKMAP_BASE ((FIXADDR_BOOT_START - PAGE_SIZE * (LAST_PKMAP + 1)) & PMD_MASK)

其中PKMAP_BASE是永久映射空間的起始地址,LAST_PKMAP則是永久映射空間的映射頁面數,定義為:

#define LAST_PKMAP 1024

另外PAGE_SHIFT和PAGE_SIZE的定義為:

#define PAGE_SHIFT               12  #define PAGE_SIZE                (_AC(1,UL) << PAGE_SHIFT)

而FIXADDR_BOOT_START是臨時固定映射空間起始地址,其的相關宏定義:

臨時記憶體映射:

#define FIXADDR_BOOT_SIZE        (__end_of_fixed_addresses << PAGE_SHIFT)    #define FIXADDR_BOOT_START       (FIXADDR_TOP - FIXADDR_BOOT_SIZE)    unsigned long __FIXADDR_TOP = 0xfffff000;    extern unsigned long __FIXADDR_TOP;    #define FIXADDR_TOP              ((unsigned long)__FIXADDR_TOP)

這裡其中的__end_of_fixed_addresses是來自fixed_addresses枚舉值,是固定映射的一個標誌。此外這裡的FIXADDR_TOP是固定映射區末尾,而另外還有一個這裡未列出的FIXADDR_START,是固定映射區起始地址。

既然到此,順便介紹一下內核空間映射情況。

內核空間如上圖,分為直接記憶體映射區(低端記憶體,線性)和高端記憶體映射區。其中直接記憶體映射區是指3G到3G+896M的線性空間,直接對應物理地址就是0到896M(前提是有超過896M的物理記憶體),其中896M是high_memory值,使用kmalloc()/kfree()介面操作申請釋放;

而高端記憶體映射區則是至超多896M物理記憶體的空間,它又分為動態映射區、永久映射區和固定映射區。

  • 動態記憶體映射區,又稱之為vmalloc映射區或非連續映射區,是指VMALLOC_START到VMALLOC_END的地址空間,申請釋放操作介面是vmalloc()/vfree(),通常用於將非連續的物理記憶體映射為連續的線性地址記憶體空間;
  • 而永久映射區,又稱之為KMAP區或持久映射區,是指自PKMAP_BASE開始共LAST_PKMAP個頁面大小的空間,操作介面是kmap()/kunmap(),用於將高端記憶體長久映射到記憶體虛擬地址空間中;
  • 最後的固定映射區,也稱之為臨時內核映射區,是指FIXADDR_START到FIXADDR_TOP的地址空間,操作介面是kmap_atomic()/kummap_atomic(),用於解決持久映射不能用於中斷處理程式而增加的臨時內核映射。

下圖是根據個人的實驗環境繪製的一張關於內核空間映射情況。

PMD_MASK涉及的宏定義:

(file:/include/asm-generic/pgtable-nopmd.h)    #define PMD_SHIFT                PUD_SHIFT    #define PMD_SIZE                 (1UL << PMD_SHIFT)    #define PMD_MASK                 (~(PMD_SIZE-1))    (file:/include/asm-generic/pgtable-nopud.h)    #define PUD_SHIFT                PGDIR_SHIFT    (file:arch/x86/include/asm/Pgtable-2level_types.h)    #define PGDIR_SHIFT              22

PMD_MASK計算結果是:0xFFC00000,其實是用於數據對齊而已。

已知PAGE_OFFSET默認的為0xC0000000,而__VMALLOC_RESERVE為:

unsigned int __VMALLOC_RESERVE = 128 << 20;

最後在個人的實驗環境上,得出MAXMEM_PFN的值為0x377fe。

Linux是一個支援多硬體平台的作業系統,各種硬體晶片的分頁並非固定的2級(頁全局目錄和頁表),僅僅Intel處理器而言,就存在3級的情況(頁全局目錄、頁中間目錄和頁表),而到了64位系統的時候就成了4級分頁。 所以Linux為了保持良好的兼容性和移植性,系統設計成了以下的4級分頁模型,根據平台環境和配置的情況,通過將頁上級目錄和頁中間目錄的索引位設置為0,從而隱藏了頁三級目錄和頁中間目錄的存在。也就是為什麼存在PMD_SHIFT、PUD_SHIFT和PGDIR_SHIFT,還有pgtable-nopmd.h、pgtable-nopud.h和Pgtable-2level_types.h的原因了。

由此管中窺豹,看到了Linux記憶體分頁映射模型的存在和相關設計,暫且也就先了解這麼多。

分析宏是一件很乏味的事情,不過以小見大卻是一件很有意思的事情。

【file:/arch/x86/mm/init_32.c】  /*     * We have more RAM than fits into lowmem - we try to put it into   * highmem, also taking the highmem=x boot parameter into account:   */  static void __init highmem_pfn_init(void)  {      max_low_pfn = MAXMEM_PFN;        if (highmem_pages == -1)          highmem_pages = max_pfn - MAXMEM_PFN;        if (highmem_pages + MAXMEM_PFN < max_pfn)          max_pfn = MAXMEM_PFN + highmem_pages;        if (highmem_pages + MAXMEM_PFN > max_pfn) {          printk(KERN_WARNING MSG_HIGHMEM_TOO_SMALL,              pages_to_mb(max_pfn - MAXMEM_PFN),              pages_to_mb(highmem_pages));          highmem_pages = 0;      }  #ifndef CONFIG_HIGHMEM      /* Maximum memory usable is what is directly addressable */      printk(KERN_WARNING "Warning only %ldMB will be used.n", MAXMEM>>20);      if (max_pfn > MAX_NONPAE_PFN)          printk(KERN_WARNING "Use a HIGHMEM64G enabled kernel.n");      else          printk(KERN_WARNING "Use a HIGHMEM enabled kernel.n");      max_pfn = MAXMEM_PFN;  #else /* !CONFIG_HIGHMEM */  #ifndef CONFIG_HIGHMEM64G      if (max_pfn > MAX_NONPAE_PFN) {          max_pfn = MAX_NONPAE_PFN;          printk(KERN_WARNING MSG_HIGHMEM_TRIMMED);      }  #endif /* !CONFIG_HIGHMEM64G */  #endif /* !CONFIG_HIGHMEM */  }

highmem_pfn_init()看起來很長,貌似很複雜,實際上僅僅是把max_low_pfn設置為MAXMEM_PFN,而highmem_pages設置為max_pfn - MAXMEM_PFN,至於後面的幾乎都是為了防止某些數據過大過小引起翻轉而做的保障性工作。需要說明的是這裡的max_low_pfn作為直接映射空間區的記憶體最大可用頁幀號,並不是896M大小記憶體的頁面數。896M只是定義高端記憶體的一個界限,至於直接映射記憶體大小隻定義了不超過896M而已。

此外還有一個準備操作,在setup_arch()函數中調用的頁表緩衝區申請操作:

early_alloc_pgt_buf():

【file:/arch/x86/mm/init.c】  void __init early_alloc_pgt_buf(void)  {      unsigned long tables = INIT_PGT_BUF_SIZE;      phys_addr_t base;        base = __pa(extend_brk(tables, PAGE_SIZE));        pgt_buf_start = base >> PAGE_SHIFT;      pgt_buf_end = pgt_buf_start;      pgt_buf_top = pgt_buf_start + (tables >> PAGE_SHIFT);  }

Tags: