Linux虛擬內存-創(chuàng)新互聯(lián)

問題
  1. 什么是虛擬內存地址 ?
  2. Linux 內核為啥要引入虛擬內存而不直接使用物理內存 ?
  3. 虛擬內存空間到底長啥樣?
  4. 內核如何管理虛擬內存?
  5. 什么又是物理內存地址 ?如何訪問物理內存?
什么是虛擬內存地址

舉一個生活中的例子,比如大家在日常生活中給親朋好友郵寄一些本地特產時,都會填寫收件人地址以及寄件人地址。以及在日常網上購物時,都會在相應電商 APP 中填寫自己的收獲地址。

創(chuàng)新互聯(lián)成立于2013年,是專業(yè)互聯(lián)網技術服務公司,擁有項目網站制作、成都網站建設網站策劃,項目實施與項目整合能力。我們以讓每一個夢想脫穎而出為使命,1280元西充做網站,已為上家服務,為西充各地企業(yè)和個人服務,聯(lián)系電話:13518219792

隨后快遞小哥就會根據我們填寫的收貨地址找到我們的真實住所,將我們網購的商品送達到我們的手里。

收貨地址是用來定位我們在現實世界中真實住所地理位置的,而現實世界中我們所在的城市,街道,小區(qū),房屋都是一磚一瓦,一草一木真實存在的。但收貨地址這個概念模型在現實世界中并不真實存在,它只是人們提出的一個虛擬概念,通過收貨地址這個虛擬概念將它和現實世界真實存在的城市,小區(qū),街道的地理位置一一映射起來,這樣我們就可以通過這個虛擬概念來找到現實世界中的具體地理位置。

綜上所述,收貨地址是一個虛擬地址,它是人為定義的,而我們的城市,小區(qū),街道是真實存在的,他們的地理位置就是物理地址。

再比如今天北京叫北京,古代北京可能叫其他名字,所以虛擬地址是可變的,物理地址是永遠不變的

我們還是以日常生活中的收貨地址為例做出類比,我們都很熟悉收貨地址的格式:xx省xx市xx區(qū)xx街道xx小區(qū)xx室,它是按照地區(qū)層次遞進的。同樣,在計算機世界中的虛擬內存地址也有這樣的遞進關系。
這里我們以 Intel Core i7 處理器為例,64 位虛擬地址的格式為:全局頁目錄項(9位)+ 上層頁目錄項(9位)+ 中間頁目錄項(9位)+ 頁表項(9位)+ 頁內偏移(12位)。共 48 位組成的虛擬內存地址。

在這里插入圖片描述

32 位虛擬地址的格式為:頁目錄項(10位)+ 頁表項(10位) + 頁內偏移(12位)。共 32 位組成的虛擬內存地址。
在這里插入圖片描述

進程虛擬內存空間中的每一個字節(jié)都有與其對應的虛擬內存地址,一個虛擬內存地址表示進程虛擬內存空間中的一個特定的字節(jié)。

為什么要使用虛擬地址訪問內存

既然物理內存地址可以直接定位到數據在內存中的存儲位置,那為什么我們不直接使用物理內存地址去訪問內存而是選擇用虛擬內存地址去訪問內存呢?

假設現在沒有虛擬內存地址,我們在程序中對內存的操作全都都是使用物理內存地址,在這種情況下,程序員就需要精確的知道每一個變量在內存中的具體位置,我們需要手動對物理內存進行布局,明確哪些數據存儲在內存的哪些位置,除此之外我們還需要考慮為每個進程究竟要分配多少內存?內存緊張的時候該怎么辦?如何避免進程與進程之間的地址沖突?等等一系列復雜且瑣碎的細節(jié)。

如果我們在單進程系統(tǒng)中比如嵌入式設備上開發(fā)應用程序,系統(tǒng)中只有一個進程,這單個進程獨享所有的物理資源包括內存資源。在這種情況下,上述提到的這些直接使用物理內存的問題可能還好處理一些,但是仍然具有很高的開發(fā)門檻。

然而在現代操作系統(tǒng)中往往支持多個進程,需要處理多進程之間的協(xié)同問題,在多進程系統(tǒng)中直接使用物理內存地址操作內存所帶來的上述問題就變得非常復雜了。

比如我們現在有這樣一個簡單的 Java 程序。

public static void main(String[] args) throws Exception {
        string i = args[0];
        ..........
    }

在程序代碼相同的情況下,我們用這份代碼同時啟動三個 JVM 進程,我們暫時將進程依次命名為 a , b , c 。
這三個進程用到的代碼是一樣的,都是我們提前寫好的,可以被多次運行。由于我們是直接操作物理內存地址,假設變量 i 保存在 0x354 這個物理地址上。這三個進程運行起來之后,同時操作這個 0x354 物理地址,這樣這個變量 i 的值不就混亂了嗎? 三個進程就會出現變量的地址沖突。
在這里插入圖片描述

