天天看點

Linux記憶體組織及映射原理

【摘要】本文主要講述linux核心中記憶體映射的相關理論。所謂記憶體映射就是将外設的記憶體空間映射到linux核心的記憶體虛拟位址空間,以後使用者(應用程式)可以友善的在使用者空間,通過存取映射後的虛拟位址來間接的操作(驅動)外設進行工作,相對通過IO接口或者是ioremap接口還需要通過核心進行緩存要來的便捷和快速。

1、位址類型

  • 使用者虛拟位址(User virtual addresses)

    這是被使用者程式見到的正常位址。使用者位址依賴于底層的硬體結構,在長度上是 32 位或者 64位, 并且每個程序有它自己的虛拟位址空間。

  • 實體位址(Physical addresses)

    在處理器和系統記憶體之間使用的位址。實體位址是 32或者 64位。

  • 總線位址(Bus addresses)

    在外設和記憶體之間使用的位址。 通常, 它們和處理器使用的實體位址相同。但在一些體系下,提供一個 I/O 記憶體管理單元(IOMMU),它在總線和主記憶體之間重映射位址。 一個 IOMMU 可以使事情簡單(例如, 使散布在記憶體中的緩沖對裝置看來是連續的)。總線位址是高度特性依賴的。

  • 核心邏輯位址(Kernel logical addresses)

    這些組成了正常的核心位址空間。這些位址映射了部分(也許全部)主存并且常常被當作是實體記憶體來對待。在大部分的體系上,邏輯位址和相關實體位址隻差一個常量偏移。邏輯位址使用硬體的本地指針大小,并且是以可能不能尋址所有的實體記憶體。邏輯位址常常存儲于 unsigned long 或者 void * 類型的變量中。從 kmalloc 傳回的記憶體就是核心邏輯位址。

  • 核心虛拟位址(Kernel virtual addresses )

    類似于邏輯位址,它們都是從核心空間位址到實體位址的映射。但核心虛拟位址不必像邏輯位址空間那樣具備線性的 一對一到實體位址的映射。但是,所有的邏輯位址都屬于核心虛拟位址,而許多核心虛拟位址卻不是邏輯位址。例如 vmalloc 配置設定的記憶體有虛拟位址(但沒有直接實體映射),kmap 函數也傳回虛拟位址,虛拟位址常常存儲于指針變量。

    如果你有邏輯位址, 宏

    __pa()

    (在 <asm/page.h> 中定義)傳回它關聯的實體位址, 實體位址可被

    __va()

    映射回邏輯位址 , 但是隻适合低記憶體頁。不同的核心函數需要不同類型位址。

2、實體記憶體的組織及配置設定

Linux中記憶體按大小分為3個級别,從下到上依次為:

  1. Page: 一個頁的大小用常量 PAGE_SIZE (定義在 <asm/page.h>) 表示,一般為 4k,頁是記憶體的一個最基本的機關。其中的位址用頁幀号和頁内偏移表示,如果使用 4096位元組頁, 那麼12 位低有效位是頁内偏移,并且剩下的高位

    訓示頁幀号(PFN)。移位來在頁幀号和位址之間轉換是一個相當普通的操作。常量宏 PAGE_SHIFT 告訴你需要移動多少位來進行這個轉換。

  2. Zone: Zone中提供了多個隊列來管理page。Zone分為3種:
    1. ZONE_DMA: 用來存放DMA讀取IO裝置的資料,核心專用、直接映射。
    2. ZONE_NORMAL:用來存放核心的相關資料,核心專用、直接映射。
    3. ZONE_HIGHMEM:高端記憶體,用來使用者程序存放資料,動态映射。
  3. Node :節點,一個CPU對應着一個Node,一個Node包括一個Zone_DMA、 ZONE_NORMAL、ZONE_HIGHMEM。
Linux記憶體組織及映射原理

Linux将記憶體配置設定分為兩種:夥伴配置設定(大記憶體)和slab配置設定(小記憶體)。

  • 夥伴配置設定:
    Linux記憶體組織及映射原理
    • 将ZONE中的 Page 分組,然後組裝為多個連結清單。連結清單中存放的是 頁塊 的集合;
    • 頁塊對應着有不同的大小,分别為 1、2、4、8 … 1024個頁。
    • 當請求(2i-1 ,2i]大小的 page 的時候,會直接請求2i 個頁, 如果對應的連結清單中有對應的頁塊,就直接配置設定。如果對應的連結清單沒有,就往上找 2i+1,如果2i+1存在,就将其分為 2 個 2i 頁塊,将其中1個2i加入到對應的連結清單中,将另外一個配置設定出去。
  • slab配置設定:
    Linux記憶體組織及映射原理
    • slub方法主要用于配置設定一些核心的資料對象。就是 将幾個頁單獨拎出來 作為緩存,裡面也維護了連結清單。每次直接從連結清單中擷取對應的記憶體,用完之後也不用清空,就直接挂到連結清單上,然後等待下次利用。

