1. 程式人生 > 其它 >虛擬記憶體分頁機制的地址對映

虛擬記憶體分頁機制的地址對映

概述

在之前的文章虛擬記憶體對分頁機制做了簡單的介紹. 還有一個疑問, 那就是如何將虛存中的邏輯地址對映為實體地址呢? 今天就來簡單分析一下.

對於一個分頁的地址來說, 一般包含兩個元素:

  • 頁號: 第幾頁
  • 偏移量: 當前頁的第幾個位元組

以下以 addr_virtual(p, o)表示一個邏輯地址, 以addr_real(p, o)表示一個實體地址(實體地址也是分頁的).

頁表

第一步先想一下, 如果要根據一個邏輯地址找到對應的實體地址, 那麼這個對應關係必然是存放在某個地方的, 因為對映是沒有規律的嘛. 應該使用什麼資料結構來儲存呢?

因為在分頁中, 是一個最小單位, 故我們只需要頁號的對映關係即可, 邏輯地址和實體地址的頁大小相同, 偏移量也是完全一樣的.

根據 key 尋找 value, 這不就是一個map嘛. 再一看這個mapkey, 頁號都是數字, 而且是順序連續的. 這不就是個陣列嘛. 漂亮, 就是一個數組.

也就是說, 這個頁表是一個以邏輯頁號為索引, 物理頁號為值的一維陣列. 那麼這麼一個地址轉換流程大致如下:

頁表中的元素並不是僅僅儲存物理頁號, 還儲存了一些額外的標誌位, 用來標識當前頁的屬性, 簡單舉幾個例子:

  • 存在位: 當前也是否載入到記憶體中了. 若沒有載入需要作業系統進行載入
  • 修改位: 當前頁在記憶體中是否被修改過. 若修改過, 則回收物理記憶體時需要將其寫會硬碟
  • 核心許可權: 當前頁是否需要核心模式才能訪問
  • 是否可讀位
  • 是否可寫位
  • 是否可執行
  • 等等

因為每個程序都擁有獨自的虛擬記憶體, 故每個程序都需要自己的頁表.

為了提高執行效率, 這個翻譯過程是通過硬體完成的, 既CPU中的記憶體管理單元mmu來完成的.

是不是看著還挺簡單的? 好, 介紹完畢, 文章到此結束.

問題與解決方案

哈哈, 開個玩笑. 哪有這麼容易就結束了. 現在簡單分析一下這個簡單模型存在的問題. 根據演算法的經驗, 大部分演算法實現, 要麼時間複雜度太高, 要麼空間複雜度太高.

時間問題

試想一下訪問一個記憶體的步驟:

  1. 查詢頁表找到對應的實體地址
  2. 訪問實體地址

查詢頁表的操作也是一次記憶體訪問. 也就是說, CPU每訪問一次記憶體就需要一次額外的記憶體訪問. 執行時間直接翻倍.

解決方案

解決的方法就是我們現在已經用爛了的: 快取. 記憶體到 CPU之間已經有了L1 L2快取, 在mmu中還存在著一個頁表的快取TLB. 每次地址翻譯的步驟如下(忽略缺頁的情況):

  1. 檢視TLB中是否存在快取, 若存在直接獲取
  2. TLB中不存在, 從記憶體頁表中獲取並放入TLB

TLB存在的前提, 是程式的訪問具有區域性性. 終於, 又是程式的區域性性救了我們.

空間問題

我們簡單計算一下要存放這個頁表需要多少空間.

在32位CPU 中, 可訪問的邏輯地址空間為4G. 假設頁大小為: 4kb, 那麼總頁數為:

4G / 4kb = (2^32) / (2^12) = 2^20 = 1mb

再假設, 頁表的每個元素需要4個位元組, 那麼需要的總空間為: 4mb. 每個程序僅僅是儲存頁表就需要4mb. 而且這還是32位, 如果是64位呢? 可以計算下看看, 結果很誇張.

解決方案

借鑑一下記憶體分頁的思路, 我們將記憶體分為 n 個頁, 就可以按需載入了. 同樣, 也可以將一個大的頁表分為n個小的頁表, 就可以進行部分載入了, 既多級頁表

以最簡單的二級頁表進行說明, 其虛擬記憶體劃分大致如下:

頁表的結構大致如下:

注意, 此時邏輯地址中頁號內容儲存了兩個內容:

  1. 一級頁表的索引
  2. 二級頁表的索引

為什麼說多級頁表解決了空間的問題呢? 再次根據程式的區域性性原理, 一級頁表中的大部分對應的值為空, 既大部分二級頁表並沒有載入到記憶體中.