所以在直接操作物理內存的情況下,我們需要知道每一個變量的位置都被安排在了哪里,而且還要注意和多個進程同時運行的時候,不能共用同一個地址,否則就會造成地址沖突。
現實中一個程序會有很多的變量和函數,這樣一來我們給它們都需要計算一個合理的位置,還不能與其他進程沖突,這就很復雜了。

而虛擬內存的引入正是要解決上述的問題,虛擬內存引入之后,進程的視角就會變得非常開闊,每個進程都擁有自己獨立的虛擬地址空間,進程與進程之間的虛擬內存地址空間是相互隔離,互不干擾的。每個進程都認為自己獨占所有內存空間,自己想干什么就干什么。

系統(tǒng)上還運行了哪些進程和我沒有任何關系。這樣一來我們就可以將多進程之間協(xié)同的相關復雜細節(jié)統(tǒng)統(tǒng)交給內核中的內存管理模塊來處理,極大地解放了程序員的心智負擔。這一切都是因為虛擬內存能夠提供內存地址空間的隔離,極大地擴展了可用空間。

在這里插入圖片描述

這樣進程就以為自己獨占了整個內存空間資源,給進程產生了所有內存資源都屬于它自己的幻覺,這其實是 CPU 和操作系統(tǒng)使用的一個障眼法罷了,任何一個虛擬內存里所存儲的數據,本質上還是保存在真實的物理內存里的。只不過內核幫我們做了虛擬內存到物理內存的這一層映射,將不同進程的虛擬地址和不同內存的物理地址映射起來。
當 CPU 訪問進程的虛擬地址時,經過地址翻譯硬件將虛擬地址轉換成不同的物理地址,這樣不同的進程運行的時候,雖然操作的是同一虛擬地址,但其實背后寫入的是不同的物理地址,這樣就不會沖突了。

程序局部性原理表現為:時間局部性和空間局部性。時間局部性是指如果程序中的某條指令一旦執(zhí)行,則不久之后該指令可能再次被執(zhí)行;如果某塊數據被訪問,則不久之后該數據可能再次被訪問??臻g局部性是指一旦程序訪問了某個存儲單元,則不久之后,其附近的存儲單元也將被訪問。
從程序局部性原理的描述中我們可以得出這樣一個結論:進程在運行之后,對于內存的訪問不會一下子就要訪問全部的內存,相反進程對于內存的訪問會表現出明顯的傾向性,更加傾向于訪問最近訪問過的數據以及熱點數據附近的數據。
根據這個結論我們就清楚了,無論一個進程實際可以占用的內存資源有多大,根據程序局部性原理,在某一段時間內,進程真正需要的物理內存其實是很少的一部分,我們只需要為每個進程分配很少的物理內存就可以保證進程的正常執(zhí)行運轉。

進程虛擬內存空間

上小節(jié)中,我們介紹了為了防止多進程運行時造成的內存地址沖突,內核引入了虛擬內存地址,為每個進程提供了一個獨立的虛擬內存空間,使得進程以為自己獨占全部內存資源。
那么這個進程獨占的虛擬內存空間到底是什么樣子呢?

本小節(jié)我們只討論進程用戶態(tài)虛擬內存空間的布局,我們先把內核態(tài)的虛擬內存空間當做一個黑盒來看待,在后面的小節(jié)中再來詳細介紹內核態(tài)相關內容。