3、虛拟位址空間的概念

虛拟位址對應的是虛拟空間,虛拟空間是全部虛拟位址的集合,用來映射實體記憶體。

1. 虛拟位址分類

Linux記憶體組織及映射原理

虛拟空間分為 使用者态 和 核心态。

32位系統中 将虛拟空間按照 1:3的比例配置設定給 核心态 和 使用者态

64位系統中 分别給 核心态 和 使用者态 配置設定了 128T。

在32位系統中,每個程序都有4G的虛拟位址空間,其中3G使用者空間,1G核心空間(linux),程序間共享核心空間,但獨享使用者空間,下圖形象地表達了這點

Linux記憶體組織及映射原理

2. 使用者态的存儲結構

Linux記憶體組織及映射原理
  • 一個程序對應的使用者态中的 各個方面的虛拟位址資訊都通過一個

    struct mm_struct

    來存儲在記憶體中,當建立程序的時候會為其配置設定記憶體存儲對應的虛拟位址資訊。
    Linux記憶體組織及映射原理
    • vm_area_struct 結構
      • 當一個使用者空間程序調用 mmap 來映射裝置記憶體到它的位址空間, 系統通過一個新 VMA 代表那個映射來響應。一個支援 mmap 的驅動(并且, 是以, 實作mmap 方法)需要來幫助那個程序來完成那個 VMA 的初始化 。
      • VMA結構體的主要成員:
        • unsigned long vm_start;

          unsigned long vm_end; 映射到的虛拟位址範圍

        • struct file *vm_file; 指向和這個區(如果有一個)關聯的 struct file 結構的指針。
        • unsigned long vm_pgoff; 檔案中區的偏移(以頁計)。 當一個檔案和裝置被映射, 這是映射在這個

          區的第一頁的檔案位置。

        • unsigned long vm_flags; 描述這個區的一套标志。裝置驅動編寫者關注的标志是 VM_IO 和

          VM_RESERVUED。VM_IO 标志 VMA 作為記憶體映射的 I/O 區。VM_RESERVED 标志該VMA不能被交換出記憶體,它應當在大部分裝置映射中設定。

        • struct vm_operations_struct *vm_ops; 核心可能會調用來在這個記憶體區上操作的一套函數,包括如下函數:
          • void (*open)(struct vm_area_struct *vma);

            任何時候一個新的引用VMA 時,它被調用來初始化VMA。
          • void (*close)(struct vm_area_struct *vma);

            當一個區被銷毀, 核心調用它的關閉操作
          • struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type);

            當一個程序試圖存取使用一個有效 VMA 的頁, 但是它目前不在記憶體中時,nopage 方法被調用以傳回一個頁指針,否則若nopage沒被定義,則傳回一個空頁。
          • int (*populate)(struct vm_area_struct *vm, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock);

            在某些頁被使用者空間存取之前,核心先将其預借到記憶體。通常驅動沒有必要來實作這個填充方法。
        • void *vm_private_data; 驅動可以用來存儲它的自身資訊的成員。
  • 記憶體映射mmap就是把裝置位址映射到上圖的紅色段了,暫且稱其為“記憶體映射段”,至于映射到哪個位址,是由作業系統配置設定的。
  • 一個程序的記憶體區可看到通過指令

    cat /proc/<pid>/maps

    來檢視, 目前程序可采用

    cat /proc/self/maps

    檢視
    # cat /proc/self/maps
    00400000-00405000 r-xp 00000000 03:01 1596291 /bin/cat text
    00504000-00505000 rw-p 00004000 03:01 1596291 /bin/cat data
    00505000-00526000 rwxp 00505000 00:00 0 bss
    3252200000-3252214000 r-xp 00000000 03:01 1237890 /lib64/ld-2.3.3.so
    3252300000-3252301000 r--p 00100000 03:01 1237890 /lib64/ld-2.3.3.so
    3252301000-3252302000 rw-p 00101000 03:01 1237890 /lib64/ld-2.3.3.so
    7fbfffe000-7fc0000000 rw-p 7fbfffe000 00:00 0 stack
    ffffffffff600000-ffffffffffe00000 ---p 00000000 00:00 0 vsyscall
    
    每行的字段是:start-end perm offset major:minor inode image
               