此時再算一下, 還是32位CPU, 頁的大小還是4kb, 頁中元素大小還是4位元組. 此時假設一級頁表每個元素負責4mb的空間. 那麼一級頁表佔用的總頁數為: 4G / 4mb = (2^32) / (2^22) = 2^10. 一級頁表佔用空間為: (2^10) * (2^2)=4kb

每個二級頁表的總頁數為: 4mb / 4kb = (2^22) / 2(12) = 2^10 = 1024, 佔用空間: (2^10) * (2^2) = 4kb

其中只有一級頁表是常駐記憶體的, 二級頁表只需要載入其中一部分. 空間直接降下來了.

但是, 又帶來一個新的問題, 現在獲取一個實體地址, 需要訪問兩次記憶體, 這不是比原來還要慢麼? 別忘了剛剛的TLB, 有了這一層快取, 大部分訪問都在mmu內部進行了. 又又又一次, 程式的區域性性原理救了我們.

多級頁表 , 將二級頁表進一步擴充套件, 就可以得到多級頁表了, 不再贅述.

程式的區域性性

知道了地址是如何對映的, 對我們平常寫程式有什麼幫助呢?

頁的轉換是根據程式的區域性性, 所以我們在寫程式碼的時候, 要儘量保證寫出來的是具有區域性性的, 舉個例子:

int main() {
    int i, j;
    int arr[1024][1024];
    // 第一種方式
    for(i = 0; i < 1024; i++){
        for(j = 0; j < 1024; j++){
            global_arr[i][j] = 0;
        }
    }
    // 第二種方式
    for(j = 0; j < 1024; j++){
        for(i = 0; i < 1024; i++){
            global_arr[i][j] = 0;
        }
    }
}

上面這段程式碼目的很簡單, 給一個1024*1024的二維陣列進行初始化. 你能看出這兩種方式有什麼不同麼?

遍歷方式不同, 方式一是一行一行的遍歷, 方式二則是一列一列的遍歷.

我們知道, 二維陣列在記憶體中是順序儲存的. 也就是說, 一個二維陣列: [[1, 2, 3], [4, 5, 6]], 在記憶體中的儲存順序是: 1, 2, 3, 4, 5, 6.

而我們這個陣列, 每行1024個int元素, 正好是4kb 一頁的大小.

因此, 方式一訪問頁的順序是: page1, page1 ... page1024, page1024, 每頁訪問1024次後,切換到下一頁, 共發生 1024 次頁的切換

而, 方式二訪問頁的順序是: page1, page2...page1024 ... page1, page2...page1024, 依次訪問每一頁, 每頁訪問1024次, 共發生 1024*1024次頁的切換

效能高下立判, 方式一更加符合區域性性原理, 方式二的訪問太跳躍了.

當然, 現在記憶體很大的時候, 所有內容都載入到了記憶體中, 同時TLB快取了所有頁的對映, 此時兩種方式是沒有差別的. 但是:

  1. TLB容量不足, 新的快取會淘汰舊的快取, 頻繁訪問不同的頁會造成更多的快取失效
  2. 若記憶體容量不足, 寫入新的頁會淘汰舊的頁, 頻繁訪問不同的頁會導致更多記憶體的換入換出.

口說無憑

當然, 口說無憑, 為了對上面頁的切換機制有個直觀的感受, 我們通過getrusage函式來獲取程式執行的頁切換資訊. 程式碼如下:

#include <stdio.h>
#include <sys/resource.h>

const int M = 1024;
// 增加列的大小, 以使得效果明顯. 10mb
const int N = 1024*10;
// 因為限制了棧的大小, 故將變數提升為全域性, 放到堆中
int global_arr[1024][1024*10];

int main() {
    int i, j;
    // 第一種方式
    for(i = 0; i < M; i++){
        for(j = 0; j < N; j++){
            global_arr[i][j] = 0;
        }
    }
    // 第二種方式
//    for(j = 0; j < N; j++){
//        for(i = 0; i < M; i++){
//            global_arr[i][j] = 0;
//        }
//    }

    struct rusage usage;
    getrusage(RUSAGE_SELF, &usage);
    printf("頁回收次數: %ld\n", usage.ru_minflt);
    printf("缺頁中斷的次數: %ld\n", usage.ru_majflt);
}

現在電腦跑這麼個小程式還是比較簡單的, 不會有什麼區別, 因此還要對程序的記憶體進行限制. 我是通過限制docker可用記憶體來實現的:

docker run -it -m 6m --memory-swap -1 debian bash

好, 萬事具備, 來看看結果:

方式一

方式二

可以看到, 方式一想比方式二要好很多.

故, 對於效能要求很高的程式, 當你沒有優化方向了, 區域性性可能會幫到你.

原文連結: https://hujingnb.com/archives/698