首先我們會想到的是一個進程運行起來是為了執(zhí)行我們交代給進程的工作,執(zhí)行這些工作的步驟我們通過程序代碼事先編寫好,然后編譯成二進制文件存放在磁盤中,CPU 會執(zhí)行二進制文件中的機器碼來驅動進程的運行。所以在進程運行之前,這些存放在二進制文件中的機器碼需要被加載進內存中,而用于存放這些機器碼的虛擬內存空間叫做代碼段。
在這里插入圖片描述
在程序運行起來之后,總要操作變量吧,在程序代碼中我們通常會定義大量的全局變量和靜態(tài)變量,這些全局變量在程序編譯之后也會存儲在二進制文件中,在程序運行之前,這些全局變量也需要被加載進內存中供程序訪問。所以在虛擬內存空間中也需要一段區(qū)域來存儲這些全局變量。

  • 那些在代碼中被我們指定了初始值的全局變量和靜態(tài)變量在虛擬內存空間中的存儲區(qū)域我們叫做數據段
  • 那些沒有指定初始值的全局變量和靜態(tài)變量在虛擬內存空間中的存儲區(qū)域我們叫做 BSS 段。這些未初始化的全局變量被加載進內存之后會被初始化為 0 值
    在這里插入圖片描述
    上面介紹的這些全局變量和靜態(tài)變量都是在編譯期間就確定的,但是我們程序在運行期間往往需要動態(tài)的申請內存,所以在虛擬內存空間中也需要一塊區(qū)域來存放這些動態(tài)申請的內存,這塊區(qū)域就叫做堆。注意這里的堆指的是 OS 堆并不是 JVM 中的堆。
    在這里插入圖片描述
    除此之外,我們的程序在運行過程中還需要依賴動態(tài)鏈接庫,這些動態(tài)鏈接庫以 .so 文件的形式存放在磁盤中,比如 C 程序中的 glibc,里邊對系統(tǒng)調用進行了封裝。glibc 庫里提供的用于動態(tài)申請堆內存的 malloc 函數就是對系統(tǒng)調用 sbrk 和 mmap 的封裝。這些動態(tài)鏈接庫也有自己的對應的代碼段,數據段,BSS 段,也需要一起被加載進內存中。
    還有用于內存文件映射的系統(tǒng)調用 mmap,會將文件與內存進行映射,那么映射的這塊內存(虛擬內存)也需要在虛擬地址空間中有一塊區(qū)域存儲。
    這些動態(tài)鏈接庫中的代碼段,數據段,BSS 段,以及通過 mmap 系統(tǒng)調用映射的共享內存區(qū),在虛擬內存空間的存儲區(qū)域叫做文件映射與匿名映射區(qū)。
    在這里插入圖片描述
    最后我們在程序運行的時候總該要調用各種函數吧,那么調用函數過程中使用到的局部變量和函數參數也需要一塊內存區(qū)域來保存。這一塊區(qū)域在虛擬內存空間中叫做棧。
    在這里插入圖片描述

總結:內核根據進程運行的過程中所需要不同種類的數據而為其開辟了對應的地址空間。分別為:

  • 用于存放進程程序二進制文件中的機器指令的代碼段
  • 用于存放程序二進制文件中定義的全局變量和靜態(tài)變量的數據段和 BSS 段。
  • 用于在程序運行過程中動態(tài)申請內存的堆。
  • 用于存放動態(tài)鏈接庫以及內存映射區(qū)域的文件映射與匿名映射區(qū)。
  • 用于存放函數調用過程中的局部變量和函數參數的棧。

以上就是我們通過一個程序在運行過程中所需要的數據所規(guī)劃出的虛擬內存空間的分布,這些只是一個大概的規(guī)劃,那么在真實的 Linux 系統(tǒng)中,進程的虛擬內存空間的具體規(guī)劃又是如何的呢?

Linux 進程虛擬內存空間 32 位機器上進程虛擬內存空間分布

在 32 位機器上,指針的尋址范圍為 2^32,所能表達的虛擬內存空間為 4 GB。所以在 32 位機器上進程的虛擬內存地址范圍為:0x0000 0000 - 0xFFFF FFFF。
其中用戶態(tài)虛擬內存空間為 3 GB,虛擬內存地址范圍為:0x0000 0000 - 0xC000 000 。
內核態(tài)虛擬內存空間為 1 GB,虛擬內存地址范圍為:0xC000 000 - 0xFFFF FFFF。
在這里插入圖片描述
但是用戶態(tài)虛擬內存空間中的代碼段并不是從 0x0000 0000 地址開始的,而是從 0x0804 8000 地址開始。
0x0000 0000 到 0x0804 8000 這段虛擬內存地址是一段不可訪問的保留區(qū),因為在大多數操作系統(tǒng)中,數值比較小的地址通常被認為不是一個合法的地址,這塊小地址是不允許訪問的。比如在 C 語言中我們通常會將一些無效的指針設置為 NULL,指向這塊不允許訪問的地址。

BSS 段的上邊就是我們經常使用到的堆空間,從圖中的紅色箭頭我們可以知道在堆空間中地址的增長方向是從低地址到高地址增長。
內核中使用 start_brk 標識堆的起始位置,brk 標識堆當前的結束位置。當堆申請新的內存空間時,只需要將 brk 指針增加對應的大小,回收地址時減少對應的大小即可。比如當我們通過 malloc 向內核申請很小的一塊內存時(128K 之內),就是通過改變 brk 位置實現的。

堆空間的上邊是一段待分配區(qū)域,用于擴展堆空間的使用。接下來就來到了文件映射與匿名映射區(qū)域。注意:在文件映射與匿名映射區(qū)的地址增長方向是從高地址向低地址增長。