3. 核心态的存儲結構

Linux記憶體組織及映射原理

Linux中的核心程式 共用一個 核心态虛拟空間。其中分為了以下幾部分:

1、直接映射區

896M,核心空間直接映射到對應的ZONE_DMA和ZONE_NORMAL中。為什麼叫做直接映射呢? 邏輯位址 直接 減去對應的內插補點就可以得到對應的實體位址。固定死了。

2、動态映射

因為所有實體記憶體的配置設定都需要核心程式進行申請,使用者程序沒有這個權限。是以核心空間一定要能映射到所有的實體記憶體位址。那麼如果都采用直接映射的話,1G大小邏輯位址的核心空間隻能映射1G大小的實體記憶體。是以引入了動态映射。

動态映射就是 核心空間的邏輯位址可以映射到 實體記憶體中的ZONE_HIGHMEM(高端記憶體)中的任何一個位址,并且在對應的實體記憶體使用完之後,可以再映射其他實體記憶體位址。

動态映射分為三種:

  1. 動态記憶體映射 :使用完對應的實體記憶體後,就可以映射其他實體記憶體了。
  2. 持久記憶體映射: 一個虛拟位址隻能映射一個實體位址。如果需要映射其他實體位址,需要解綁。
  3. 固定記憶體映射: 隻能被某些特定的函數來調用引用實體位址。

3、動态記憶體映射和直接映射的差別

動态映射和直接映射的差別就是邏輯位址到實體位址的轉化規則。直接映射的規則是死的,一個邏輯位址對應的實體位址是固定的。通過邏輯位址加或者減去一個數,就可以得到對應的實體位址。動态映射是動态的綁定,每個邏輯位址對應的實體位址是動态的,通過頁表進行查詢

使用者空間映射:使用者空間 采用 動态映射,每個虛拟位址可以被映射到一個實體位址,映射到ZONE_HIGHMEM。為什麼使用者空間不采用直接映射呢?因為實體記憶體是多個程序所有的,每個程序都有一個使用者空間。如果采用直接映射的話,對應的實體位址是會沖突的。其使用者空間的邏輯位址大小都為3G,是以存在邏輯位址相同,但是對應的實體位址不同。需要通過頁表來轉化,一個程序會對應一個頁表。

4、虛拟位址映射到實體記憶體(記憶體映射)

虛拟位址通過 頁表 将 虛拟位址 轉化為 實體位址。每個程序都對應着一個頁表,而核心隻有一個頁表。

虛拟空間 和 實體記憶體 都按照 4k 來分頁,一個虛拟空間中的頁 和 實體記憶體中頁 是 一一對應的。

映射流程圖:

Linux記憶體組織及映射原理

使用者态申請記憶體時,隻會申請對應的虛拟位址,不會直接為其配置設定實體記憶體,而是等到真正通路記憶體的時候,産生缺頁中斷,然後核心才會為其配置設定,然後為其建立映射,也就是建立對應的頁表項。

1.頁表映射原理

如下圖所示,将虛拟位址中的頁号 通過頁表轉化為 對應的實體頁号,然後通過頁内偏移量 就可以得到對應的 實體位址了。

Linux記憶體組織及映射原理

2.三級頁表(32位系統)

一個程序需要一個映射4G空間的頁表,每個頁表對應4KB大小,是以就需要1M個頁表記錄來描述。

假如 1 個 頁表記錄需要 4個位元組,那麼就需要 4MB。而且頁表記錄是通過下标來對應的,通過虛拟頁号來乘以對應的頁表項大小來計算得到對應的位址的。是以Linux将 4M 分為 1K個 4K, 一個4K對應着一個page,用來存儲對應的真正的頁表記錄。将 1K 個 page 分開存放,就不要求連續的4M了。

如果将4M 分成 1K 個離散的 page的話,虛拟位址又怎麼對應的頁表号呢?利用指針,存儲1K個位址,分别指向這1K個page, 位址的大小為4個位元組,也就是32位,完全可以表示整個記憶體的位址範圍。1K * 4個位元組,正好是一個page 4k,是以 也就是利用 1個 page來存儲對應的頁表記錄索引。

Linux記憶體組織及映射原理

是以 我們的虛拟位址尋找過程如下:

  1. 找到對應的頁表記錄索引位置,因為有1K個索引,是以用10位就可以表示了
  2. 通過索引可以找到對應的真正的頁表位址,對應的有1K個頁表記錄,是以用10位就可以表示了
  3. 1個頁有4K,通過12位就可以表示其頁内偏移量了。

