日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

虚拟内存分页机制的地址映射

發(fā)布時間:2024/8/23 编程问答 37 豆豆
生活随笔 收集整理的這篇文章主要介紹了 虚拟内存分页机制的地址映射 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

概述

在之前的文章虛擬內(nèi)存對分頁機制做了簡單的介紹. 還有一個疑問, 那就是如何將虛存中的邏輯地址映射為物理地址呢? 今天就來簡單分析一下.

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

  • 頁號: 第幾頁
  • 偏移量: 當前頁的第幾個字節(jié)

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

頁表

第一步先想一下, 如果要根據(jù)一個邏輯地址找到對應的物理地址, 那么這個對應關系必然是存放在某個地方的, 因為映射是沒有規(guī)律的嘛. 應該使用什么數(shù)據(jù)結(jié)構(gòu)來存儲呢?

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

根據(jù) key 尋找 value, 這不就是一個map嘛. 再一看這個map的key, 頁號都是數(shù)字, 而且是順序連續(xù)的. 這不就是個數(shù)組嘛. 漂亮, 就是一個數(shù)組.

也就是說, 這個頁表是一個以邏輯頁號為索引, 物理頁號為值的一維數(shù)組. 那么這么一個地址轉(zhuǎn)換流程大致如下:

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

  • 存在位: 當前也是否加載到內(nèi)存中了. 若沒有加載需要操作系統(tǒng)進行加載
  • 修改位: 當前頁在內(nèi)存中是否被修改過. 若修改過, 則回收物理內(nèi)存時需要將其寫會硬盤
  • 內(nèi)核權(quán)限: 當前頁是否需要內(nèi)核模式才能訪問
  • 是否可讀位
  • 是否可寫位
  • 是否可執(zhí)行
  • 等等

因為每個進程都擁有獨自的虛擬內(nèi)存, 故每個進程都需要自己的頁表.

為了提高運行效率, 這個翻譯過程是通過硬件完成的, 既CPU中的內(nèi)存管理單元mmu來完成的.

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

問題與解決方案

哈哈, 開個玩笑. 哪有這么容易就結(jié)束了. 現(xiàn)在簡單分析一下這個簡單模型存在的問題. 根據(jù)算法的經(jīng)驗, 大部分算法實現(xiàn), 要么時間復雜度太高, 要么空間復雜度太高.

時間問題

試想一下訪問一個內(nèi)存的步驟:

  • 查找頁表找到對應的物理地址
  • 訪問物理地址
  • 查找頁表的操作也是一次內(nèi)存訪問. 也就是說, CPU每訪問一次內(nèi)存就需要一次額外的內(nèi)存訪問. 執(zhí)行時間直接翻倍.

    解決方案

    解決的方法就是我們現(xiàn)在已經(jīng)用爛了的: 緩存. 內(nèi)存到 CPU之間已經(jīng)有了L1 L2緩存, 在mmu中還存在著一個頁表的緩存TLB. 每次地址翻譯的步驟如下(忽略缺頁的情況):

  • 查看TLB中是否存在緩存, 若存在直接獲取
  • 若TLB中不存在, 從內(nèi)存頁表中獲取并放入TLB
  • TLB存在的前提, 是程序的訪問具有局部性. 終于, 又是程序的局部性救了我們.

    空間問題

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

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

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

    再假設, 頁表的每個元素需要4個字節(jié), 那么需要的總空間為: 4mb. 每個進程僅僅是存儲頁表就需要4mb. 而且這還是32位, 如果是64位呢? 可以計算下看看, 結(jié)果很夸張.

    解決方案

    借鑒一下內(nèi)存分頁的思路, 我們將內(nèi)存分為 n 個頁, 就可以按需加載了. 同樣, 也可以將一個大的頁表分為n個小的頁表, 就可以進行部分加載了, 既多級頁表

    以最簡單的二級頁表進行說明, 其虛擬內(nèi)存劃分大致如下:

    頁表的結(jié)構(gòu)大致如下:

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

  • 一級頁表的索引
  • 二級頁表的索引
  • 為什么說多級頁表解決了空間的問題呢? 再次根據(jù)程序的局部性原理, 一級頁表中的大部分對應的值為空, 既大部分二級頁表并沒有加載到內(nèi)存中.

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

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

    其中只有一級頁表是常駐內(nèi)存的, 二級頁表只需要加載其中一部分. 空間直接降下來了.

    但是, 又帶來一個新的問題, 現(xiàn)在獲取一個物理地址, 需要訪問兩次內(nèi)存, 這不是比原來還要慢么? 別忘了剛剛的TLB, 有了這一層緩存, 大部分訪問都在mmu內(nèi)部進行了. 又又又一次, 程序的局部性原理救了我們.

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

    程序的局部性

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

    頁的轉(zhuǎn)換是根據(jù)程序的局部性, 所以我們在寫代碼的時候, 要盡量保證寫出來的是具有局部性的, 舉個例子:

    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的二維數(shù)組進行初始化. 你能看出這兩種方式有什么不同么?

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

    我們知道, 二維數(shù)組在內(nèi)存中是順序存儲的. 也就是說, 一個二維數(shù)組: [[1, 2, 3], [4, 5, 6]], 在內(nèi)存中的存儲順序是: 1, 2, 3, 4, 5, 6.

    而我們這個數(shù)組, 每行1024個int元素, 正好是4kb 一頁的大小.

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

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

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

    當然, 現(xiàn)在內(nèi)存很大的時候, 所有內(nèi)容都加載到了內(nèi)存中, 同時TLB緩存了所有頁的映射, 此時兩種方式是沒有差別的. 但是:

  • 若TLB容量不足, 新的緩存會淘汰舊的緩存, 頻繁訪問不同的頁會造成更多的緩存失效
  • 若內(nèi)存容量不足, 寫入新的頁會淘汰舊的頁, 頻繁訪問不同的頁會導致更多內(nèi)存的換入換出.
  • 口說無憑

    當然, 口說無憑, 為了對上面頁的切換機制有個直觀的感受, 我們通過getrusage函數(shù)來獲取程序運行的頁切換信息. 代碼如下:

    #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("頁回收次數(shù): %ld\n", usage.ru_minflt);printf("缺頁中斷的次數(shù): %ld\n", usage.ru_majflt); }

    現(xiàn)在電腦跑這么個小程序還是比較簡單的, 不會有什么區(qū)別, 因此還要對進程的內(nèi)存進行限制. 我是通過限制docker可用內(nèi)存來實現(xiàn)的:

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

    好, 萬事具備, 來看看結(jié)果:

    方式一

    方式二

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

    故, 對于性能要求很高的程序, 當你沒有優(yōu)化方向了, 局部性可能會幫到你.

    原文鏈接: https://hujingnb.com/archives/698

    創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎

    總結(jié)

    以上是生活随笔為你收集整理的虚拟内存分页机制的地址映射的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。