接下來用戶態(tài)虛擬內存空間的最后一塊區(qū)域就是棧空間了,在這里會保存函數運行過程所需要的局部變量以及函數參數等函數調用信息。??臻g中的地址增長方向是從高地址向低地址增長。每次進程申請新的棧地址時,其地址值是在減少的。

在??臻g的上邊就是內核空間了,進程雖然可以看到這段內核空間地址,**但是就是不能訪問。**這就好比我們在飯店里雖然可以看到廚房在哪里,但是廚房門上寫著 “廚房重地,閑人免進” ,我們就是進不去。

64 位機器上進程虛擬內存空間分布

我們知道在 32 位機器上,指針的尋址范圍為 2^32,所能表達的虛擬內存空間為 4 GB。
那么我們理所應當的會認為在 64 位機器上,指針的尋址范圍為 2^64,所能表達的虛擬內存空間為 16 EB 。虛擬內存地址范圍為:0x0000 0000 0000 0000 0000 - 0xFFFF FFFF FFFF FFFF 。
好家伙 !!! 16 EB 的內存空間,筆者都沒見過這么大的磁盤,在現實情況中根本不會用到這么大范圍的內存空間,
事實上在目前的 64 位系統(tǒng)下只使用了 48 位來描述虛擬內存空間,尋址范圍為 2^48 ,所能表達的虛擬內存空間為 256TB。

其中低 128 T 表示用戶態(tài)虛擬內存空間,虛擬內存地址范圍為:0x0000 0000 0000 0000 - 0x0000 7FFF FFFF F000 。
高 128 T 表示內核態(tài)虛擬內存空間,虛擬內存地址范圍為:0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF 。
這樣一來就在用戶態(tài)虛擬內存空間與內核態(tài)虛擬內存空間之間形成了一段 0x0000 7FFF FFFF F000 - 0xFFFF 8000 0000 0000 的地址空洞,我們把這個空洞叫做 canonical address 空洞。
在這里插入圖片描述
大家注意到在低 128T 的用戶態(tài)地址空間:0x0000 0000 0000 0000 - 0x0000 7FFF FFFF F000 范圍中,所以虛擬內存地址的高 16 位全部為 0 。
如果一個虛擬內存地址的高 16 位全部為 0 ,那么我們就可以直接判斷出這是一個用戶空間的虛擬內存地址。
同樣的道理,在高 128T 的內核態(tài)虛擬內存空間:0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF 范圍中,所以虛擬內存地址的高 16 位全部為 1 。
也就是說內核態(tài)的虛擬內存地址的高 16 位全部為 1 ,如果一個試圖訪問內核的虛擬地址的高 16 位不全為 1 ,則可以快速判斷這個訪問是非法的。

那么處于 canonical address 空洞 :0x0000 7FFF FFFF F000 - 0xFFFF 8000 0000 0000 范圍內的地址的高 16 位 不全為 0 也不全為 1 。如果某個虛擬地址落在這段 canonical address 空洞區(qū)域中,那就是既不在用戶空間,也不在內核空間,肯定是非法訪問了。
未來我們也可以利用這塊 canonical address 空洞,來擴展虛擬內存地址的范圍,比如擴展到 56 位。
在這里插入圖片描述
從上圖中我們可以看出 64 位系統(tǒng)中的虛擬內存布局和 32 位系統(tǒng)中的虛擬內存布局大體上是差不多的。主要不同的地方有2點:

  • 就是前邊提到的由高 16 位空閑地址造成的 canonical address 空洞。在這段范圍內的虛擬內存地址是不合法的,因為它的高 16 位既不全為 0 也不全為 1
  • 在代碼段跟數據段的中間還有一段不可以讀寫的保護段,它的作用是防止程序在讀寫數據段的時候越界訪問到代碼段,這個保護段可以讓越界訪問行為直接崩潰,防止它繼續(xù)往下運行。

你是否還在尋找穩(wěn)定的海外服務器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機房具備T級流量清洗系統(tǒng)配攻擊溯源,準確流量調度確保服務器高可用性,企業(yè)級服務器適合批量采購,新人活動首月15元起,快前往官網查看詳情吧

名稱欄目:Linux虛擬內存-創(chuàng)新互聯(lián)
URL網址:http://muchs.cn/article48/cdcsep.html

成都網站建設公司_創(chuàng)新互聯(lián),為您提供品牌網站制作、微信小程序域名注冊、外貿建站、定制網站、網站收錄

廣告

聲明:本網站發(fā)布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經允許不得轉載,或轉載時需注明來源: 創(chuàng)新互聯(lián)

成都網站建設公司