是以虛拟位址被分為了三部分:

  1. 10位 表示索引偏移
  2. 10位 表示頁表記錄偏移
  3. 12位 表示頁内偏移

雖然這種方式增加了索引項,進而增加了記憶體消耗,但是減少了連續記憶體的使用,通過離散的記憶體就可以存儲頁表。

3.五級頁表(64位系統)

Linux記憶體組織及映射原理

4.TLB和虛拟記憶體

  • TLB

TLB就是一個緩存,放在CPU中。用來将虛拟位址和對應的實體位址進行緩存。

當查詢對應的實體位址的時候,首先查詢TLB,如果TLB中存在對應的記錄,就直接傳回。如果不存在,就再去查詢頁表。

  • 虛拟記憶體

虛拟記憶體 指的是 将硬碟中劃出一段 swap分區 當作 虛拟的記憶體,用來存放記憶體中暫時用不到的記憶體頁,等到需要的時候再從 swap 分區中 将對應的記憶體頁調入到 記憶體中。 硬碟此時相當于一個虛拟的記憶體。

從邏輯上能夠運作更大記憶體的程式,因為程式運作的時候并不需要把所有資料都加載到記憶體中,隻需要将目前運作必要的相關程式和資料加載到記憶體中就可以了,當需要其他資料和程式的時候,再将其調入。

相較于真正的記憶體加載,虛拟記憶體需要将資料在記憶體和磁盤中不斷切換,這是一個耗時的操作,是以速度比不上真正的記憶體加載。

小結:

  1. 虛拟空間 和 實體記憶體 都分為 核心空間 和 使用者空間。
  2. 虛拟位址需要通過頁表轉化為實體位址,然後才能通路。
  3. 使用者虛拟空間 隻能映射 實體記憶體中的使用者記憶體,無法映射到實體記憶體中的核心記憶體,也就是說,使用者程序隻能操作使用者記憶體。
  4. 核心空間 隻能被 核心 申請使用,使用者程序隻能操作使用者空間的實體記憶體和虛拟空間。
  5. 當使用者程序 調用系統調用的時候,會将其對應的代碼和資料運作在核心空間中。是以當調用 核心空間 讀取檔案或者網絡資料的時候,首先會将資料拷貝到記憶體空間,然後在将資料從核心空間拷貝到使用者空間。因為 使用者程序不能通路核心空間。

5. struct page及其操作接口

系統中每一個實體頁有一個 struct page。這個結構的一些成員包括下列:

  • atomic_t count

    :這個頁的引用數。當這個 count 掉到 0,這頁被傳回給空閑清單。
  • void *virtual

    :如果這頁被映射,它就代表該頁在核心中的虛拟位址,否則設為NULL。低記憶體頁一直被映射,高記憶體頁常常不是. 這個成員不是在所有體系上出現; 它通常隻在頁的核心虛拟位址無法輕易計算時被編譯. 如果你想檢視這個成員, 正确的方法是使用 page_address 宏。
  • unsigned long flags

    :一套描述頁狀态的位标志。這些包括 PG_locked(它訓示該頁在記憶體中已被加鎖)以及 PG_reserved(它防止記憶體管理系統使用該頁)。

在 struct page 指針和虛拟位址之間轉換的函數和宏:

  • struct page *virt_to_page(void *kaddr);

    這個宏, 定義在 <asm/page.h>, 采用一個核心邏輯位址并傳回它被關聯的 struct page 指針。 因為它需要一個邏輯位址,它不使用來自vmalloc 的記憶體或者高記憶體。
    • kmap

      為系統中的任何頁傳回一個核心虛拟位址。對于低記憶體頁它隻傳回頁的邏輯位址,對于高記憶體頁 kmap 在核心位址空間的一個專用部分中建立一個特殊的映射。使用 kmap 建立的映射應當使用 kunmap 來釋放。因為kmap 調用維護一個計數器,即同時調用kmap的映射是有數量限制的,是以最好不要在它們上停留太長時間。還要注意 kmap 在沒有映射可用時可能會睡眠。其原型如下:
    #include <linux/highmem.h>
    void *kmap(struct page *page);
    void kunmap(struct page *page);
               
    • #include <linux/highmem.h>
      #include <asm/kmap_types.h>
      void *kmap_atomic(struct page *page, enum km_type type);
      void kunmap_atomic(void *addr, enum km_type type);
                 
  • 【參考文章清單】:
    1. LDD3
    2. Linux驅動mmap記憶體映射
    3. Linux中記憶體管理詳解

繼續閱讀